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):
o
和o1
的__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ū)別
-
prototype
實(shí)際上是一個(gè)指針氨鹏,指向構(gòu)造函數(shù)的原型對(duì)象 - 對(duì)象和函數(shù)都有
__proto__
屬性,但是只有函數(shù)才有prototype
屬性(.bind()返回的函數(shù)沒(méi)有prototype
屬性)压状。 - **我們可以使用
__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ì)象原型)
-
對(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
-
函數(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é):
因?yàn)?
val
是o
的動(dòng)態(tài)屬性杉辙,所以只有實(shí)例化的對(duì)象o
可以訪問(wèn)到這個(gè)實(shí)例屬性模捂。由于
Obj
本身只是一個(gè)空的構(gòu)造函數(shù),其本身不具備屬性和方法蜘矢,我們后來(lái)增加的foo
屬性和say
方法都是添加在Obj.prototype
屬性上的狂男。-
通過(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
唾琼、foo
、say()
澎剥,則在其原型Function
(Obj.__proto__
)中查找锡溯,但是也沒(méi)有找到。于是在其原型Object
(Obj.__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ì)觀察 person
和 person2
咙好,二者都是 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í)叛甫,Array
和Object
類(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í)晕拆,遇上null
和 undefined
則會(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