第5章 :原型
5.1 [[Prototype]]
JavaScript的对象都有一个
[[Prototype]]
内置属性,它是一个对象的引用。对象在创建时[[Prototype]]
属性就会被赋予值。
//举例:创建一个myObj对象,然后读取其属性值var myObj = { a : 2}; myObj.a; // 2
之前说过,当访问对象的属性时,会触发
[[Get]]
操作。[[Get]]
操作第一步是检查对象本身是否有这个属性,如果有就使用;但是如果没有这个属性,就需要使用对象的[[Prototype]]
链了。
var anotherObj = { a : 2}// Object.create()方法是根据传入的对象,创建一个新对象,并且原型链上关联到传入的对象var myObj = Object.create(anotherObj); myObj.a; // 2
实际上,所有访问属性的操作,都会查询
[[Prototype]]
链,包括for..in
遍历、in
操作等。
5.1.1 object.prototype
所有的
[[Prototype]]
的尽头都执行内置的object.prototype
对象,导致所有的普通对象都包含Object.prototype
对象的许多通用的功能,比如toString()
、valueOf()
、hasOwnProperty()
、isPrototypeOf()
等方法。
5.1.2 属性设置和屏蔽
myObj.foo = 'bar';
当通过.操作符访问对象进行属性赋值/属性追加时,有如下几种情况:
如果在
[[Prototype]]
链上也不包含该属性,属性会被直接添加到当前对象myObj
上;如果在
[[Prototype]]
链上存在该属性:
且属性非只读(
writable:false
),那么在当前对象上添加一个名为foo
的新属性,系统视它为继承了原型对象的屏蔽属性;但属性是只读(
writable:true
),那么将无法修改属性,也无法创建新属性。允许在严格模式下,代码会抛出一个错误;非严格模式下,这条赋值语句会被忽略;且属性是个
setter
,那就一定会调用这个setter
,属性的赋值屏蔽于myObj
如果当前对象中包含该属性,则修改当前对象的属性值(此时即便对象的原型链上包含同名的属性,根据就近原则,也会被当前对象的属性值屏蔽)
如果当前对象不包含该属性,就会遍历查找对象的原型链是否有该属性:
值得注意的是,
++
操作符会造成隐式屏蔽
var anotherObj = { a : 2}//创建一个原型关联anotherObj的新对象var myObj = Object.create(anotherObject);//不管子对象还是父对象,a属性值都输出2anotherObj.a; // 2myObjt.a; // 2//父对象中拥有a,而子对象中不拥有aanotberObj.hasOwnProperty('a'); // truemyObj.hasOwnProperty('a'); //false//执行++操作,造成隐式屏蔽myObj.a++;//再看发现子类自己拥有了a属性,屏蔽了原型链上的同名属性anotherObj.a; // 2myObj.a; // 3myObj.hasOwnProperty('a'); // ture
5.2 “类”
如第4章提到的,JavaScript中没有类。面向类的语言需要通过类作为对象的蓝图来创建对象,而JavaScript是直接创建对象的。实际上JavaScript才是真正应该被称为“面向对象”的语言。
5.2.1 “类”函数
function Foo(){ // ...}var a = new Foo();Object.getPrototypeOf(a) === Foo.prototype; // ture
如上面代码所示,
new Foo()
会生成一个新对象a
,新对象内部的[[Prototype]]
链关联到了Foo.prototype
原型对象。这里可以对比其他面向类的语言中,类可以被实例化多次,就像用模具制作东西一样,而实例化就意味着“把类的行为复制到物理对象中”,每一个新实例都会重复这个过程。但JavaScript没有类似的复制机制,只是通过
[[Prototype]]
原型链将对象关联起来。
关于名称
这个通过
[[Prototype]]
将对象关联起来的机制被称为“ 原型继承 ”但原型继承这个术语比较容易造成混淆,影响大家对JavaScript机制真实原理的理解。毕竟继承意味着复制操作,但JavaScript并不会复制对象属性。而是在对象之间穿件一个关联,对象通过委托访问另一个对象的属性和函数。委托 这个术语更能准确的描述JavaScript中对象的关联机制。
差异继承 就是指在描述对象行为时,不描述普遍的特质。比如,描述汽车时,我们更多会说“汽车是一个有四个轮子的交通工具”,但不会说“汽车是拥有引擎发动机(通用特质)的交通工具”
5.2.2 “构造函数”
之所以让我们认为
Foo
是一个“类”,一个原因是关键字new
,在面向类的语言中构造类实例时也会用到new
;另一个原因是,Foo()
的调用看起来像是执行了类的构造函数方法。
function Foo(){ // ...} Foo.prototype.constructor === Foo; // truevar a = new Foo(); a.constructor === Foo; // true
可见
Foo.prototype.constructor
属性和Foo
函数相等,创建的a
对象的constructor
属性,和Foo
函数也相等。凡此种种,我们很容易就误认为
Foo()
是一个构造函数。但实际上,Foo()
本身并不是构造函数,和其他普通的函数没有任何区别。只是,在函数调用的前面加上new
关键字后,变成一种“构造函数的调用方式”。
function NothingSpecial(){ console.log("Don't mind me"); }var a = new NothingSpecial(); // Don't mind mea; // {}
如上述代码所示,
NothingSpecial
只是一个普通的函数,使用new
调用时,它会构造一个对象并赋值给变量a
。对于“构造函数”更准确的解释,应该是“带有new的函数调用”。
5.2.3 技术
//定义一个Foo函数,函数里将this.name属性重新赋值function Foo(name){ this.name = name; }//为Foo.prototype原型对象定义一个myName()方法,返回name值Foo.prototype.myName = function(){ return this.name; }//通过new的方式调用Foo()函数,返回a、b两个对象var a = new Foo('a');var b = new Foo('b');//调用a、b对象的myName()方法a.myName(); // 'a'b.myName(); // 'b'
看这段代码,调用了
new Foo()
后,创建的a
和b
对象都拥有了myName()
方法。看起来像是在创建
a
和b
时,把Foo.prototype
原型对象复制到两个对象中,但事实上并不是。正如本章前文介绍
[[Get]]
算法时提到的,当访问对象的属性不存在时,会通过其的[[[Prototype]]
原型链来查找。因此,在创建的过程中,a
和b
的[[Prototype]]
原型对象会关联到Foo.prototype
上。当访问a.myName
时会通过原型链,委托关联到Foo.prototype
回顾“构造函数”
之前讨论
.constructor
属性时说过,虽然a.constructor === Foo
为true
,看起来a
的构造函数(constructor
)就是Foo()
函数,但事实不是这样的。就如同myName
属性一样,a
对象本身并无.constructor
属性,查找的其实是Foo.prototype
原型对象的myName
属性(也可以说,a.constructor
被委托给了Foo.prototype
)。而Foo.prototype
本身的.constructor
属性,是在Foo
函数声明时的默认属性。我们可以做个试验来验证这一点:
//定义Foo函数function Foo(){};//将Foo的原型对象赋值为空Foo.prototype = {};//创建对象avar a = new Foo();//测试console.log(a.constructor === Foo); // falseconsole.log(a.constructor === Object); // true
a.constructor
属性本来委托给Foo.prototype
,但我们已经把Foo.prototype
重新赋值为空,所以她继续委托,直至最顶端的Object.prototype
。总之,对象的
.constructor
属性并不表示构造对象的函数。它只是一个不可变、且不可枚举,但可以被修改的属性,所以.constructor
属性是不可靠且不安全的引用。
5.3 原型继承
Object.create()
方法会创建一个新对象,并把对象内部的[[Prototype]]
关联到指定的对象,但缺点是通过创建一个新对象替代旧对象,而不是直接修改已有对象。
/** * ---------------------- * 【示例:继承JS类并进行实例化】 * 西瓜继承自水果,水果拥有type(食物类型)属性,西瓜拥有name(水果名称)属性 * ---------------------- *///声明Fruit对象,并定义getType()方法function Fruit(type){ this.type = type; } Fruit.prototype.getType = function(){ return this.type; }//声明WateMelon对象,继承Fruit,并拥有getName()方法function WateMelon(type,name){ Fruit.call(this,type); this.name = name; }//通过Object.create()来继承WateMelon.prototype = Object.create(Fruit.prototype); WateMelon.prototype.getName = function(){ return this.name; }//创建实例var watemelon = new WateMelon('水果','西瓜');console.log('食物品种:',watemelon.getType());console.log('食物名称:',watemelon.getName());
想要直接修改对象的
[[Prototype]]
关联,在ES6之前,只能通过.__proto__
属性来设置,但这个属性不能兼容所有浏览器,ES6添加了Object.setPrototypeOf()
方法来实现。
Object.setPrototypeOf(WateMelon.prototype,Fruit.prototype);
检查“类”关系
我们如何知道
watemelon
的委托对象是WateMelon
呢?在传统的面向类环境中,检查实例对象的继承祖先通常被称为内省或者反射。第一种方法是站在“类函数”的角度来判断,通过
instanceof
操作符判断实例对象是否属于某个类函数:
watemelon instanceof Watemelon; // truewatemelon instanceof Fruit; // true
instanceof
操作符的左边是一个实例对象watemelon
,右边是一个函数。instanceof
会检查在watemelon
的原型链中,是否有指向Watemelon
或者Fruit
的原型对象。
注意:
instanceof
判断的是实例对象和类的原型关联,而如果想判断两个实例对象之间是否有原型关联,则instanceof
无法实现。
第二种方法是通过
.isPrototypeOf()
方法,判断原型对象是否和指定的实例对象有关系:
WateMelon.prototype.isPrototypeOf(watemelon);
isPrototypeOf()
是基类Object
的方法,判断watemelon的原型对象是不是Watemelon。
注意:一定是要用原型对象调用
isPrototypeOf()
方法来做判断,如果用类函数(WateMelon
)来调用,则返回false
。
除此之外,还可以通过
Object.getPrototypeOf(watemelon)
来获取实例对象的原型链。
Object.getPrototypeOf(watemelon) === WateMelon.prototype; // true
5.4 对象关联
5.4.1 创建关联
我们推荐用
create()
方法创建新对象,它可以充分发挥[[Prototype]]
机制的威力,而是用new构造函数调用的方式,通过类来创建两个对象之间的关系,避免了一些不必要的麻烦,比如.prototype
和.constructor
属性引用的问题。由于
Object.create()
方法是ES5新增的函数,如果想要在ES5之前的环境使用,可以通过简单的polyfill代码来处理:
if(!Object.create){ Object.crate = function(o){ //通过一个临时的F类,建立指定原型关系 function F(){}; F.prototype = o; return new F(); } }
5.4.2 关联关系是备用
通过
[[Prototype]]
原型机制可以为对象之间建立关联关系,处理当对象属性或方法“缺失”时,可以提供一个备用选项。
var a = { echo : function(){ console.log('hello'); } };var b = Object.create(a); b.echo(); // 'hello'
但从某种程度上来看,代码却变得难以理解和维护,
b
明明没有定义echo()
方法,这东西是哪来的?当然,我们可以通过 内部委托 的方式让API的设计更加清晰:
var a = { echo : function(){ console.log('hello'); } };var b = Object.create(a); b.doEcho = function(){ this.echo(); }; b.doEcho(); // 'hello'
5.5 小结
当访问对象中并不存在的属性时,
[[Get]]
操作会查找对象内部[[Prototype]]
关联的对象。这个关联关系,实际上定义了一条“原型链”。原型链的顶端是
Object.prototype
,toString()
、valueOf()
和其他通用的功能都挂载在该对象上。关联两个对象最常用的方式是用
new
关键字进行函数调用,通常我们称为“构造函数调用”。虽然JavaScript的
new
机制看起来,和传统面向类语言的“类初始化”和“类继承”很相似,但是有一个很重要的区别,就是不会进行复制,对象之间是通过内部的[[Prototype]]
链关联的。出于各种原因,相比起术语“原型继承”,“委托”更适合。因为对象之间的关系不是复制,而是委托。
作者:梁同学de自言自语
链接:https://www.jianshu.com/p/11c894d15130