原生JS - 原型與原型鏈

JavaScript 常被描述為一種基于原型的語(yǔ)言 (prototype-based language)——每個(gè)對(duì)象擁有一個(gè)原型對(duì)象催首,對(duì)象以其原型為模板、從原型繼承方法和屬性。原型對(duì)象也可能擁有原型,并從中繼承方法和屬性,一層一層疹尾、以此類(lèi)推。這種關(guān)系常被稱(chēng)為原型鏈 (prototype chain)骤肛,它解釋了為何一個(gè)對(duì)象會(huì)擁有定義在其他對(duì)象中的屬性和方法纳本。

準(zhǔn)確地說(shuō),這些屬性和方法定義在Object的構(gòu)造器函數(shù)(constructor functions)之上的prototype屬性上腋颠,而非對(duì)象實(shí)例本身繁成。

1. 查看構(gòu)造函數(shù)的原型

function Obj(){}
console.log(Obj.prototype);
/*
* {
*   constructor: ? Obj(), // 構(gòu)造函數(shù)
*   __proto__: Object // 原型對(duì)象
* }
*/ 

2. 向給構(gòu)造函數(shù)的原型添加屬性和方法

在前面聲明構(gòu)造函數(shù) Obj 的時(shí)候,我們并沒(méi)有在里面寫(xiě)入任何東西∈缑担現(xiàn)在我想給這個(gè)構(gòu)造函數(shù)添加一個(gè)屬性和一個(gè)方法巾腕。

Obj.prototype.foo = "bar";
Obj.prototype.say = function(){
    console.log(this.foo);
}
console.log(Obj.prototype);

/*
* {
*   foo: "bar",
*   say: ? (),
*   constructor: ? Obj(), // 構(gòu)造函數(shù)
*   __proto__: Object // 原型對(duì)象
* }
*/ 

我們發(fā)現(xiàn),剛才添加的屬性和方法通過(guò)原型的方式添加到了構(gòu)造函數(shù) Obj 中⌒踺铮現(xiàn)在我們通過(guò)new實(shí)例化一個(gè) Obj 對(duì)象出來(lái)尊搬,同時(shí)給這個(gè)實(shí)例化的對(duì)象一個(gè)新的屬性和方法。

var o = new Obj();
o.val = 123;
o.sayHi = function(){console.log("Hi")};
console.log(o);

/*
* {
*   val: 123,
*   sayHi: ? (),
*   __proto__: {
*       foo: "bar",
*       say: ? (),
*       constructor: ? Obj(), // 構(gòu)造函數(shù)
*       __proto__: Object // 原型對(duì)象
*   }
* }
*/ 

我們發(fā)現(xiàn)土涝, o 有一個(gè) val 屬性佛寿;而在 o 的原型上,有屬性 foo 和方法 say 回铛。

我們?cè)賹?shí)例化一個(gè) Obj 對(duì)象看看狗准,但是這次,不給他增加 val 屬性了茵肃。

var o1 = new Obj();
console.log(o1);

/*
* {
*   __proto__: {
*       foo: "bar",
*       say: ? (),
*       constructor: ? Obj(), // 構(gòu)造函數(shù)
*       __proto__: Object // 原型對(duì)象
*   }
* }
*/ 

我們發(fā)現(xiàn),o1 中沒(méi)有 val 屬性袭祟;但是在 o1 的原型上验残,仍然有屬性 foo 和方法 say

由此我們可以發(fā)現(xiàn):

  • oo1__proto__ 屬性就是 Obj.prototype 巾乳, __proto__ 屬性就是我們所說(shuō)的原型
  • 我們可以再所有實(shí)例化的 Obj 對(duì)象中訪問(wèn)到原型的屬性您没,但是無(wú)法在某一個(gè)屬性中訪問(wèn)其他對(duì)象獨(dú)有的動(dòng)態(tài)方法和屬性(也叫實(shí)例方法和屬性)。例:我們無(wú)法在 o1 中訪問(wèn)到 o 中的 val 屬性胆绊。

3. prototype__proto__ 的區(qū)別

  1. prototype 實(shí)際上是一個(gè)指針氨鹏,指向構(gòu)造函數(shù)的原型對(duì)象
  2. 對(duì)象和函數(shù)都有 __proto__ 屬性,但是只有函數(shù)才有 prototype 屬性(.bind()返回的函數(shù)沒(méi)有 prototype 屬性)压状。
  3. **我們可以使用 __proto__ 去訪問(wèn)一個(gè)對(duì)象的原型對(duì)象仆抵,即圖中的 [[prototype]] **

但是沒(méi)有官方的方法用于直接訪問(wèn)一個(gè)對(duì)象的原型對(duì)象——原型鏈中的“連接”被定義在一個(gè)內(nèi)部屬性中跟继。在 JavaScript 語(yǔ)言標(biāo)準(zhǔn)中用 [[prototype]] 表示(參見(jiàn) ECMAScript)。然而镣丑,大多數(shù)現(xiàn)代瀏覽器還是提供了一個(gè)名為 __proto__ (前后各有2個(gè)下劃線)的屬性舔糖。(來(lái)源MDN - 對(duì)象原型)

  1. 對(duì)象中使用 __proto__ 給原型對(duì)象添加屬性和方法(null除外)

    function NewObj(){}
    var a = new NewObj();
    a.__proto__.aaa = "aaa";
    
    console.log(a);
    /*
    NewObj = {
        __proto__:{
            aaa: "aaa"
            constructor: ? NewObj()
            __proto__: Object
        }
    }
    */
    
    console.log(NewObj.prototype); 
    /*
    {
        aaa: "aaa",
        constructor: ? NewObj(),
        __proto__: Object
    }
    */ 
    
    console.log(a.__proto__ === NewObj.prototype); // true
    

    因?yàn)?a 的原型對(duì)象是 NewObj ,因此給 a 的原型添加屬性莺匠,就是給 NewObj 添加屬性金吗,和直接使用 NewObj.prototype 添加屬性一樣(通過(guò)下面代碼驗(yàn)證)。

    console.log(a.__proto__ === NewObj.prototype); // true
    
  1. 函數(shù)中使用 prototype__proto__ 都可以給函數(shù)或者函數(shù)的原型對(duì)象添加屬性趣竣。

    我們上面已經(jīng)通過(guò)使用 prototype 給構(gòu)造函數(shù)增加屬性和方法摇庙,這里只演示使用 __proto__ 的情況

    NewObj.__proto__.bbb = "bbb";
    
    console.dir(NewObj);
    console.dir(Obj);
    

    通過(guò)打印的結(jié)果,我們發(fā)現(xiàn)不僅僅 NewObj 中有了 bbb 這個(gè)屬性遥缕, Obj 中也有了 bbb 這個(gè)屬性卫袒。

    哪怕我們是先創(chuàng)建的 Obj ,再創(chuàng)建的 NewObj 通砍,繼而添加的 NewObj.__proto__.bbb 屬性玛臂,也是如此。

    我們通過(guò)下面的驗(yàn)證可以知道封孙,兩個(gè)構(gòu)造函數(shù)的原型都是 Function 迹冤,也就是說(shuō)它們都是 Function 的實(shí)例。那么當(dāng)我們給其中任意一個(gè)函數(shù)的 __proto__ 加屬性或方法的時(shí)候虎忌,其他的函數(shù)( Function 的實(shí)例)都可以在 __proto__ 中找到新增的屬性或方法泡徙。

    console.log(NewObj.__proto__ === Function.prototype); // true
    console.log(Obj.__proto__ === Function.prototype); // true
    

4. 實(shí)例化的對(duì)象沒(méi)有原型上已有的屬性,為什么還能訪問(wèn)到膜蠢?

目前堪藐,我們已知 Obj 屬性有一個(gè) foo 屬性和一個(gè) say 方法,實(shí)例化的對(duì)象 o 只有一個(gè) val 屬性挑围。我們?cè)囍鴱?Obj 礁竞、 Obj 原型和 o 三個(gè)角度去訪問(wèn)一下這兩個(gè)屬性和一個(gè)方法。

// Obj
console.log("Obj.val:" + Obj.val); // Obj.val:undefined
console.log("Obj.foo:" + Obj.foo); // Obj.foo:undefined
console.log("Obj.say:" + Obj.say); // Obj.say:undefined
Obj.say(); // TypeError: Obj.say is not a function

// Obj.prototype
console.log("Obj.prototype.val:" + Obj.prototype.val); // Obj.prototype.val:undefined
console.log("Obj.prototype.foo:" + Obj.prototype.foo); // Obj.prototype.foo:bar
console.log("Obj.prototype.say:" + Obj.prototype.say); // Obj.prototype.say:function(){ console.log(this.foo); }
Obj.prototype.say(); // bar

// o
console.log("o.val:" + o.val); // o.val:123
console.log("o.foo:" + o.foo); // o.foo:bar
console.log("o.say:" + o.say); // o.say:function(){ console.log(this.foo); }
o.say(); // bar

總結(jié):

  1. 因?yàn)?valo 的動(dòng)態(tài)屬性杉辙,所以只有實(shí)例化的對(duì)象 o 可以訪問(wèn)到這個(gè)實(shí)例屬性模捂。

  2. 由于 Obj 本身只是一個(gè)空的構(gòu)造函數(shù),其本身不具備屬性和方法蜘矢,我們后來(lái)增加的 foo 屬性和 say 方法都是添加在 Obj.prototype 屬性上的狂男。

  3. 通過(guò)前面的例子我們可以知道,我們?cè)L問(wèn)到的原型屬性都在 __proto__ 上品腹,而 prototype 只是指向該構(gòu)造函數(shù)的原型對(duì)象岖食。因此,當(dāng)我們?cè)L問(wèn) Obj 上的屬性時(shí)舞吭,如果 Obj 自身沒(méi)有該屬性或方法泡垃。則會(huì)在其原型對(duì)象( Obj.__proto__ )中查找這個(gè)屬性或方法析珊,如果沒(méi)有,則繼續(xù)向上( Obj.__proto__.__proto__ )查找兔毙。

    所以:

    • Obj 本身沒(méi)有 val 唾琼、 foosay()澎剥,則在其原型 FunctionObj.__proto__ )中查找锡溯,但是也沒(méi)有找到。于是在其原型 ObjectObj.__proto__.__proto__ )中查找哑姚,同樣也沒(méi)有找到三者祭饭,因此結(jié)果是undefined。
    • Obj.prototype 的指向的是 Obj 的原型對(duì)象叙量,而在 Obj.prototype 中找到了 foo 倡蝙、 say() ,因此可以打印出來(lái)绞佩。同上的原因沒(méi)有找到 val 寺鸥,因此無(wú)法打印刨疼。
    • 同樣的方法也可以知道 o 為什么三個(gè)都可以打印出來(lái)懒鉴。

5. 如果實(shí)例化的對(duì)象和原型有同名屬性或方法...

  • 給實(shí)例化的對(duì)象 o 新增一個(gè) foo 屬性姐直,而原型對(duì)象上的 foo 仍然存在姨涡。

    o.foo = "hello";
    console.log(o.foo); // hello
    console.log(o.__proto__.foo); // bar
    
  • 刪除實(shí)例化對(duì)象上的屬性

    刪除后, o.foo 打印的是原型鏈上的 foo 屬性炬搭。

    delete o.foo;
    console.log(o.foo); // bar
    console.log(o.__proto__.foo); // bar
    
  • 刪除對(duì)象原型上的屬性

    刪除后饲常,由于原型鏈上也沒(méi)有 foo 這個(gè)屬性了鳍征,所以是 undefined 涯呻。

    delete o.__proto__.foo;
    // 或執(zhí)行  delete Obj.prototype.foo;
    console.log(o.foo); // undefined
    console.log(o.__proto__.foo); // undefined
    

6. 使用 create() 創(chuàng)建對(duì)象

我們知道可以使用 Object.create() 創(chuàng)建對(duì)象凉驻,傳入的參數(shù)是一個(gè)對(duì)象。

例如:

var o2 = Object.create(o);
console.log(o2.__proto__ === o); // true
console.log(o2.__proto__.__proto__ === Obj.prototype); // true

顯然复罐, o2 的原型對(duì)象是 o (繼承)涝登。

7. 使用 constructor 創(chuàng)建對(duì)象

除了直接 new 構(gòu)造函數(shù),我們還可以通過(guò) new 實(shí)例化對(duì)象的 constructor 來(lái)創(chuàng)建一個(gè)新的實(shí)例化對(duì)象效诅。為了區(qū)分缀拭,我們新建一個(gè)構(gòu)造函數(shù),然后通過(guò)實(shí)例化傳入?yún)?shù)填帽,來(lái)看看效果。

function HowOldAreYou(age){
    this.age = age;
}
var person = new HowOldAreYou(18);
console.log(person); // HowOldAreYou {age: 18}

現(xiàn)在使用實(shí)例化對(duì)象的 constructor 來(lái)創(chuàng)建:

var person2 = new person.constructor(25);
console.log(person2); // HowOldAreYou {age: 25}

仔細(xì)觀察 personperson2 咙好,二者都是 HowOldAreYou 的實(shí)例化對(duì)象篡腌,且從控制臺(tái)直接打印的結(jié)果也能看出來(lái),二者也不屬于繼承關(guān)系勾效。下面的代碼也可以簡(jiǎn)單驗(yàn)證

console.log(person.__proto__ === person2.__proto__); // true

Tips:

通常嘹悼,我們?cè)谑褂?typeof 判斷數(shù)據(jù)類(lèi)型時(shí)叛甫, ArrayObject 類(lèi)型的結(jié)果都是 "object" 。那么杨伙,我們現(xiàn)在也可以使用 constructor 的方式對(duì)二者進(jìn)行區(qū)分其监。

console.log([].constructor === Array); // true
console.log({}.constructor === Object); // true

8. 關(guān)于 null

我在翻閱一些資料的時(shí)候,發(fā)現(xiàn)有很多地方在講解原型的時(shí)候限匣,都會(huì)單獨(dú)標(biāo)注 null除外 抖苦。這是為什么?

當(dāng)我們嘗試打印 null 的類(lèi)型

typeof null; // object

結(jié)果似乎不是我們想的那樣米死,這也讓很多人都將 null 當(dāng)做一個(gè) JavaScript 對(duì)象锌历。而事實(shí)是,這應(yīng)該算是JavaScript 語(yǔ)言本身的一個(gè) bug峦筒。 這是因?yàn)?/p>

編程語(yǔ)言最后的形式都是二進(jìn)制究西,所以 JavaScript 中的對(duì)象在底層肯定也是以二進(jìn)制表示的。在JavaScript的底層中物喷,如果前三位都是零的情況卤材,就會(huì)被判定為對(duì)象。而底層中 null 的二進(jìn)制表示都是零峦失。所以在對(duì) null 的類(lèi)型判定時(shí)扇丛,會(huì)把 null 判定為 object。

null 本身沒(méi)有任何屬性和方法宠进,有很多方法(例如 for...in... )在使用時(shí)晕拆,遇上nullundefined 則會(huì)跳過(guò)不執(zhí)行。這也幫助我們理解了材蹬,萬(wàn)物皆對(duì)象实幕,任何數(shù)據(jù)類(lèi)型的原型鏈的頂端都是 Object 。同時(shí)堤器,有下面這張圖昆庇,應(yīng)該也可以更好的理解這種中間的關(guān)系了。

參考資料:

對(duì)象原型 - MDN: https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object_prototypes

一篇文章看懂proto和prototype的關(guān)系及區(qū)別: http://www.reibang.com/p/7d58f8f45557

js的原型和原型鏈:http://www.reibang.com/p/be7c95714586

JavaScript 中的 null 是一個(gè)對(duì)象嗎: http://www.reibang.com/p/f2c5aa0fb5f0

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末闸溃,一起剝皮案震驚了整個(gè)濱河市整吆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌辉川,老刑警劉巖表蝙,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異乓旗,居然都是意外死亡府蛇,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)屿愚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)汇跨,“玉大人务荆,你說(shuō)我怎么就攤上這事∏钏欤” “怎么了函匕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蚪黑。 經(jīng)常有香客問(wèn)我盅惜,道長(zhǎng),這世上最難降的妖魔是什么祠锣? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任酷窥,我火速辦了婚禮,結(jié)果婚禮上伴网,老公的妹妹穿的比我還像新娘蓬推。我一直安慰自己,他們只是感情好澡腾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布沸伏。 她就那樣靜靜地躺著,像睡著了一般动分。 火紅的嫁衣襯著肌膚如雪毅糟。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天澜公,我揣著相機(jī)與錄音姆另,去河邊找鬼。 笑死坟乾,一個(gè)胖子當(dāng)著我的面吹牛迹辐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播甚侣,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼明吩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了殷费?” 一聲冷哼從身側(cè)響起印荔,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎详羡,沒(méi)想到半個(gè)月后仍律,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡实柠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年染苛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡茶行,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出登钥,到底是詐尸還是另有隱情畔师,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布牧牢,位于F島的核電站看锉,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏塔鳍。R本人自食惡果不足惜伯铣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望轮纫。 院中可真熱鬧腔寡,春花似錦、人聲如沸掌唾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)糯彬。三九已至凭语,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間撩扒,已是汗流浹背似扔。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留搓谆,地道東北人炒辉。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像挽拔,于是被迫代替她去往敵國(guó)和親辆脸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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