JavaScript 作為一門面對對象語言,但是卻不支持接口繼承,只支持實現繼承。JavaScript 中實現繼承就是通過原型鏈來實現。
簡介
首先了解構造函數、原型對象和實例之間的關系。
- 通過 new 操作符加構造函數的方式可以創建實例。
- 構造函數具有相應的原型對象。
- 在原型對象中定義的原型方法在所有實例中都能夠訪問到。
原理
接下來了解它們之間的關系是如何構建的。
構造函數
在 JavaScript 中聲明構造函數的時候,都會為這個函數創建一個 prototype 屬性。
//聲明構造函數
function Person(name) {
this.name = name;
}

在瀏覽器下看 Person 構造函數函數就是以上這樣,prototype 屬性指向了這個構造函數的原型對象。
(實際上聲明任何函數都會有這個屬性,只不過在構造函數中這個屬性才能夠發揮作用)
原型對象
聲明構造函數之后會為這個構造函數創建一個原型對象,為這個原型對象寫入屬性就可以在實例中訪問到。
//聲明構造函數
function Person(name) {
this.name = name;
}
//為原型對象寫入屬性和方法
Person.prototype.age = 18;
Person.prototype.sayName = function() {
console.log(this.name);
};
在瀏覽器中展示如下

在瀏覽器中可以清楚地看到它的內容,其中 constructor 屬性指回了構造函數本身。constructor 屬性和 prototype 屬性構建起了構造函數和原型對象之間的相互關系。
實例
用 new 操作符可以進行實例化,創建這個構造函數的一個實例。
//聲明構造函數
function Person(name) {
this.name = name;
}
//為原型對象寫入屬性和方法
Person.prototype.age = 18;
Person.prototype.sayName = function() {
console.log(this.name);
};
//實例化
var person1 = new Person("demonly");
new 操作符的作用就是首先新建一個對象,然后在這個對象中執行構造函數中的代碼,最終返回這個對象。
依然還是在瀏覽器中看這個對象。

其中包含有構造函數寫入的 name 屬性和一個 _proto_ 屬性。 _proto_ 屬性指向這個實例的構造函數的原型對象,這是由瀏覽器生成的一個內部屬性,無法被訪問也無法修改。

由實例訪問原型對象中的屬性和方法就是通過 _proto_ 屬性。當訪問一個屬性或者方法時,首先會尋找實例中是否存在這個屬性,如果沒有的話就會到 _proto_ 指向的原型對象中尋找。因此只需要在原型對象中添加屬性和方法就讓這個構造函數創建的所有實例都能訪問到,包括在添加屬性之前實例化的實例。
為了更加直觀地表現它們的關系畫了這樣一張圖。

原型鏈
原型鏈的實現就是重寫構造函數的 prototype 屬性的指向,從而使得瀏覽器在實例中訪問屬性的時候能夠在自定義的對象中找到。
我們現在再加入一個構造函數。
//聲明構造函數
function Person(name) {
this.name = name;
}
//新建構造函數
function Animal() {};
//為新類型的原型對象寫入方法
Animal.prototype.breath = function() {
console.log("i can breath");
}
//繼承
Person.prototype = new Animal();
//為原型對象寫入屬性和方法
Person.prototype.sayName = function() {
console.log(this.name);
};
//實例化
var person1 = new Person("demonly");
繼承的實現部分我們改寫了 Person 構造函數的 prototype 的指向,使它指向了 Animal 類型的一個實例。在瀏覽器中我們也可以看到 _proto_ 屬性的指向變成了 Animal 的實例。

我們在 Person 類型的實例中也可以訪問 Animal 類型的方法。

當我們調用 breath 方法的時候,瀏覽器會首先在這個實例本身中尋找 breath 方法,沒有找到就轉為到 _proto_ 所指向的 Person 的原型對象(即 Animal 類型的實例)中尋找,依然沒有找到就轉為到 _proto_ 所指向的 Animal 的原型對象中尋找。
原型鏈就是這樣通過 _proto_ 屬性的逐級連接實現的。如果再改寫 Animal 的原型對象,那么就可以再增加原型鏈的長度。

這里需要注意的是,重寫原型對象僅僅改變了 Person 的prototype 屬性的指向,而在這之前寫入的原型方法都還會留在原來的原型對象上,在這之前實例化的實例的 _proto_ 屬性也依然會指向原來的原型對象。