你真的理解JS的繼承了嗎?

噫籲嚱,js之難,難於上青天javascript

本文小綱介紹

請先看下圖,若是各位大佬以爲soeasy,請直接 插隊這裏 查看class繼承 。java

js 的繼承是經過原型鏈實現的,因此想要理解 js 的繼承,必須先吃透原型鏈! ios

原型和原型鏈

js 中的幾乎全部對象都有一個特殊的[[Prototype]]內置屬性,用來指定對象的原型對象,這個屬性實質上是對其餘對象的引用。在瀏覽器中通常都會暴露一個私有屬性 __proto__,其實就是[[Prototype]]的瀏覽器實現。假若有一個對象var obj = {},那麼能夠經過obj.__proto__ 訪問到其原型對象Object.prototype,即obj.__proto__ === Object.prototype。對象有[[Prototype]]指向一個原型對象,原型對象自己也是對象也有本身的[[Prototype]]指向別的原型對象,這樣串接起來,就組成了原型鏈。es6

var obj = [1, 2, 3]
obj.__proto__ === Array.prototype // true
Number.prototype.__proto__ === Object.prototype // true
Array.prototype.__proto__ === null // true
obj.toString()
複製代碼

能夠看出,上例中存在一個從objnull的原型鏈,以下:數組

obj----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null
複製代碼

上例中最後一行調用obj.toString()方法的時候,js 引擎就是沿着這條原型鏈查找toString方法的。js 首先在obj對象自身上查找toString方法;未找到,繼續沿着原型鏈查找Array.prototype上有沒有toString;未找到,繼續沿着原型鏈在Object.prototype上查找。最終在Object.prototype上找到了toString方法,因而淚流滿面的調用該方法。這就是原型鏈最基本的做用。瀏覽器

上面我說「js 中的幾乎全部對象都有一個特殊的[[Prototype]]內置屬性」,爲何不是所有呢?由於 js 能夠建立沒有內置屬性[[Prototype]]的對象:閉包

var o = Object.create(null)
o.__proto__ // undefined
複製代碼

Object.create是 es5 的方法,全部瀏覽器都已支持。該方法建立並返回一個新對象,並將新對象的原型對象賦值爲第一個參數。在上例中,Object.create(null)建立了一個新對象並將對象的原型對象賦值爲null。此時對象 o 是沒有內置屬性[[Prototype]]的(不知道爲何o.__proto__不是null,但願知道的大佬評論解釋下,萬分感激)。函數

es5寄生組合繼承

es5 的繼承是經過修改子類的原型對象來實現的:ui

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}
SubType.prototype = Object.create(SuperType.prototype, {
  constructor: {
    value: SubType,
    enumerable: false,
    writable: true,
    configurable: true
  }
})
SubType.prototype.sayAge = function(){
  alert(this.age);
}; 
let instance = new SubType('gim', '17');
instance.sayName(); // 'gim'
instance.sayAge(); // '17'
複製代碼
  1. 首先,這段代碼聲明瞭父類 SuperType
  2. 其次,聲明瞭父類的原型對象方法 sayName
  3. 再次,聲明瞭子類 SubType,並在將來將要新建立的 SubType 實例環境上調用父類 SuperType.call,以獲取父類中的 namecolors 屬性。
  4. 再次,用 Object.create() 方法把子類的原型對象上的 __proto__ 屬性指向了父類的原型對象,並把子類構造函數從新賦值爲子類。
  5. 而後,給子類的原型對象上添加方法 sayAge。
  6. 最後初始化實例對象instance。(調用new SubType('gim', '17')的時候會生成一個__proto__指向SubType.prototype的空對象,而後把this指向這個空對象。在添加完name、colors、age屬性以後,返回這個‘空對象’,也就是說instance最終就是這個‘空對象’。)

此時,代碼中生成的原型鏈關係以下圖所示(下面三張圖擼了一下午,喜歡的幫忙點個贊謝謝):this

es5寄生組合繼承

  • 子類的原型對象的 __proto__ 指向父類的原型對象。 圖中有兩種顏色的帶箭頭的線,紅色的線是咱們生成的實例的原型鏈,是咱們之因此能調用到 instance.sayName()instance.sayAge() 的根本所在。當調用instance.sayName()的時候,js引擎會先查找instance對象中的自有屬性。未找到sayName屬性,則繼續沿原型鏈查找,此時instance經過內置原型__proto__鏈到了SubType.prototype對象上。但在SubType.prototype上也未找到sayName屬性,繼續沿原型鏈查找,此時SubType.prototype__proto__鏈到了SuperType.prototype對象上。在對象上找到了sayName屬性,因而查找結束,開始調用。所以調用instance.sayName()至關於調用了instance.__proto__.__proto__.sayName(),只不過前者中sayName函數內this指向instance實例對象,然後者sayName函數內的this指向了SuperType.prototype(instance.__proto__.__proto__ === SuperType.prototype)對象。

  • 在 es5 的實現中,子類構造函數的 __proto__ 直接指向的是 Function.prototype 黑色的帶箭頭的線則是 es5 繼承中產生的‘反作用’,使得全部的函數的 __proto__ 指向了 Function.prototype,並最終指向 Object.prototype,從而使得咱們聲明的函數能夠直接調用 toString(定義在Function.prototype上)、hasOwnProperty(定義在Object.prototype上) 等方法,如:SubType.toString()、SubType.hasOwnProperty()等。

    下面看看es6中有哪些不一樣吧。

es6的繼承

es6 的繼承是由 class ... extends ... 實現的:

class SuperType {
  constructor(name) {
    this.name = name
    this.colors = ["red", "blue", "green"];
  }
  sayName() {
    alert(this.name)
  }
}
class SubType extends SuperType {
  constructor(name, age){
    super(name)
    this.age = age
  }
  sayAge() {
    alert(this.age)
  }
}
let instance = new SubType('gim', '17');
instance.sayName(); // 'gim'
instance.sayAge(); // '17'
複製代碼

能夠明顯的發現這段代碼比以前的更加簡短和美觀。es6 class 實現繼承的核心在於使用關鍵字 extends 代表繼承自哪一個父類,而且在子類構造函數中必須調用 super 關鍵字,super(name)至關於es5繼承實現中的 SuperType.call(this, name)

雖然結果可能如你所料的實現了原型鏈繼承,可是這裏仍是有個須要注意的點值得一說。

es6的class ... extends ...繼承

如圖,es6中的 class 繼承存在兩條繼承鏈:

  1. 子類的原型對象的__proto__屬性,表示方法的繼承,老是指向父類的prototype屬性。 這點倒和經典繼承是一致的。 如紅線所示,子類SubTypeprototype屬性的__proto__指向父類SuperTypeprototype屬性。 至關於調用Object.setPrototypeOf(SubType.prototype, SuperType.prototype); 由於和經典繼承相同,這裏再也不累述。

  2. 子類構造函數的__proto__屬性,表示構造函數的繼承,老是指向父類。 這是個值得注意的點,和es5中的繼承不一樣,如藍線所示,子類SubType__proto__指向父類SuperType。至關於調用了Object.setPrototypeOf(SubType, SuperType); es5繼承中子類和父類的內置原型直接指向的都是Function.prototype,因此說Function是全部函數的爸爸。而在es6class...extends...實現的繼承中,子類的內置原型直接指向的是父類。 之因此注意到這點,是由於看 kyle 大佬的《你不知道的javascript 下》的時候,看到了class MyArray extends Array{}var arr = MyArray.of(3)這兩行代碼,很不理解爲何MyArray上面爲何能調到of方法。由於按照es5中繼承的經驗,MyArray.__proto__應該指向了Function.prototype,然後者並無of方法。當時感受世界觀都崩塌了,爲何我之前的認知失效了?次日重翻阮一峯老師的《ECMAScript6入門》才發現原來class實現的繼承是不一樣的。

知道了這點,就能夠根據需求靈活運用Array類構造本身想要的類了:

class MyArray extends Array {
  [Symbol.toPrimitive](hint){
    if(hint === 'default' || hint === 'number'){	  
      return this.reduce((prev,curr)=> prev+curr, 0)
    }else{
      return this.toString()
    }
  }
}
let arr = MyArray.of(2,3,4);
arr+''; // '9'
複製代碼

元屬性Symbol.toPrimitive定義了MyArray的實例發生強制類型轉換的時候應該執行的方法,hint的值多是default/number/string中的一種。如今,實例arr可以在發生加減乘除的強制類型轉換的時候,數組內的每項會自動執行加性運算。

以上就是js實現繼承的兩種模式,能夠發現class繼承和es5寄生組合繼承有類似之處,也有不一樣的地方。雖然class繼承存在一些問題(如暫不支持靜態屬性等),可是子類的內置原型指向父類這點是個不錯的改變,這樣咱們就能夠利用原生構造函數(Array等)構建本身想要的類了。

kyle大佬提到的行爲委託

在讀《你不知道的javascript 上》的時候,感觸頗多。這本書真的是本良心書籍,讓我學會了LHS/RHS,讀懂了閉包,瞭解了詞法做用域,完全理解了this指向,基本懂了js的原型鏈繼承。因此當時就忍不住又從頭讀了一遍。若是說諸多感覺中最大的感覺是啥,那必定是行爲委託了。我第一次見過有大佬可以如此強悍(至少沒見過國內的大佬這麼牛叉的),強悍到直接號召讀者抵制js的繼承模式(不管寄生組合繼承仍是class繼承),而且提倡使用行爲委託模式實現對象的關聯。我真的被折服了,要知道class但是w3c委員會制定出的標準,而且已經普遍的應用到了業界中。關鍵的關鍵是,我確實認爲行爲委託確實更加清晰簡單(若有異議請指教)。

let SuperType = {
  initSuper(name) {
    this.name = name
    this.color = [1,2,3]
  },
  sayName() {
    alert(this.name)
  }
}
let SubType = {
  initSub(age) {
    this.age = age
  },
  sayAge() {
    alert(this.age)
  }
}
Object.setPrototypeOf(SubType,SuperType)
SubType.initSub('17')
SubType.initSuper('gim')
SubType.sayAge() // 'gim'
SubType.sayName() // '17'
複製代碼

這就是模仿上面js繼承的兩個例子,利用行爲委託實現的對象關聯。行爲委託的實現很是超級極其的簡單,就是把父對象關聯到子對象的內置原型上,這樣就能夠在子對象上直接調用父對象上的方法。行爲委託生成的原型鏈沒有class繼承生成的原型鏈的複雜關係,一目瞭然。固然class有其存在的道理,可是在些許場景下,應該是行爲委託更加合適吧。但願 safari 儘快實現Object.setPrototypeOf()方法,都 es10 了還未實現 es6 規定的方法,真是夠了。(P.S.終於在9.13號ios實現了 setPrototypeOf 方法,真的是 f 了)

kyle大佬倡導的行爲委託

小子愚鈍,若是行爲委託徹底可以實現實現class繼承的功能,並且更加簡單和清晰,咱們開發的過程當中爲何不愉快的嘗試用一下呢?

相關文章
相關標籤/搜索