Prototype
JS中的对象有一个特殊的Prototype内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时Prototype属性都会被赋予一个非空的值。
Prototype引用有什么用呢?
当你试图引用对象的属性时会触发Get操作,如果无法在对象本身找到需要的属性,就会继续访问对象的Prototype链,这个过程会持续到找到匹配的属性名或者查找完整条[[Prototype]]链。如果是后者的话,[[Get]]操作的返回值是undefined。
使用for..in遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到的属性都会被枚举。使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)
Object.prototype
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype
。所以它包含JavaScript中许多通用的功能。比如说.toString()和.valueOf(),hasOwnProperty(..)。
属性设置和屏蔽
给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。
myObject.foo = "bar";
如果myObject对象中包含名为foo的普通数据访问属性,这条赋值语句只会修改已有的属性值。
如果foo不是直接存在于myObject中,[[Prototype]]链就会被遍历,类似[[Get]]操作。
如果原型链上找不到foo,foo就会被直接添加到myObject上。
如果属性名foo既出现在myObject中也出现在myObject的[[Prototype]]链上层,那么就会发生屏蔽。myObject中包含的foo属性会屏蔽原型链上层的所有foo属性,因为myObject.foo总是会选择原型链中最底层的foo属性。
下面我们分析一下如果foo不直接存在于myObject中而是存在于原型链上层时myObject.foo = "bar"会出现的三种情况。
- 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性。(只有这个会发生屏蔽)
- 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在myObject上创建屏蔽属性。如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
- 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setter。foo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这个setter。
如果你希望在第二种和第三种情况下也屏蔽foo,那就不能使用=操作符来赋值,而是使用Object.defineProperty(..)来向myObject添加foo。
隐式产生屏蔽
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++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty("a"); // true
++操作相当于myObject.a = myObject.a + 1。因此++操作首先会通过Prototype查找属性a并从anotherObject.a获取属性值2,然后给这个值加1,接着用Put将值赋给中新建的屏蔽属性a
修改委托属性时一定要小心。如果想让anotherObject.a的值增加,唯一的办法是 anotherObject.a++。
类
JS和面向类的语言不同,他并没有类来作为对象的抽象模式。JS中只有对象。
实际上,JS才真正应该被称为“面向对象”的语言,因为他是少有的可以不通过类,直接创建对象的语言。
在JS中根本不存在类!对象直接定义自己的行为。
类函数
JS一直有一种奇怪的行为:模仿类
这种“类似类”的行为利用了函数的一种特殊特性:
所有函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,他会指向另一个对象。
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
调用new Foo()时会创建a,其中一步就是将a内部的prototype链接到Foo.prototype所指向的对象。
最后我们得到了两个对象,他们相互关联。我们并没有初始化一个类,实际上我们并没有从类中复制任何行为到一个对象中,只是让两个对象相互关联。
在面向类的语言中,实例化(或者继承)一个类就意味着“把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。
但是在JavaScript中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。
实际上,绝大多数JavaScript开发者不知道的秘密是,new Foo()这个函数调用实际上并没有直接创建关联,这个关联只是一个意外的副作用。new Foo()只是间接完成了我们的目标:一个关联到其他对象的新对象。
原型继承(不准确)
在JavaScript中,我们并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将它们关联起来。
这个机制通常被称为原型继承。他常常被视为动态语言版本的类继承。但是违背了动态脚本中对应的语义。
容易混淆的组合术语“原型继承”(以及使用其他面向类的术语比如“类”、“构造函数”、“实例”、“多态”,等等)严重影响了大家对于JavaScript机制真实原理的理解。
委托
继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript会在两
个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。委托这个术语可以更加准确地描述JavaScript中对象的关联机制。
构造函数
function Foo() {
// ...
}
var a = new Foo();
到底是什么让我们认为Foo是一个“类”呢?其中一个原因是我们看到了关键字new。看起来我们执行了类的构造函数方法,Foo()的调用方式很像初始化类时类构造函数的调用方式。
Foo.prototype还有另一个绝招。
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype默认(在代码中第一行声明时!)有一个公有并且不可枚举的属性.constructor
,这个属性引用的是对象关联的函数(本例中是Foo)。此外,我们可以看到通过“构造函数”调用new Foo()创建的对象a也有一个a.constructor
属性,指向“创建这个对象的函数”。
当你在普通的函数调用前面加上new关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。
function NothingSpecial() {
console.log( "Don't mind me!" );
}
var a = new NothingSpecial();
// "Don't mind me!"
a; // {}
NothingSpecial
只是一个普通的函数,但是使用new调用时,它就会构造一个对象并赋值给a,这看起来像是new的一个副作用(无论如何都会构造一个对象)。但是NothingSpecial本身并不是一个构造函数。
在JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。
函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。
回顾“构造函数”
之前讨论.constructor属性时我们说过,看起来a.constructor === Foo为真意味着a确实有一个指向Foo的.constructor属性,但是事实不是这样。
把.constructor属性指向Foo看作是a对象由Foo“构造”非常容易理解,但这只不过是一种虚假的安全感。a.constructor只是通过默认的[[Prototype]]委托指向Foo,这和构造”毫无关系。相反,对于.constructor的错误理解很容易对你自己产生误导。
举例来说,Foo.prototype的.constructor属性只是Foo函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的.prototype对象引用,那么新对象并不会自动获得.constructor属性。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象
var a1 = new Foo(); //Object(..)并没有“构造”a1
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
Object(..)并没有“构造”a1,对吧?看起来应该是Foo()“构造”了它。如果你认为“constructor”表示“由……构造”的话,a1.constructor应该是Foo,但是它并不是Foo!
a1并没有.constructor属性,所以它会委托[[Prototype]]链上的Foo.prototype。但是这个对象也没有.constructor属性(不过默认的Foo.prototype对象有这个属性!),所以它会继续委托,这次会委托给委托链顶端的Object.prototype。这个对象有.constructor属性,指向内置的Object(..)函数。
constructor并不表示被构造
实际上,对象的.constructor属性会默认指向一个函数,这个函数也有一个叫做.prototype的引用指向这个对象。“constructor”和“prototype”这两个词本身的含义可能适用也可能不适用。最好的办法是记住这一点“constructor并不表示(对象)被(这个函数)构造”
a1.constructor是一个非常不可靠并且不安全的引用。通常来说要尽量避免使用这些引用。
(原型)继承
function Foo(name) {
this.name = name; //调用位置:Foo.call(this, name); this绑定:2.call绑
定,this为 指定的对象,这里的指定对象为 Bar 对象
}
Foo.prototype.myName = function () {
return this.name; //调用位置:a.myName(); this绑定:3.上下文对象:a
};
function Bar(name, label) {
Foo.call(this, name); //调用位置:new Bar("a", "obj a"); this绑定:1.new绑定,
this为 Bar 对象
this.label = label;
}
// 我们创建了一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
// 注意!现在没有Bar.prototype.constructor了
// 如果你需要这个属性的话可能需要手动修复一下它
Bar.prototype.myLabel = function () {
return this.label; //调用位置:a.myLabel(); this绑定:3.上下文对象:a
};
var a = new Bar("a", "obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"
这段代码的核心部分就是语句Bar.prototype = Object.create( Foo.prototype )。调用Object.create(..)会凭空创建一个“新”对象并把新对象内部的[[Prototype]]关联到你指定的对象(本例中是Foo.prototype)。
换句话说,这条语句的意思是:“创建一个新的Bar.prototype对象并把它关联到Foo.prototype”。
注意,下面这两种方式是常见的错误做法,实际上它们都存在一些问题:
// 和你想要的机制不一样!
Bar.prototype = Foo.prototype;//
// 基本上满足你的需求,但是可能会产生一些副作用 :( 上面这里会多个name:undefined
Bar.prototype = new Foo();
Bar.prototype = Foo.prototype 只是让Bar.prototype直接引用Foo.prototype对象。因此执行如Bar.prototype.mylabel = ...的赋值语句会直接修改Foo.prototype对象本身。
Bar.prototype = new Foo() 会创建关联到Bar.prototype的新对象,但它使用的是构造函数调用,如果函数Foo有一些副作用(如写日志、修改状态、注册到其他对象、给this添加数据属性等),就会影响到Bar()的“后代”。
Object.create(..)
要创建一个合适的关联对象,我们必须使用Object.create(..)而不是具有副作用的Foo()。这样做唯一的缺点是需要创建一个新对象然后把旧对象抛弃掉(垃圾回收机制),不能直接修改已有的默认对象。
Object.setPrototypeOf(..)
如果能有一个标准并且可靠的方法来修改对象的Prototype关联就好了。
在ES6之前,我们只能通过设置.__prototype__属性来实现,但这个方法并不是标准并且无法兼容所有浏览器。
ES6添加了辅助函数Object.setPrototypeOf(..),来修改关联。
修改对象的[[prototype]]关联方法:
//ES6之前需要抛弃默认的Bar.prototype Bar.prototype = Object.create(Foo.prototype); // ES6开始可以直接修改现有的Bar.prototype Object.setPrototypeOf(Bar.prototype,Foo.prototype)
检查“类”的关系
假设有对象a,如何寻找对象a委托的对象(如果存在的话)呢?
检查一个实例(JavaScript中的对象)的继承祖先(JavaScript中的委托关联)通常被称为内省(或者反射)。
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
方法1.站在“类”的角度来判断:
a instanceof Foo; // true
instanceof:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象?
这个方法只能处理对象(a)和函数(带.prototype引用的Foo)之间的关系。如果想判断两个对象之间是否通过[[Prototype]]链关联,只用instanceof无法实现。
方法2 isPrototypeOf()
Foo.prototype.isPrototypeOf( a ); // true
在本例中,我们实际上并不关心(甚至不需要)Foo,我们只需要一个可以用来判断的对象(本例中是Foo.prototype)就行。
isPrototypeOf(..):在a的整条[[Prototype]]链中是否出现过Foo.prototype?
同样的问题,同样的答案,但是第二种方法并不需要间接引用函数,他的.prototype属性会被自动访问。
我们只需要两个对象就可以判断它们之间的关系。举例来说:
// 非常简单:b是否出现在c的[[Prototype]]链中?
b.isPrototypeOf( c );
我们也可以直接获取一个对象的[[Prototype]]链。在ES5中,标准的方法是:
Object.getPrototypeOf( a );
Object.getPrototypeOf( a ) === Foo.prototype; // true
绝大多数(不是所有!)浏览器也支持一种非标准的方法来访问内部[[Prototype]]属性:
a.__proto__ === Foo.prototype; // true
这个奇怪的.__proto__
(在ES6之前并不是标准!)属性“神奇地”引用了内部的[[Prototype]]对象,如果你想直接查找(甚至可以通过.__proto__.__ptoto__
...来遍历)原型链的话,这个方法非常有用。
和我们之前说过的.constructor一样,.__proto__
实际上并不存在于你正在使用的对象中(本例中是a)。实际上,它和其他的常用函数(.toString()、.isPrototypeOf(..),等等)样,存在于内置的Object.prototype中。(它们是不可枚举的,参见第2章。)此外,.__proto__
看起来很像一个属性,但是实际上它更像一个getter/setter。
.__proto__
是可设置属性,之前的代码中使用ES6的Object.setPrototypeOf(..)进行设置。然而,通常来说你不需要修改已有对象的[[Prototype]]。
我们只有在一些特殊情况下(我们前面讨论过)需要设置函数默认.prototype对象的[[Prototype]],让它引用其他对象(除了Object.prototype)。这样可以避免使用全新的对象替换默认对象。此外,最好把[[Prototype]]对象关联看作是只读特性,从而增加代码的可读性。
对象关联
现在我们知道了,[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。
原型链:通常来说,这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
创建关联
那[[Prototype]]机制的意义是什么呢?
本章前面曾经说过Object.create(..)是一个大英雄,现在是时候来弄明白为什么了:
var foo = {
something: function() {
console.log( "Tell me something good..." );
}
};
var bar = Object.create( foo );
bar.something(); // Tell me something good...
Object.create(..)会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样我们就可以充分发挥[[Prototype]]机制的威力(委托)并且避免不必要的麻烦(比如使用new的构造函数调用会生成.prototype和.constructor引用)。
Object.create(null)会创建一个拥有空(null)[[Prototype]]连接的对象,这个对象无法进行委托,由于这个对象没有原型链。所以instanceof操作符无法进行判断,因此总会返回false。这些特殊的空[[Prototype]]对象通常被称为“字典”,他们完全不会受到原型链的干扰,非常适合用来存储数据。
我们并不需要类来创建两个对象之间的关系,只需要通过委托来关联对象就足够了。而Object.create(..)不包含任何“类的诡计”,所以它可以完美地创建我们想要的关联关系。
关联关系是必备的
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"
但是如果你这样写只是为了让myObject在无法处理属性或者方法时可以使用备用的anotherObject,那么你的软件就会变得有点“神奇”,而且很难理解和维护。
当你给开发者设计软件时,假设要调用myObject.cool(),如果myObject中不存在cool()时这条语句也可以正常工作的话,那你的API设计就会变得很“神奇”,对于未来维护你软件的开发者来说这可能不太好理解。
但是你可以让你的API设计不那么“神奇”,同时仍然能发挥[[Prototype]]关联的威力:
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // 内部委托! 这里this绑定myObject
};
myObject.doCool(); // "cool!"
这里我们调用的myObject.doCool()是实际存在于myObject中的,这可以让我们的API设计更加清晰(不那么“神奇”)。从内部来说,我们的实现遵循的是委托设计模式(参见第6章),通过[[Prototype]]委托到anotherObject.cool()。
内部委托比起直接委托可以让API接口设计更加清晰。