Stories

Detail Return Return

JavaScript原型鏈:從構造函數、原型、對象實例的關係説起 - Stories Detail

一開始看MDN的JavaScript指南,沒看明白。主要原因是關於構造函數(constructor)、原型(prototype)、對象實例(object, instance)之間關係的描述太少;直接就給我整個原型鏈讓我挺懵逼的。
於是靠百度來搞懂。我覺得先從這三者關係入手,然後回頭理解原型鏈更容易。

相關資料:
(側重關係)構造函數、對象實例、原型對象三者之間的關係
(側重原型鏈)JS重點整理之JS原型鏈徹底搞清楚

一、關係:一個層級中構造函數、原型、對象實例的關係
我認為在原型鏈、層級體系中,最不重要的反而是對象實例。但是,它能起到入口的作用,幫助我們訪問、探尋原型、構造函數。同時,要討論三者關係,就只討論一個層級內的三者,先忘掉繼承、其他層級的事情。

1、預備:構建原型鏈、層級的“柴犬代碼”
下面先用代碼構建Shiba->Dog->Animal這條原型鏈,共三個層級:(我們稱這段代碼為“柴犬代碼”,可以先跳過柴犬代碼,看下面構造函數、原型、對象實例三者相互訪問的代碼)

function Animal() {
    this.type = 'animal';
    this.name = 'default'
    this.age = 0;
}
Animal.prototype.sayHi = function() {
    console.log('Hi, I am a(n) ' + this.type + '. My name is ' + this.name + '.');
}

function Dog() {
    this.type = 'dog';
}
Dog.prototype = new Animal();

function Shiba(name='default', age=0) {
    this.type = 'shiba';
    this.name = name;
    this.age = age;
}
Shiba.prototype = new Dog();

2、從對象實例出發訪問原型、構造函數
我們聲明一個Shiba的對象實例gougou,然後通過gougou訪問Shiba這一層級的原型、構造函數:

var gougou = new Shiba('gougou', 2);
console.log(gougou.__proto__); // 原型
console.log(gougou.__proto__.constructor); // 構造函數
console.log(gougou.__proto__.constructor === Shiba); // true

這裏要説明的問題是:
(1) 對象實例可以通過__proto__訪問原型。(不要問為什麼)
(2) 原型可以通過constructor訪問構造函數。(因為最後一行輸出為true)

2、從構造函數出發訪問原型
實際上,構造函數和原型之間可以雙向訪問:

console.log(Shiba.prototype); // 原型
console.log(Shiba.prototype === gougou.__proto__); // true

這裏説明的問題是:
構造函數可以通過prototype訪問原型。(因為第二行輸出為true)

3、小結三者的訪問方式
小結:
(1) 對象實例:自己聲明的變量名。
(2) 原型:使用對象實例的__proto__屬性訪問;如果知道構造函數名稱(類名),可以用構造函數的prototype屬性訪問。
(3) 構造函數:如果是自定義的構造函數,自己就知道構造函數名稱;但很多時候是JavaScript內置的構造函數,可以通過原型的constructor屬性訪問。

一般情況下,要想訪問當前層次的構造函數、原型,是以對象實例作為入口,先訪問原型,再從原型訪問構造函數。

4、構造函數、原型、對象實例的關係、分工
我認為,構造函數、原型、對象實例三者的訪問方式已經能説明他們的關係了。那麼,為什麼要有這樣的結構分工呢?好處是什麼?
從作用上來説:
(1) 構造函數居核心、支配地位,持有原型,並指導對象實例的生成(new)。
(2) 原型一方面幫助構造函數存儲方法,供構造函數生成的對象調用,避免對象實例各自存儲一份方法(避免浪費空間);另一方面直接形成原型鏈,構建了層級結構、繼承關係。
(3) 對象實例應該作為原型鏈、層級體系的附屬品。對象實例的屬性是構造函數指導生成的;對象實例的方法,是構造函數持有的原型提供的。構造函數可以生產無數個對象實例。

二、原型鏈:將原型串成鏈、搭建層級
1、原型鏈繼承:“顯式串聯”層級
我們回頭看那段柴犬代碼。代碼中已經構建好Shiba->Dog->Animal這條原型鏈,注意到“串起來”的兩行代碼:

Dog.prototype = new Animal();
Shiba.prototype = new Dog();

這裏要説明的問題是:
將Dog層級中的原型,指定為Animal層級的一個對象實例,就形成了Dog->Animal這段原型鏈,Dog繼承了Animal的屬性、方法。(Dog對Animal説:拿來吧你)
Shiba和Dog的關係也一樣。因此,Shiba->Dog->Animal這條原型鏈構建好了。

2、繼續上溯:堆__proto__就完事
既然當前層級的原型是上一層級的對象實例,那麼我們可以順藤摸瓜,沿着原型鏈上溯各個層次直到null。null沒有原型,它是原型鏈的最頂端或者終點。
在柴犬代碼中,以對象實例gougou為入口,訪問它的原型gougou.__proto__,這是在Shiba層級;同時,原型又是Dog層級的對象實例,繼續通過__proto__屬性訪問……如此逐層往上即可。代碼如下:

console.log('Shiba層級的原型:\n', gougou.__proto__);
console.log('Shiba層級的構造函數:\n', gougou.__proto__.constructor);

console.log('Dog層級的原型:\n', gougou.__proto__.__proto__);
console.log('Dog層級的構造函數:\n', gougou.__proto__.__proto__.constructor);

console.log('Animal層級的原型:\n', gougou.__proto__.__proto__.__proto__);
console.log('Animal層級的構造函數:\n', gougou.__proto__.__proto__.__proto__.constructor);

這裏已經有點數不清有多少個__proto__了。反正這裏説明的問題是:
(1) 確實可以沿着原型鏈往上訪問各個層級的原型,以及構造函數。
(2) 當前層級的原型,就是更高層級的對象實例(柴犬代碼中用type屬性做了標記,可以分辨是Animal對象實例還是Dog對象實例)。

3、完整的原型鏈:上溯至null
注意到Animal層級打印的原型,直接就是一個對象字面量了。實際上,這就是Objct層級的一個對象實例。完整的原型鏈是:Shiba->Dog->Animal->Object->null。
不想再從gougou出發寫那麼多__proto__了,還有另一種方式訪問Animal層級的原型:構造函數Animal的prototype屬性。上溯代碼如下,這次我們驗證為主:

console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

這裏説明的問題是:
通過Animal.prototype.__proto__獲取到了上一層級的原型,剛好等於Object層級的原型,也就是説Animal的上一層級就是Object。但是Object層級再往上就沒有所謂層級了,只剩一個null了。“null層級”只有原型null,沒有構造函數。

完成。
最後,分享下關於JavaScript原型鏈學習的個人感受:
(1) 擺脱傳統繼承中子類繼承父類的慣性思維。JavaScript中的構造函數看着像是“類名”,實際上和繼承沒啥關係,原型鏈繼承靠的是原型。
(2) 擺脱“原型”字眼隱含的繼承含義。這可能是我自己想當然了,看到到“對象的原型”這種字眼就想當然認為原型是對象的“父類”。實際上,原型和對象應該放到同一層級內討論;不同層級我們用原型説話。

user avatar toopoo Avatar yinzhixiaxue Avatar front_yue Avatar littlelyon Avatar anchen_5c17815319fb5 Avatar hard_heart_603dd717240e2 Avatar shuirong1997 Avatar jiavan Avatar xiaolei_599661330c0cb Avatar zzd41 Avatar nqbefgvs Avatar hyfhao Avatar
Favorites 81 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.