XiaoboTalk

Javascript 原型链

在大部分的面向对象语言中,都会有继承,代码关键字上体现为 superclass (不同的语言命名不同),但是 js 中并没有这样的概念。js 实现继承是通过原型链。
当谈到继承时,JavaScript 只有一种结构:对象。每个对象(object)都有一个私有属性指向另一个名为原型(prototype)的对象。原型对象也有一个自己的原型,层层向上直到一个对象的原型为 null。根据定义,null 没有原型,并作为这个原型链(prototype chain)中的最后一个环节。

一、理解 __proto__prototype

__proto__ : 自己是从哪个模版中构造出来的的,我的父辈是谁。
prototype : 只有函数对象才有的属性:表示自己构造儿子实例的时候,所使用的模版。
备注: 遵循 ECMAScript 标准,符号 someObject.[[Prototype]] 用于标识 someObject 的原型。内部插槽 [[Prototype]] 可以通过 Object.getPrototypeOf() 和 Object.setPrototypeOf() 函数来访问。这个等同于 JavaScript 的非标准但被许多 JavaScript 引擎实现的属性 __proto__ 访问器。为在保持简洁的同时避免混淆,在我们的符号中会避免使用 obj.__proto__,而是使用 obj.[[Prototype]] 作为代替。其对应于 Object.getPrototypeOf(obj)
它不应与函数的 func.prototype 属性混淆,后者指定在给定函数被用作构造函数时分配给所有对象实例的 [[Prototype]]
JavaScript 对象是动态的属性(指其自有属性)“包”。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
const o = { a: 1, b: 2, // __proto__ 设置了 [[Prototype]]。它在这里被指定为另一个对象字面量。 __proto__: { b: 3, c: 4, }, }; // o.[[Prototype]] 具有属性 b 和 c。 // o.[[Prototype]].[[Prototype]] 是 Object.prototype(我们会在下文解释其含义)。 // 最后,o.[[Prototype]].[[Prototype]].[[Prototype]] 是 null。 // 这是原型链的末尾,值为 null, // 根据定义,其没有 [[Prototype]]。 // 因此,完整的原型链看起来像这样: // { a: 1, b: 2 } ---> { b: 3, c: 4 } ---> Object.prototype ---> null console.log(o.a); // 1 // o 上有自有属性“a”吗?有,且其值为 1。 console.log(o.b); // 2 // o 上有自有属性“b”吗?有,且其值为 2。 // 原型也有“b”属性,但其没有被访问。 // 这被称为属性遮蔽(Property Shadowing) console.log(o.c); // 4 // o 上有自有属性“c”吗?没有,检查其原型。 // o.[[Prototype]] 上有自有属性“c”吗?有,其值为 4。 console.log(o.d); // undefined // o 上有自有属性“d”吗?没有,检查其原型。 // o.[[Prototype]] 上有自有属性“d”吗?没有,检查其原型。 // o.[[Prototype]].[[Prototype]] 是 Object.prototype 且 // 其默认没有“d”属性,检查其原型。 // o.[[Prototype]].[[Prototype]].[[Prototype]] 为 null,停止搜索, // 未找到该属性,返回 undefined。
JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 中,任何函数都被可以添加到对象上作为其属性。函数的继承与其他属性的继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。
当继承的函数被调用时,this 值指向的是当前继承的对象,而不是拥有该函数属性的原型对象。
const parent = { value: 2, method() { return this.value + 1; }, }; console.log(parent.method()); // 3 // 当调用 parent.method 时,“this”指向了 parent // child 是一个继承了 parent 的对象 const child = { __proto__: parent, }; console.log(child.method()); // 3 // 调用 child.method 时,“this”指向了 child。 // 又因为 child 继承的是 parent 的方法, // 首先在 child 上寻找“value”属性。但由于 child 本身 // 没有名为“value”的自有属性,该属性会在 // [[Prototype]] 上被找到,即 parent.value。 child.value = 4; // 在 child,将“value”属性赋值为 4。 // 这会遮蔽 parent 上的“value”属性。 // child 对象现在看起来是这样的: // { value: 4, __proto__: { value: 2, method: [Function] } } console.log(child.method()); // 5 // 因为 child 现在拥有“value”属性,“this.value”现在表示 // child.value

二、从顶层的 Object 和 Function 开始理解

  • 最顶层是 Object 原型,它是所有对象的祖宗原型。
  • Object 的第一个后代,Function 原型,Function 原型是所有函数的祖宗。
  • Object() 函数的 __proto 也是 Function,只不过 Object() 函数的 prototype 是 Object 原型。Function 自己没有 prototype (值为 undefinde)。
notion image
如果不是通过构造函数创建的对象,它的原型默认就是 Object 原型:
const a = {}; console.log(a.__proto__); // Object 原型 console.log(a.__proto__.__proto__); // null,因为 Object 原型的 __proto_指向 null
并没有任何代码手动指定对象的__proto__ ,但是 a 对象依然有__proto__ 原型。这是因为 js 字面量语法会隐式设置 __proto__这被称为字面量的隐式构造函数。

通过函数构造对象

所有的 js 函数 (箭头函数除外),都有 prototype 属性,函数自身的原型(__proto__)指向 Function。函数创建后,解释器会自动给函数创建一个 prototype 属性。该属性指向一个与函数同名的原型。这个原型的作用,函数构造自己的实例时参考的模版。这个原型的原型 (__ proto__) 是 Object 原型。
function Foo() {} // 默认,Foo 函数会有一个 prototype 叫 Foo.prototype, // Foo.__proto__ 是顶级函数原型 Function // 使用 Foo 构造两个对象 var b = new Foo(20); var c = new Foo(30);
notion image

构造函数

原型的强大之处在于,如果一组属性应该出现在每一个实例上,那我们就可以重用它们——尤其是对于方法。假设我们要创建多个盒子,其中每一个盒子都是一个对象,包含一个可以通过 getValue 函数访问的值。一个简单的实现可能是:
const boxes = [ { value: 1, getValue() { return this.value; } }, { value: 2, getValue() { return this.value; } }, { value: 3, getValue() { return this.value; } }, ];
这是不够好的,因为每一个实例都有自己的,做相同事情的函数属性,这是冗余且不必要的。相反,我们可以将 getValue 移动到所有盒子的 [[Prototype]] 上:
const boxPrototype = { getValue() { return this.value; }, }; const boxes = [ { value: 1, __proto__: boxPrototype }, { value: 2, __proto__: boxPrototype }, { value: 3, __proto__: boxPrototype }, ];
这样,所有盒子的 getValue 方法都会引用相同的函数,降低了内存使用率。但是,手动绑定每个对象创建的 __proto__ 仍旧非常不方便。这时,我们就可以使用构造函数,它会自动为每个构造的对象设置 [[Prototype]]。构造函数是使用 new 调用的函数。
// 一个构造函数 function Box(value) { this.value = value; } // 使用 Box() 构造函数创建的所有盒子都将具有的属性 Box.prototype.getValue = function () { return this.value; }; const boxes = [new Box(1), new Box(2), new Box(3)];
上面的构造函数可以重写为
class Box { constructor(value) { this.value = value; } // 在 Box.prototype 上创建方法 getValue() { return this.value; } }
类是构造函数的语法糖,这意味着你仍然可以修改 Box.prototype 来改变所有实例的行为。
(全文完)