js中的繼承

前言

在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具體做了哪些事情呢?

先說兩條鏈:

  1. 子類的__proto__屬性表示構造函數的繼承, 總指向父類
  2. 子類的prototype屬性的__proto__屬性表示方法的繼承, 總是指向父類的prototype屬性

所以上面的例子的鏈關係:

MyStream.__proto__ === EventEmitter//這是構造函數的__proto__鏈, 又可能在extends語法糖內部使用到了這條鏈, 因爲extends的繼承方式是先構造父類然後由子類對這個對象加工,最後由子類返回
MyStream.prototype._proto__===EventEmitter.prototype//這條鏈就是原型鏈的繼承

關於extends由幾點注意事項:

  1. class中定義的原型方法是不可枚舉的
  2. class定義的方法的prototype屬性是不可以更改其指向的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章