繼承的實現方式及原型概述 | JavaScript 隨筆

圖片描述

對於 OO 語言,有一句話叫「Everything is object」,雖然 JavaScript 不是嚴格意義上的面嚮對象語言,但若是想要理解 JS 中的繼承,這句話必須時刻銘記於心。node

JS 的語法很是靈活,因此有人以爲它簡單,由於怎麼寫都是對的;也有人以爲它難,由於很難解釋某些語法的設計,誰能告訴我爲何 typeof null 是 object 而 typeof undefined 是 undefined 嗎?而且這是在 null == undefined 的前提下。不少咱們自認爲「懂」了的知識點,細細琢磨起來,仍是會發現有不少盲點,「無畏源於無知」吧……編程

1. 簡單對象

既然是講繼承,天然是從最簡單的對象提及:瀏覽器

var dog = {
  name: 'tom'
}

這即是對象直接量了。每個對象直接量都是 Object 的子類,即app

dog instanceof Object; // true

2. 構造函數

JS 中的構造函數與普通函數並無什麼兩樣,只不過在調用時,前面加上了 new 關鍵字,就當成是構造函數了。函數

function Dog(name) {
      this.name = name;
    }


var dog = new Dog('tom');

dog instanceof Dog; // true

兩個問題,第一,不加 new 關鍵字有什麼後果?測試

那麼 Dog 函數中的 this 在上下文(Context)中被解釋爲全局變量,具體在瀏覽器端的話是 window 對象,在 node 環境下是一個 global 對象。this

第二,dog 的值是什麼?很簡單,undefined 。Do>g 函數沒有返回任何值,執行結束後,dog 的值天然是 undefined 。spa

關於 new 的過程,這裏也順便介紹一下,這個對後面理解原型(prototype)有很大的幫助:prototype

建立一個空的對象,僅包含 Object 的屬性和方法。
將 prototype 中的屬性和方法建立一份引用,賦給新對象。
將 this 上的屬性和方法新建一份,賦給新對象。
返回 this 對象,忽略 return 語句。
須要明確的是,prototype 上的屬性和方法是實例間共享的,this 上的屬性和方法是每一個實例獨有的。設計

3. 引入 prototype

如今爲 Dog 函數加上 prototype,看一個例子:

function Dog(name) {
  this.name = name;
  this.bark = function() {};
}

Dog.prototype.jump = function() {};
Dog.prototype.species = 'Labrador';
Dog.prototype.teeth = ['1', '2', '3', '4'];

var dog1 = new Dog('tom'),
    dog2 = new Dog('jerry');

dog1.bark !== dog2.bark; // true
dog1.jump === dog2.jump; // true

dog1.teeth.push('5');
dog2.teeth; // ['1', '2', '3', '4', '5']

看到有註釋的那三行應該能夠明白「引用」和「新建」的區別了。

那麼咱們常常說到的「原型鏈」究竟是什麼呢?這個術語出如今繼承當中,它用於表示對象實例中的屬性和方法來自於何處(哪一個父類)。好吧,這是筆者的解釋。

- Object
  bark: Dog/this.bark()
  name: 'tom'
- __proto__: Object
    jump: Dog.prototype.jump()
    species: 'Labrador'
  + teeth: Array[4]
  + constructor: Dog()
  + __proto__: Object

上面的是 dog1 的原型鏈,不知道夠不夠直觀地描述「鏈」這個概念。

其中,bark 和 name 是定義在 this 中的,因此最頂層能夠看到它倆。
而後,每個對象都會有一個 proto 屬性(IE 11+),它表示定義在原型上的屬性和方法,因此 jump、species 和 teeth 天然就在這兒了。

最後就一直向上找 proto 中的屬性和方法。

4. 繼承的幾種實現

4.1 經過 call 或者 apply

繼承在編程中有兩種說法,一個叫 inherit,另外一個是 extend 。前者是嚴格意義上的繼承,即存在父子關係,然後者僅僅是一個類擴展了另外一個類的屬性和方法。那麼 call 和 apply 就屬於後者的範疇。怎麼說?

function Animal(gender) {
  this.gender = gender;
}

function Dog(name, gender) {
  Animal.call(this, gender);
  this.name = name;
}

var dog = new Dog('tom', 'male');

dog instanceof Animal; // false

雖然在 dog 對象中有 gender 屬性,但 dog 卻不是 Animal 類型。甚至,這種方式只能「繼承」父類在 this 上定義的屬性和方法,並不能繼承 Animal.prototype 中的屬性和方法。

4.2 經過 prototype 實現繼承

要實現繼承,必須包含「原型」的概念。下面是很經常使用的繼承方式。

function Dog(name) {
  Animal.call(this);
}

Dog.prototype = new Animal(); // 先假設 Animal 函數沒有參數
Dog.prototype.constructor = Dog;

var dog = new Dog('tom');

dog instanceof Animal; // true

繼承的結果有兩個:

得到父類的屬性和方法;
正確經過 instanceof 的測試。
prototype 也是對象,它是建立實例時的裝配機,這個在前面有提過。new Animal() 的值包含 Animal 實例全部的屬性和方法,既然它賦給了 Dog 的 prototype,那麼 Dog 的實例天然就得到了父類的全部屬性和方法。

而且,經過這個例子能夠知道,改變 Dog 的 prototype 屬性能夠改變 instanceof 的測試結果,也就是改變了父類。

而後,爲何要在 Dog 的構造函數中調用 Animal.call(this)?

由於 Animal 中可能在 this 上定義了方法和函數,若是沒有這句話,那麼全部的這一切都會給到 Dog 的 prototype 上,根據前面的知識咱們知道,prototype 中的屬性和方法在實例間是共享的。

咱們但願將這些屬性和方法依然保留在實例自身的空間,而不是共享,所以須要重寫一份。

至於爲何要修改 constructor,只能說是爲了正確的顯示原型鏈吧,它並不會影響 instanceof 的判斷。或者有其餘更深的道理我並不知道……

4.3 利用空對象實現繼承

上面的繼承方式已經近乎完美了,除了兩點:

Animal 有構造參數,而且使用了這些參數怎麼辦?
在 Dog.prototype 中多了一份定義在 Animal 實例中冗餘的屬性和方法。

function Animal(name) {
  name.doSomething();
}

function Dog(name) {
  Animal.call(this, name);
}

Dog.prototype = new Animal(); // 因爲沒有傳入name變量,在調用Animal的構造函數時,會出錯
Dog.prototype.constructor = Dog;

這個問題能夠經過一個空對象來解決(改自 Douglas Crockford)。

function DummyAnimal() {}
DummyAnimal.prototype = Animal.prototype;

Dog.prototype = new DummyAnimal();
Dog.prototype.constructor = Dog;

他的原始方法是下面的 object:

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

Dog.prototype = object(Animal.prototype);
Dog.prototype.constructor = Dog;

4.4 利用 proto 實現繼承

如今就只剩下一個問題了,如何把冗餘屬性和方法去掉?

其實,從第 3 小節介紹原型的時候就提到了 proto 屬性,instanceof 運算符是經過它來判斷是否屬於某個類型的。

因此咱們能夠這麼繼承:

function Dog() {
  Animal.call(this);
}

Dog.prototype = {
  __proto__: Animal.prototype,
  constructor: Dog
};

若是不考慮兼容性的話,這應該是從 OO 的角度來看最貼切的繼承方式了。

4.5 拷貝繼承

這個方式也只能稱之爲 extend 而不是 inherit,因此也不必展開說。

像 Backbone.Model.extend、jQuery.extend 或者 _.extend 都是拷貝繼承,能夠稍微看一下它們是怎麼實現的。(或者等我本身再好好研究以後過來把這部分補上吧)

5. 我的小結

當咱們在討論繼承的實現方式時,給個人感受就像孔乙己在炫耀「茴香豆」的「茴」有幾種寫法同樣。繼承是 JS 中佔比很大的一塊內容,因此不少庫都有本身的實現方式,它們並無使用我認爲的「最貼切」的方法,爲何?JS 就是 JS,它生來就設計得很是靈活,因此咱們爲何不利用這個特性,而非得將 OO 的作法強加於它呢?

經過繼承,咱們更多的是但願得到父類的屬性和方法,至因而否要保證嚴格的父類/子類關係,不少時候並不在意,而拷貝繼承最能體現這一點。對於基於原型的繼承,會在代碼中看到各類用 function 定義的類型,而拷貝繼承更通用,它只是將一個對象的屬性和方法拷貝(擴展)到另外一個對象而已,並不關心原型鏈是什麼。

固然,在我鼓吹拷貝繼承多麼多麼好時,基於原型的繼承天然有它不可取代的理由。因此具體問題得具體分析,當具體的使用場景沒定下來時,就不存在最好的方法。

我的看法,能幫助你們更加理解繼承一點就最好,若是有什麼不對的,請多多指教!

文章來自:http://1ke.co/course/393

相關文章
相關標籤/搜索