原型链
JavaScript中的每个对象,都内置一个_proto_属性,这个属性是编程不可见的,它实际上是对另一个对象或者null的引用。
当一个对象引用一个属性时,JavaScript引擎会先从此对象自身属性表中查找,如果找到则进行相应读写操作,若没有在自身的属性表中找到,则在_proto_
属性引用的对象的性表中查找,如此往复,直到找到这个属性或者_proto_
属性指向null
为止。
这个_proto_
的引用链,被称作原型链。
注意,此处有一个性能优化的问题:往原型链越深处搜索,耗费的时间越多。
原型链和构造函数
JavaScript是一种面向对象的语言,并且可以进行原型继承。
JavaScript中的函数有一个属性prototype
,这个prototype
属性是一个对象,它的一个属性constructor
引用该函数自身。即:
func.prototype.constructor === func; // ==> true
这个属性有什么用呢?我们知道一个,一个函数使用new
操作符调用时,会作为构造函数返回一个新的对象。这个对象的_proto_
属性引用其构造函数的prototype
属性。
因此这个就不难理解了:
var obj = new Func(); obj.constructor == Func; // ==> true
还有这个:
obj instanceof Func; // ==> true
也是通过查找原型链上的constructor
属性实现的。
构造函数生成的不同实例的_proto_
属性是对同一个prototype
对象的引用。所以修改prototype
对象会影响所有的实例。
“子类”继承实现的几种方式
之所以子类要加引号,是因为这里说“类”的概念是不严谨的。JavaScript是一门面向对象的语言,但是它跟Java等语言不同,在ES6标准出炉之前,它是没有类的定义的。
但是熟悉Java等语言的程序员,也希望使用JavaScript时,跟使用Java相似,通过类生成实例,通过子类复用代码。那么在ES6之前,怎么做到像如下代码一样使用类似"类"的方式呢?
var parent = new Parent("Sam"); var child = new Children("Samson"); parent.say(); // ==> "Hello, Sam!" child.say(); // ==> "Hello, Samson! hoo~~" child instanceof Parent; // ==> true
我们看到,这里我们把构造函数当做类来用。
以下我们讨论一下实现的几种方式:
最简单的方式
结合原型链的概念,我们很容易就能写出这样的代码:
function Parent(name){ this.name = name; } Parent.prototype.say = function(){ console.log("Hello, " + this.name + "!"); } function Children(name){ this.name = name; } Children.prototype = new Parent(); Children.prototype.say = function(){ console.log("Hello, " + this.name + "! hoo~~"); }
这个方式缺点很明显:作为子类的构造函数需要依赖一个父类的对象。这个对象中的属性name
根本毫无用处。
第一次改进
// ... Children.prototype = Parent.prototype; // ...
这样就不会产生无用的父类属性了。
然而,这样的话子类和父类的原型就引用了同一个对象,修改子类的prototype
也会影响父类的原型。
这时候我们发现:
parent.say(); // ==> "Hello,Sam! hoo~~"
这第一次改进还不如不改。
第二次改进——临时构造函数/Object.create()
function F(){ // empty } F.prototype = Parent.prototype; Children.prototype = new F(); // ... parent.say(); // ==> "Hello, Sam!" child.say(); // ==> "Hello, Samson! hoo~~"
这样一来,修改子类的原型只是修改了F
的一个实例的属性,并没有改变Parent.prototype
,从而解决了上面的问题。
在ES5的时代,我们还可以直接这样:
Children.prototype = Object.create(Parent.prototype);
这里的思路是一样的,都是让子类的prototype
不直接引用父类prototype
。目前的现代浏览器几乎已经添加了对这个方法的支持。(但我们下面会仍以临时构造函数为基础)
但是细细思考,这个方案仍有需要优化的地方。例如:如何让父类的构造函数逻辑直接运用到子类中,而不是再重新写一遍一样的?这个例子中只有一个name
属性的初始化,那么假设有很多属性且逻辑一样的话,岂不是没有做到代码重用?
第三次改进——构造函数方法借用
使用apply/call
,实现“方法重用”的思想。
function Children(name){ Parent.apply(this, arguments); // do other initial things }
“圣杯”模式
现在完整的代码如下:
function Parent(name){ this.name = name; } Parent.prototype.say = function(){ console.log("Hello, " + this.name + "!"); } function Children(name){ Parent.apply(this, arguments); // do other initial things } function F(){ // empty } F.prototype = Parent.prototype; Child.prototype = new F(); Children.prototype.say = function(){ console.log("Hello, " + this.name + "! hoo~~"); }
这就是所谓“圣杯”模式,听着很高大上吧?
以上就是ES3的时代,我们用来实现原型继承的一个近似最佳实践。
“圣杯”模式的问题
“圣杯”模式依然存在一个问题:虽然父类和子类实例的继承的prototype
对象不是同一个实例,但是这两个prototype
对象上面的属性引用了同样的对象。
假设我们有:
Parent.prototype.a = { x: 1}; // ...
那么即使是“圣杯”模式下,依然会有这样的问题:
parent.x // ==> 1 child.x // ==> 1 child.x = 2; parent.x // ==>2
问题在于,JavaScript的拷贝不是 深拷贝(deepclone)
要解决这个问题,我们可以利用属性递归遍历,自己实现一个深拷贝的方法。这个方法在这里我就不写了。
ES6
ES6极大的支持了工程化,它的标准让浏览器内部实现类和类的继承:
class Parent { constructor(name) { //构造函数 this.name = name; } say() { console.log("Hello, " + this.name + "!"); } } class Children extends Parent { constructor(name) { //构造函数 super(name); //调用父类构造函数 // ... } say() { console.log("Hello, " + this.name + "! hoo~~"); } }
从此走上强类型的不归路。。。
原文链接:https://segmentfault.com/a/1190000006146779 侵删