面向?qū)ο蟮恼Z(yǔ)言有一個(gè)標(biāo)志,那就是它們都有類的概念,而通過(guò)類可以創(chuàng)建任意多個(gè)具有相同屬性和方法的對(duì)象。ECMAScript中沒(méi)有類的概念,因此它的對(duì)象也和基于類的語(yǔ)言中的對(duì)象有所不同。
ECMA-262把對(duì)象定義為:無(wú)序?qū)傩缘募希鋵傩钥梢园局怠?duì)象或者函數(shù)。我們可以把ECMAScript的對(duì)象想象成散列表:無(wú)非就是一組key/value,其中value可以是數(shù)據(jù)或者函數(shù)。
理解對(duì)象
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function() {
console.log(this.name);
}
早期的JS開(kāi)發(fā)者經(jīng)常使用這個(gè)模式創(chuàng)建新對(duì)象。幾年后,對(duì)象字面量成為創(chuàng)建這種對(duì)象的首選模式。
用對(duì)象字面量創(chuàng)建上例對(duì)象如下:
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
console.log(this.name);
}
};
屬性類型
ECMAScript中有兩種屬性:數(shù)據(jù)屬性和訪問(wèn)器屬性。
1. 數(shù)據(jù)屬性
包含一個(gè)數(shù)據(jù)值的位置。在這個(gè)位置可以讀取和寫入值。數(shù)據(jù)屬性有4個(gè)描述行為的特性。
[[Configurable]]
:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性特性,能否把屬性修改為訪問(wèn)器屬性。
特性默認(rèn)值:true[[Enumerable]]
:表示能否通過(guò)for-in
循環(huán)返回屬性。
特性默認(rèn)值:true[[Writable]]
:表示能否修改屬性值(可寫)。
特性默認(rèn)值:true[[Value]]
:表示這個(gè)屬性的數(shù)據(jù)值。讀取屬性時(shí),從這個(gè)位置讀;寫入屬性值,把新值保存在這。
特性默認(rèn)值:true
直接使用對(duì)面字面量定義的屬性,他們的數(shù)據(jù)屬性全部為true
var person = {
name: "Nicholas"
}
這里創(chuàng)建的名為name的屬性,指定的值為"Nicholas"。也就是說(shuō),[[Value]]
特性將被設(shè)置為"Nicholas",而對(duì)這個(gè)值任何修改都將反應(yīng)在這個(gè)位置。
var person = {
name: "Nicholas"
}
// Writable
person.name = "John";
console.log(person.name); // John
// Enumerable
for (var i in person) {
console.log(person[i]); // John
}
// Configurable
delete person.name;
console.log(person.name); // undefined
當(dāng)我們需要修改數(shù)據(jù)屬性默認(rèn)值時(shí),必須使用ECMAScript的Object.defineProperty()
方法。
Object.defineProperty() 方法會(huì)直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性, 并返回這個(gè)對(duì)象。
Object.defineProperty(obj, prop, descriptor)
- obj:要在其上定義屬性的對(duì)象。
- prop:要定義或修改的屬性的名稱。
- descriptor:將被定義或修改的屬性描述符
這個(gè)方法接受三個(gè)參數(shù):屬性所在對(duì)象、屬性名字、一個(gè)描述符對(duì)象。
其中,描述符(descriptor)對(duì)象屬性必須是:configurable,enumerable,writable和value。設(shè)置其中的一或多個(gè)值。
var person = {};
Object.defineProperty(person,"name",{
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "John";
console.log(person.name); // "Nicholas"
在這里,name屬性被設(shè)置為只讀(writable:false),如果嘗試為它指定新值。
嚴(yán)格模式:拋出錯(cuò)誤
非嚴(yán)格模式:賦值操作將被忽略。
類似規(guī)則也適用于不可配制的屬性。例如:
var person = {};
Object.defineProperty(person,"name",{
configurable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "John";
console.log(person.name); // "Nicholas"
configurable設(shè)置為false,表示不能從對(duì)象中刪除屬性。
還記著之前的
[[Configurable]]
:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性特性,能否把屬性修改為訪問(wèn)器屬性。
特性默認(rèn)值:true
如果對(duì)這個(gè)屬性值調(diào)用delete,則在非嚴(yán)格模式下什么會(huì)不會(huì)發(fā)生,嚴(yán)格模式下會(huì)導(dǎo)致錯(cuò)誤。
并且,一旦把屬性定義為不可配置的,就不能再把它變回可配置了。此時(shí),再調(diào)用Object.defineProperty()
方法修改除writable
之外的特性,都會(huì)導(dǎo)致錯(cuò)誤。
var person = {};
Object.defineProperty(person,"name",{
configurable: false,
value: "Nicholas"
});
// 拋出錯(cuò)誤
Object.defineProperty(person,"name",{ // TypeError: Cannot redefine property: name
configurable: true,
value: "Nicholas"
});
也就是說(shuō),可以多次調(diào)用Object.defineProperty()
方法修改同一個(gè)屬性,但在把configurable
設(shè)置為false之后就會(huì)有限制了。
另外,在configurable:false情況下,在當(dāng)writable:false想要設(shè)置為true時(shí),也會(huì)報(bào)錯(cuò)。
var person = {};
Object.defineProperty(person,"name",{
configurable: false,
writable: false,
value: "Nicholas"
});
person.name = "John";
console.log(person.name); // "Nicholas"
Object.defineProperty(person,"name",{ // TypeError: Cannot redefine property: name
writable: true
});
var person = {};
Object.defineProperty(person,"name",{
configurable: false,
writable: true,
value: "Nicholas"
});
person.name = "John";
console.log(person.name); // "John"
Object.defineProperty(person,"name",{
writable: false
});
Object.defineProperty(person,"name",{ // TypeError: Cannot redefine property: name
writable: true
});
調(diào)用Object.defineProperty()
創(chuàng)建一個(gè)新屬性時(shí),如果不指定,configurable,enumerable,writable默認(rèn)值都是false。
var person = {};
Object.defineProperty(person,"name",{
value: "Nicholas"
});
console.log(person.name); // Nicholas
person.name = "John";
console.log(person.name); // Nicholas 說(shuō)明writable:false
for (var i in person) { // enumerable:false
console.log(i);
}
多數(shù)情況下,可能沒(méi)有必要利用Object.defineProperty()方法提供了這些高級(jí)功能,不過(guò),理解這些概念對(duì)理解JS對(duì)象非常有用。
1. 訪問(wèn)器屬性
訪問(wèn)器屬性不包含數(shù)據(jù)值;它們包含一對(duì)兒getter和setter函數(shù)(不過(guò),這兩個(gè)函數(shù)都不是必需的)。在讀取訪問(wèn)器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;在寫入訪問(wèn)器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。訪問(wèn)器屬性有如下4個(gè)特性:
[[Configurable]]
:表示能否通過(guò)delete刪除屬性從而重新定義屬性,能否修改屬性特性,能否把屬性修改為數(shù)據(jù)屬性。
特性默認(rèn)值:true[[Enumerable]]
:表示能否通過(guò)for-in
循環(huán)返回屬性。
特性默認(rèn)值:true[[Get]]
:在讀取屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值:undefined[[Set]]
:在寫入屬性時(shí)調(diào)用的函數(shù)。默認(rèn)值:undefined
var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book,"year",{ // year作為訪問(wèn)器屬性,內(nèi)部方法實(shí)現(xiàn)讀取和寫入
get: function() {
return this._year;
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
console.log(book.edition); // 1
book.year = 2005;
console.log(book.edition); // 2
_year
前面的下劃線是一種常用記號(hào),用戶表示只能通過(guò)對(duì)象方法訪問(wèn)的屬性。
訪問(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)?。這是使用訪問(wèn)器屬性的常見(jiàn)方式,即設(shè)置一個(gè)屬性的值會(huì)導(dǎo)致其他屬性發(fā)生變化。
PS:有同學(xué)看到這覺(jué)得疑問(wèn),為什么給book.year會(huì)調(diào)用內(nèi)部的函數(shù)?還記得之前這句話嗎?
在讀取訪問(wèn)器屬性時(shí),會(huì)調(diào)用getter函數(shù),這個(gè)函數(shù)負(fù)責(zé)返回有效的值;
在寫入訪問(wèn)器屬性時(shí),會(huì)調(diào)用setter函數(shù)并傳入新值,這個(gè)函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù)。
所以,叫“訪問(wèn)器屬性”啦23333。
相比之前的數(shù)據(jù)屬性,從名字可以一窺全豹。
數(shù)據(jù)屬性多用于存放數(shù)據(jù),訪問(wèn)器屬性則多了兩個(gè)可以讀寫的getter函數(shù)與setter函數(shù)。
當(dāng)然,不一定非要同時(shí)指定getter函數(shù)和setter函數(shù)。
- 只指定getter意味著屬性不能寫,嘗試寫入屬性會(huì)被忽略,嚴(yán)格模式下會(huì)報(bào)錯(cuò)。
- 只指定setter意味著屬性不能讀,否則在非嚴(yán)格模式下會(huì)返回undefined,嚴(yán)格模式下會(huì)報(bào)錯(cuò)。
定義多個(gè)屬性
Object.defineProperties() 方法直接在一個(gè)對(duì)象上定義新的屬性或修改現(xiàn)有屬性,并返回該對(duì)象。
Object.defineProperties(obj, props)
- obj:在其上定義或修改屬性的對(duì)象。
- props:要定義其可枚舉屬性或修改的屬性描述符的對(duì)象。對(duì)象中存在的屬性描述符主要有兩種:數(shù)據(jù)描述符和訪問(wèn)器描述符.
var book = {
_year: 2004,
edition: 1
};
Object.defineProperties(book,{
_year: {
writable: true,
value: 2004
},
edition: {
writable: true,
value: 1
},
year: {
get: function() {
return this._year;
}
},
set: function(newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
上例代碼在book對(duì)象上定義了兩個(gè)數(shù)據(jù)屬性(_year和edition)和一個(gè)訪問(wèn)器屬性(year)。
讀取屬性的特性
Object.getOwnPropertyDescriptor(obj, prop)
- obj:需要查找的目標(biāo)對(duì)象
- prop:目標(biāo)對(duì)象內(nèi)屬性名稱(String類型)
var book = {
_year: 2004,
edition: 1
};
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); // true
console.log(typeof descriptor.get); // undefined
var descriptor = Object.getOwnPropertyDescriptor(book,"year");
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ì)于訪問(wèn)器屬性year,value等于undefined,enumerable是false,而get是一個(gè)指向getter函數(shù)的指針。
在JavaScript中,可以針對(duì)任何對(duì)象——包括DOM對(duì)象和DOM對(duì)象,使用Object.getOwnProperty-Descriptor()方法。
創(chuàng)建對(duì)象
工廠模式
舊時(shí)的開(kāi)發(fā)人員在考慮到ES中無(wú)法創(chuàng)建類,開(kāi)發(fā)人員就發(fā)明了一種函數(shù),用函數(shù)來(lái)封裝以特定接口創(chuàng)建對(duì)象的細(xì)節(jié)。
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 o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
工廠模式的解決了創(chuàng)建多個(gè)相似對(duì)象的問(wèn)題,但卻沒(méi)有對(duì)象識(shí)別的問(wèn)題(即怎么樣知道一個(gè)對(duì)象的類型)。
構(gòu)造函數(shù)模式
ECMAScript中的構(gòu)造函數(shù)可用來(lái)創(chuàng)建特定類型的對(duì)象。像Object和Array這樣的原生構(gòu)造函數(shù),在運(yùn)行時(shí)會(huì)自動(dòng)出現(xiàn)在執(zhí)行環(huán)境中。此外,也可以創(chuàng)建自定義的構(gòu)造函數(shù)。從而定義自定義對(duì)象類型的屬性和方法。
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");
在這個(gè)例子中,Person()函數(shù)取代了createPerson()函數(shù),Person()中的代碼除了與createPerson()中相同的部分外,還存在以下特別之處。
- 沒(méi)有顯式創(chuàng)建對(duì)象
- 直接將屬性和方法賦給了this對(duì)象;
- 沒(méi)有return語(yǔ)句
此外,函數(shù)名使用大寫字母開(kāi)頭。按照慣例,構(gòu)造函數(shù)始終都應(yīng)該以一個(gè)大寫字母開(kāi)頭,而非構(gòu)造函數(shù)則應(yīng)該以一個(gè)小寫字母開(kāi)頭。這個(gè)做法借鑒了其他OO語(yǔ)言,主要是為了區(qū)別ES中的其他函數(shù);因?yàn)闃?gòu)造函數(shù)本身也是函數(shù),只不過(guò)可以用來(lái)創(chuàng)建對(duì)象而已。
創(chuàng)建Person的新實(shí)例,必須使用new操作符。以這種方式調(diào)用的構(gòu)造函數(shù)會(huì)經(jīng)歷以下4個(gè)步驟:
- 創(chuàng)建一個(gè)新對(duì)象(隱式)
- 將構(gòu)造函數(shù)的作用域賦值給新對(duì)象(this引用就指向這個(gè)新對(duì)象);
- 執(zhí)行構(gòu)造函數(shù)中 的代碼(為這個(gè)新處理添加屬性)
- 返回新對(duì)象
person1和person2都保存著Person的一個(gè)不同的實(shí)例。這兩個(gè)對(duì)象都有一個(gè)constructor(構(gòu)造函數(shù))屬性,該屬性指向Person。
console.log(person1.constructor === Person); // true
console.log(person2.constructor === Person); // true
對(duì)象的constructor屬性最初用來(lái)標(biāo)識(shí)對(duì)象類型的。但是,提到檢測(cè)對(duì)象類型,還是instanceof操作符可靠一些。我們這里創(chuàng)建的所有對(duì)象既是Object的實(shí)例,也是Person的實(shí)例,這一點(diǎn)通過(guò)instanceof操作符可以驗(yàn)證。
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
1. 將構(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ù),如果不經(jīng)過(guò)new操作符,那它和普通函數(shù)也沒(méi)有什么兩樣。
// constructor function
var person = new Person("Nicholas",29,"Software Engineer");
person.sayName(); // "Nicholas"
// normal function
Person("Greg",27,"Doctor"); // this引用指向window
window.sayName(); // "Greg"
// 在另一個(gè)對(duì)象的作用域調(diào)用
var o = new Object();
Person.call(o,"Kristen",25,"Nurse");
o.sayName(); // "Kristen"
2. 構(gòu)造函數(shù)的問(wèn)題
構(gòu)造函數(shù)模式雖然好用,但也并非沒(méi)有缺點(diǎn)。使用構(gòu)造函數(shù)的主要問(wèn)題,就是每個(gè)方法都要在每個(gè)實(shí)例上重新寫一遍。在前面的例子中,person1和person2都有一個(gè)名為sayName()的方法,但那兩個(gè)方法不是同一個(gè)Function的實(shí)例。他們不是同一個(gè)函數(shù)。ES中的函數(shù)是對(duì)象,因此沒(méi)定義一個(gè)函數(shù),也就是實(shí)例化了一個(gè)對(duì)象。從邏輯角度講,相當(dāng)于這樣:
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)"); // 與聲明函數(shù)邏輯上等價(jià)
}
以這種方式創(chuàng)建函數(shù),會(huì)導(dǎo)致不同的作用域鏈和標(biāo)識(shí)符解析。
console.log(person1.sayName === person2.sayName); // false
然而,創(chuàng)建兩個(gè)完成同樣任務(wù)的Function實(shí)例的確沒(méi)有必要;況且有this對(duì)象在,根本不用在執(zhí)行代碼前就把函數(shù)綁定到特定對(duì)象上面。可以通過(guò)把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外來(lái)解決這個(gè)問(wèn)題。
理解:因?yàn)閠his對(duì)象的存在,我么只需要定義一個(gè)方法(函數(shù))就夠了,通過(guò)指定this來(lái)解決這個(gè)問(wèn)題。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.sayName === person2.sayName); // true
person1和person2就共享了一個(gè)全局作用域sayName()函數(shù),但是新問(wèn)題又來(lái)了。
在全局作用域中定義的函數(shù)實(shí)際上只能被某個(gè)對(duì)象調(diào)用,這讓全局作用域有點(diǎn)名不副實(shí)。并且,如果需要定義很多方法,那么就要定義很多個(gè)全局函數(shù),那么這個(gè)自定義的引用類型就沒(méi)有絲毫封裝性可言。
原型模式
每一個(gè)函數(shù)都有一個(gè)prototype屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途包括可以由特定類型的所有實(shí)例共享的屬性和方法。
理解:在函數(shù)上的prototype對(duì)象上的屬性和方法,可以共享給函數(shù)的所有實(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();
person1.sayName(); // "Nicholas"
var person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName === person2.sayName); // true
1. 理解原型對(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òu)造函數(shù))屬性,這個(gè)屬性指向一個(gè)prototype屬性所在函數(shù)的指針。
創(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è)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMA-262第5版管這個(gè)指針叫[[Prototype]]
。雖然在腳本中沒(méi)有標(biāo)準(zhǔn)的方式訪問(wèn)[[Prototype]]
,但Firefox,Safari,Chrome在每個(gè)對(duì)象上都支持一個(gè)屬性__proto__
;而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見(jiàn)的。
雖然在所有實(shí)現(xiàn)都無(wú)法訪問(wèn)到[[Prototype]]
,但可以通過(guò)isPrototypeOf()
方法來(lái)確定對(duì)象之間是否存在這種關(guān)系。從本質(zhì)上將,如果[[Prototype]]
指向調(diào)用Object.isPrototypeOf()
方法的對(duì)象(Person.prototype),那么這個(gè)方法就會(huì)返回true。
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
ES5新增了一個(gè)新方法,叫Object.getPrototypeOf(),在所有支持的實(shí)現(xiàn)中,這個(gè)方法返回[[Prototype]]
的值。
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
這里的第一行代碼只是確定Object.getPrototypeOf()返回的對(duì)象實(shí)際就是這個(gè)對(duì)象的原型。第二行代碼取得了原型對(duì)象中name屬性的值,Nicholas。使用Object.getPrototypeOf()可以方便的取得一個(gè)對(duì)象的原型,而這在利用原型實(shí)現(xiàn)繼承的情況下是非常重要的。
每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí),都會(huì)執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索首先從對(duì)象實(shí)例開(kāi)始。如果在實(shí)例中找到了具有給定名字的屬性,則返回屬性的值;如果沒(méi)有找到,則繼續(xù)搜索指針指向的原型對(duì)象,在原型對(duì)象中查找具有給定名字的屬性,有,則返回屬性值。
雖然可以通過(guò)對(duì)象實(shí)例訪問(wèn)保存在原型中的值,但卻不能通過(guò)對(duì)象實(shí)例重寫原型中的值。如果我們?cè)趯?shí)例添加一個(gè)屬性,而該屬性與實(shí)例原型的一個(gè)屬性同名,那我們就在實(shí)例中創(chuàng)建該屬性,該屬性將會(huì)屏蔽原型中的那個(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
console.log(person2.name); // Nicholas
person1.name被一個(gè)新值給屏蔽了,因?yàn)閷?shí)例上本身存在一個(gè)自有屬性。
當(dāng)為對(duì)象實(shí)例添加一個(gè)屬性時(shí),這個(gè)屬性就會(huì)屏蔽原型對(duì)象中保存的同名屬性;換句話說(shuō),添加這個(gè)屬性會(huì)阻止我們?cè)L問(wèn)原型中的那個(gè)屬性,但不會(huì)修改屬性。即使將這個(gè)屬性設(shè)置為null,也只會(huì)在實(shí)例中設(shè)置這個(gè)屬性,而不會(huì)恢復(fù)其指向原型的鏈接。
不過(guò),使用delete操作符則可以完全刪除實(shí)例屬性,從而讓我們重新訪問(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"
console.log(person1.name); // "Nicholas"
delete person1.name;
console.log(pesron1.name); // "Nichoals"
使用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"; // "Greg" 自有屬性
console.log(person1.name); // true
console.log(person2.name); // "Nicholas" 原型屬性
console.log(person2.hasOwnProperty("name")); // false
delete person1.name;
console.log(person1.name); // "Nichoals" 原型屬性
console.log(person1.hasOwnProperty("name")); // false
2. 原型與in操作符
有兩種方式使用in操作符
- 單獨(dú)使用
- 在for-in循環(huán)中使用
單獨(dú)使用時(shí),in操作符會(huì)在通過(guò)對(duì)象能夠訪問(wèn)給定屬性時(shí)返回true,無(wú)論自有屬性還是原型屬性。
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;
console.log(person1.name); // "Nicholas"
console.log(person1.hasOwnProperty("name")); // false
console.log("name" in person1); // true
我們可以根據(jù)這兩個(gè)方法寫一個(gè)判斷屬性是否在原型上的函數(shù):
hasPrototypeProperty()
function Person() {};
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
console.log(this.name);
}
function hasPrototypeProperty(object,name) {
return !object.hasOwnProperty(name) && (name in object);
}
var person = new Person();
console.log(hasPrototypeProperty(person,"name")); // true
person.name = "Greg";
console.log(hasPrototypeProperty(person,"name")); // false
在使用for-in循環(huán)時(shí),返回的是所有能夠通過(guò)對(duì)象訪問(wèn)、可枚舉(enumerable)的屬性,其中既包括存在實(shí)例中的屬性,也包括存在于原型中的屬性。屏蔽了原型中不可枚舉屬性(即將[[Enumerable]]設(shè)置為false)的實(shí)例屬性也會(huì)在for-in循環(huán)中返回,因?yàn)楦鶕?jù)規(guī)定,所有開(kāi)發(fā)人員定義的屬性都是可枚舉的。
要取得對(duì)象上所有可枚舉的實(shí)例屬性,可以使用ES5的Object.keys()方法。這個(gè)方法接受一個(gè)對(duì)象作為參數(shù),返回一個(gè)包含所有可枚舉屬性的字符串?dāng)?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"]
Object.getOwnPropertyNames()
如果你想要得到所有實(shí)例屬性,無(wú)論它是否可枚舉,都可以使用Object.getOwnPropertyNames()
方法。
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.getOwnPropertyNames(Person.prototype);
console.log(keys);
/*
0:"constructor"
1:"name"
2:"age"
3:"job"
4:"sayName"
*/
3. 更簡(jiǎn)單的原型語(yǔ)法
沒(méi)添加一個(gè)屬性和方法都要敲一遍Person.prototype。為減少不必要的輸入,也為了從視覺(jué)上優(yōu)化封裝原型功能,更常見(jiàn)的做法是用一個(gè)包含所有屬性和方法的對(duì)象字面量來(lái)重寫整個(gè)原型對(duì)象。
function Person() {};
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
console.log(this.name);
}
}
Person.prototype設(shè)置為等于一個(gè)對(duì)象字面量形式創(chuàng)建的新對(duì)象。最終結(jié)果相同,但有一個(gè)例外,constructor屬性不再指向Person了。
原來(lái),每創(chuàng)建一個(gè)函數(shù),就會(huì)同時(shí)創(chuàng)建它的prototype對(duì)象,這個(gè)對(duì)象也會(huì)自動(dòng)獲得constructor屬性。而我們?cè)谶@里完全重寫了默認(rèn)的prototype對(duì)象,因此constructor屬性也就變成了新對(duì)象的constructor屬性(指向Object構(gòu)造函數(shù)),不在指向Person函數(shù)。此時(shí),盡管instanceo操作符還能返回正確的結(jié)果,但通過(guò)constructor已經(jīng)無(wú)法確定對(duì)象類型了。
var friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
console.log(friend.constructor == Person); // false
console.log(friend.constructor == Object); // true
在此,用instanceof操作符測(cè)試Object和Person仍然返回true,但constructor屬性則等于Object而不等于Person了。如果constructor的值真的很重要,可以將它設(shè)置回適當(dāng)?shù)闹怠?/p>
function Person() {};
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
console.log(this.name);
}
};
以上代碼特意包含了一個(gè)constructor屬性,并將它的值設(shè)置為Person,從而確保了通過(guò)該屬性能夠訪問(wèn)到適當(dāng)?shù)闹怠?br> 注意,以這種方式重設(shè)constructor屬性會(huì)導(dǎo)致它的[[Enumerable]]特性被設(shè)置為true。默認(rèn)情況下,原生的constructor屬性是不可枚舉的,因此如果使用兼容ES5的JS引擎,可以試一試Object.defineProperty()。
Object.defineProperty(Person.prototype, "constrcutor", {
enumerable: false,
value: Person
});
4. 原型的動(dòng)態(tài)性
由于在原型中查找值的過(guò)程是一次搜索,因此我們對(duì)原型對(duì)象所做的任何修改都能立刻從實(shí)例上反映出來(lái)——即使是先創(chuàng)建實(shí)例后修改原型也是如此。
var friend = new Person();
Person.prototype.sayHi = function () {
console.log("Hi");
};
friend.sayHi(); // "Hi"
實(shí)例與原型之間的松散鏈接關(guān)系,當(dāng)我們調(diào)用friend.sayHi(),首先會(huì)在實(shí)例中搜索名為sayHi的屬性,沒(méi)找到則像原型中查找,因?yàn)閷?shí)例與原型之間的鏈接不過(guò)是一個(gè)指針,而非一個(gè)副本,因此就可以在原型中找到新的sayHi屬性并返回引用在那里的函數(shù)。
盡管可以隨時(shí)為原型添加屬性和方法,并且修改能夠立即在所有對(duì)象實(shí)例中反映出來(lái),但如果是重寫整個(gè)原型對(duì)象,那么情況就不一樣了。
調(diào)用構(gòu)造函數(shù)會(huì)為實(shí)例添加一個(gè)指向最初原型的[[Prototype]]
指針,而把原型修改為另一個(gè)對(duì)象就等于切斷了這個(gè)聯(lián)系。
function Person() {};
var friend = new Person();
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function () {
console.log(this.name);
}
}
friend.sayName(); // TypeError: sayName is not a function
在這個(gè)例子中,先創(chuàng)建Person一個(gè)實(shí)例,然后又重寫了原型對(duì)象。然后調(diào)用sayName函數(shù)發(fā)生了錯(cuò)誤,因?yàn)閒riend指向的原型中不包含以該名字命名的屬性。
重寫原型對(duì)象,切斷了現(xiàn)有原型和任何之前已經(jīng)存在的對(duì)象實(shí)例之間的聯(lián)系;它們引用的仍然是最初的原型。
5. 原生對(duì)象的原型
原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創(chuàng)建的。例如,在Array.prototype中可以找到sort()方法,String.prototype中可以找到substring()方法。
console.log(typeof Array.prototype.sort); // funciton
console.log(typeof String.prototype.substring); // function
通過(guò)原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,而且也可以定義新方法。可以像修改自定義對(duì)象的原型一樣修改原生對(duì)象的原型。
String.prototype.startsWidth = function(text) {
return this.indexOf(text) === 0;
};
var msg = "Hello World";
console.log(msg.startsWith("Hello")); // true
盡管可以這樣做,但我們不推薦在產(chǎn)品化的程序中修改原生對(duì)象原型,會(huì)導(dǎo)致嚴(yán)重的意外。
6. 原型對(duì)象的問(wèn)題
原型模式省略了為構(gòu)造函數(shù)傳遞初始化參數(shù)這一環(huán)節(jié),所有實(shí)例在默認(rèn)情況下都將取得相同的屬性值。但還不是最大的問(wèn)題。
原型中所有屬性是被很多實(shí)例共享的,這種共享對(duì)于函數(shù)非常合適。對(duì)于那些包含基本類型值的屬性倒也說(shuō)得過(guò)去,可以在實(shí)例上添加同名屬性,屏蔽原型對(duì)應(yīng)屬性。
但是,對(duì)于引用類型值的屬性來(lái)說(shuō),就有問(wèn)題了:
function Person() {};
Person.prototype = {
constructor: Person,
name: "Nichoals",
age: 29,
job: "Software Engineer",
friends: ["friend.1","friend.2"],
sayName: function() {
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("friend.3");
console.log(person1.friends); // Array(3) ["friend.1", "friend.2", "friend.3"]
console.log(person2.friends); // // Array(3) ["friend.1", "friend.2", "friend.3"]
console.log(person1.friends === person1.friends); // true
假如初衷是為了在所有實(shí)例中共享一個(gè)數(shù)組,那沒(méi)什么問(wèn)題,但是,實(shí)例一般都是要有屬于自己的全部屬性的。
組合使用構(gòu)造函數(shù)模式和原型模式
構(gòu)造函數(shù)用于定義實(shí)例屬性,原型模式用于定義方法和共享的屬性。
另外,這種混成模式還支持像構(gòu)造函數(shù)傳遞參數(shù)。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["friend.1","friend.2"];
}
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
};
var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg","27",'Doctor');
person1.friends.push("friend.3");
console.log(person1.friends); // Array(3) ["friend.1", "friend.2", "friend.3"]
console.log(person2.friends); // Array(2) ["friend.1", "friend.2"]
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName); // true
這種構(gòu)造函數(shù)與原型混成的模式,是ES中使用最廣泛、認(rèn)同度最高的一種創(chuàng)建自定義類型的方法。可以說(shuō),用來(lái)定義引用類型的一種默認(rèn)模式。
動(dòng)態(tài)原型模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
if (typeof this.sayName != "function") {
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}
var friend = new Person("Nicholas",29,"Software Engineer");
friend.sayName(); // "Nicholas"
在if語(yǔ)句中,只在sayName()方法不存在的情況下,才會(huì)將它添加到原型中。這段代碼只會(huì)在初次調(diào)用構(gòu)造函數(shù)時(shí)才會(huì)執(zhí)行。此后,原型完成初始化,不需要再做什么修改了。
不過(guò)要記住,這里對(duì)原型做的修改,會(huì)立即在所有實(shí)例中得到反映 。其中,if語(yǔ)句檢查的可以是初始化之后應(yīng)該存在的任何屬性或方法——不必用一大堆if語(yǔ)句檢查每個(gè)屬性和方法;只要檢查其中一個(gè)即可。
使用動(dòng)態(tài)原型模式時(shí),不能使用對(duì)象字面量重寫原型,這樣會(huì)切斷現(xiàn)有實(shí)例與新原型之間的聯(lián)系。
穩(wěn)妥構(gòu)造函數(shù)模式
Douglas Crockford 發(fā)明了JavaScript中的穩(wěn)妥對(duì)象(durable objects)這個(gè)慨念。所謂穩(wěn)妥對(duì)象,指的是沒(méi)有公共屬性,而且其方法也不引用this對(duì)象。穩(wěn)妥對(duì)象最適合在一些安全的環(huán)境(這些環(huán)境會(huì)禁止使用this和new),或者在防止數(shù)據(jù)被其他應(yīng)用程序改動(dòng)時(shí)使用。
function Person(name,age,job) {
// 創(chuàng)建要返回的對(duì)象
var o = new Object();
// 可以在這里定義私有變量和函數(shù)
// 添加方法
o.sayName = function() {
console.log(name);
};
// 返回對(duì)象
return o;
}
在以這種模式創(chuàng)建的對(duì)象中,除了使用sayName()方法之外,沒(méi)有其他方法訪問(wèn)name的值。
可以這樣使用穩(wěn)妥的Person構(gòu)造函數(shù)。
var friend = Person("Nicholas",29,"Software Engineer");
friend.sayName(); // "Nicholas"
這樣,變量friend中保存的是一個(gè)穩(wěn)妥對(duì)象,而除了對(duì)象sayName()方法外,沒(méi)有別的方法可以訪問(wèn)數(shù)據(jù)成員。即使有其他代碼會(huì)給這個(gè)對(duì)象添加方法或數(shù)據(jù)成員,但也不可能有別的方法訪問(wèn)轉(zhuǎn)入到構(gòu)造函數(shù)中的原始數(shù)據(jù),很安全。
繼承
繼承是OO語(yǔ)言中的一個(gè)最為人津津樂(lè)道的概念。許多OO語(yǔ)言都支持兩種繼承方式:接口繼承和實(shí)現(xiàn)繼承。接口繼承值繼承方法簽名,實(shí)現(xiàn)繼承則繼承實(shí)際的方法。由于函數(shù)沒(méi)有簽名,在ES中無(wú)法實(shí)現(xiàn)接口繼承。ES只支持實(shí)現(xiàn)繼承,而且其實(shí)現(xiàn)繼承主要依靠原型鏈完成。
原型鏈
ES中描述了原型鏈的概念,并將原型鏈作為主要方法。基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。
每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。那么假如我們讓一些對(duì)象等于另一個(gè)類型的實(shí)例,結(jié)果會(huì)怎樣?顯然,此時(shí)的一些對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著一個(gè)指向另一個(gè)構(gòu)造函數(shù)的指針。假如另一個(gè)原型又是另一個(gè)類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。
實(shí)現(xiàn)原型鏈的一中基本模式:
function SuperType() {
this.property = true;
};
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
};
// 繼承了SuperType
SubType.prototype = new SuperType(); // 確立繼承關(guān)系
SubType.prototype.getSubValue = function() {
return this.subproperty;
};
var instance = new SubType();
console.log(instance.getSuperValue()); // true
上例中,定義了兩個(gè)類型:SuperType和SubType。每個(gè)類型分別有一個(gè)屬性和一個(gè)方法。它們的主要區(qū)別是SubType繼承了SuperType,而繼承是通過(guò)創(chuàng)建SuperType的實(shí)例,并將該實(shí)例賦給SubType.prototype實(shí)現(xiàn)的。
實(shí)現(xiàn)的本質(zhì)時(shí)重寫原型對(duì)象,代之以一個(gè)新類型的實(shí)例。換句話說(shuō),原來(lái)存在于SuperType實(shí)例中的所有屬性和方法,現(xiàn)在也存在于SubType.prototype中了。
上面代碼中,我們沒(méi)有使用SubType默認(rèn)提供的原型,而是給它換了一個(gè)新原型;這個(gè)新原型就是SuperType的一個(gè)實(shí)例。于是,新原型不僅具有作為一個(gè)SuperType的實(shí)例所擁有的全部屬性和方法,而且其內(nèi)部還有一個(gè)指針,指向了SuperType的原型。
要注意instance.constructor現(xiàn)在指向的是SuperType,這是因?yàn)樵瓉?lái)SubType.prototype中的constructor被重寫的 緣故。
通過(guò)實(shí)現(xiàn)原型鏈,本質(zhì)上擴(kuò)展了原型搜索機(jī)制
調(diào)用instance.getSuperValue(),1)搜索實(shí)例;2)搜索SubType.prototype;3)搜索SuperType.prototype。
1. 別忘記默認(rèn)的原型
事實(shí)上,前面例子的原型鏈還少一環(huán)。引用類型默認(rèn)繼承Object,而這個(gè)繼承也是通過(guò)原型鏈實(shí)現(xiàn)的。函數(shù)的默認(rèn)原型都是Object的實(shí)例,因此默認(rèn)原型都會(huì)包含一個(gè)內(nèi)部指針,指向Object.prototype。這也是所有自定義類型都會(huì)繼承toString(),valueOf()等默認(rèn)方法的原因。
完整的原型鏈所示:
一句話:SubType繼承了SuperType,而SuperType繼承了Object。
2. 確定原型和實(shí)例的關(guān)系
可以通過(guò)兩種方式確定原型和實(shí)例的關(guān)系。第一種是使用instanceof操作符。只要用這個(gè)操作符來(lái)測(cè)試實(shí)例與原型鏈中出現(xiàn)過(guò)的構(gòu)造函數(shù)。
console.log(instance instanceof SubType);
console.log(instance instanceof SuperType);
console.log(instance instanceof Object);
由于原型鏈的關(guān)系,測(cè)試這三個(gè)構(gòu)造函數(shù)的結(jié)果都返回了true;
第二種方式使用isPrototypeOf()。同樣,只要是原型鏈中出現(xiàn)過(guò)的原型,都可以說(shuō)是原型鏈所派生的實(shí)例的原型。因此也會(huì)返回true。
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
3. 謹(jǐn)慎的定義方法
子類型有時(shí)候需要覆蓋超類型中的某個(gè)方法,或者需要添加超類型中不存在的某個(gè)方法。但不管怎么樣,給原型添加方法的代碼一定要放在替換原型的語(yǔ)句之后。
function SuperType() {
this.prototype = true;
}
SuperType.prototype.getSuperValue = function() {
console.log(this.prototype);
};
function SubType() {
this.subprototype = false;
}
// inherit
SubType.prototype = new SuperType();
// Add new method
SubType.prototype.getSubValue = function() {
console.log()
}
// Rewrite SuperType inside method
SubType.prototype.getSuperValue = function() {
console.log(false);
}
var instance = new SubType();
instance.getSuperValue(); // false
在 SubType的實(shí)例鏈接到SuperType后定義的兩個(gè)方法,第一個(gè)方法getSubValue()被添加到了SubType中。第二個(gè)方法getSuperValue()是原型鏈已經(jīng)存在的一個(gè)方法,但重寫這個(gè)方法將會(huì)屏蔽原來(lái)的那個(gè)方法。胡那句話說(shuō),SubType的實(shí)例調(diào)用getSuperValue()時(shí),調(diào)用的就是這個(gè)重新定義的方法;通過(guò)SuperType的實(shí)例調(diào)用getSuperValue()時(shí),還會(huì)繼續(xù)調(diào)用原來(lái)的那個(gè)方法。
還有一點(diǎn)需要注意,即在通過(guò)原型鏈實(shí)現(xiàn)繼承時(shí),不能使用對(duì)象字面量創(chuàng)建原型方法。因?yàn)檫@樣做會(huì)重寫原型鏈。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
console.log(this.property);
};
function SubType() {
this.subproperty = false;
}
// 繼承SuperType的原型
SubType.prototype = new SuperType();
SubType.prototype = {
getSubValue: function() {
console.log(this.subproperty);
},
someOhterMethod: function() {
console.log(false);
}
}
var instance = new SubType();
instance.getsuper(); // Error!
以上代碼展示了剛剛SuperType的實(shí)例賦值給原型,緊接著又將原型替換成一個(gè)對(duì)象字面量而導(dǎo)致的問(wèn)題。由于現(xiàn)在的原型鏈包含的是一個(gè)Object的實(shí)例,而非SuperType的實(shí)例,因此我們?cè)O(shè)想中的原型鏈已經(jīng)被切斷——SuperType和SubType之間已經(jīng)沒(méi)有關(guān)系了。
4. 原型鏈的問(wèn)題
原型鏈雖然很強(qiáng)大,可以用它來(lái)實(shí)現(xiàn)繼承,但它也存在一些問(wèn)題。其中,最主要問(wèn)題來(lái)自包含引用類型值的原型。
function SuperType() {
this.colors = ["red","blue","green"];
}
function SubType() {};
// 繼承了SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // Array(4) ["red", "blue", "green", "black"]
var instance2 = new SubType();
console.log(instance2.colors); // Array(4) ["red", "blue", "green", "black"]
這個(gè)例子中的SuperType構(gòu)造函數(shù)定義了一個(gè)colors屬性,該屬性包含一個(gè)數(shù)組(引用類型)。SuperType的每個(gè)實(shí)例都會(huì)包含自己數(shù)組的colors屬性。當(dāng)SubType通過(guò)原型鏈繼承了SuperType之后,SubType.prototype就變成了SuperType的一個(gè)實(shí)例,因此它擁有了一個(gè)它自己的colors屬性。
但是,SubType的所有實(shí)例都會(huì)共享這一個(gè)colors屬性。而我們對(duì)instanceof1.colors的修改能夠通過(guò)instance2.colors反映出來(lái)。
原型鏈的第二個(gè)問(wèn)題是:在創(chuàng)建子類型的實(shí)例時(shí),不能向超類型的構(gòu)造函數(shù)中傳遞參數(shù)。或者應(yīng)該說(shuō):是沒(méi)有辦法在不影響所有對(duì)象實(shí)例的情況下,給超類型的構(gòu)造函數(shù)傳遞參數(shù)。
有鑒于此,實(shí)踐中很少會(huì)單獨(dú)使用原型鏈。
借用構(gòu)造函數(shù)
在解決原型中包含引用類型值所帶來(lái)問(wèn)題的過(guò)程中,開(kāi)發(fā)者開(kāi)始使用一種叫做借用構(gòu)造函數(shù)的技術(shù)(也叫偽造對(duì)象或者經(jīng)典繼承)。這種技術(shù)的基本思想相當(dāng)簡(jiǎn)單,即在子類型構(gòu)造函數(shù)的內(nèi)部調(diào)用超類型構(gòu)造函數(shù),別忘了,函數(shù)只不過(guò)是在特定環(huán)境中執(zhí)行代碼的對(duì)象,因此通過(guò)apply和call方法也可以在新創(chuàng)建的對(duì)象上執(zhí)行構(gòu)造函數(shù)。
function SuperType() {
this.colors = ["red","blue","green"];
}
function SubType() {
// 繼承了SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // ["red","blue","green","black"];
var instance2 = new SubType();
console.log(instance2.colors); // ["red","blue","green"];
代碼中加粗的那一行代碼"借調(diào)"了超類型的構(gòu)造函數(shù),通過(guò)call()方法,我們實(shí)際上是在(未來(lái)將要)新創(chuàng)建的SubType實(shí)例的環(huán)境下調(diào)用了SuperType或者函數(shù)。這樣一來(lái),就會(huì)在新SubType對(duì)象上執(zhí)行SuperType()函數(shù)定義的所有對(duì)象初始化代碼。結(jié)果,SubType的每個(gè)實(shí)例就都會(huì)具有自己的colors屬性的副本了。
這里可能有一些反直覺(jué),以下改寫幫助理解:
function SuperType() {
this.colors = ["red","blue","green"];
}
function SubType() {
// 繼承了SuperType
SuperType.call(this);
}
SuperType.call(window);
console.log(window.colors); // Array(3) ["red", "blue", "green"]
SuperType.call(SubType);
console.log(SubType.colors); // Array(3) ["red", "blue", "green"]
console.log(window.hasOwnProperty("colors")); // true
console.log(SubType.hasOwnProperty("colors")); // true
1. 傳遞參數(shù)
相對(duì)于原型鏈而言,借用構(gòu)造函數(shù)有一個(gè)很大優(yōu)勢(shì),即可以在子類型構(gòu)造函數(shù)中超類型構(gòu)造函數(shù)傳參。
function SuperType(name) {
this.name = name;
}
function SubType() {
// 繼承SuperType,同時(shí)傳遞參數(shù)
SuperType.call(this,"Nicholas");
// 實(shí)例屬性
this.age = 29;
}
var instance = new SubType();
console.log(instance.name); // "Nicholas"
console.log(instance.age); // 29
2.借用構(gòu)造函數(shù)的問(wèn)題
如果僅僅是借用構(gòu)造函數(shù),那么也將無(wú)法避免構(gòu)造函數(shù)模式存在的問(wèn)題——方法都在構(gòu)造函數(shù)中定義,因此函數(shù)復(fù)用就無(wú)從談起了。而且,超類中原型定義的方法,對(duì)子類型而言也是不可見(jiàn)的,結(jié)果所有類型都只能使用構(gòu)造函數(shù)模式。
組合繼承
組合繼承,有時(shí)候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長(zhǎng)的一種繼承模式。其背后的思路是原型鏈實(shí)現(xiàn)對(duì)象原型屬性和方法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,即通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(shí)例都有自己的屬性。
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name,age) {
// inherit
SuperType.call(this,name);
this.age = age;
}
// inherit method
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
console.log(this.age);
}
var instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");
console.log(instance1.colors); // Array(4) ["red", "blue", "green", "black"]
instance1.sayName(); // "Nicholas"
instance1.sayAge(); // 29
var instance2 = new SubType("Greg",27);
console.log(instance2.colors); // Array(3) ["red", "blue", "green"]
instance2.sayName(); // "Greg"
instance2.sayAge(); // 27
這個(gè)例子中,SuperType
構(gòu)造函數(shù)定義了兩個(gè)屬性:name
和colors
。SuperType
的原型定義了一個(gè)方法sayName()
。SubType
構(gòu)造函數(shù)在調(diào)用SuperType
構(gòu)造函數(shù)時(shí)傳入了name
參數(shù),緊接著又定義了方法sayAge()
。這樣一來(lái),就可以讓兩個(gè)不同的SubType
實(shí)例即分別擁有自己屬性———包括colors
屬性,又可以使用相同的方法了。
組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了它們的優(yōu)點(diǎn),成為JS中最常用的繼承模式。而且instanceof
和isPrototypeOf()
也能用于識(shí)別基于組合繼承創(chuàng)建的對(duì)象。
原型式繼承
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
在object()函數(shù)內(nèi)部,先創(chuàng)建一個(gè)臨時(shí)性的構(gòu)造函數(shù),然后將傳入的對(duì)象作為這個(gè)構(gòu)造函數(shù)的原型,最后返回了這個(gè)臨時(shí)類型的一個(gè)新實(shí)例。從本質(zhì)上講,object()對(duì)傳入其中的對(duì)象執(zhí)行了一次潛復(fù)制。
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // Array(5) ["Shelby", "Court", "Van", "Rob", "Barbie"]
原型式繼承,要求必須有一個(gè)對(duì)象可以作為另一個(gè)對(duì)象的基礎(chǔ)。如果有這么一個(gè)對(duì)象的話,可以把它傳遞給object()函數(shù),然后在根據(jù)具體需求對(duì)得到的對(duì)象加以修改即可。在這個(gè)例子中,可以作為另一個(gè)對(duì)象基礎(chǔ)的是person對(duì)象。于是將它傳入object函數(shù)中,然后該函數(shù)返回一個(gè)新對(duì)象。這個(gè)新對(duì)象將person作為原型,所以它的原型中就包含一個(gè)基本類型值屬性和一個(gè)引用類型值屬性。這意味著person.friends不僅屬于person所有,而且也會(huì)被anotherPerson以及yetAnotherPerson共享。
實(shí)際上,這相當(dāng)于又創(chuàng)建了person對(duì)象的兩個(gè)副本 。
ECMAScript5通過(guò)新增Object.create()方法規(guī)范化了原型式的繼承。這個(gè)方法接受兩個(gè)參數(shù):一個(gè)用作新對(duì)象原型的對(duì)象和(可選的)一個(gè)為新對(duì)象定義額外屬性的對(duì)象。在傳入一個(gè)參數(shù)的情況下,Object.create()與object()方法的行為相同。
var person = {
name: "Nicholas",
friends: ["Shelby","Court","Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // Array(5) ["Shelby", "Court", "Van", "Rob", "Barbie"]
Object.create()方法的第二個(gè)參數(shù)與Object.defineProperties()方法的第二個(gè)參數(shù)格式相同:每個(gè)屬性都是通過(guò)自己的描述符定義的。以這種方式指定的任何屬性都會(huì)覆蓋原型對(duì)象上的同名屬性,例如:
var person = {
name: "Nicholas",
friends: ["Shelby","Court","Van"]
};
var anotherPerson = Object.create(person,{
name: {value: "Greg"}
});
console.log(anotherPerson.name); // "Greg"
在沒(méi)有必要興師動(dòng)眾的創(chuàng)建構(gòu)造函數(shù),而只想讓一個(gè)對(duì)象與另一個(gè)對(duì)象保持類似的情況下,原型式繼承完全可以勝任的。
不過(guò)別忘了,包含引用類型值的屬性始終都會(huì)共享相應(yīng)的值,就像使用原型模式一樣。
寄生式繼承
寄生式繼承是與原型式繼承緊密相關(guān)的一種思路。寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似,即創(chuàng)建一個(gè)僅用于封裝繼承過(guò)程的函數(shù),該函數(shù)在內(nèi)部以某種方式來(lái)增強(qiáng)對(duì)象,最后在返回對(duì)象。
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function createAnother(original) {
var clone = object(original); // 通過(guò)調(diào)用函數(shù)創(chuàng)建一個(gè)新對(duì)象
clone.sayHi = function() { // 以某種方式增強(qiáng)這個(gè)對(duì)象
console.log("hi");
};
return clone; // 返回這個(gè)對(duì)象
}
在這個(gè)例子中,createAnother()函數(shù)接收了一個(gè)參數(shù),也就是將要作為新對(duì)象基礎(chǔ)的對(duì)象。然后,把這個(gè)對(duì)象(original)傳遞給object()函數(shù),將返回的結(jié)果賦值給clone。再為clone對(duì)象添加一個(gè)新方法sayHi(),最后返回clone對(duì)象。可以這樣使用createAnother()函數(shù):
var person = {
name: "Nicholas",
friends: ["Shelby","Court","Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi
繼承組合式繼承
組合繼承是JavaScript最常用的繼承模式,不過(guò)它也有自己的不足。組合繼承最大的問(wèn)題就是無(wú)論什么情況下,都會(huì)調(diào)用兩次超類型操作函數(shù):一次創(chuàng)建子類型原型時(shí)候,另一次在子類型構(gòu)造函數(shù)內(nèi)部。子類型最終會(huì)包含超類型對(duì)象的全部實(shí)例屬性,但我們不得不在在調(diào)用子類型構(gòu)造函數(shù)時(shí)重寫這些屬性。
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {console.log(this.name)};
function SubType(name,age) {
SuperType.call(this,name); // 第二次調(diào)用 SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); // 第一次調(diào)用SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {console.log(this.age)};
在第一次調(diào)用SuperType構(gòu)造函數(shù)時(shí),SubType.prototype會(huì)得到兩個(gè)屬性:name和colors;它們都是SuperType的實(shí)例屬性,只不過(guò)現(xiàn)在位于SubType的原型中。
當(dāng)調(diào)用SubType構(gòu)造函數(shù)時(shí),又會(huì)調(diào)用一次SuperType構(gòu)造函數(shù),這一次又在新對(duì)象上創(chuàng)建了實(shí)例屬性name和colors。于是,這兩個(gè)屬性就屏蔽了原型中兩個(gè)同名屬性。
過(guò)程如下圖:
有兩組name和colors屬性:一組在實(shí)例上,一組在SubType原型中,這就是調(diào)用兩次SuperType構(gòu)造函數(shù)的記過(guò)。解決這個(gè)問(wèn)題的方法——寄生組合式繼承。
所謂寄生組合式繼承,即通過(guò)借用構(gòu)造函數(shù)來(lái)繼承屬性,通過(guò)原型鏈的混成形式來(lái)繼承方法。其背后的基本思路是:不必為了指定子類型的原型而調(diào)用超類型的構(gòu)造函數(shù),我們所需要的無(wú)非就是超類型原型的一個(gè)副本而已。
寄生組合式繼承基本模式:
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function ineritPrototype(subType,superType) {
var prototype = object(superType.prototype); // 創(chuàng)建對(duì)象
prototype.constructor = subType; // 增強(qiáng)對(duì)象
subType.prototype = prototype; // 指定對(duì)象
}
這個(gè)示例中的inheritPrototype()函數(shù)實(shí)現(xiàn)了寄生組合式繼承的最簡(jiǎn)單形式。這個(gè)函數(shù)接受兩個(gè)參數(shù):子類型構(gòu)造函數(shù)和超類型構(gòu)造函數(shù)。在函數(shù)內(nèi)部,第一步創(chuàng)建超類型原型的一個(gè)副本。第二步是為創(chuàng)建的副本添加constructor屬性,從而彌補(bǔ)因重寫原型而失去的默認(rèn)的constructor屬性。
最有一步,將新創(chuàng)建的對(duì)象(即副本)賦值給子類型的原型。這樣,就可以調(diào)用inheritPrototype()函數(shù)的語(yǔ)句,去替換前面例子中為子類型原型賦值的語(yǔ)句了。
function object(o) {
function F() {};
F.prototype = o;
return new F();
}
function inheritPrototype(subType,superType) {
var prototype = object(superType.prototype); // 創(chuàng)建對(duì)象
prototype.constructor = subType; // 增強(qiáng)對(duì)象
subType.prototype = prototype; // 指定對(duì)象
}
function SuperType(name) {
this.name = name;
this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function() {console.log(this.name)};
function SubType(name,age) {
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function() {
console.log(this.age);
}
這個(gè)例子的高效率體現(xiàn)在它只調(diào)用了一次SuperType構(gòu)造函數(shù),并且因此避免了在SubType.prototype上面創(chuàng)建不必要、多余的屬性。與此同時(shí),原型鏈還能保持不變;因此,還能夠正常使用instanceof和isPrototypeof()。