《JavaScript 高級程序設計》 讀書筆記--從原型鏈複習繼承

這一篇進入正題來複習一下 JavaScript 中對象的繼承。「高程」中一共列舉了 6 種繼承的方式。看起來是有些嚇人,但仔細梳理就能發現其中也是有一個演變過程的。這篇筆記就是我本身對這個過程的理解。若是有不足的地方,還但願各位能夠指出。java

再次安利一下「高程」,真的寫得很是棒。有必定基礎和項目經驗的同窗絕對要去看一看,能提升很多。瀏覽器

基礎概念

在進入繼承以前,咱們再把一些基本概念複習一下。數據結構

0. 構造函數、原型對象、實例

談到對象,必定會出現這三個概念。在 JavaScript 中,原型對象不須要咱們手動去定義,當咱們定義一個類或者構造函數後,JavaScript 會自動生成對應的原型對象,咱們能夠經過 prototype 屬性訪問;咱們寫的 function A(){} 就是構造函數;而咱們new 調用構造函數返回的值就是對象的實例函數

// 構造函數:其實就是通常的函數。函數名大寫只是一個約定規則而已。
// 事實上除了 new 以外,咱們也能夠像調用通常的函數同樣使用。
// 在 ES6 中就是 Class 裏面的 constructor
function A(){
  this.a = 'a'
}

// 原型對象:當咱們定義對象的時候,就會生成一個對應的原型對象。
// 該函數的 prototype 屬性就指向原型對象,這個就是原型鏈的精髓。
A.prototype

// 實例: 用 new 調用構造函數的返回值。
var a = new A()
複製代碼

1. 原型鏈

下面再來看一下原型鏈的概念。原型鏈的概念咱們必定不會陌生。那麼就很少說了,直接上圖。這是一個最基本的原型鏈,咱們要仔細理解這張圖而且搞清楚 實例對象原型構造函數以及與 Object 之間的關係。 學習

在複習完上面兩個概念以後(特別是原型鏈),相信在後面理解繼承的時候會有所幫助。若是在後面有所困惑的話,不妨回來看看基礎概念。this

下面就開始進入正題。spa


JavaScript 中的繼承

1. 原型鏈繼承

咱們對上面基礎的例子作一個拓展。再加入一個對象 B,咱們一樣畫成圖。 prototype

AB 是互相獨立的兩個對象(類),而且它們都是 繼承 了 JavaScript 的對象之祖——Object對象。請注意,在通常的對象中,就已經存在了一個 對象Object對象 的繼承關係了。3d

那麼如今咱們要讓對象B去繼承對象A,就能夠模仿對象和Object的關係,修改B的原型對象的指向。 code

這麼一來,B就能夠順着原型鏈訪問到A了。因爲如今B.prototype = A.prototype了,那麼在B.prototype上作的任何修改都會影響到A.prototype了。因此咱們再稍微調整一下,讓 B.prototype = new A()

這樣,完整的原型鏈繼承的關係圖就出來了。

簡單的示例代碼以下,小夥伴們能夠在 Chrome 中試玩一下。

function A(){ this.a = 'a' }
function B(){ this.b = 'b' }

// 不要這麼作,由於修改 B 的 prototype 會影響 A 的 prototype
// B.prototype = A.prototype
B.prototype = new A()
B.prototype.constructor = B
複製代碼

對象的 constructor

在上面的示例代碼中,最後一行咱們對對象 B 的構造函數從新進行了賦值。這是由於,當咱們改變了 B.prototype 的時候,會切斷原來 B 的構造函數與 B.prototype 之間的聯繫。

雖然這個屬性對咱們的繼承關係沒有影響(instanceof 方法結果仍然正確)。可是從代碼含義上來講,咱們最好仍是修改稱爲正確的指向。

另外,對於實例來講 a.constructor.prototype === A.prototype // true。即咱們能夠經過實例的構造函數去給對象原型添加屬性和方法。儘管沒人會推薦咱們這麼去作,但讓屬性指向正確的值會比較好。

和對象的建立同樣,原型鏈的方法是比較簡單的。可是也有一個明顯的缺陷,就是「沒法」對父類傳不一樣的值。即 B.prototype = new A(xx) 時以後全部的 B 的示例都會帶上這個值,所以就產生了侷限性。

回想一下在建立對象時,咱們是怎麼解決這個問題的?

2. 構造函數繼承

在建立對象時,咱們知道不一樣的實例在建立時只要向構造函數中傳入不一樣的值,就會獲得不一樣的值。那麼回到繼承上,爲了解決原型鏈繼承沒法向父類傳遞不一樣值的問題,咱們一樣也須要藉助構造函數。

在進入正題前,咱們再看一下構造函數,而後想一下若是不用 new 調用構造函數會是怎樣? 下面是一個 Person 的構造函數。通常來講咱們使用 new 關鍵字建立 Person 的實例。可是有沒有想過,構造函數也是函數,若是咱們不用 new 而是普通地調用會是怎麼狀況呢?

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

// 直接調用會是怎麼狀況呢?
Person('Kizunaai', 2, 'female')
複製代碼

熟悉 this 特性的小夥伴確定能反應過來。獨立調用函數時,若在非嚴格模式下,this 指向的是 window(瀏覽器環境)。那麼咱們看一下 window

window 中果真就有了 age 這個屬性,而且值爲 2。也就是說,直接調用構造函數就至關於把構造函數中的屬性賦給調用它的對象了。

好,趁熱打鐵,咱們直接來看代碼。

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

function VTuber(name, age, sex){
  // 調用 Person 的構造函數實際上就是把 Person 的值賦給 VTuber
  // 在 ES6 中就是 super()
  Person.call(this, name, age, sex)
}

var kizunaai = new VTuber('kizunaai', 2, 'female')
var luna = new VTuber('luna', 100, 'female')

kizunaai.name // 'kizunaai'
luna.name // 'luna'
複製代碼

咱們在 Chrome 中分別打印一下以前的實例。能夠看到 VTuber 的實例只是包含了 Person 的屬性而已,而在原型鏈上二者是沒有任何關係的。

因此再想一下,構造函數的方法其實真的是「繼承」嗎?由於對於「子類」來講,是沒有辦法調用父類原型上的方法的。而在用構造函數建立對象時咱們就已經知道,把方法寫在構造函數裏顯然不是一個好的解決方法。

3. 組合式繼承

既然原型鏈和構造函數正好能彌補互相之間的缺陷,組合起來咱們能愉快地進行繼承了。也沒什麼新的知識點,就直接上代碼了。

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

Person.prototype.sayHello = function(){
  return `${this.name} say hello ~`
}

function VTuber(name, age, sex){
  // 構造函數保證了不一樣值的傳遞
  Person.call(this, name, age, sex)
}

// 原型鏈保證了方法的傳遞(還有意義上)
VTuber.prototype = new Person()
VTuber.prototype.constructor = VTuber

var kizunaai = new VTuber('kizunaai', 2, 'female')
var luna = new VTuber('luna', 100, 'female')

kizunaai.name // 'kizunaai'
luna.sayHello() // 'luna say hello ~'
複製代碼

4. 寄生組合式繼承

組合式繼承是咱們最經常使用的繼承方法,幾乎能夠說是知足了咱們的需求。硬要挑刺的話,也就是父類的構造函數調用兩次的問題了。

// 以以前的代碼爲例
// 第一次在子類的構造函數中調用
Person.call(this, name, age, sex)

// 第二次在創建原型鏈時調用
VTuber.prototype = new Person()
複製代碼

其中第一次是必定省不掉的,要下功夫的話就是在第二次創建原型鏈的時候了。仍是之前面的代碼爲例,咱們就這麼繼承,數據結構會是怎麼樣的?

能夠看到在 VTuber.prototype 上也有 name, age, sex 三個屬性,但實際上這三個屬性根本沒有意義。那麼解決的思路就有了,咱們須要藉助一個空的對象來搭一座橋。(千萬別說讓 VTuber.prototype = Person.prototype 了,理由參考原型鏈那部分)

function Person(name, age, sex){
  this.name = name
  this.age = age
  this.sex = sex
}

Person.prototype.sayHello = function(){
  return `${this.name} say hello ~`
}

function VTuber(name, age, sex){
  // 構造函數保證了不一樣值的傳遞
  Person.call(this, name, age, sex)
}

// 咱們要借用一個空對象做爲過渡
// VTuber.prototype = new Person()
function A(){}
A.prototype = Person.prototype
VTuber.prototype = new A()

VTuber.prototype.constructor = VTuber

var kizunaai = new VTuber('kizunaai', 2, 'female')
var luna = new VTuber('luna', 100, 'female')

kizunaai.name // 'kizunaai'
luna.sayHello() // 'luna say hello ~'
複製代碼

上面咱們借用了一個 A 來打了個橋。這樣一來就在原型鏈上就沒有多餘的屬性了。(其實兩次構造函數是確定要調的,只是第二次調誰的問題)

而這種搭橋的方式,在「高程」中也被稱爲是原型式繼承。其中關於原型式和寄生式分別和原型鏈、構造函數相對應,感受沒有這兩種直觀並且也不經常使用(我的感受)因此就不作展開了,有興趣的小夥伴仍是推薦去閱讀「高程」。

小結

至此,有關於繼承的筆記就到此爲止。這一篇順着對象的原型鏈的概念開始,介紹了 JavaScript 中對象繼承的幾種方式。其中組合繼承的方式是咱們最多見也是用的最廣的,咱們須要好好了解一下。

在看書的過程當中,像這樣給本身拋點問題,找一找方法間的演變脈絡即頗有趣也很容易理解。不知道各位小夥伴有什麼好的學習方法呢?不妨互相交流一下吧~

相關文章
相關標籤/搜索