前言
在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的繼承需要繼承兩個內容:構造函數的繼承和原型的繼承, 最常用的繼承方式是組合繼承(後半句來自紅寶書)
Object.create
上文中的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這就不得而知了. 只要我們搞懂了這裏兩種方法都可以使用不就好了嗎?
ES6中的extends語法糖
雖然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具體做了哪些事情呢?
先說兩條鏈:
- 子類的__proto__屬性表示構造函數的繼承, 總指向父類
- 子類的prototype屬性的__proto__屬性表示方法的繼承, 總是指向父類的prototype屬性
所以上面的例子的鏈關係:
MyStream.__proto__ === EventEmitter//這是構造函數的__proto__鏈, 又可能在extends語法糖內部使用到了這條鏈, 因爲extends的繼承方式是先構造父類然後由子類對這個對象加工,最後由子類返回
MyStream.prototype._proto__===EventEmitter.prototype//這條鏈就是原型鏈的繼承
關於extends由幾點注意事項:
- class中定義的原型方法是不可枚舉的
- class定義的方法的prototype屬性是不可以更改其指向的