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

原型鏈是一種機制,指的是 JavaScript 每一個對象都有一個內置的 __proto__ 屬性指向建立它的構造函數的 prototype(原型)屬性。原型鏈的做用是爲了實現對象的繼承,要理解原型鏈,須要先從函數對象constructornewprototype__proto__ 這五個概念入手。javascript

函數對象

前面講過,在 JavaScript 裏,函數即對象,程序能夠隨意操控它們。好比,能夠把函數賦值給變量,或者做爲參數傳遞給其餘函數,也能夠給它們設置屬性,甚至調用它們的方法。下面示例代碼對「普通對象」和「函數對象」進行了區分。前端

普通對象:java

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

函數對象:git

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

簡單的說,凡是使用 function 關鍵字或 Function 構造函數建立的對象都是函數對象。並且,只有函數對象才擁有 prototype (原型)屬性。github

constructor 構造函數

函數還有一種用法,就是把它做爲構造函數使用。像 ObjectArray 這樣的原生構造函數,在運行時會自動出如今執行環境中。此外,也能夠建立自定義的構造函數,從而自定義對象類型的屬性和方法。以下代碼所示:segmentfault

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() 函數能夠經過下列任何一種方式來調用。app

// 看成構造函數使用
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__ 屬性,此處只爲解釋原型鏈而演示)。讓咱們來看下面代碼和圖片:

圖中展現了 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

那麼,假如咱們讓原型對象等於另外一個類型的實例,結果會怎麼樣呢?顯然,此時的原型對象將包含一個指向另外一個原型的指針,相應地,另外一個原型中也包含着一個指向另外一個構造函數的指針。假如另外一個原型又是另外一個類型的實例,那麼上述關係依然成立,如此層層遞進,就構成了實例與原型的鏈條。這就是所謂原型鏈的基本概念。

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

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

上圖中,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…,獲取最新動態。


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

相關文章
相關標籤/搜索