[[Prototype]]

JavaScript 中的对象有一个内部属性,在语言规范中称为 [[Prototype]],它只是一个其他对象的引用。几乎所有的对象在被创建时,它的这个属性都被赋予了一个非 null

  1. var anotherObject = {
  2. a: 2
  3. };
  4. // 创建一个链接到 `anotherObject` 的对象
  5. var myObject = Object.create( anotherObject );
  6. myObject.a; // 2

现在让 myObject [[Prototype]] 链到了 anotherObject。虽然很明显 myObject.a 实际上不存在,但是无论如何属性访问成功了(在 anotherObject 中找到了),而且确实找到了值 2

Object.prototype

每个 普通[[Prototype]] 链的最顶端,是内建的 Object.prototype。这个对象包含各种在整个 JS 中被使用的共通工具,因为 JavaScript 中所有普通(内建,而非被宿主环境扩展的)的对象都“衍生自”Object.prototype 对象

设置与遮蔽属性

  1. myObject.foo = "bar";

以下来 考察 myObject.foo = "bar" 赋值的三种场景,当 foo 不直接存在myObject,但 存在myObject[[Prototype]] 链的更高层时:

  1. 如果一个普通的名为 foo 的数据访问属性在 [[Prototype]] 链的高层某处被找到,而且没有被标记为只读(writable:false,那么一个名为 foo 的新属性(也就是代表新添加)就直接添加到 myObject 上,形成一个 遮蔽属性
  2. 如果一个 foo[[Prototype]] 链的高层某处被找到,但是它被标记为 只读(writable:false ,那么设置既存属性和在 myObject 上创建遮蔽属性都是 不允许 的。如果代码运行在 strict mode 下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽
  3. 如果一个 foo[[Prototype]] 链的高层某处被找到,而且它是一个 setter(也就是set 属性名()的特殊函数),那么这个 setter 总是被调用。没有 foo 会被添加到(也就是遮蔽在)myObject 上,这个 foo setter 也不会被重定义。

案例如下:

  1. var anotherObject = {
  2. a: 2
  3. };
  4. var myObject = Object.create( anotherObject );
  5. anotherObject.a; // 2
  6. myObject.a; // 2
  7. anotherObject.hasOwnProperty( "a" ); // true
  8. myObject.hasOwnProperty( "a" ); // false
  9. myObject.a++; // 这里就产生了隐式的遮蔽,为myObject产生了一个新的属性 并且赋值++
  10. anotherObject.a; // 2
  11. myObject.a; // 3
  12. myObject.hasOwnProperty( "a" ); // true

“类”

在所有语言中,JavaScript 几乎是独一无二的,也许是唯一的可以被称为“面向对象”的语言,因为可以根本没有类而直接创建对象的语言很少,而 JavaScript 就是其中之一。JavaScript 只有 对象

  1. function Foo() {
  2. // ...
  3. }
  4. var a = new Foo();
  5. Object.getPrototypeOf( a ) === Foo.prototype; // true

当通过调用 new Foo() 创建 a 时,会发生的事情之一是,a 得到一个内部 [[Prototype]] 链接,此链接链到 Foo.prototype 所指向的对象。

在 JavaScript 中,我们不从一个对象(“类”)向另一个对象(“实例”) 拷贝。我们在对象之间制造 链接。对于 [[Prototype]] 机制,视觉上,箭头的移动方向是从右至左,由下至上。
image.png

这种机制常被称为“原型继承(prototypal inheritance)”

“继承”意味着 拷贝 操作,而 JavaScript 不拷贝对象属性(原生上,默认地)。相反,JS 在两个对象间建立链接,一个对象实质上可以将对属性/函数的访问 委托 到另一个对象上。对于描述 JavaScript 对象链接机制来说,“委托”是一个准确得多的术语

“构造器”(Constructors)

  1. function Foo() {
  2. // ...
  3. }
  4. Foo.prototype.constructor === Foo; // true
  5. var a = new Foo();
  6. a.constructor === Foo; // true

Foo.prototype 对象默认地得到一个公有的,称为 .constructor的不可枚举属性,而且这个属性回头指向这个对象关联的函数(这里是 Foo)。另外,我们看到被“构造器”调用 new Foo() 创建的对象 a 看起来 也拥有一个称为 .constructor 的属性,也相似地指向“创建它的函数”

Foo 不会比你的程序中的其他任何函数“更像构造器”。函数自身 不是 构造器。但是,当你在普通函数调用前面放一个 new 关键字时,这就将函数调用变成了“构造器调用”。事实上,new 在某种意义上劫持了普通函数并将它以另一种方式调用:构建一个对象,外加这个函数要做的其他任何事(也就是执行函数体)

!!!“构造器”是在前面
new 关键字调用的任何函数~!

函数不是构造器,但是当且仅当 new 被使用时,函数调用是一个“构造器调用”。

原型继承

有这样一段原型继承示例代码:

  1. function Foo(name) {
  2. this.name = name; //注意这里的this代表是什么?? -->a : new对象会使得this绑定到新的对象上
  3. }
  4. Foo.prototype.myName = function() {
  5. return this.name;
  6. };
  7. function Bar(name,label) {
  8. Foo.call( this, name );
  9. this.label = label;
  10. }
  11. // 这里,我们创建一个新的 `Bar.prototype` 链接链到 `Foo.prototype`
  12. // 而Bar.prototype = Foo.prototype 不会创建新对象让 Bar.prototype 链接
  13. // 它只是让 Bar.prototype 成为 Foo.prototype 的另一个引用,
  14. Bar.prototype = Object.create( Foo.prototype );
  15. // 如果你有依赖这个属性的习惯的话,它可以被手动“修复”。
  16. Bar.prototype.myLabel = function() {
  17. return this.label;
  18. };
  19. var a = new Bar( "a", "obj a" );
  20. a.myName(); // "a"
  21. a.myLabel(); // "obj a"

比较一下 ES6 之前和 ES6 标准的技术如何处理将 Bar.prototype 链接至 Foo.prototype

  1. // ES6 以前
  2. // 扔掉默认既存的 `Bar.prototype`
  3. Bar.prototype = Object.create( Foo.prototype );
  4. // ES6+
  5. // 修改既存的 `Bar.prototype`
  6. Object.setPrototypeOf( Bar.prototype, Foo.prototype );

考察“类”关系

如果你有一个对象 a 并且希望找到它委托至哪个对象呢(如果有的话)?考察一个实例(一个 JS 对象)的继承血统(在 JS 中是委托链接)

function Foo() {
  // ...
}

Foo.prototype.blah = ...;

var a = new Foo();

我们如何自省 a 来找到它的“祖先”(委托链)呢?
a instanceof Foo; // true

instanceof 操作符的左侧操作数接收一个普通对象,右侧操作数接收一个 函数instanceof 表达的意思是:a 的整个 [[Prototype]] 链中,有没有出现那个被 Foo.prototype 所随便指向的对象

我们 只需要 两个
对象** 来考察它们之间的关系。比如:

// 简单地:`b` 在 `c` 的 `[[Prototype]]` 链中出现过吗?
b.isPrototypeOf( c );

注意,这种方法根本不要求有一个函数(“类”)。它仅仅使用对象的直接引用 bc,来查询他们的关系。换句话说,我们上面的 isRelatedTo(..) 工具是内建在语言中的,它的名字叫 isPrototypeOf(..)

.__proto__ 虽然看起来像一个属性,但实际上将它看做是一个 getter/setter(见第三章)更合适。

大致地,我们可以这样描述 .__proto__ 的实现(见第三章,对象属性的定义):

Object.defineProperty( Object.prototype, "__proto__", {
    get: function() {
        return Object.getPrototypeOf( this );
    },
    set: function(o) {
        // ES6 的 setPrototypeOf(..)
        Object.setPrototypeOf( this, o );
        return o;
    }
} );

**__proto__** 读作“dunder proto”

创建链接

var foo = {
    something: function() {
        console.log( "Tell me something good..." );
    }
};

var bar = Object.create( foo );

bar.something(); // Tell me something good...

Object.create(..) 创建了一个链接到我们指定的对象(foo)上的新对象(bar),这给了我们 [[Prototype]] 机制的所有力量(委托),而且没有 new 函数作为类和构造器调用产生的所有没必要的复杂性,搞乱 .prototype.constructor 引用,或任何其他的多余的东西

注意: Object.create(null) 创建一个拥有空(也就是 null[[Prototype]] 链接的对象,如此这个对象不能委托到任何地方。因为这样的对象没有原形链,instancof 操作符(前 面解释过)没有东西可检查,所以它总返回 false。由于他们典型的用途是在属性中存储数据,这种特殊的空 [[Prototype]] 对象经常被称为“字典(dictionaries)”,这主要是因为它们不可能受到在 [[Prototype]] 链上任何委托属性/函数的影响,所以它们是纯粹的扁平数据存储

myObject 上没有 cool() 方法时调用 myObject.cool()也能工作的一个比较好的方案就是采用原型链接的方式:

var anotherObject = {
    cool: function() {
        console.log( "cool!" );
    }
};

//创建对象myObject链接到对象anotherObject
var myObject = Object.create( anotherObject );

myObject.doCool = function() {
    this.cool(); // internal delegation!
};

myObject.doCool(); // "cool!"

这里,我们调用 myObject.doCool(),它是一个 实际存在于 myObject 上的方法,这使我们的 API 设计更清晰(没那么“魔性”)。在它内部,我们的实现依照 委托设计模式,利用 [[Prototype]] 委托到 anotherObject.cool()

虽然这些 JavaScript 机制看起来和传统面向类语言的“初始化类”和“类继承”类似,而在 JavaScript 中的关键区别是,没有拷贝发生。取而代之的是对象最终通过 [[Prototype]] 链链接在一起。