JavaScript 函數(shù)原型鏈解析

JavaScript中,函數(shù)原型鏈?zhǔn)亲顝?qiáng)大也是最容易讓人迷惑的特性慈缔。長(zhǎng)期以來(lái)對(duì)于prototype__proto__的一知半解導(dǎo)致在實(shí)際開(kāi)發(fā)中經(jīng)常遇到難以排查的問(wèn)題船逮,所以有必要將JavaScript中的原型概念理解清楚。

1. __proto__ vs prototype

1.1 __proto__

JavaScript中所有對(duì)象都擁有一個(gè)__proto__用來(lái)表示其原型繼承斋扰,所謂的原型鏈也就是根據(jù)__proto__一層層向上追溯爷速。JavaScript中有一個(gè)內(nèi)置屬性[[prototype]](注意不是prototype)來(lái)表征其原型對(duì)象央星,大多數(shù)瀏覽器支持通過(guò)__proto__來(lái)對(duì)齊進(jìn)行訪問(wèn)。一個(gè)普通對(duì)象的__proto__Object.prototype:

var a = {
    'h' : 1
}

// output: true
a.__proto__ === Object.prototype

1.2 prototype

prototype是只有函數(shù)才有的屬性惫东。

當(dāng)創(chuàng)建函數(shù)時(shí)莉给,JavaScript會(huì)自動(dòng)給函數(shù)創(chuàng)建一個(gè)prototype屬性,并指向原型對(duì)象functionname.prototype廉沮。

JavaScript可以通過(guò)prototype__proto__在兩個(gè)對(duì)象之間建立一個(gè)原型關(guān)系颓遏,實(shí)現(xiàn)方法和屬性的共享,從而實(shí)現(xiàn)繼承滞时。

1.3 構(gòu)造函數(shù)創(chuàng)建對(duì)象實(shí)例

JavaScript中的函數(shù)對(duì)象有兩個(gè)不同的內(nèi)部方法:[[Call]]Construct叁幢。

如果不通過(guò)new關(guān)鍵字來(lái)調(diào)用函數(shù)(比如call,apply等)坪稽,則執(zhí)行[[Call]]方法曼玩,該種方式只是單純地執(zhí)行函數(shù)體,并不創(chuàng)建函數(shù)對(duì)象窒百。

如果通過(guò)new關(guān)鍵字來(lái)調(diào)用函數(shù)黍判,執(zhí)行的是[[Constrcut]]方法,該方法會(huì)創(chuàng)建一個(gè)實(shí)例對(duì)象篙梢,同時(shí)將該對(duì)象的__proto__屬性執(zhí)行構(gòu)造函數(shù)的prototype也即functionname.prototype,從而繼承該構(gòu)造函數(shù)下的所有實(shí)例和方法顷帖。

有了以上概念后,來(lái)看一個(gè)例子:

function Foo(firstName, lastName){
    this.firstName = firstName;
    this.lastName = lastName; 
}
Foo.prototype.logName = function(){
    Foo.combineName();
    console.log(this.fullName);
}
Foo.prototype.combineName = function(){
    this.fullName = `${this.firstName} ${this.lastName}`
}

var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName();
console.log(foo.fullName); // Sanfeng Zhang
foo.logName(); // Uncaught TypeError: Foo.combineName is not a function

明明聲明了Foo.prototype.logName,但是Foo.combineName卻出錯(cuò)渤滞,其原因在于原型鏈理解出錯(cuò)贬墩。

首先來(lái)看下foo的原型鏈:

var foo = new Foo('Sanfeng', 'Zhang'):

通過(guò)new創(chuàng)建一個(gè)函數(shù)對(duì)象,此時(shí)JavaScript會(huì)給創(chuàng)建出來(lái)對(duì)象的__proto__賦值為functionname.protoye也即Foo.prototype,所以foo.combineName可以正常訪問(wèn)combineName蔼水。其完整原型鏈為:

foo.__proto__ === Foo.prototype
foo.__proto__.__proto__ === Foo.prototype.__proto__ === Object.prototype
foo.__proto__.__proto__.__proto__ === Foo.prototype.__proto__.__proto__ === Object.prototype.__proto__ === null

[圖片上傳失敗...(image-21e9dd-1513432632315)]

接下來(lái)看下Foo的原型鏈:

直接通過(guò)Foo.combineName調(diào)用時(shí)震糖,JavaScript會(huì)從Foo.__proto__找起录肯,而Foo.__proto__指向Function.prototype,所以根本無(wú)法找到掛載在Foo.prototype上的combineName方法趴腋。

其完整原型鏈為:

Foo.__proto__ = Function.prototype;
Foo.__proto__.__proto__ = Function.prototype.__proto__;
Foo.__proto__.__proto__.__proto__ = Function.prototype.__proto__.__proto__ = Object.prototype.__proto__ = null;

[圖片上傳失敗...(image-8e415a-1513432632315)]

接下來(lái)做一下變形:

function Foo(firstName, lastName){
    this.firstName = firstName;
    this.lastName = lastName; 
}

Foo.__proto__.combineName = function() {
    console.log('combine name');
}

Foo.combineName(); // combine name
Funciton.combineName(); // combine name
var foo = new Foo('Sanfeng', 'Zhang');
foo.combineName(); // foo.combineName is not a function

這次是在Foo.__proto__上注冊(cè)的combineName,所以實(shí)例對(duì)象foo無(wú)法訪問(wèn)到,但是Function Foo可以訪問(wèn)到,另外我們看到因?yàn)?code>Foo.__proto__指向Function.prototype优炬,所以可以直接通過(guò)Function.combineName訪問(wèn)颁井。

2 原型繼承

理解清楚了__proto__prototype的聯(lián)系和區(qū)別后,我們來(lái)看下如何利用兩者實(shí)現(xiàn)原型繼承蠢护。首先來(lái)看一個(gè)例子:

function Student(props) {
    this.name = props.name || 'unamed';
}

Student.prototype.hello = function () {
    console.log('Hello ' + this.name);
}

var xiaoming = new Student({name: 'xiaoming'}); // Hello xiaoming

這個(gè)很好理解:

xiaoming -> Student.prototype -> Object.prototype -> null

接下來(lái)雅宾,我們來(lái)創(chuàng)建一個(gè)PrimaryStudent:

function PrimaryStudent(props) {
   Student.call(this, props);
   this.grade = props.grade || 1;
}

其中Student.call(this, props);僅僅執(zhí)行Student方法,不創(chuàng)建對(duì)象葵硕,參考1.3節(jié)中的[[Call]]眉抬。

此時(shí)的原型鏈為:

new PrimaryStudent() -> PrimaryStudent.prototype -> Object.prototype -> null

可以看到,目前PrimaryStudentStudent并沒(méi)有任何關(guān)聯(lián)懈凹,僅僅是借助Student.call(this, props);聲明了name屬性蜀变。

要想繼承Student必須要實(shí)現(xiàn)如下的原型鏈:

new PrimaryStudent() -> PrimaryStudent.prototype -> Student.prototype -> Object.prototype -> null

當(dāng)然可以直接進(jìn)行如下賦值:

PrimaryStudent.prototype = Student.prototype

但這樣其實(shí)沒(méi)有任何意義,如此一來(lái)介评,所以在PrimaryStudent上掛載的方法都是直接掛載到Student的原型上了库北,PrimaryStudent就顯得可有可無(wú)了。

那如何才能將方法掛載到PrimaryStudent而不是Student上呢们陆?其實(shí)很簡(jiǎn)單寒瓦,在PrimaryStudentStudent之間插入一個(gè)新的對(duì)象作為兩者之間的橋梁:

function F() {}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 此時(shí)就相當(dāng)于在new F()對(duì)象上添加方法
PrimaryStudent.prototype.getGrade = function() {
    
}

如此一來(lái)就實(shí)現(xiàn)了PrimaryStudent與Student的繼承:

new PrimaryStudent() -> new PrimaryStudent().__proto__ -> PrimaryStudent.prototype -> new F() -> new F().__proto__ -> F.prototype -> Student.prototype -> Object.prototype -> null

3 關(guān)鍵字new

實(shí)際開(kāi)發(fā)中,我們總是通過(guò)一個(gè)new來(lái)創(chuàng)建對(duì)象坪仇。那么為什么new可以創(chuàng)建一個(gè)我們需要的對(duì)象杂腰?其與普通的函數(shù)執(zhí)行有什么不同呢?
來(lái)看下下面這段代碼:

function fun() {
    console.log('fun');
}
fun();
var f = new fun();

其對(duì)應(yīng)的輸出都是一樣的:

fun
fun

但實(shí)際上椅文,兩者有著本質(zhì)的區(qū)別颈墅,前者是普通的函數(shù)執(zhí)行,也即在當(dāng)前活躍對(duì)象執(zhí)行環(huán)境內(nèi)直接執(zhí)行函數(shù)fun雾袱。
new fun()的實(shí)質(zhì)卻是創(chuàng)建了一個(gè)fun對(duì)象恤筛,其含義等同于下文代碼:

function new(constructor) {
 var obj = {}
 Object.setPrototypeOf(obj, constructor.prototype);
 return constructor.apply(obj, [...arguments].slice(1)) || obj
} 

可以看到,當(dāng)我們執(zhí)行new fun()時(shí)芹橡,實(shí)際執(zhí)行了如下操作:

  • 創(chuàng)建了一個(gè)新的對(duì)象毒坛。
  • 新對(duì)象的原型繼承自構(gòu)造函數(shù)的原型。
  • 以新對(duì)象的 this 執(zhí)行構(gòu)造函數(shù)林说。
  • 返回新的對(duì)象煎殷。如果構(gòu)造函數(shù)返回了一個(gè)對(duì)象,那么這個(gè)對(duì)象會(huì)取代整個(gè) new 出來(lái)的結(jié)果

從中也可以看到腿箩,其實(shí)new關(guān)鍵字也利用了原型繼承來(lái)實(shí)現(xiàn)對(duì)象創(chuàng)建豪直。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市珠移,隨后出現(xiàn)的幾起案子弓乙,更是在濱河造成了極大的恐慌末融,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件暇韧,死亡現(xiàn)場(chǎng)離奇詭異勾习,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)懈玻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門巧婶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人涂乌,你說(shuō)我怎么就攤上這事艺栈。” “怎么了湾盒?”我有些...
    開(kāi)封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵眼滤,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我历涝,道長(zhǎng)诅需,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任荧库,我火速辦了婚禮堰塌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘分衫。我一直安慰自己场刑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布蚪战。 她就那樣靜靜地躺著牵现,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邀桑。 梳的紋絲不亂的頭發(fā)上瞎疼,一...
    開(kāi)封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音壁畸,去河邊找鬼贼急。 笑死,一個(gè)胖子當(dāng)著我的面吹牛捏萍,可吹牛的內(nèi)容都是我干的太抓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼令杈,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼走敌!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起逗噩,我...
    開(kāi)封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤掉丽,失蹤者是張志新(化名)和其女友劉穎跌榔,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體机打,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年片迅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了残邀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡柑蛇,死狀恐怖芥挣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耻台,我是刑警寧澤空免,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站盆耽,受9級(jí)特大地震影響蹋砚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜摄杂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一坝咐、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧析恢,春花似錦墨坚、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至柑船,卻和暖如春帽撑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鞍时。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工油狂, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人寸癌。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓专筷,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親蒸苇。 傳聞我的和親對(duì)象是個(gè)殘疾皇子磷蛹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容