理解對(duì)象
創(chuàng)建自定義對(duì)象的最簡(jiǎn)單方式就是創(chuàng)建一個(gè)Object實(shí)例,然后再為它添加屬性和方法。
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
alert(this.name);
}
或是對(duì)象字面量語(yǔ)法式創(chuàng)建。
var person = {
name:"Nicholas",
age:29,
job:"Software Engineer",
sayName:function() {
alert(this.name);
}
};
這兩種例子所達(dá)到的效果是相同的,都有相同屬性和方法。這些屬性在創(chuàng)建時(shí)都帶有一些特征值(characteristic),JavaScript通過(guò)這些特征值來(lái)定義它們的行為。
屬性類(lèi)型
ECMA5在定義只有內(nèi)部采用的特性(attribute)時(shí),描述了屬性(property)的各種特征。為了表示特性是內(nèi)部值,該規(guī)范把他們放在了兩對(duì)兒方括號(hào)中,例如[[Enumerable]]。
ECMAScipt中有兩種屬性:數(shù)據(jù)屬性和訪(fǎng)問(wèn)器屬性。
數(shù)據(jù)屬性
數(shù)據(jù)屬性包含一個(gè)數(shù)據(jù)值的位置。在這個(gè)位置可以讀取和寫(xiě)入值。數(shù)據(jù)屬性有4個(gè)描述其行為的特性。
[[Configurable]]:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪(fǎng)問(wèn)器屬性。如直接在對(duì)象上定義屬性,默認(rèn)值為
true[[Enumerable]]:表示能否通過(guò)for-in循環(huán)返回屬性。默認(rèn)值為
true,即可枚舉的。[[Writable]]:表示能否修改屬性的值。默認(rèn)值為
true。即可寫(xiě)的。[[Value]]:包含這個(gè)屬性的數(shù)據(jù)值。讀取屬性值的時(shí)候,從這個(gè)位置讀;寫(xiě)入屬性值的時(shí)候,新值就保存在這個(gè)位置。默認(rèn)值
undefined。
var person = {
name:"Nicholas"
};
這里創(chuàng)建了一個(gè)名為name的屬性,為它指定的值是Nicholas。也就是說(shuō),[[Value]]特性將被設(shè)置為Nicholas,而對(duì)這個(gè)值的任何修改都將反映在這個(gè)位置。
如若要修改屬性默認(rèn)特性,必須使用ECMAScript5的Object.defineProperty()方法。這個(gè)方法接受三個(gè)參數(shù):屬性所在的對(duì)象、屬性的名字和一個(gè)描述的對(duì)象。其中描述符(descriptor)對(duì)象的屬性必須是:configurable、enumerable、writable和value。設(shè)置其中的一或多個(gè)值,可以修改對(duì)應(yīng)的特性值。
var person = {};
Object.defineProperty(person,"name",{
writable:false,
value:"Nicholas"
});
console.log(person.name); //"Nicholas"
person.name = "Greg";
console.log(person.name); //"Nicholas"
這個(gè)例子創(chuàng)建了一個(gè)名為name的屬性,它的值是只讀的。這個(gè)屬性的值是不可修改的,如果嘗試的為它指定新值,則在非嚴(yán)格模式下,賦值操作將被忽視;嚴(yán)格模式下,賦值操作將會(huì)拋出錯(cuò)誤。
"use strict"
var person = {};
Object.defineProperty(person,"name",{
writable:false,
value:"Nicholas"
});
console.log(person.name); //"Nicholas"
person.name = "Greg";
// "Uncaught TypeError: Cannot assign to read only property 'name' of #<Object>"
類(lèi)似規(guī)則也適用與不可配置的屬性。例如:
var person = {};
Object.defineProperty(person,"name",{
configurable:false,
value:"Nicholas"
})
console.log(person.name); //"Nicholas"
delete person.name;
console.log(person.name); //"Nicholas"
把configurable設(shè)置為false,表示不能從對(duì)象中刪除屬性。如果對(duì)這個(gè)屬性調(diào)用delete,則在非嚴(yán)格模式下什么也不會(huì)發(fā)生,在嚴(yán)格模式下會(huì)導(dǎo)致錯(cuò)誤。而且,一旦把屬性定義為不可配置的,就不能再把它便會(huì)可配置了。此時(shí),再調(diào)用Object.defineProperty()方法修改除writable之外的特性,都會(huì)導(dǎo)致錯(cuò)誤:
var person = {};
Object.defineProperty(person,"name",{
configurable:false,
value:"Nicholas"
})
Object.defineProperty(person,"name",{
configurable:true,
value:"Nicholas"
})
//"Uncaught TypeError: Cannot redefine property: name"
或
var person = {};
Object.defineProperty(person,"name",{
configurable:false,
value:"Nicholas"
})
person.name = "Jason";
console.log(person.name); // Nicholas
栗子
var person = {};
Object.defineProperty(person,"name",{
configurable:false,
value:"Nicholas"
})
Object.defineProperty(person,"name",{
writable:true
})
person.name = "Jason";
console.log(person.name);
// configurable為false,writable是唯一可修改的特性
// 哪怕writable為true,也無(wú)法重新設(shè)置屬性值
可以多次調(diào)用Object.defineProperty()方法修改同一個(gè)屬性,但在把congfigurable特性設(shè)置為false之后就會(huì)有限制了。
在調(diào)用Object.defineProperty()方法創(chuàng)建一個(gè)新的屬性時(shí),如果不指定,configurable、enumerable、writable特性值的默認(rèn)值都是false。
簡(jiǎn)化而言就是,按照普通的使用方法(對(duì)象字面量,構(gòu)造對(duì)象)定義對(duì)象,創(chuàng)建屬性時(shí),特性默認(rèn)值都為true,value特性默認(rèn)值undefined。
但是使用Object.defineProperty()來(lái)創(chuàng)建屬性,則三個(gè)特性值都將為false。
IE瀏覽器支持?jǐn)?shù)據(jù)屬性的最低版本支持為IE8
訪(fǎng)問(wèn)器屬性
訪(fǎng)問(wèn)器屬性不包含數(shù)據(jù)值;它們包含一對(duì)兒getter和setter函數(shù)就(不過(guò),這兩個(gè)函數(shù)都不是必須的)。在讀取訪(fǎng)問(wèn)器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;在寫(xiě)入訪(fǎng)問(wèn)器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。訪(fǎng)問(wèn)器屬性有如下4個(gè)特性。
[[Configurable]]:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為訪(fǎng)問(wèn)器屬性。如直接在對(duì)象上定義屬性,默認(rèn)值為
true[[Enumerable]]:表示能否通過(guò)for-in循環(huán)返回屬性。默認(rèn)值為
true,即可枚舉的。[[Get]]:在讀取屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為undefined。
[[Set]]:在寫(xiě)入屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值為undefined。
訪(fǎng)問(wèn)器屬性不能直接定義,必須使用Object.defineProperty()來(lái)定義。
var book = {
_year:2004,
edition:1
};
Object.defineProperty(book,"year",{
get:function(){
return this._year;
},
set:function(newValue) {
if (newValue>2004) {
this._year = newValue;
this.edition = this.edition+newValue-2004;
}
}
});
book.year = 2005;
console.log(book.edition); // 2
以上代碼創(chuàng)建了一個(gè)book對(duì)象,并給它定義兩個(gè)默認(rèn)屬性:_year和edition。_year前面的下劃線(xiàn)是一種常用的記號(hào),用于表示只能通過(guò)對(duì)象方法訪(fǎng)問(wèn)的屬性。而訪(fǎng)問(wèn)器屬性year則包含一個(gè)getter函數(shù)和一個(gè)setter函數(shù)。getter函數(shù)返回_year的值,setter函數(shù)通過(guò)計(jì)算來(lái)確定正確的版本,因此,把year屬性修改為2005會(huì)導(dǎo)致_year變成2005,而edition變?yōu)?。這是使用訪(fǎng)問(wèn)器屬性的常見(jiàn)方式,即設(shè)置一個(gè)屬性的值會(huì)導(dǎo)致其他屬性發(fā)生變化。
不一定非要同時(shí)指定getter和setter。只指定getter意味屬性是不能寫(xiě),嘗試寫(xiě)入屬性會(huì)被忽略。在嚴(yán)格模式下,嘗試寫(xiě)入只指定了getter函數(shù)的屬性會(huì)拋出錯(cuò)誤。類(lèi)似地,只指定setter函數(shù)的屬性也不能讀,否則在非嚴(yán)格模式下會(huì)返回undefined,嚴(yán)格模式下會(huì)拋出錯(cuò)誤。
"use strict"
var book = {
_year:2004,
edition:1
};
Object.defineProperty(book,"year",{
get:function(){
return this._year;
}
});
book.year = 2004;
// "Uncaught TypeError: Cannot set property year of #<Object> which has only a getter"
// 嘗試寫(xiě)入失敗
在這些方法之前,一般使用兩個(gè)非標(biāo)準(zhǔn)方法:__defineGetter__(),__defineSetter__()
var book = {
_year:2004,
edition:1
};
// 定義訪(fǎng)問(wèn)器屬性的舊有方法
book.__defineGetter__("year",function() {
return this._year;
});
book.__defineSetter__("year",function(newValue) {
if (newValue>2004) {
this._year = newValue,
this.edition += newValue-2004;
}
});
book.year = 2005; // 寫(xiě)入屬性
console.log(book.edition); // 2
// 修改了_year
在不支持Object.defineProperty()方法的瀏覽器中不能修改Configurable和Enumerable。
定義多個(gè)屬性
由于為對(duì)象定義多個(gè)屬性的可能性很大,ES5定義了一個(gè)Object.defineProperties()方法。利用這個(gè)方法可以通過(guò)描述符一次定義多個(gè)屬性。這個(gè)方法接收兩個(gè)對(duì)象參數(shù):第一個(gè)對(duì)象是要添加和修改其屬性的對(duì)象,第二個(gè)對(duì)象的屬性與第一個(gè)對(duì)象中要添加或修改的屬性一一對(duì)應(yīng)。
var book = {};
Object.defineProperties(book,{
_year:{
value:2004,
writable:true
},
edition:{
value:1,
writable:true
},
year:{
get:function() {
return this._year;
},
set:function(newValue) {
if (newValue>2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
book.year = 2018;
console.log(book.edition); //15
支持Object.defineProperties()方法的瀏覽器有IE9+、FireFox4+、Safari5+、Opera12+和Chrome。
支持屬性的特性
使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個(gè)方法接收兩個(gè)參數(shù):屬性所在的對(duì)象和要讀取其描述符的屬性名稱(chēng)。返回值是一個(gè)對(duì)象,如果是訪(fǎng)問(wèn)器屬性,這個(gè)對(duì)象的屬性有configurable、enumerable、get和set;如果是數(shù)據(jù)屬性,這個(gè)對(duì)象的屬性有configurable,enumerable,writable,value。
var book = {};
Object.defineProperties(book,{
_year:{
value:2004
},
edition:{
value:1
},
year:{
get:function() {
return this._year;
},
set:function(newValue) {
if (newValue>2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value); // 2004
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // undefined 對(duì)象是數(shù)據(jù)屬性
var descriptor = Object.getOwnPropertyDescriptor(book,"year"); // 對(duì)象是訪(fǎng)問(wèn)器屬性
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // function
對(duì)于數(shù)據(jù)屬性_year,value等于最初的值,configurable是false,而get等于undefined。
對(duì)于訪(fǎng)問(wèn)器屬性year,value等于undefined,enumerable是false,而get是一個(gè)指向getter函數(shù)的指針。
創(chuàng)建對(duì)象
工廠模式
function createPerson(name,age,job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return 0;
}
var person1 = createPerson("Nicholas",29,"Software Engineer");
var person2 = createPerson("Greg",27,"Doctor");
函數(shù)createPerson()能夠根據(jù)接受的參數(shù)來(lái)構(gòu)建一個(gè)包含所有必要信息的person對(duì)象??梢詿o(wú)數(shù)次調(diào)用這個(gè)函數(shù),而每次它都返回一個(gè)包含三個(gè)屬性一個(gè)方法的對(duì)象。工廠模式雖然解決了創(chuàng)建多個(gè)相似對(duì)象的問(wèn)題,但卻沒(méi)有解決對(duì)象識(shí)別的問(wèn)題。
構(gòu)造函數(shù)模式
ECMAScript中的構(gòu)造函數(shù)可用來(lái)創(chuàng)建特定類(lèi)型的對(duì)象。像Object和Array這樣的原生構(gòu)造函數(shù),在運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中。此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù),從而定義自定義對(duì)象類(lèi)型的屬性和方法。
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("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg",27,"Doctor");
Person()函數(shù)取代了createPerson()函數(shù)。Person()函數(shù)中的代碼除了與createPerson()相同的部分外,還存在一些不同之初。
- 沒(méi)有顯式地創(chuàng)建對(duì)象
- 直接將屬性和方法賦給了this對(duì)象
- 沒(méi)有return語(yǔ)句
此外還應(yīng)該注意到函數(shù)名Person使用的是大寫(xiě)字母P。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以大寫(xiě)字母開(kāi)頭,而非構(gòu)造函數(shù)則應(yīng)該以小寫(xiě)字母開(kāi)頭。這個(gè)做法借鑒其他OO語(yǔ)言,主要是為了區(qū)別于ECMAScript中的其他函數(shù);因?yàn)闃?gòu)造函數(shù)本身也是函數(shù),只不過(guò)可以用來(lái)創(chuàng)建對(duì)象而已。
要?jiǎng)?chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用構(gòu)造函數(shù)會(huì)經(jīng)歷4個(gè)步驟:
- 創(chuàng)建一個(gè)新對(duì)象;
- 將構(gòu)造函數(shù)的作用域給新對(duì)象(因此this就指向了新對(duì)象);
- 執(zhí)行構(gòu)造函數(shù)中的代碼(為這個(gè)新對(duì)象添加屬性);
- 返回新對(duì)象。
person1和person2分別保存著Person的一個(gè)不同的實(shí)例。這兩個(gè)對(duì)象都有一個(gè)constructor(構(gòu)造函數(shù))屬性,該屬性指向Person。
對(duì)象的constructor屬性最初是用來(lái)表示對(duì)象類(lèi)型的。但是提到檢測(cè)對(duì)象類(lèi)型,還是instanceof操作符可靠一些。
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
創(chuàng)建自定義的構(gòu)造函數(shù)意味著將來(lái)可以將它的實(shí)例標(biāo)識(shí)為一種特定的類(lèi)型;而這正是構(gòu)造函數(shù)模式勝過(guò)工廠模式的地方。person1和person2之所以同時(shí)是Object的實(shí)例,是因?yàn)樗袑?duì)象均繼承自O(shè)bject。
將構(gòu)造函數(shù)當(dāng)做函數(shù)
構(gòu)造函數(shù)與其他函數(shù)唯一區(qū)別,在于調(diào)用它們的方式不同。不過(guò),構(gòu)造函數(shù)畢竟也是函數(shù),不存在定義構(gòu)造函數(shù)的特殊語(yǔ)法。任何函數(shù),只要通過(guò)new操作符來(lái)調(diào)用,那它就可以作為構(gòu)造函數(shù);而任何函數(shù),如果不通過(guò)new操作符來(lái)調(diào)用,那它跟普通函數(shù)也沒(méi)任何區(qū)別。
function Person(name,age,job) {
this.name= name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
// 當(dāng)做構(gòu)造函數(shù)調(diào)用
var person = new Person("Nicholas",29,"Software Engineer");
person.sayName(); // "Nicholas"
// 作為普通函數(shù)調(diào)用
Person("Greg",28,"Doctor"); //添加到window
window.sayName(); // "Greg"
// 在另一個(gè)對(duì)象的作用域中調(diào)用
var o = new Object();
Person.call(o,"Kristen",25,"Nurse");
o.sayName(); // "Kristen"
可以看到,不使用new操作符調(diào)用Person()的情況,屬性和方法都被添加給window對(duì)象。
最后,也可以使用call()或者apply()在某個(gè)特殊對(duì)象的作用域中調(diào)用Person()函數(shù),這里是在對(duì)象o的作用域中調(diào)用的,因此調(diào)用后的o就擁有了所有屬性和sayName()方法。
構(gòu)造函數(shù)的問(wèn)題
構(gòu)造函數(shù)模式雖然好用,但也并不是沒(méi)有缺點(diǎn)。
構(gòu)造函數(shù)的主要問(wèn)題,就是每個(gè)方法都要在實(shí)例上重新創(chuàng)建一遍。person1和person2都有一個(gè)名為sayName()的方法,但那兩個(gè)方法不是同一個(gè)Function的實(shí)例。ECMAScript中的函數(shù)是對(duì)象,因此每定義一個(gè)函數(shù),也就是實(shí)例化了一個(gè)對(duì)象。從邏輯角度講,構(gòu)造函數(shù)也可以這樣定義。
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function(
"console.log(this.name)"
)
}
從這個(gè)角度上看構(gòu)造函數(shù),更容易明白每個(gè)Person實(shí)例都包含一個(gè)不同的Function實(shí)例(以顯示name屬性)。以這種方式創(chuàng)建函數(shù),會(huì)導(dǎo)致不同的作用域鏈和標(biāo)識(shí)符解析,但創(chuàng)建Function新實(shí)例的機(jī)制仍然是相同的。因此,不同實(shí)例上的同名函數(shù)時(shí)不相等的,以下證明:
var person1 = new Person();
var person2 = new Person();
console.log(person1.sayName == person2.sayName); // false
但是創(chuàng)建兩個(gè)一樣的功能的Function實(shí)例沒(méi)有的確沒(méi)有必要;況且有this對(duì)象在,根本不同在執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面。因此,大可像下面這樣,通過(guò)把函數(shù)定義轉(zhuǎn)移到外部來(lái)解決問(wèn)題。
function sayName() { // 全局函數(shù)sayName,任何Perzon的實(shí)例均可使用sayName函數(shù)
console.log(this.name);
}
var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg",27,"Dcoter");
person1.sayName();
person2.sayName();
將sayName屬性設(shè)置成全局的sayName函數(shù),這樣一來(lái),sayName包含的是一個(gè)指向函數(shù)的指針,因此person1和person2對(duì)象就共享了全局作用域中定義的同一個(gè)sayName函數(shù)。
但是新問(wèn)題是:在全局作用域中定義的函數(shù)實(shí)際上只能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。而更讓人無(wú)法接受的是:如果對(duì)象需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),這樣這個(gè)自定義引用類(lèi)型就絲毫沒(méi)有封裝性可言了,但我們可以用原型模式來(lái)解決。
原型模式
我們創(chuàng)建的每一個(gè)函數(shù)都要一個(gè)prototype原型屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類(lèi)型的所有實(shí)例共享的屬性和方法。如果按照字面意思理解,那么prototype就是通過(guò)調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)實(shí)例對(duì)象的原型對(duì)象。
換句話(huà)說(shuō),不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息,而是可以這些信息直接添加到原型對(duì)象中:
function Person() {
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
};
var person1 = new Person();
person1.sayName(); // Nicholas
var person2 = new Person();
person2.sayName(); // Nicholas
console.log(person1.sayName == person2.sayName); // true
我們將sayName函數(shù)方法和所有屬性直接添加到了Person的prototype屬性中,構(gòu)造函數(shù)變成了空函數(shù)。即便如此,也仍然可以通過(guò)調(diào)用構(gòu)造函數(shù)來(lái)創(chuàng)建新對(duì)象,而且新對(duì)象還會(huì)具有相同的屬性和方法。但與構(gòu)造函數(shù)不同的是,新對(duì)象的這些屬性和方法是由所有實(shí)例共享的。換句話(huà)說(shuō),person1和person2訪(fǎng)問(wèn)的都是同一組屬性和同一個(gè)sayName函數(shù)。要理解原型模式的工作原理,必須先理解ECMAScript中原型對(duì)象的性質(zhì)。
理解原型對(duì)象
無(wú)法什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象。默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor屬性,這個(gè)屬性指向prototype屬性所在函數(shù)的指針。
如前面的就是:
console.log(Person.prototype.constructor == Person); // true
創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型對(duì)象默認(rèn)只會(huì)取得constructor屬性;至于其他方法,則都是從Object繼承而來(lái)的。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例之后,該實(shí)例內(nèi)部將包含一個(gè)指針,指向構(gòu)造函數(shù)對(duì)象原型對(duì)象。ECMA-262第5版管這個(gè)指針叫[[Prototype]] 。雖然在腳本中沒(méi)有標(biāo)準(zhǔn)方式訪(fǎng)問(wèn)prototype,但FireFox、Safari和Chrome在每個(gè)對(duì)象上都支持一個(gè)屬性proto;而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見(jiàn)的。不過(guò),要明確的真正重要的一點(diǎn)就是,這個(gè)鏈接存在于實(shí)例與構(gòu)造函數(shù)之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間。如圖:

上圖展示了Person構(gòu)造函數(shù)、Person的原型屬性以及Person現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。
Person.prototype指向了原型對(duì)象,而Person.prototype.constructor又指回了Person。原型對(duì)象中除了包含constru屬性之外,還包括后來(lái)添加的其他屬性。Person的每個(gè)實(shí)例person1和person2都包含一個(gè)內(nèi)部屬性,該屬性?xún)H僅指向了Person.prototype;換句話(huà)說(shuō),它們和構(gòu)造函數(shù)沒(méi)有直接關(guān)系。此外,雖然這兩個(gè)實(shí)例都不包含屬性和方法,但我們可以調(diào)用person1.sayName(),這是通過(guò)查找對(duì)象屬性的過(guò)程來(lái)實(shí)現(xiàn)的。
雖然在所有實(shí)現(xiàn)中都無(wú)法訪(fǎng)問(wèn)到[[Prototype]],但可以通過(guò)isPrototypeOf()方法來(lái)確定對(duì)象之間是否存在這種關(guān)系。從本質(zhì)講,如果[[Prototype]]指向調(diào)用isPrototypeOf()方法的對(duì)象,那么這個(gè)方法就返回true。
function Person() {
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
};
var person1 = new Person();
var person2 = new Person();
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
// MDN栗子
function Foo() {}
function Bar() {}
function Baz() {}
Bar.prototype = Object.create(Foo.prototype);
Baz.prototype = Object.create(Bar.prototype);
var baz = new Baz();
// console.log(Baz.prototype.isPrototypeOf(baz)); // true
// console.log(Bar.prototype.isPrototypeOf(baz)); // true
// console.log(Foo.prototype.isPrototypeOf(baz)); // true
// console.log(Object.prototype.isPrototypeOf(baz)); // true
// 將Foo的原型創(chuàng)建到Bar的原型中,將Bar原型創(chuàng)建到Baz的原型中,構(gòu)造原型鏈
// 所以Baz的實(shí)例可以通過(guò)__proto__查找Baz.prototype,并一直向上搜尋到Bar和Foo的原型中。
ES5新增一個(gè)新方法,叫Object.getPrototypeOf(),在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]的值。
console.log(
Object.getPrototypeOf(person1) == Person.prototype
); // true
console.log(
Object.getPrototypeOf(person1).name; // "Nicholas"
);
這里第一類(lèi)代碼只是確定Object.getPrototypeOf()返回的對(duì)象實(shí)際就是這個(gè)對(duì)象的原型。
第二行第二類(lèi)代碼取得原型中name屬性的值。
使用Object.getPrototypeOf()可以方便地取得一個(gè)對(duì)象的原型,而這在利用原型實(shí)現(xiàn)繼承的情況下是十分重要的。
代碼讀取某個(gè)對(duì)象屬性時(shí),都是按照一個(gè)搜索鏈執(zhí)行的,首先從對(duì)象本身開(kāi)始,有,則返回,沒(méi)有,繼續(xù)沿著原型鏈向上......這就是原型鏈的通俗抽象描述。
雖然可以通過(guò)對(duì)象實(shí)例訪(fǎng)問(wèn)保存在原型中的值,但是卻不能通過(guò)對(duì)象實(shí)例重寫(xiě)原型中的值。如果我們?cè)趯?shí)例中添加一個(gè)屬性,和實(shí)例原型中的屬性同名,那屬性就會(huì)自動(dòng)屏蔽原型中的那個(gè)屬性。
function Person() {
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
console.log(person1.name); // "Greg"————來(lái)自實(shí)例
console.log(person2.name); // "Nicholas"————來(lái)自原型
添加一個(gè)屬性只會(huì)阻止我們?cè)L問(wèn)原型中的屬性,但不會(huì)修改它。即使將它設(shè)置為null,也只會(huì)在實(shí)例中設(shè)置這個(gè)屬性,而不會(huì)恢復(fù)其指向原型的鏈接。
var person1 = new Person();
var person2 = new Person();
person1.name = null;
console.log(person1.name); // null
console.log(person1.__proto__.name); // "Nicholas"
不過(guò),可以使用delete操作符刪除實(shí)例屬性,從而重新訪(fǎng)問(wèn)到原型中的屬性。
function Person() {
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
console.log(person1.name); //"Greg" 來(lái)自原型
console.log(person2.name); // "Nicholas" 來(lái)自原型
delete person1.name;
console.log(person1.name); // "Nicholas"
delete person1.name; // 再次刪除name屬性會(huì)發(fā)生什么?
console.log(person1.name); // "Nicholas"
delete person1.__proto__.name; // 必須通過(guò)__proto__才能訪(fǎng)問(wèn)person1的原型(prototype)
console.log(person1.name); // undefined
hasOwnProperty
使用hasOwnProperty()方法可以檢測(cè)一個(gè)屬性是存在于實(shí)例中,還是存在于原型中。這個(gè)方法(不要忘了它是從Object繼承來(lái)的)只在給定屬性存在于對(duì)象實(shí)例中時(shí),才會(huì)返回true。
function Person() {
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
person1.name = "Greg"; // 來(lái)自實(shí)例
console.log(person1.name);
console.log(person1.hasOwnProperty("name")); // true
console.log(person2.name); // 來(lái)自原型
console.log(person2.hasOwnProperty("name")); // false
delete person1.name;
console.log(person1.name); // "Nicholas" 刪除了實(shí)例中的name屬性,則會(huì)讀取原型中的
console.log(person1.hasOwnProperty("name")); // false
通過(guò)這個(gè)方法,什么時(shí)候訪(fǎng)問(wèn)的實(shí)例屬性,什么時(shí)候訪(fǎng)問(wèn)的是原型屬性就一清二楚了。調(diào)用person1.hasOwnProperty("name")時(shí),只有當(dāng)person1自身內(nèi)部具有name屬性才會(huì)返回true,因?yàn)橹挥羞@時(shí)候name才是一個(gè)實(shí)例屬性,而非原型屬性。
下圖展示實(shí)現(xiàn)與原型的關(guān)系。

原型與in操作符
有兩種方式使用in操作符:?jiǎn)为?dú)使用和在for in循環(huán)中使用。在單獨(dú)使用中,in操作符會(huì)在通過(guò)對(duì)象能夠訪(fǎng)問(wèn)給定屬性時(shí)返回true,無(wú)論該屬性存在于實(shí)例中還是原型中。
function Person() {
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
};
var person1 = new Person();
var person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
person1.name = "Greg";
console.log(person1.name); // "Greg" 自有屬性
console.log(person1.hasOwnProperty("name")); // true
console.log("name" in person1); // true
console.log(person2.name); // "Nicholas" 繼承屬性
console.log(person2.hasOwnProperty("name")); // false
console.log("name" in person2); // true
delete person1.name; // 刪除了自有屬性name
console.log(person1.name); // "Nicholas" 繼承屬性
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
以上,name屬性要么是直接在對(duì)象上訪(fǎng)問(wèn)到,要么是通過(guò)原型訪(fǎng)問(wèn)到的。因此調(diào)用in始終返回true,無(wú)論該屬性存在于實(shí)例中還是原型匯總。
同時(shí)使用hasOwnProperty()方法和in操作符,就可以確定該屬性到底是存在于對(duì)象中,還是存在于原型中。
function hasPrototypeProperty(object,name) {
return !object.hasOwnProperty(name) && (name in object);
}
在使用for in循環(huán)時(shí),返回的是所有能夠通過(guò)對(duì)象訪(fǎng)問(wèn)、可枚舉的(enumerable)屬性,其中,既包括存在于實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(enumerable屬性為false)的實(shí)例屬性也會(huì)在for-in循環(huán)中返回,因此根據(jù)規(guī)定,所有開(kāi)發(fā)人員定義的屬性都是可枚舉的,只有IE8及更早版本存在例外。
IE早期版本有一個(gè)Bug,即屏蔽不可枚舉屬性的實(shí)例屬性不會(huì)出現(xiàn)在for-in循環(huán)中。
var o = {
toString:function() {
return "My Object";
}
};
for (var prop in o) {
if (prop == "toString") {
console.log("Found toString"); // 在IE中不會(huì)顯示
}
}
當(dāng)以上代碼運(yùn)行時(shí),應(yīng)該會(huì)有一個(gè)警告框,表明找到了toString方法。這里的對(duì)象o定義了一個(gè)名為toString()的方法,該方法屏蔽了原型中(不可枚舉)的toString()方法。在IE中,由于其實(shí)現(xiàn)認(rèn)為原型的toString方法被打上了值為false的enumerable標(biāo)記,因此應(yīng)該跳過(guò)該屬性,結(jié)果我們不會(huì)看到警告框。
要取得對(duì)象上所有可枚舉的實(shí)例屬性,可以使用ECMAScript5的object.keys()方法。這個(gè)方法接收一個(gè)對(duì)象為參數(shù),返回一個(gè)包含所有可枚舉屬性的字符數(shù)組。例如:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
var keys = Object.keys(Person.prototype);
console.log(keys); // ["name","age","job","sayName"]
var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1keys = Object.keys(p1);
console.log(p1keys); // ["name","age"]
變量keys保存一個(gè)數(shù)組,數(shù)組中是可枚舉屬性的集合,順序和在for-in循環(huán)中是一樣的。如果通過(guò)Person的實(shí)例調(diào)用,則object.keys()返回的數(shù)組只包含"name"和”age"兩個(gè)實(shí)例屬性。
如果你想要得到所有實(shí)例屬性,無(wú)論它是否可枚舉,都可以使用Object.getOwnPropertyNames()方法。