《JavaScript 闖關記》之原型及原型鏈

劼哥stone發表於2016-12-20

原型鏈是一種機制,指的是 JavaScript 每個物件都有一個內建的 __proto__ 屬性指向建立它的建構函式的 prototype(原型)屬性。原型鏈的作用是為了實現物件的繼承,要理解原型鏈,需要先從函式物件constructornewprototype__proto__ 這五個概念入手。

函式物件

前面講過,在 JavaScript 裡,函式即物件,程式可以隨意操控它們。比如,可以把函式賦值給變數,或者作為引數傳遞給其他函式,也可以給它們設定屬性,甚至呼叫它們的方法。下面示例程式碼對「普通物件」和「函式物件」進行了區分。

普通物件:

var o1 = {};
var o2 = new Object();複製程式碼

函式物件:

function f1(){};
var f2 = function(){};
var f3 = new Function('str','console.log(str)');複製程式碼

簡單的說,凡是使用 function 關鍵字或 Function 建構函式建立的物件都是函式物件。而且,只有函式物件才擁有 prototype (原型)屬性。

constructor 建構函式

函式還有一種用法,就是把它作為建構函式使用。像 ObjectArray 這樣的原生建構函式,在執行時會自動出現在執行環境中。此外,也可以建立自定義的建構函式,從而自定義物件型別的屬性和方法。如下程式碼所示:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        console.log(this.name);
    };
}

var person1 = new Person("Stone", 28, "Software Engineer");
var person2 = new Person("Sophie", 29, "English Teacher");複製程式碼

在這個例子中,我們建立了一個自定義建構函式 Person(),並通過該建構函式建立了兩個普通物件 person1person2,這兩個普通物件均包含3個屬性和1個方法。

你應該注意到函式名 Person 使用的是大寫字母 P。按照慣例,建構函式始終都應該以一個大寫字母開頭,而非建構函式則應該以一個小寫字母開頭。這個做法借鑑自其他面嚮物件語言,主要是為了區別於 JavaScript 中的其他函式;因為建構函式本身也是函式,只不過可以用來建立物件而已。

new 操作符

要建立 Person 的新例項,必須使用 new 操作符。以這種方式呼叫建構函式實際上會經歷以下4個步驟:

  1. 建立一個新物件;
  2. 將建構函式的作用域賦給新物件(因此 this 就指向了這個新物件);
  3. 執行建構函式中的程式碼(為這個新物件新增屬性);
  4. 返回新物件。

將建構函式當作函式

建構函式與其他函式的唯一區別,就在於呼叫它們的方式不同。不過,建構函式畢竟也是函式,不存在定義建構函式的特殊語法。任何函式,只要通過 new 操作符來呼叫,那它就可以作為建構函式;而任何函式,如果不通過 new 操作符來呼叫,那它跟普通函式也不會有什麼兩樣。例如,前面例子中定義的 Person() 函式可以通過下列任何一種方式來呼叫。

// 當作建構函式使用
var person = new Person("Stone", 28, "Software Engineer");
person.sayName(); // "Stone"

// 作為普通函式呼叫
Person("Sophie", 29, "English Teacher"); // 新增到 window
window.sayName(); // "Sophie"

// 在另一個物件的作用域中呼叫
var o = new Object();
Person.call(o, "Tommy", 3, "Baby");
o.sayName(); // "Tommy"複製程式碼

這個例子中的前兩行程式碼展示了建構函式的典型用法,即使用 new 操作符來建立一個新物件。接下來的兩行程式碼展示了不使用 new 操作符呼叫 Person() 會出現什麼結果,屬性和方法都被新增給 window 物件了。當在全域性作用域中呼叫一個函式時,this 物件總是指向 Global 物件(在瀏覽器中就是 window 物件)。因此,在呼叫完函式之後,可以通過 window 物件來呼叫 sayName() 方法,並且還返回了 "Sophie" 。最後,也可以使用 call()(或者 apply())在某個特殊物件的作用域中呼叫 Person() 函式。這裡是在物件 o 的作用域中呼叫的,因此呼叫後 o 就擁有了所有屬性和 sayName() 方法。

建構函式的問題

建構函式模式雖然好用,但也並非沒有缺點。使用建構函式的主要問題,就是每個方法都要在每個例項上重新建立一遍。在前面的例子中,person1person2 都有一個名為 sayName() 的方法,但那兩個方法不是同一個 Function 的例項。因為 JavaScript 中的函式是物件,因此每定義一個函式,也就是例項化了一個物件。從邏輯角度講,此時的建構函式也可以這樣定義。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("console.log(this.name)"); // 與宣告函式在邏輯上是等價的
}複製程式碼

從這個角度上來看建構函式,更容易明白每個 Person 例項都包含一個不同的 Function 例項(sayName() 方法)。說得明白些,以這種方式建立函式,雖然建立 Function 新例項的機制仍然是相同的,但是不同例項上的同名函式是不相等的,以下程式碼可以證明這一點。

console.log(person1.sayName == person2.sayName);  // false複製程式碼

然而,建立兩個完成同樣任務的 Function 例項的確沒有必要;況且有 this 物件在,根本不用在執行程式碼前就把函式繫結到特定物件上面。因此,大可像下面這樣,通過把函式定義轉移到建構函式外部來解決這個問題。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

function sayName(){
    console.log(this.name);
}

var person1 = new Person("Stone", 28, "Software Engineer");
var person2 = new Person("Sophie", 29, "English Teacher");複製程式碼

在這個例子中,我們把 sayName() 函式的定義轉移到了建構函式外部。而在建構函式內部,我們將 sayName 屬性設定成等於全域性的 sayName 函式。這樣一來,由於 sayName 包含的是一個指向函式的指標,因此 person1person2 物件就共享了在全域性作用域中定義的同一個 sayName() 函式。這樣做確實解決了兩個函式做同一件事的問題,可是新問題又來了,在全域性作用域中定義的函式實際上只能被某個物件呼叫,這讓全域性作用域有點名不副實。而更讓人無法接受的是,如果物件需要定義很多方法,那麼就要定義很多個全域性函式,於是我們這個自定義的引用型別就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型來解決。

prototype 原型

我們建立的每個函式都有一個 prototype(原型)屬性。使用原型的好處是可以讓所有物件例項共享它所包含的屬性和方法。換句話說,不必在建構函式中定義物件例項的資訊,而是可以將這些資訊直接新增到原型中,如下面的例子所示。

function Person(){}

Person.prototype.name = "Stone";
Person.prototype.age = 28;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName();   // "Stone"

var person2 = new Person();
person2.sayName();   // "Stone"

console.log(person1.sayName == person2.sayName);  // true複製程式碼

在此,我們將 sayName() 方法和所有屬性直接新增到了 Personprototype 屬性中,建構函式變成了空函式。即使如此,也仍然可以通過呼叫建構函式來建立新物件,而且新物件還會具有相同的屬性和方法。但與前面的例子不同的是,新物件的這些屬性和方法是由所有例項共享的。換句話說,person1person2 訪問的都是同一組屬性和同一個 sayName() 函式。

理解原型物件

在預設情況下,所有原型物件都會自動獲得一個 constructor(建構函式)屬性,這個屬性包含一個指向 prototype 屬性所在函式的指標。就拿前面的例子來說,Person.prototype.constructor 指向 Person。而通過這個建構函式,我們還可繼續為原型物件新增其他屬性和方法。

雖然可以通過物件例項訪問儲存在原型中的值,但卻不能通過物件例項重寫原型中的值。如果我們在例項中新增了一個屬性,而該屬性與例項原型中的一個屬性同名,那我們就在例項中建立該屬性,該屬性將會遮蔽原型中的那個屬性。來看下面的例子。

function Person(){}

Person.prototype.name = "Stone";
Person.prototype.age = 28;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "Sophie";
console.log(person1.name);     // "Sophie",來自例項
console.log(person2.name);     // "Stone",來自原型複製程式碼

在這個例子中,person1name 被一個新值給遮蔽了。但無論訪問 person1.name 還是訪問 person2.name 都能夠正常地返回值,即分別是 "Sophie"(來自物件例項)和 "Stone"(來自原型)。當訪問 person1.name 時,需要讀取它的值,因此就會在這個例項上搜尋一個名為 name 的屬性。這個屬性確實存在,於是就返回它的值而不必再搜尋原型了。當訪問 person2. name 時,並沒有在例項上發現該屬性,因此就會繼續搜尋原型,結果在那裡找到了 name 屬性。

當為物件例項新增一個屬性時,這個屬性就會遮蔽原型中儲存的同名屬性;換句話說,新增這個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設定為 null ,也只會在例項中設定這個屬性,而不會恢復其指向原型的連線。不過,使用 delete 操作符則可以完全刪除例項屬性,從而讓我們能夠重新訪問原型中的屬性,如下所示。

function Person(){}

Person.prototype.name = "Stone";
Person.prototype.age = 28;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

person1.name = "Sophie";
console.log(person1.name);     // "Sophie",來自例項
console.log(person2.name);     // "Stone",來自原型

delete person1.name;
console.log(person1.name);     // "Stone",來自原型複製程式碼

在這個修改後的例子中,我們使用 delete 操作符刪除了 person1.name,之前它儲存的 "Sophie" 值遮蔽了同名的原型屬性。把它刪除以後,就恢復了對原型中 name 屬性的連線。因此,接下來再呼叫 person1.name 時,返回的就是原型中 name 屬性的值了。

更簡單的原型語法

前面例子中每新增一個屬性和方法就要敲一遍 Person.prototype。為減少不必要的輸入,也為了從視覺上更好地封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的物件字面量來重寫整個原型物件,如下面的例子所示。

function Person(){}

Person.prototype = {
    name : "Stone",
    age : 28,
    job: "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};複製程式碼

在上面的程式碼中,我們將 Person.prototype 設定為等於一個以物件字面量形式建立的新物件。最終結果相同,但有一個例外:constructor 屬性不再指向 Person 了。前面曾經介紹過,每建立一個函式,就會同時建立它的 prototype 物件,這個物件也會自動獲得 constructor 屬性。而我們在這裡使用的語法,本質上完全重寫了預設的 prototype 物件,因此 constructor 屬性也就變成了新物件的 constructor 屬性(指向 Object 建構函式),不再指向 Person 函式。此時,儘管 instanceof 操作符還能返回正確的結果,但通過 constructor 已經無法確定物件的型別了,如下所示。

var friend = new Person();

console.log(friend instanceof Object);        // true
console.log(friend instanceof Person);        // true
console.log(friend.constructor === Person);    // false
console.log(friend.constructor === Object);    // true複製程式碼

在此,用 instanceof 操作符測試 ObjectPerson 仍然返回 true,但 constructor 屬性則等於 Object 而不等於 Person 了。如果 constructor 的值真的很重要,可以像下面這樣特意將它設定回適當的值。

function Person(){}

Person.prototype = {
    constructor : Person,
    name : "Stone",
    age : 28,
    job: "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};複製程式碼

以上程式碼特意包含了一個 constructor 屬性,並將它的值設定為 Person ,從而確保了通過該屬效能夠訪問到適當的值。

注意,以這種方式重設 constructor 屬性會導致它的 [[Enumerable]] 特性被設定為 true。預設情況下,原生的 constructor 屬性是不可列舉的,因此如果你使用相容 ECMAScript 5 的 JavaScript 引擎,可以試一試 Object.defineProperty()

function Person(){}

Person.prototype = {
    name : "Stone",
    age : 28,
    job : "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
}; 

// 重設建構函式,只適用於 ECMAScript 5 相容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});複製程式碼

原型的動態性

由於在原型中查詢值的過程是一次搜尋,因此我們對原型物件所做的任何修改都能夠立即從例項上反映出來,即使是先建立了例項後修改原型也照樣如此。請看下面的例子。

var friend = new Person();

Person.prototype.sayHi = function(){
    console.log("hi");
};

friend.sayHi();   // "hi"(沒有問題!)複製程式碼

以上程式碼先建立了 Person 的一個例項,並將其儲存在 friend 中。然後,下一條語句在 Person.prototype 中新增了一個方法 sayHi()。即使 person 例項是在新增新方法之前建立的,但它仍然可以訪問這個新方法。其原因可以歸結為例項與原型之間的鬆散連線關係。當我們呼叫 friend.sayHi() 時,首先會在例項中搜尋名為 sayHi 的屬性,在沒找到的情況下,會繼續搜尋原型。因為例項與原型之間的連線只不過是一個指標,而非一個副本,因此就可以在原型中找到新的 sayHi 屬性並返回儲存在那裡的函式。

儘管可以隨時為原型新增屬性和方法,並且修改能夠立即在所有物件例項中反映出來,但如果是重寫整個原型物件,那麼情況就不一樣了。我們知道,呼叫建構函式時會為例項新增一個指向最初原型的 [[Prototype]] 指標,而把原型修改為另外一個物件就等於切斷了建構函式與最初原型之間的聯絡。請記住:例項中的指標僅指向原型,而不指向建構函式。看下面的例子。

function Person(){}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name : "Stone",
    age : 28,
    job : "Software Engineer",
    sayName : function () {
        console.log(this.name);
    }
};

friend.sayName();   // Uncaught TypeError: friend.sayName is not a function複製程式碼

在這個例子中,我們先建立了 Person 的一個例項,然後又重寫了其原型物件。然後在呼叫 friend.sayName() 時發生了錯誤,因為 friend 指向的是重寫前的原型物件,其中並不包含以該名字命名的屬性。

原生物件的原型

原型的重要性不僅體現在建立自定義型別方面,就連所有原生的引用型別,都是採用這種模式建立的。所有原生引用型別(ObjectArrayString,等等)都在其建構函式的原型上定義了方法。例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到 substring() 方法,如下所示。

console.log(typeof Array.prototype.sort);       // "function"
console.log(typeof String.prototype.substring); // "function"複製程式碼

通過原生物件的原型,不僅可以取得所有預設方法的引用,而且也可以定義新方法。可以像修改自定義物件的原型一樣修改原生物件的原型,因此可以隨時新增方法。下面的程式碼就給基本包裝型別 String 新增了一個名為 startsWith() 的方法。

String.prototype.startsWith = function (text) {
    return this.indexOf(text) === 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));   // true複製程式碼

這裡新定義的 startsWith() 方法會在傳入的文字位於一個字串開始時返回 true。既然方法被新增給了 String.prototype ,那麼當前環境中的所有字串就都可以呼叫它。由於 msg 是字串,而且後臺會呼叫 String 基本包裝函式建立這個字串,因此通過 msg 就可以呼叫 startsWith() 方法。

儘管可以這樣做,但我們不推薦在產品化的程式中修改原生物件的原型。如果因某個實現中缺少某個方法,就在原生物件的原型中新增這個方法,那麼當在另一個支援該方法的實現中執行程式碼時,就可能會導致命名衝突。而且,這樣做也可能會意外地重寫原生方法。

原型物件的問題

原型模式也不是沒有缺點。首先,它省略了為建構函式傳遞初始化引數這一環節,結果所有例項在預設情況下都將取得相同的屬性值。雖然這會在某種程度上帶來一些不方便,但還不是原型的最大問題。原型模式的最大問題是由其共享的本性所導致的。

原型中所有屬性是被很多例項共享的,這種共享對於函式非常合適。對於那些包含基本值的屬性倒也說得過去,畢竟(如前面的例子所示),通過在例項上新增一個同名屬性,可以隱藏原型中的對應屬性。然而,對於包含引用型別值的屬性來說,問題就比較突出了。來看下面的例子。

function Person(){}

Person.prototype = {
    constructor: Person,
    name : "Stone",
    age : 28,
    job : "Software Engineer",
    friends : ["ZhangSan", "LiSi"],
    sayName : function () {
        console.log(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("WangWu");

console.log(person1.friends);    // "ZhangSan,LiSi,WangWu"
console.log(person2.friends);    // "ZhangSan,LiSi,WangWu"
console.log(person1.friends === person2.friends);  // true複製程式碼

在此,Person.prototype 物件有一個名為 friends 的屬性,該屬性包含一個字串陣列。然後,建立了 Person 的兩個例項。接著,修改了 person1.friends 引用的陣列,向陣列中新增了一個字串。由於 friends 陣列存在於 Person.prototype 而非 person1 中,所以剛剛提到的修改也會通過 person2.friends(與 person1.friends 指向同一個陣列)反映出來。假如我們的初衷就是像這樣在所有例項中共享一個陣列,那麼對這個結果我沒有話可說。可是,例項一般都是要有屬於自己的全部屬性的。

建構函式和原型結合

所以,建構函式用於定義例項屬性,而原型用於定義方法和共享的屬性。結果,每個例項都會有自己的一份例項屬性的副本,但同時又共享著對方法的引用,最大限度地節省了記憶體。下面的程式碼重寫了前面的例子。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["ZhangSan", "LiSi"];
}

Person.prototype = {
    constructor : Person,
    sayName : function(){
        console.log(this.name);
    }
}

var person1 = new Person("Stone", 28, "Software Engineer");
var person2 = new Person("Sophie", 29, "English Teacher");

person1.friends.push("WangWu");
console.log(person1.friends);    // "ZhangSan,LiSi,WangWu"
console.log(person2.friends);    // "ZhangSan,LiSi"
console.log(person1.friends === person2.friends);    // false
console.log(person1.sayName === person2.sayName);    // true複製程式碼

在這個例子中,例項屬性都是在建構函式中定義的,而由所有例項共享的屬性 constructor 和方法 sayName() 則是在原型中定義的。而修改了 person1.friends(向其中新增一個新字串),並不會影響到 person2.friends,因為它們分別引用了不同的陣列。

這種建構函式與原型混成的模式,是目前在 JavaScript 中使用最廣泛、認同度最高的一種建立自定義型別的方法。可以說,這是用來定義引用型別的一種預設模式。

__proto__

為什麼在建構函式的 prototype 中定義了屬性和方法,它的例項中就能訪問呢?

那是因為當呼叫建構函式建立一個新例項後,該例項的內部將包含一個指標 __proto__,指向建構函式的原型。Firefox、Safari 和 Chrome 的每個物件上都有這個屬性 ,而在其他瀏覽器中是完全不可見的(為了確保瀏覽器相容性問題,不要直接使用 __proto__ 屬性,此處只為解釋原型鏈而演示)。讓我們來看下面程式碼和圖片:

《JavaScript 闖關記》之原型及原型鏈

圖中展示了 Person 建構函式、Person 的原型屬性以及 Person 現有的兩個例項之間的關係。在此,Person.prototype.constructor 指回了 PersonPerson.prototype 中除了包含 constructor 屬性之外,還包括後來新增的其他屬性。此外,要格外注意的是,雖然這兩個例項都不包含屬性和方法,但我們卻可以呼叫 person1.sayName()。這是因為內部指標 __proto__ 指向 Person.prototype,而在 Person.prototype 中能找到 sayName() 方法。

我們來證實一下,__proto__ 是不是真的指向 Person.prototype 的?如下程式碼所示:

function Person(){}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true複製程式碼

既然,__proto__ 確實是指向 Person.prototype,那麼使用 new 操作符建立物件的過程可以演變為,為例項物件的 __proto__ 賦值的過程。如下程式碼所示:

function Person(){}

// var person = new Person(); 
// 上一行程式碼等同於以下過程 ==> 
var person = {};
person.__proto__ = Person.prototype;
Person.call(person);複製程式碼

這個例子中,我先建立了一個空物件 person,然後把 person.__proto__ 指向了 Person 的原型物件,便繼承了 Person 原型物件中的所有屬性和方法,最後又以 person 為作用域執行了 Person 函式,person 便就擁有了 Person 的所有屬性和方法。這個過程和 var person = new Person(); 完全一樣。

簡單來說,當我們訪問一個物件的屬性時,如果這個屬性不存在,那麼就會去 __proto__ 裡找,這個 __proto__ 又會有自己的 __proto__,於是就這樣一直找下去,直到找到為止。在找不到的情況下,搜尋過程總是要一環一環地前行到原型鏈末端才會停下來。

原型鏈

JavaScript 中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法。其基本思想是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。簡單回顧一下建構函式、原型和例項的關係:每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。如下圖所示:(圖源:segmentfault.com,作者:manxisuo

《JavaScript 闖關記》之原型及原型鏈

那麼,假如我們讓原型物件等於另一個型別的例項,結果會怎麼樣呢?顯然,此時的原型物件將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標。假如另一個原型又是另一個型別的例項,那麼上述關係依然成立,如此層層遞進,就構成了例項與原型的鏈條。這就是所謂原型鏈的基本概念。

上面這段話比較繞口,程式碼更容易理解,讓我們來看看實現原型鏈的基本模式。如下程式碼所示:

function Father(){
    this.value = true;
}
Father.prototype.getValue = function(){
    return this.value;
};

function Son(){
    this.value2 = false;
}

// 繼承了 Father
Son.prototype = new Father();

Son.prototype.getValue2 = function (){
    return this.value2;
};

var son = new Son();
console.log(son.getValue());  // true複製程式碼

以上程式碼定義了兩個型別:FatherSon。每個型別分別有一個屬性和一個方法。它們的主要區別是 Son 繼承了 Father,而繼承是通過建立 Father 的例項,並將該例項賦給 Son.prototype 實現的。實現的本質是重寫原型物件,代之以一個新型別的例項。換句話說,原來存在於 Father 的例項中的所有屬性和方法,現在也存在於 Son.prototype 中了。在確立了繼承關係之後,我們給 Son.prototype 新增了一個方法,這樣就在繼承了 Father 的屬性和方法的基礎上又新增了一個新方法。

我們再用 __proto__ 重寫上面程式碼,更便於大家的理解:

function Father(){
    this.value = true;
}
Father.prototype.getValue = function(){
    return this.value;
};

function Son(){
    this.value2 = false;
}

// 繼承了 Father
// Son.prototype = new Father(); ==>
Son.prototype = {};
Son.prototype.__proto__ = Father.prototype;
Father.call(Son.prototype);

Son.prototype.getValue2 = function (){
    return this.value2;
};

// var son = new Son(); ==>
var son = {};
son.__proto__ = Son.prototype;
Son.call(son);

console.log(son.getValue()); // true
console.log(son.getValue === son.__proto__.__proto__.getValue); // true複製程式碼

從以上程式碼可以看出,例項 son 呼叫 getValue() 方法,實際是經過了 son.__proto__.__proto__.getValue 的過程的,其中 son.__proto__ 等於 Son.prototype,而 Son.prototype.__proto__ 又等於 Father.prototype,所以 son.__proto__.__proto__.getValue 其實就是 Father.prototype.getValue

事實上,前面例子中展示的原型鏈還少一環。我們知道,所有引用型別默然都繼承了 Obeject,而這個繼承也是通過原型鏈實現的。大家要記住,所有函式的預設原型都是 Object 的例項,因此預設原型都會包含一個內部指標 __proto__,指向 Object.prototype。這也正是所有自定義型別都會繼承 toString()valueOf() 等預設方法的根本原因。

下圖展示了原型鏈實現繼承的全部過程。(圖源:segmentfault.com,作者:manxisuo

《JavaScript 闖關記》之原型及原型鏈

上圖中,pprototype 屬性,[p]__proto__ 指物件的原型,[p] 形成的鏈(虛線部分)就是原型鏈。從圖中可以得出以下資訊:

  • Object.prototype 是頂級物件,所有物件都繼承自它。
  • Object.prototype.__proto__ === null ,說明原型鏈到 Object.prototype 終止。
  • Function.__proto__ 指向 Function.prototype

關卡

根據描述寫出對應的程式碼。

// 挑戰一
// 1.定義一個建構函式 Animal,它有一個 name 屬性,以及一個 eat() 原型方法。
// 2.eat() 的方法體為:console.log(this.name + " is eating something.")。
// 3.new 一個 Animal 的例項 tiger,然後呼叫 eat() 方法。
// 4.用 __proto__ 模擬 new Animal() 的過程,然後呼叫 eat() 方法。

var Animal = function(name){
    // 待補充的程式碼
};

var tiger = new Animal("tiger");
// 待補充的程式碼

var tiger2 = {};
// 待補充的程式碼複製程式碼
// 挑戰二
// 1.定義一個建構函式 Bird,它繼承自 Animal,它有一個 name 屬性,以及一個 fly() 原型方法。
// 2.fly() 的方法體為:console.log(this.name + " want to fly higher.");。
// 3.new 一個 Bird 的例項 pigeon,然後呼叫 eat() 和 fly() 方法。
// 4.用 __proto__ 模擬 new Bird() 的過程,然後用程式碼解釋 pigeon2 為何能呼叫 eat() 方法。

var Bird = function(name){
      // 待補充的程式碼
}

var pigeon = new Bird("pigeon");
// 待補充的程式碼

var pigeon2 = {};
// 待補充的程式碼複製程式碼
// 挑戰三
// 1.定義一個建構函式 Swallow,它繼承自 Bird,它有一個 name 屬性,以及一個 nesting() 原型方法。
// 2.nesting() 的方法體為:console.log(this.name + " is nesting now.");。
// 3.new 一個 Swallow 的例項 yanzi,然後呼叫 eat()、fly() 和 nesting() 方法。
// 4.用 __proto__ 模擬 new Swallow() 的過程,然後用程式碼解釋 yanzi2 為何能呼叫 eat() 方法。

var Swallow = function(name){
      // 待補充的程式碼
}

var yanzi = new Swallow("yanzi");
// 待補充的程式碼

var yanzi2 = {};
// 待補充的程式碼複製程式碼

更多

關注微信公眾號「劼哥舍」回覆「答案」,獲取關卡詳解。
關注 github.com/stone0090/j…,獲取最新動態。


本文對你有幫助?歡迎掃碼加入前端學習小組微信群:

《JavaScript 闖關記》之原型及原型鏈

相關文章