[[Prototype]]
JavaScript 中的对象有一个内部属性,在语言规范中称为 [[Prototype]]
,它只是一个其他对象的引用。几乎所有的对象在被创建时,它的这个属性都被赋予了一个非 null
值
var anotherObject = {
a: 2
};
// 创建一个链接到 `anotherObject` 的对象
var myObject = Object.create( anotherObject );
myObject.a; // 2
现在让 myObject
[[Prototype]]
链到了 anotherObject
。虽然很明显 myObject.a
实际上不存在,但是无论如何属性访问成功了(在 anotherObject
中找到了),而且确实找到了值 2
。
Object.prototype
每个 普通 的 [[Prototype]]
链的最顶端,是内建的 Object.prototype
。这个对象包含各种在整个 JS 中被使用的共通工具,因为 JavaScript 中所有普通(内建,而非被宿主环境扩展的)的对象都“衍生自”Object.prototype
对象
设置与遮蔽属性
myObject.foo = "bar";
以下来 考察 myObject.foo = "bar"
赋值的三种场景,当 foo
不直接存在 于 myObject
,但 存在 于 myObject
的 [[Prototype]]
链的更高层时:
- 如果一个普通的名为
foo
的数据访问属性在[[Prototype]]
链的高层某处被找到,而且没有被标记为只读(writable:false
),那么一个名为foo
的新属性(也就是代表新添加)就直接添加到myObject
上,形成一个 遮蔽属性。 - 如果一个
foo
在[[Prototype]]
链的高层某处被找到,但是它被标记为 只读(writable:false
) ,那么设置既存属性和在myObject
上创建遮蔽属性都是 不允许 的。如果代码运行在strict mode
下,一个错误会被抛出。否则,这个设置属性值的操作会被无声地忽略。不论怎样,没有发生遮蔽。 - 如果一个
foo
在[[Prototype]]
链的高层某处被找到,而且它是一个 setter(也就是set 属性名()的特殊函数),那么这个 setter 总是被调用。没有foo
会被添加到(也就是遮蔽在)myObject
上,这个foo
setter 也不会被重定义。
案例如下:
var anotherObject = {
a: 2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 这里就产生了隐式的遮蔽,为myObject产生了一个新的属性 并且赋值++
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
“类”
在所有语言中,JavaScript 几乎是独一无二的,也许是唯一的可以被称为“面向对象”的语言,因为可以根本没有类而直接创建对象的语言很少,而 JavaScript 就是其中之一。JavaScript 只有 对象
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
当通过调用 new Foo()
创建 a
时,会发生的事情之一是,a
得到一个内部 [[Prototype]]
链接,此链接链到 Foo.prototype
所指向的对象。
在 JavaScript 中,我们不从一个对象(“类”)向另一个对象(“实例”) 拷贝。我们在对象之间制造 链接。对于 [[Prototype]]
机制,视觉上,箭头的移动方向是从右至左,由下至上。
这种机制常被称为“原型继承(prototypal inheritance)”
“继承”意味着 拷贝 操作,而 JavaScript 不拷贝对象属性(原生上,默认地)。相反,JS 在两个对象间建立链接,一个对象实质上可以将对属性/函数的访问 委托 到另一个对象上。对于描述 JavaScript 对象链接机制来说,“委托”是一个准确得多的术语
“构造器”(Constructors)
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype
对象默认地得到一个公有的,称为 .constructor
的不可枚举属性,而且这个属性回头指向这个对象关联的函数(这里是 Foo
)。另外,我们看到被“构造器”调用 new Foo()
创建的对象 a
看起来 也拥有一个称为 .constructor
的属性,也相似地指向“创建它的函数”
Foo
不会比你的程序中的其他任何函数“更像构造器”。函数自身 不是 构造器。但是,当你在普通函数调用前面放一个 new
关键字时,这就将函数调用变成了“构造器调用”。事实上,new
在某种意义上劫持了普通函数并将它以另一种方式调用:构建一个对象,外加这个函数要做的其他任何事(也就是执行函数体)
!!!“构造器”是在前面 用 new
关键字调用的任何函数~!
函数不是构造器,但是当且仅当 new
被使用时,函数调用是一个“构造器调用”。
原型继承
有这样一段原型继承示例代码:
function Foo(name) {
this.name = name; //注意这里的this代表是什么?? -->a : new对象会使得this绑定到新的对象上
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// 这里,我们创建一个新的 `Bar.prototype` 链接链到 `Foo.prototype`
// 而Bar.prototype = Foo.prototype 不会创建新对象让 Bar.prototype 链接
// 它只是让 Bar.prototype 成为 Foo.prototype 的另一个引用,
Bar.prototype = Object.create( Foo.prototype );
// 如果你有依赖这个属性的习惯的话,它可以被手动“修复”。
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
比较一下 ES6 之前和 ES6 标准的技术如何处理将 Bar.prototype
链接至 Foo.prototype
:
// ES6 以前
// 扔掉默认既存的 `Bar.prototype`
Bar.prototype = Object.create( Foo.prototype );
// ES6+
// 修改既存的 `Bar.prototype`
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 );
注意,这种方法根本不要求有一个函数(“类”)。它仅仅使用对象的直接引用 b
和 c
,来查询他们的关系。换句话说,我们上面的 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]]
链链接在一起。