在Javascript对象有一个特殊的prototype内置属性,它其实就是对于其他对象的引用。
当我们试图访问一个对象下的某个属性的时候,会在JS引擎触发一个GET的操作,首先会查找这个对象是否存在这个属性,如果没有找的话,则继续在prototype关联的对象上查找,以此类推。如果在后者上也没有找到的话,继续查找的prototype,这一系列的链接就被称为原型链。
在javascript中对象都是可以使用tostring(),valueOf()方法,函数都是可以使用call(),apply(),因为普通对象都是通过prototype链最终指向了内置的Object.prototype,而函数都是指向了Function.prototype。
基于prototype机制实现"继承"
在javascipt中实现继承背后是依靠prototype机制的,javascript与其他面向类的语言不同,它是没有类作为对象的抽象模式去实现继承(在ES6中是有class这个关键字的,也是prototype机制的一种语法糖),但是我们可以在javascript中写出模仿类的代码,这里来看看常见的构造函数模式+原型模式:
function Animal(name,color){ this.name = name; this.color = color; } Animal.prototype.type = "动物"; Animal.prototype.msg = function(){ alert( "I am "+this.name+" my color is "+this.color); } var cat = new Animal("cat","black"); console.log(cat.name); //cat console.log(cat.type); //动物 console.log(cat.constructor === Animal); //true
函数Animal就可以看做是一个'类',里面定义了name和color属性,主要看下划线的代码,cat是通过new关键字将Animal实现“构造函数调用”产生的新对象,并传入两个参数。
一般我们通过new来调用函数,会发生以下的操作:
1、创建一个全新的对象;
2、这个新对象会被执行prototyp链接;
3、这个新对象会被绑定函数调用的this上;
4、如果函数没有返回对象,那么new表达式中的函数调用会自动返回这个新对象
可以看到,使用getPrototypeOf获取cat对象的原型是全等于Animal.prototype的,cat也如预期那样继承了Animal的name、color属性,当然也继承了type属性和msg方法了,这段代码展示了两个技巧:
·this.name = name给每个对象都添加了name属性,有点像类实例封装的数据值;
·Animal.prototype.type和Animal.prototype.msg会给Animal.prototype对象添加一个属性和方法,现在cat.type和cat.msg()是可以正常工作的,背后是依靠prototype机制,因为在创建cat对象的时候,内部的prototype都会关联到Animal.prototype上了,当cat中无法找到type(或者msg)的时候,就会到Animal.prototype上找到。
它是函数,为什么会被我们认为是"类"?
在javascript中有个约定俗成的惯例,就是“构造函数”(或者叫类函数)首字母需要大写,其实这个Animal函数和其他普通的函数是没有任何区别的,只是我们在调用这个函数的时候加上一个new的关键字,把这个函数调用变成一个“构造函数调用”。
大概就是这个函数都是要通过new来调用了吧。
其实还有个更重要的原因,来看一段代码:
function Animal(name,color){ this.name = name; this.color = color; } Animal.prototype.type = "动物"; Animal.prototype.msg = function(){ alert( "I am "+this.name+" my color is "+this.color); } var cat = new Animal("cat","black"); console.log(cat.constructor === Animal); //true
新创建的对象cat里面有个constructor属性,它是全等于函数Animal,看起来就像是cat对象是由Animal函数构造出来的
但事实是cat.consructor引用同样被关联到Animal.prototype上,Animal.prototype.constructor默认指向了Animal,来看一段代码验证一下这个观点
function Foo(){ name : 'x' }
Foo.prototype = { } var foo = new Foo(); console.log(foo.constructor === Foo); //false console.log(foo.constructor === Object); //true
上面代码定义了一个函数Foo同时修改了原型对象prototype,foo是通过new调用Foo产生的新对象,但是foo.constructor并不等于Foo,原因是foo上并不存在constructor属性,所以它会委托原型链到Foo.prototpe上查找,但是这个对象上也没有constructor属性,所以会继续在原型链找,直到原型链的终点--Object.prototype,这个对象有constructor属性,并指向了内置的Object()函数。
foo是由Foo构造出来这个观点是错误的。
原型继承和类继承的区别?
在面向类的语言中,类可以被复制多次,就像模具制作东西一样,实例一个类的时候就会将类的行为复制一份到物理对象中,但是在javascript中,是不存在这种复制机制的,在创建实例对象时,它们的prototype会被关联到“构造函数”原型对象上。拿之前的栗子来说就是cat对象的prototype被关联到Animal.prototype上,当我们想访问cat.type时,就会在原型链上去查找,而不是另外复制一份出来保存到新对象上
来看一段代码:
function Foo(){ name : 'x' } Foo.prototype = { friends : ['y','z','c'] } var foo = new Foo(); console.log(foo.friends); //["y", "z", "c"] Foo.prototype.friends.push('a'); //向Foo.prototype.friends添加一个a console.log(foo.friends); //["y", "z", "c", "a"]
继承的打开方式
在javascript中有许许多多的继承方式:原型继承、借用构造函数、寄生继承等等。
假设我们现在有两个“类”,SuperType和SubType,我们想要SubType去继承SuperType,就要修改Subtype的原型了,常见的写法有:
·SubType.prototype = SuperType.protype
·Subtype.protype = new SuperType()
安利另一种写法,来看代码:
function Foo(name){ this.name = name; } Foo.prototype.sayName = function(){ console.log(this.name); } function Bar(name,label){ Foo.call(this,name); //借用Foo函数 this.label = label; } Bar.prototype = Object.create(Foo.prototype); //创建Bar.prototype对象并关联到Foo.prototype上 //在Bar.prototype添加一个方法 Bar.prototype.sayLabel = function(){ console.log(this.label); } var a = new Bar('x','person'); a.sayName(); //x a.sayLabel(); //person
这段代码的核心语句就是下划线的Bar.prototype = Object.create(Foo.prototype),调用Object.create会凭空创建一个新对象并把新对象内部的prototype关联到你指定的对象上。
为什么要安利这种写法呢?
·Bar.prototype = Foo.prototype并不会创建一个关联到Bar.prototype的新对象,这样只是让Bar.prototype直接引用Foo.prototype对象,当我们试图在Bar.prototype添加(或者修改)属性或者方法时,就相当于修改了Foo.prototype对象本身了,这会影响到后面创建的后代,显然不是我们想要的
·Bar.prototype = new Foo()的确会创建一个关联到Bar.prototype的新对象,这样写法会调用Foo函数,假如Foo里面有一些讨厌的操作:比如向this添加属性、写日志、修改状态等等也会影响到后代,这也不是我们想看到的
因此,需要创建一个合适的关联对象,通过使用 Object.create
在ES6新增了一个辅助函数Object.setPrototypeOf,它可以修改对象的prototype关联,用法如下:
Object.setPrototypeOf(Bar.prototype,Foo.prototype);
结束语:这篇博文酝酿了挺久,关于原型链可以展开来细讲的点很多,也很凌乱,一直想把这些点串起来,也是自己对于这些知识点理解不够透彻、掌握不熟练,如果文中出现错误的地方,欢迎大家指正,如果这篇博文有些许帮助,点下右下角的推荐哈:)
参考资料:《你不知道的javascript》