JS中的继承
原型链
提到继承,不得不说的是JS中的原型链概念。JS不像传统的面向对象语言,在ES6出现以前,JS标准中并没有类的概念,所以往往是通过原型链的特性实现面向对象的继承。看过一些资料以后,发现其实原型链的概念十分简单,一句话总结:JS中的每个对象都有一个__proto__属性,而这个属性指向它的构造函数的prototype属性,而因为函数在JS中也是对象,所以函数本身也有__proto__属性,而在对象上进行属性查找就会沿着这条链一直向上进行,直到原型链的终点Object.prototype(Object.prototype.__proto__属性为null)。还有一个重要的点是构造函数的prototype对象有一个constructor属性,这个属性指向构造函数本身,我们可以利用这个特性建立对象和其构造函数之间的联系,因为我们知道,对于bar.constructor属性的查找,如果bar本身没有constructor属性,就会在其__proto__属性上查找,而__proto__又指向其构造器的prototype,所以依据这个特性,就能找到某对象的构造函数。另外一个便捷的判断实例构造器的方法是利用instanceof操作符,这个操作符会沿着对象的原型链一直向上查找直到对象的__proto__的__proto__…等于函数的prototype属性,或是一直到Object.prototype都没有相等,返回false。
实现继承
因为我们要实现类的继承,这里定义父类为Parent,子类为Child,先看一下最基本实现继承的代码:
1 | function Parent(name) { |
实现继承分为两个步骤:
- 在子类中调用父类的构造函数
- 建立原型链(继承链)
JS中的每个函数都有一个call方法和一个apply方法,这两个方法的作用都是改变调用上下文this的指向,我们在子类中调用Parent.call(this, name);就是表明我们需要调用Parent这个函数,并且将Parent构造函数中的this设置为Child中的this,在通过new操作符实例化对象时,这个this表现为新创建的对象,借此就实现了在子类中调用父类的构造函数。apply方法也用于改变函数调用的上下文,只是传入的参数以数组方式体现。
我们知道,原型链的查找规则是先从对象本身开始,然后沿着__proto__一直向上。我们要将两个类链接起来,就需要在对象的__proto__上做调整,由于__proto__指向构造函数的prototype,所以我们设置子类的构造器的prototype,让其__proto__指向父类的prototype,当我们在当前实例对象或者其__proto__上查找不到要抄找的属性或方法时,就会去查找父类的构造函数的prototype,所以我们就建立了父类和字类之间的联系。
Node.js中的继承
Node.js中的util核心模块提供了一个inherits函数,这个函数封装了建立原型链的实现。传入的第一个参数为子类,传入的第二个参数为父类。
1 | const inherits = require('util').inherits; |
因为__proto__不是JS标准的一部分,所以人为设置并不推荐。其实有一个Object.create方法用于根据一个对象来创造另一个对象,并且新对象的__proto__指向该方法传入的参数。
1 | let foo = {}; |
所以上面的例子就可以改写成
1 | function Parent(name) { |
但是这种方法相当于Child.prototype = {__proto__: Parent.prototype},因此就会丢失Child.prototype.constructor属性,所以我们在Object.create还可以传入第二个参数,该参数为一个对象,表明创建的新对象需要额外添加的属性。
1 | function Parent(name) { |
查看了一下最新的nodejs中inherits函数的实现,如下所示:
1 | function inherits(ctor, superCtor) { |
关键性的一句是最后一句,我们利用了Object.setPrototypeOf方法直接将子类的prototype的__proto__关联到父类的prototype上。我们还发现在调用inherits函数时还将在子类的构造器上额外定义一个super_属性,用这个属性可以方便的找到父类构造器。并且官方文档不建议我们使用该方法,可以使用ES6内建的对类和继承的支持。
曾经的nodejs采用过下面这种实现:
1 | exports.inherits = function(ctor, superCtor) { |