• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-04-12 13:38 Aet 隐藏边栏 |   抢沙发  3 
文章评分 1 次,平均分 5.0

原型继承

概述

  1. 例如,我们有一个 user 对象及其属性和方法,并希望将 adminguest 作为基于 user 稍加修改的变体
    1. 想重用 user 中的内容,而不是复制/重新实现它的方法,而只是在其之上构建一个新的对象
  2. 原型继承(Prototypal inheritance) 这个语言特性能够帮助我们实现这一需求

[[Prototype]]

  1. JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]](如规范中所命名的)
    1. 它要么为 null
    2. 要么就是对另一个对象的引用
    3. 该对象被称为“原型”
  2. 特殊的名字 __proto__

  1. 现在,如果我们从 rabbit 中读取一个它没有的属性,JavaScript 会自动从 animal 中获取

  1. 在这儿我们可以说 “animalrabbit 的原型”,或者说 “rabbit 的原型是从 animal 继承而来的”
    1. 因此,如果 animal 有许多有用的属性和方法,那么它们将自动地变为在 rabbit 中可用
    2. 这种属性被称为“继承”

  1. 两个限制:
    1. 引用不能形成闭环。如果我们试图给 __proto__ 赋值但会导致引用形成闭环时,JavaScript 会抛出错误
    2. __proto__ 的值可以是对象,也可以是 null。而其他的类型都会被忽略

__proto__ [[Prototype]]getter/setter

  1. 注意,__proto__ 与内部的 [[Prototype]] 不一样
  2. __proto__[[Prototype]]getter/setter
  3. __proto__ 属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数
    1. Object.getPrototypeOf/Object.setPrototypeOf 来取代 __proto__ 去 get/set 原型

写入不使用原型

  1. 原型仅用于读取属性
  2. 对于写入/删除操作可以直接在对象上进行

  1. 访问器(accessor)属性是一个例外,因为赋值(assignment)操作是由 setter 函数处理的。因此,写入此类属性实际上与调用函数相同
    1. 也就是这个原因,所以下面这段代码中的 admin.fullName 能够正常运行:
    2. (*) 行中,属性 admin.fullName 在原型 user 中有一个 getter,因此它会被调用。在 (**) 行中,属性在原型中有一个 setter,因此它会被调用

this的值

  1. set fullName(value)this 的值是什么?属性 this.namethis.surname 被写在哪里:在 user 还是 admin
    1. this 根本不受原型的影响
    2. 无论在哪里找到方法:在一个对象还是在原型中。在一个方法调用中,this 始终是点符号 . 前面的对象
    3. 因此,setter 调用 admin.fullName= 使用 admin 作为 this,而不是 user
  2. 这是一件非常重要的事儿,因为我们可能有一个带有很多方法的大对象,并且还有从其继承的对象
    1. 当继承的对象运行继承的方法时,它们将仅修改自己的状态,而不会修改大对象的状态
    2. 例如,这里的 animal 代表“方法存储”,rabbit 在使用其中的方法
    3. 调用 rabbit.sleep() 会在 rabbit 对象上设置 this.isSleeping

  1. 如果我们还有从 animal 继承的其他对象,像 birdsnake 等,它们也将可以访问 animal 的方法
    1. 但是,每个方法调用中的 this 都是在调用时(点符号前)评估的对应的对象,而不是 animal
    2. 因此,当我们将数据写入 this 时,会将其存储到这些对象中
    3. 所以,方法是共享的,但对象状态不是

for..in

  1. for..in 循环也会迭代继承的属性

  1. 如果这不是我们想要的,并且我们想排除继承的属性,那么这儿有一个内建方法
    1. obj.hasOwnProperty(key):如果 obj 具有自己的(非继承的)名为 key 的属性,则返回 true
    2. 因此,我们可以过滤掉继承的属性(或对它们进行其他操作):

注意

  1. 方法 rabbit.hasOwnProperty 来自哪儿?
    1. 我们并没有定义它
    2. 该方法是 Object.prototype.hasOwnProperty 提供的。换句话说,它是继承的
  2. 如果 for..in 循环会列出继承的属性,那为什么 hasOwnProperty 没有像 eatsjumps 那样出现在 for..in 循环中?
    1. 答案很简单:它是不可枚举的
    2. 就像 Object.prototype 的其他属性,hasOwnPropertyenumerable:false 标志
    3. 并且 for..in 只会列出可枚举的属性。这就是为什么它和其余的 Object.prototype 属性都未被列出
  3. 几乎所有其他键/值获取方法都忽略继承的属性
    1. 几乎所有其他键/值获取方法,例如 Object.keysObject.values 等,都会忽略继承的属性
    2. 它们只会对对象自身进行操作。不考虑 继承自原型的属性

F.prototype

概述

  1. 还记得,可以使用诸如 new F() 这样的构造函数来创建一个新对象
  2. 如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 [[Prototype]]

注意1

  1. JavaScript 从一开始就有了原型继承。这是 JavaScript 编程语言的核心特性之一
  2. 但是在过去,没有直接对其进行访问的方式
    1. 唯一可靠的方法是构造函数的 "prototype" 属性
    2. 目前仍有许多脚本仍在使用它
  3. 请注意,这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性
    1. 这听起来与“原型”这个术语很类似,但这里我们实际上指的是具有该名字的常规属性

示例

  1. 设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]] 赋值为 animal
  2. F.prototype 属性仅在 new F 被调用时使用,它为新对象的 [[Prototype]] 赋值
    1. 如果在创建之后,F.prototype 属性有了变化(F.prototype = <another object>),那么通过 new F 创建的新对象也将随之拥有新的对象作为 [[Prototype]],但已经存在的对象将保持旧有的值
    2. 就是说,当Rabbit.prototype后续更新为其他对象后,不会改变以前创建的对象的Rabbit.prototype的内容,但是后续新创建的对象的Rabbit.prototype是更新后的新值

默认的 F.prototype,构造器属性

  1. 每个函数都有 "prototype" 属性,即使我们没有提供它
  2. 默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身

  1. 通常,如果我们什么都不做,constructor 属性可以通过 [[Prototype]] 给所有 rabbits 使用:

  1. 我们可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器
    1. 当我们有一个对象,但不知道它使用了哪个构造器(例如它来自第三方库),并且我们需要创建另一个类似的对象时,用这种方法就很方便

注意2

  1. 但是,关于 "constructor" 最重要的是
    1. JavaScript 自身并不能确保正确的 "constructor" 函数值
    2. 它存在于函数的默认 "prototype" 中,但仅此而已。之后会发生什么 —— 完全取决于我们
    3. 特别是,如果我们将整个默认 prototype 替换掉,那么其中就不会有 "constructor"

  1. 因此,为了确保正确的 "constructor",我们可以选择添加/删除属性到默认 "prototype",而不是将其整个覆盖:
    1. 或者,也可以手动重新创建 constructor 属性:

原生的原型

概述

  1. "prototype" 属性在 JavaScript 自身的核心部分中被广泛地应用
  2. 所有的内建构造函数都用到了它

Object.prototype

  1. 假如我们输出一个空对象:
    1. 生成字符串 "[object Object]" 的代码在哪里?
    2. 那就是一个内建的 toString 方法,但是它在哪里呢?obj 是空的!
    3. 然而简短的表达式 obj = {}obj = new Object() 是一个意思,其中 Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象

  1. new Object() 被调用(或一个字面量对象 {...} 被创建),这个对象的 [[Prototype]] 属性被设置为 Object.prototype
    1. 所以,之后当 obj.toString() 被调用时,这个方法是从 Object.prototype 中获取的

  1. 请注意在 Object.prototype 上方的链中没有更多的 [[Prototype]]

其他内建原型

  1. 其他内建对象,像 ArrayDateFunction 及其他,都在 prototype 上挂载了方法
  2. 例如,当我们创建一个数组 [1, 2, 3],在内部会默认使用 new Array() 构造器
    1. 因此 Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法
    2. 这样内存的存储效率是很高的
  3. 按照规范,所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”

  1. 一些方法在原型上可能会发生重叠,例如,Array.prototype 有自己的 toString 方法来列举出来数组的所有元素并用逗号分隔每一个元素
    1. 正如我们之前看到的那样,Object.prototype 也有 toString 方法,但是 Array.prototype 在原型链上更近,所以数组对象原型上的方法会被使用

  1. 浏览器内的工具,像 Chrome 开发者控制台也会显示继承性(可能需要对内建对象使用 console.dir):

基本数据类型

  1. 最复杂的事情发生在字符串、数字和布尔值上
    1. 它们并不是对象
    2. 但是如果我们试图访问它们的属性,那么临时包装器对象将会通过内建的构造器 StringNumberBoolean 被创建
    3. 它们提供给我们操作字符串、数字和布尔值的方法然后消失
  2. 这些对象对我们来说是无形地创建出来的
    1. 大多数引擎都会对其进行优化,但是规范中描述的就是通过这种方式
    2. 这些对象的方法也驻留在它们的 prototype 中,可以通过 String.prototypeNumber.prototypeBoolean.prototype 进行获取

注意1

  1. nullundefined 没有对象包装器
    1. 特殊值 nullundefined 比较特殊
    2. 它们没有对象包装器,所以它们没有方法和属性。并且它们也没有相应的原型

更改原生原型

  1. 原生的原型是可以被修改的
  2. 例如,我们向 String.prototype 中添加一个方法,这个方法将对所有的字符串都是可用的:
    1. 在开发的过程中,我们可能会想要一些新的内建方法,并且想把它们添加到原生原型中
    2. 但这通常是一个很不好的想法

注意2

  1. 原型是全局的,所以很容易造成冲突
  2. 如果有两个库都添加了 String.prototype.show 方法,那么其中的一个方法将被另一个覆盖
  3. 所以,通常来说,修改原生原型被认为是一个很不好的想法
  4. 在现代编程中,只有一种情况下允许修改原生原型。那就是 polyfilling
    1. Polyfilling 是一个术语,表示某个方法在 JavaScript 规范中已存在,但是特定的 JavaScript 引擎尚不支持该方法,那么我们可以通过手动实现它,并用以填充内建原型
    2. 例如:

从原型中借用

  1. 比如有一种简单的方法可以使用数组的 join 方法:
    1. 这个技巧被称为 方法借用(method borrowing
    2. 从常规数组 [].join 中获取(借用)join 方法,并使用 [].join.callarguments 的上下文中运行它

  1. 一些原生原型的方法通常会被借用
    1. 例如,如果我们要创建类数组对象,则可能需要向其中复制一些 Array 方法

  1. 上面这段代码有效,是因为内建的方法 join 的内部算法只关心正确的索引和 length 属性
    1. 它不会检查这个对象是否是真正的数组。许多内建方法就是这样
  2. 另一种方式是通过将 obj.__proto__ 设置为 Array.prototype,这样 Array 中的所有方法都自动地可以在 obj 中使用了
    1. 但是如果 obj 已经从另一个对象进行了继承,那么这种方法就不可行了
    2. 因为这样会覆盖掉已有的继承
    3. 此处 obj 其实已经从 Object 进行了继承,但是 Array 也继承自 Object,所以此处的方法借用不会影响 obj 对原有继承的继承,因为 obj 通过原型链依旧继承了 Object
    4. 请记住,我们一次只能继承一个对象

原型方法,没有 proto 的对象

概述

  1. 使用 obj.__proto__ 设置或读取原型被认为已经过时且不推荐使用(deprecated)了
  2. 现代的获取/设置原型的方法有:
    1. Object.getPrototypeOf(obj)—— 返回对象 obj[[Prototype]]
    2. Object.setPrototypeOf(obj, proto) —— 将对象 obj[[Prototype]] 设置为 proto
  3. __proto__ 不被反对的唯一的用法是在创建新对象时,将其用作属性:{ __proto__: ... }
  4. 虽然,也有一种特殊的方法:
    1. Object.create(proto, descriptors) —— 利用给定的 proto 作为 [[Prototype]] 和可选的属性描述来创建一个空对象

  1. Object.create 方法更强大,因为它有一个可选的第二参数:属性描述器
    1. 我们可以在此处为新对象提供额外的属性,就像这样:

  1. 可以使用 Object.create 来实现比复制 for..in 循环中的属性更强大的对象克隆方式:
    1. 此调用可以对 obj 进行真正准确地拷贝,包括所有的属性:可枚举和不可枚举的,数据属性和 setters/getters —— 包括所有内容,并带有正确的 [[Prototype]]

注意

  1. 如果速度很重要,就请不要修改已存在的对象的 [[Prototype]]
    1. 从技术上来讲,我们可以在任何时候 get/set [[Prototype]]
    2. 但是通常我们只在创建对象的时候设置它一次,自那之后不再修改:rabbit 继承自 animal,之后不再更改
    3. 并且,JavaScript 引擎对此进行了高度优化。用 Object.setPrototypeOfobj.__proto__= “即时”更改原型是一个非常缓慢的操作,因为它破坏了对象属性访问操作的内部优化
    4. 因此,除非你知道自己在做什么,或者 JavaScript 的执行速度对你来说完全不重要,否则请避免使用它

"Very plain" objects

  1. 对象可以用作关联数组(associative arrays)来存储键/值对
    1. 但是如果我们尝试在其中存储 用户提供的 键(例如:一个用户输入的字典),我们可以发现一个有趣的小故障:所有的键都正常工作,除了 "__proto__"
    2. 如果用户输入 __proto__,那么在第四行的赋值会被忽略
    3. __proto__ 属性很特殊:它必须是一个对象或者 null。字符串不能成为原型。这就是为什么将字符串赋值给 __proto__ 会被忽略

  1. 想要存储键值对,然而键名为 "__proto__" 的键值对没有被正确存储。所以这是一个 bug
    1. 怎么避免这样的问题呢?
    2. 首先,我们可以改用 Map 来代替普通对象进行存储,这样一切都迎刃而解:

  1. 现在,我们想要将一个对象用作关联数组,并且摆脱此类问题,我们可以使用一些小技巧:
    1. Object.create(null) 创建了一个空对象,这个对象没有原型([[Prototype]]null):
    2. 因此,它没有继承 __proto__getter/setter 方法。现在,它被作为正常的数据属性进行处理,因此上面的这个示例能够正常工作

  1. 我们可以把这样的对象称为 “very plain” 或 “pure dictionary” 对象,因为它们甚至比通常的普通对象(plain object{...} 还要简单
    1. 缺点是这样的对象没有任何内建的对象的方法,例如 toString
    2. 但是它们通常对关联数组而言还是很友好

  1. 请注意,大多数与对象相关的方法都是 Object.something(...)
    1. 例如 Object.keys(obj) —— 它们不在 prototype 中,因此在 “very plain” 对象中它们还是可以继续使用:

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享