在js中, 对象就是键值对的集合.形如:{键1:值1,键2:值2}
. 看起来很简单的样子, 但是提起笔想要在js中使用面向对象的方式来编程时, 就有点力不从心了.始终感觉对js的面向对象的继承理解不是很清楚.在红宝书中有很多的继承方式. 无奈理解不到位, 记也记不住. 最近看了gulp源码,发现它就是把Gulp写成了构造函数的形式.地址:Gulp
其中在Gulp内部调用了Undertaker.call(this)
, 又在Gulp外部写了util.inherits(Gulp, Undertaker)
.初看有点蒙啊(没错, 我就是这么菜).后来看了uitl模块的文的文档, 才知道util.inherits方法主要是用来:从一个构造函数中继承原型方法到另一个. 也就是说Gulp继承了Undertaker的原型.那么具体是如何继承的呢?不知道! 所以我就去github上面看了下node的一点源码.具体操作如下:
function inherits(ctor, superCtor) {
if (ctor === undefined || ctor === null)
throw new ERR_INVALID_ARG_TYPE('ctor', 'Function', ctor);
if (superCtor === undefined || superCtor === null)
throw new ERR_INVALID_ARG_TYPE('superCtor', 'Function', superCtor);
if (superCtor.prototype === undefined) {
throw new ERR_INVALID_ARG_TYPE('superCtor.prototype',
'Object', superCtor.prototype);
}
Object.defineProperty(ctor, 'super_', {
value: superCtor,
writable: true,
configurable: true
});
Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
}
再看一次这里的代码, 感觉有点奇怪啊.给ctor添加一个super_属性, 属性值为superCtor. 所以我们可以再ctor上的super_上获取superCtor.
再看最后一句:Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
所以执行完这句之后就变成了:Gulp.prototype.__proto__ = Undertaker.prototype
, 我们可正常在Gulp.prototype上添加属性
再说说在Gulp内部调用的Undertaker.call(this)
, 由于Undertaker(用于控制gulp任务的执行顺序)也是一个构造函数,而此时this是gulp的实例对象, 所以这里是继承了Undertaker的构造函数
小结论: js里的继承是基于原型的,但是我们不能单独的使用原型继承, 因为会存在变量共享的问题, 在一处改变了值, 会意外的影响到其他地方.所以往往是和借用构造函数的继承方法一起使用的.
什么是借用构造函数的继承方法!!?? 请思考上文中提到的Undertaker.call(this)
. 没错这就是Gulp借用了Undertaker的构造函数, 然后在它内部使其this指向自己的实例执行.
接上面的小结论: js的继承需要继承两个内容:构造函数的继承和原型的继承, 最常用的继承方式是组合继承(后半句来自红宝书)
上文中的util.inherits
使用了Object.setPrototypeOf
方法来实现原型的继承, 但是MDN上又说Object.setPrototypeOf
的性能不好, 建议使用Object.create
方法.所以这里题外话一下Object.create
方法
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
所以util.inherits方法可以将Object.setPrototypeOf(ctor.prototype, superCtor.prototype)
,改写为:ctor.protytype=Object.create(superCtor.prototype)
.
至于node的源码中为啥不使用MDN说的Object.create方法而是使用性能比较差的Object.setPrototypeOf这就不得而知了. 只要我们搞懂了这里两种方法都可以使用不就好了吗?
虽然util.inherits方法提供了原型继承的方法, 但是它建议使用原生的ES6的extends语法糖来写.
我们就来看看ES6的写法是如何的:
const EventEmitter = require('events');
class MyStream extends EventEmitter {
write(data) {
this.emit('data', data);
}
}
const stream = new MyStream();
stream.on('data', (data) => {
console.log(`接收的数据:"${data}"`);
});
stream.write('使用 ES6');
写法还是挺简单的, 那么extends具体做了哪些事情呢?
先说两条链:
所以上面的例子的链关系:
MyStream.__proto__ === EventEmitter//这是构造函数的__proto__链, 又可能在extends语法糖内部使用到了这条链, 因为extends的继承方式是先构造父类然后由子类对这个对象加工,最后由子类返回
MyStream.prototype._proto__===EventEmitter.prototype//这条链就是原型链的继承
关于extends由几点注意事项: