本文繼續(xù)對JavaScript高級程序設(shè)計第四版 第八章 對象、類與面向?qū)ο缶幊?進行學(xué)習(xí)
在第六章第一節(jié)亏推,曾經(jīng)簡單介紹過創(chuàng)建Object学赛。
一、創(chuàng)建Object
顯式地創(chuàng)建 Object 的實例有兩種方式吞杭。第一種是使用 new 操作符和 Object 構(gòu)造函數(shù)盏浇,如下所示:
let person = new Object();
person.name = "Nicholas";
person.age = 29;
另一種方式是使用對象字面量(object literal)表示法。在使用對象字面量表示法定義對象時芽狗,并不會實際調(diào)用 Object 構(gòu)造函數(shù)绢掰。
let person = {
name: "Nicholas",
age: 29
};
雖然使用哪種方式創(chuàng)建 Object 實例都可以,但實際上開發(fā)者更傾向于使用對象字面量表示法童擎。這是因為對象字面量代碼更少滴劲,看起來也更有封裝所有相關(guān)數(shù)據(jù)的感覺。
二顾复、屬性的類型
屬性分兩種:數(shù)據(jù)屬性和訪問器屬性班挖。
1. 數(shù)據(jù)屬性
- [[Configurable]]:表示屬性是否可以通過 delete 刪除并重新定義,是否可以修改它的特性芯砸,以及是否可以把它改為訪問器屬性萧芙。默認情況下,所有直接定義在對象上的屬性的這個特性都是 true假丧,如前面的例子所示双揪。
- [[Enumerable]]:表示屬性是否可以通過 for-in 循環(huán)返回。默認情況下虎谢,所有直接定義在對象上的屬性的這個特性都是 true盟榴,如前面的例子所示。
- [[Writable]]:表示屬性的值是否可以被修改婴噩。默認情況下擎场,所有直接定義在對象上的屬性的這個特性都是 true羽德,如前面的例子所示。
- [[Value]]:包含屬性實際的值迅办。這就是前面提到的那個讀取和寫入屬性值的位置宅静。這個特性的默認值為 undefined。
let person = {};
Object.defineProperty(person, "name", {
writable: false,
value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"
這個例子創(chuàng)建了一個名為 name 的屬性并給它賦予了一個只讀的值"Nicholas"站欺。這個屬性的值就不能再修改了姨夹,在非嚴格模式下嘗試給這個屬性重新賦值會被忽略。在嚴格模式下矾策,嘗試修改只讀屬性的值會拋出錯誤磷账。
Object.defineProperty()方法接收 3 個參數(shù):要給其添加屬性的對象、屬性的名稱和一個描述符對象贾虽。
2.訪問器屬性
訪問器屬性是不能直接定義的逃糟,必須使用 Object.defineProperty()。下面是一個例子:
// 定義一個對象蓬豁,包含偽私有成員 year_和公共成員 edition
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year", {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
});
book.year = 2018;
console.log(book.edition); // 2
在這個例子中绰咽,對象 book 有兩個默認屬性:year_和 edition。year_中的下劃線常用來表示該屬性并不希望在對象方法的外部被訪問地粪。另一個屬性 year 被定義為一個訪問器屬性取募,其中獲取函數(shù)簡單地返回 year_的值,而設(shè)置函數(shù)會做一些計算以決定正確的版本(edition)蟆技。因此玩敏,把 year 屬性修改為 2018 會導(dǎo)致 year_變成 2018,edition 變成 2付魔。這是訪問器屬性的典型使用場景聊品,即設(shè)置一個屬性值會導(dǎo)致一些其他變化發(fā)生。
獲取函數(shù)和設(shè)置函數(shù)不一定都要定義几苍。只定義獲取函數(shù)意味著屬性是只讀的翻屈,嘗試修改屬性會被忽略。在嚴格模式下妻坝,嘗試寫入只定義了獲取函數(shù)的屬性會拋出錯誤伸眶。類似地,只有一個設(shè)置函數(shù)的屬性是不能讀取的刽宪,非嚴格模式下讀取會返回 undefined厘贼,嚴格模式下會拋出錯誤。
3.Object.defineProperties()
在一個對象上同時定義多個屬性的可能性是非常大的圣拄。為此嘴秸,ECMAScript 提供了bject.defineProperties()方法。
let book = {};
Object.defineProperties(book, {
year_: {
value: 2017
},
edition: {
value: 1
},
year: {
get() {
return this.year_;
},
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue;
this.edition += newValue - 2017;
}
}
}
});
這段代碼在 book 對象上定義了兩個數(shù)據(jù)屬性 year_和 edition,還有一個訪問器屬性 year岳掐。最終的對象跟上一節(jié)示例中的一樣凭疮。唯一的區(qū)別是所有屬性都是同時定義的,并且數(shù)據(jù)屬性的configurable串述、enumerable 和 writable 特性值都是 false执解。
4.讀取屬性的特性
使用 Object.getOwnPropertyDescriptor()方法可以取得指定屬性的屬性描述符。這個方法接收兩個參數(shù):屬性所在的對象和要取得其描述符的屬性名纲酗。
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"
ECMAScript 2017 新增了 Object.getOwnPropertyDescriptors()靜態(tài)方法衰腌。這個方法實際上會在每個自有屬性上調(diào)用 Object.getOwnPropertyDescriptor()并在一個新對象中返回它們。
三觅赊、合并對象
JavaScript 開發(fā)者經(jīng)常覺得“合并”(merge)兩個對象很有用右蕊。更具體地說,就是把源對象所有的本地屬性一起復(fù)制到目標對象上茉兰。有時候這種操作也被稱為“混入”(mixin)尤泽,因為目標對象通過混入源對象的屬性得到了增強欣簇。
/**
* 多個源對象
*/
dest = {};
result = Object.assign(dest, { a: 'foo' }, { b: 'bar' });
console.log(result); // { a: foo, b: bar }
Object.assign()實際上對每個源對象執(zhí)行的是淺復(fù)制规脸。如果多個源對象都有相同的屬性,則使用最后一個復(fù)制的值熊咽。
四莫鸭、對象標識及相等判定
在 ECMAScript 6 之前,有些特殊情況即使是===操作符也無能為力:
// 這些是===符合預(yù)期的情況
console.log(true === 1); // false
console.log({} === {}); // false
console.log("2" === 2); // false
// 這些情況在不同 JavaScript 引擎中表現(xiàn)不同横殴,但仍被認為相等
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// 要確定 NaN 的相等性被因,必須使用極為討厭的 isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
為改善這類情況,ECMAScript 6 規(guī)范新增了 Object.is()衫仑,這個方法與===很像梨与,但同時也考慮到了上述邊界情形。這個方法必須接收兩個參數(shù):
console.log(Object.is(true, 1)); // false
console.log(Object.is({}, {})); // false
console.log(Object.is("2", 2)); // false
// 正確的 0文狱、-0粥鞋、+0 相等/不等判定
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// 正確的 NaN 相等判定
console.log(Object.is(NaN, NaN)); // true
要檢查超過兩個值,遞歸地利用相等性傳遞即可:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) &&
(rest.length < 2 || recursivelyCheckEqual(...rest));
}
五瞄崇、增強的對象語法
ECMAScript 6 為定義和操作對象新增了很多極其有用的語法糖特性呻粹。這些特性都沒有改變現(xiàn)有引擎的行為,但極大地提升了處理對象的方便程度苏研。
1. 屬性值簡寫
在給對象添加變量的時候等浊,開發(fā)者經(jīng)常會發(fā)現(xiàn)屬性名和變量名是一樣的。例如:
let name = 'Matt';
let person = {
name: name
};
console.log(person); // { name: 'Matt' }
為此摹蘑,簡寫屬性名語法出現(xiàn)了筹燕。簡寫屬性名只要使用變量名(不用再寫冒號)就會自動被解釋為同名的屬性鍵。如果沒有找到同名變量,則會拋出 ReferenceError撒踪。以下代碼和之前的代碼是等價的:
let name = 'Matt';
let person = {
name
};
console.log(person); // { name: 'Matt' }
2.可計算屬性
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey]: 'Matt',
[ageKey]: 27,
[jobKey]: 'Software engineer'
};
console.log(person); // { name: 'Matt', age: 27, job: 'Software engineer' }
因為被當作 JavaScript 表達式求值踪少,所以可計算屬性本身可以是復(fù)雜的表達式,在實例化時再求值:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[getUniqueKey(nameKey)]: 'Matt',
[getUniqueKey(ageKey)]: 27,
[getUniqueKey(jobKey)]: 'Software engineer'
};
console.log(person); // { name_0: 'Matt', age_1: 27, job_2: 'Software engineer' }
注意 可計算屬性表達式中拋出任何錯誤都會中斷對象創(chuàng)建糠涛。如果計算屬性的表達式有副作用援奢,那就要小心了,因為如果表達式拋出錯誤忍捡,那么之前完成的計算是不能回滾的集漾。
3.簡寫方法名
在給對象定義方法時,通常都要寫一個方法名砸脊、冒號具篇,然后再引用一個匿名函數(shù)表達式,如下所示:
let person = {
sayName: function(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
以下代碼和之前的代碼在行為上是等價的:
let person = {
sayName(name) {
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
六凌埂、對象解構(gòu)
// 使用對象解構(gòu)
let person = {
name: 'Matt',
age: 27
};
let { name: personName, age: personAge } = person;
console.log(personName); // Matt
console.log(personAge); // 27
使用解構(gòu)驱显,可以在一個類似對象字面量的結(jié)構(gòu)中,聲明多個變量瞳抓,同時執(zhí)行多個賦值操作埃疫。如果想讓變量直接使用屬性的名稱,那么可以使用簡寫語法孩哑,比如:
let person = {
name: 'Matt',
age: 27
};
let { name, age } = person;
console.log(name); // Matt
console.log(age); // 27
解構(gòu)賦值不一定與對象的屬性匹配栓霜。賦值的時候可以忽略某些屬性,而如果引用的屬性不存在横蜒,則該變量的值就是 undefined:
let person = {
name: 'Matt',
age: 27
};
let { name, job } = person;
console.log(name); // Matt
console.log(job); // undefined
關(guān)于解構(gòu)要了解更多胳蛮,參見原書。
七丛晌、創(chuàng)建對象
雖然使用 Object 構(gòu)造函數(shù)或?qū)ο笞置媪靠梢苑奖愕貏?chuàng)建對象仅炊,但這些方式也有明顯不足:創(chuàng)建具有同樣接口的多個對象需要重復(fù)編寫很多代碼。
綜觀 ECMAScript 規(guī)范的歷次發(fā)布澎蛛,每個版本的特性似乎都出人意料抚垄。ECMAScript 5.1 并沒有正式支持面向?qū)ο蟮慕Y(jié)構(gòu),比如類或繼承瓶竭。但是督勺,正如接下來幾節(jié)會介紹的,巧妙地運用原型式繼承可以成功地模擬同樣的行為斤贰。
ECMAScript 6 開始正式支持類和繼承智哀。ES6 的類旨在完全涵蓋之前規(guī)范設(shè)計的基于原型的繼承模式。不過荧恍,無論從哪方面看瓷叫,ES6 的類都僅僅是封裝了 ES5.1 構(gòu)造函數(shù)加原型繼承的語法糖而已屯吊。注意 不要誤會:采用面向?qū)ο缶幊棠J降?JavaScript 代碼還是應(yīng)該使用 ECMAScript 6 的類。但不管怎么說摹菠,理解 ES6 類出現(xiàn)之前的慣例總是有益無害的盒卸。特別是 ES6 的類定義本身就相當于對原有結(jié)構(gòu)的封裝。因此次氨,在介紹 ES6 的類之前蔽介,本書會循序漸進地介紹被類取代的那些底層概念。
1.工廠模式
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("Nicholas", 29, "Software Engineer");
let person2 = createPerson("Greg", 27, "Doctor");
這里煮寡,函數(shù) createPerson()接收 3 個參數(shù)虹蓄,根據(jù)這幾個參數(shù)構(gòu)建了一個包含 Person 信息的對象⌒宜海可以用不同的參數(shù)多次調(diào)用這個函數(shù)薇组,每次都會返回包含 3 個屬性和 1 個方法的對象。這種工廠模式雖然可以解決創(chuàng)建多個類似對象的問題坐儿,但沒有解決對象標識問題(即新創(chuàng)建的對象是什么類型)律胀。
2.構(gòu)造函數(shù)模式
前面的例子使用構(gòu)造函數(shù)模式可以這樣寫:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
在這個例子中,Person()構(gòu)造函數(shù)代替了 createPerson()工廠函數(shù)貌矿。實際上炭菌,Person()內(nèi)部的代碼跟 createPerson()基本是一樣的,只是有如下區(qū)別站叼。
- 沒有顯式地創(chuàng)建對象娃兽。
- 屬性和方法直接賦值給了 this。
- 沒有 return尽楔。
另外,要注意函數(shù)名 Person 的首字母大寫了第练。按照慣例阔馋,構(gòu)造函數(shù)名稱的首字母都是要大寫的,非構(gòu)造函數(shù)則以小寫字母開頭娇掏。這是從面向?qū)ο缶幊陶Z言那里借鑒的呕寝,有助于在 ECMAScript 中區(qū)分構(gòu)造函數(shù)和普通函數(shù)。畢竟 ECMAScript 的構(gòu)造函數(shù)就是能創(chuàng)建對象的函數(shù)婴梧。要創(chuàng)建 Person 的實例下梢,應(yīng)使用 new 操作符。以這種方式調(diào)用構(gòu)造函數(shù)會執(zhí)行如下操作塞蹭。
- (1) 在內(nèi)存中創(chuàng)建一個新對象孽江。
- (2) 這個新對象內(nèi)部的[[Prototype]]特性被賦值為構(gòu)造函數(shù)的 prototype 屬性。
- (3) 構(gòu)造函數(shù)內(nèi)部的 this 被賦值為這個新對象(即 this 指向新對象)番电。
- (4) 執(zhí)行構(gòu)造函數(shù)內(nèi)部的代碼(給新對象添加屬性)岗屏。
- (5) 如果構(gòu)造函數(shù)返回非空對象辆琅,則返回該對象;否則这刷,返回剛創(chuàng)建的新對象婉烟。
上一個例子的最后,person1 和 person2 分別保存著 Person 的不同實例暇屋。這兩個對象都有一個constructor 屬性指向 Person似袁,如下所示:
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
constructor 本來是用于標識對象類型的。不過咐刨,一般認為 instanceof 操作符是確定對象類型更可靠的方式叔营。前面例子中的每個對象都是 Object 的實例,同時也是 Person 的實例所宰,如下面調(diào)用instanceof 操作符的結(jié)果所示:
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
定義自定義構(gòu)造函數(shù)可以確保實例被標識為特定類型绒尊,相比于工廠模式,這是一個很大的好處仔粥。在這個例子中婴谱,person1 和 person2 之所以也被認為是 Object 的實例,是因為所有自定義對象都繼承自 Object(后面再詳細討論這一點)躯泰。
構(gòu)造函數(shù)不一定要寫成函數(shù)聲明的形式谭羔。賦值給變量的函數(shù)表達式也可以表示構(gòu)造函數(shù):
let Person = function(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Object); // true
console.log(person2 instanceof Person); // true
構(gòu)造函數(shù)與普通函數(shù)唯一的區(qū)別就是調(diào)用方式不同。除此之外麦向,構(gòu)造函數(shù)也是函數(shù)瘟裸。并沒有把某個函數(shù)定義為構(gòu)造函數(shù)的特殊語法。任何函數(shù)只要使用 new 操作符調(diào)用就是構(gòu)造函數(shù)诵竭,而不使用 new 操作符調(diào)用的函數(shù)就是普通函數(shù)话告。比如,前面的例子中定義的 Person()可以像下面這樣調(diào)用:
// 作為構(gòu)造函數(shù)
let person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); // "Nicholas"
// 作為函數(shù)調(diào)用
Person("Greg", 27, "Doctor"); // 添加到 window 對象
window.sayName(); // "Greg"
// 在另一個對象的作用域中調(diào)用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"
這個例子一開始展示了典型的構(gòu)造函數(shù)調(diào)用方式卵慰,即使用 new 操作符創(chuàng)建一個新對象沙郭。然后是普通函數(shù)的調(diào)用方式,這時候沒有使用 new 操作符調(diào)用 Person()裳朋,結(jié)果會將屬性和方法添加到 window 對象病线。這里要記住,在調(diào)用一個函數(shù)而沒有明確設(shè)置 this 值的情況下(即沒有作為對象的方法調(diào)用鲤嫡,或者沒有使用 call()/apply()調(diào)用)送挑,this 始終指向 Global 對象(在瀏覽器中就是 window 對象)。
因此在上面的調(diào)用之后暖眼,window 對象上就有了一個 sayName()方法惕耕,調(diào)用它會返回"Greg"。最后展示的調(diào)用方式是通過 call()(或 apply())調(diào)用函數(shù)罢荡,同時將特定對象指定為作用域赡突。這里的調(diào)用將對象 o 指定為 Person()內(nèi)部的 this 值对扶,因此執(zhí)行完函數(shù)代碼后,所有屬性和 sayName()方法都會添加到對象 o 上面惭缰。
構(gòu)造函數(shù)雖然有用浪南,但也不是沒有問題。構(gòu)造函數(shù)的主要問題在于漱受,其定義的方法會在每個實例上都創(chuàng)建一遍络凿。因此對前面的例子而言,person1 和 person2 都有名為 sayName()的方法昂羡,但這兩個方法不是一個 Function 實例絮记。我們知道,ECMAScript 中的函數(shù)是對象虐先,因此每次定義函數(shù)時怨愤,都會初始化一個對象。邏輯上講蛹批,這個構(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òu)造函數(shù)可以更清楚地知道撰洗,每個 Person 實例都會有自己的 Function 實例用于顯示 name 屬性。當然了腐芍,以這種方式創(chuàng)建函數(shù)會帶來不同的作用域鏈和標識符解析差导。但創(chuàng)建新 Function實例的機制是一樣的。因此不同實例上的函數(shù)雖然同名卻不相等猪勇,如下所示:
console.log(person1.sayName == person2.sayName); // false
因為都是做一樣的事设褐,所以沒必要定義兩個不同的 Function 實例。況且泣刹,this 對象可以把函數(shù)與對象的綁定推遲到運行時助析。要解決這個問題,可以把函數(shù)定義轉(zhuǎn)移到構(gòu)造函數(shù)外部:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
let person1 = new Person("Nicholas", 29, "Software Engineer");
let person2 = new Person("Greg", 27, "Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
在這里项玛,sayName()被定義在了構(gòu)造函數(shù)外部貌笨。在構(gòu)造函數(shù)內(nèi)部,sayName 屬性等于全局 sayName()函數(shù)襟沮。因為這一次 sayName 屬性中包含的只是一個指向外部函數(shù)的指針,所以 person1 和 person2共享了定義在全局作用域上的 sayName()函數(shù)昌腰。這樣雖然解決了相同邏輯的函數(shù)重復(fù)定義的問題开伏,但全局作用域也因此被搞亂了,因為那個函數(shù)實際上只能在一個對象上調(diào)用遭商。如果這個對象需要多個方法固灵,那么就要在全局作用域中定義多個函數(shù)。這會導(dǎo)致自定義類型引用的代碼不能很好地聚集一起劫流。這個新問題可以通過原型模式來解決巫玻。
3.原型模式
每個函數(shù)都會創(chuàng)建一個 prototype 屬性丛忆,這個屬性是一個對象,包含應(yīng)該由特定引用類型的實例共享的屬性和方法仍秤。實際上熄诡,這個對象就是通過調(diào)用構(gòu)造函數(shù)創(chuàng)建的對象的原型。使用原型對象的好處是诗力,在它上面定義的屬性和方法可以被對象實例共享凰浮。原來在構(gòu)造函數(shù)中直接賦給對象實例的值,可以直接賦值給它們的原型苇本,如下所示:
let Person = function() {};
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
這里袜茧,所有屬性和 sayName()方法都直接添加到了 Person 的 prototype 屬性上,構(gòu)造函數(shù)體中什么也沒有瓣窄。但這樣定義之后笛厦,調(diào)用構(gòu)造函數(shù)創(chuàng)建的新對象仍然擁有相應(yīng)的屬性和方法。與構(gòu)造函數(shù)模式不同俺夕,使用這種原型模式定義的屬性和方法是由所有實例共享的裳凸。因此 person1 和 person2 訪問的都是相同的屬性和相同的 sayName()函數(shù)。要理解這個過程啥么,就必須理解 ECMAScript 中原型的本質(zhì)登舞。
1. 理解原型
無論何時,只要創(chuàng)建一個函數(shù)悬荣,就會按照特定的規(guī)則為這個函數(shù)創(chuàng)建一個 prototype 屬性(指向原型對象)菠秒。默認情況下,所有原型對象自動獲得一個名為 constructor 的屬性氯迂,指回與之關(guān)聯(lián)的構(gòu)造函數(shù)践叠。對前面的例子而言,Person.prototype.constructor 指向 Person嚼蚀。然后禁灼,因構(gòu)造函數(shù)而異,可能會給原型對象添加其他屬性和方法轿曙。
在自定義構(gòu)造函數(shù)時弄捕,原型對象默認只會獲得 constructor 屬性,其他的所有方法都繼承自O(shè)bject导帝。每次調(diào)用構(gòu)造函數(shù)創(chuàng)建一個新實例守谓,這個實例的內(nèi)部[[Prototype]]指針就會被賦值為構(gòu)造函數(shù)的原型對象。腳本中沒有訪問這個[[Prototype]]特性的標準方式您单,但 Firefox斋荞、Safari 和 Chrome會在每個對象上暴露proto屬性,通過這個屬性可以訪問對象的原型虐秦。在其他實現(xiàn)中平酿,這個特性完全被隱藏了凤优。關(guān)鍵在于理解這一點:實例與構(gòu)造函數(shù)原型之間有直接的聯(lián)系,但實例與構(gòu)造函數(shù)之間沒有蜈彼。
這種關(guān)系不好可視化筑辨,但可以通過下面的代碼來理解原型的行為:
/**
* 構(gòu)造函數(shù)可以是函數(shù)表達式
* 也可以是函數(shù)聲明,因此以下兩種形式都可以:
* function Person() {}
* let Person = function() {}
*/
function Person() {}
/**
* 聲明之后柳刮,構(gòu)造函數(shù)就有了一個
* 與之關(guān)聯(lián)的原型對象:
*/
console.log(typeof Person.prototype);
console.log(Person.prototype);
// {
// constructor: f Person(),
// __proto__: Object
// }
/**
* 如前所述挖垛,構(gòu)造函數(shù)有一個 prototype 屬性
* 引用其原型對象,而這個原型對象也有一個
* constructor 屬性秉颗,引用這個構(gòu)造函數(shù)
* 換句話說痢毒,兩者循環(huán)引用:
*/
console.log(Person.prototype.constructor === Person); // true
/**
* 正常的原型鏈都會終止于 Object 的原型對象
* Object 原型的原型是 null
*/
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
console.log(Person.prototype.__proto__);
// {
// constructor: f Object(),
// toString: ...
// hasOwnProperty: ...
// isPrototypeOf: ...
// ...
// }
let person1 = new Person(),
person2 = new Person();
/**
* 構(gòu)造函數(shù)塔嬉、原型對象和實例
* 是 3 個完全不同的對象:
*/
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
/**
* 實例通過__proto__鏈接到原型對象九孩,
* 它實際上指向隱藏特性[[Prototype]]
*
* 構(gòu)造函數(shù)通過 prototype 屬性鏈接到原型對象
*
* 實例與構(gòu)造函數(shù)沒有直接聯(lián)系,與原型對象有直接聯(lián)系
*/
console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true
/**
* 同一個構(gòu)造函數(shù)創(chuàng)建的兩個實例
* 共享同一個原型對象:
*/
console.log(person1.__proto__ === person2.__proto__); // true
/**
* instanceof 檢查實例的原型鏈中
* 是否包含指定構(gòu)造函數(shù)的原型:
*/
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
圖 8-1 展示了 Person 構(gòu)造函數(shù)何之、Person 的原型對象和 Person 現(xiàn)有兩個實例之間的關(guān)系菇怀。注意凭舶,Person.prototype 指向原型對象,而 Person.prototype.contructor 指回 Person 構(gòu)造函數(shù)爱沟。原型對象包含 constructor 屬性和其他后來添加的屬性帅霜。Person 的兩個實例 person1 和 person2 都只有一個內(nèi)部屬性指回 Person.prototype,而且兩者都與構(gòu)造函數(shù)沒有直接聯(lián)系呼伸。另外要注意身冀,雖然這兩個實例都沒有屬性和方法,但 person1.sayName()可以正常調(diào)用括享。這是由于對象屬性查找機制的原因搂根。
雖然不是所有實現(xiàn)都對外暴露了[[Prototype]],但可以使用 isPrototypeOf()方法確定兩個對象之間的這種關(guān)系铃辖。本質(zhì)上剩愧,isPrototypeOf()會在傳入?yún)?shù)的[[Prototype]]指向調(diào)用它的對象時返回 true,如下所示:
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
這里通過原型對象調(diào)用 isPrototypeOf()方法檢查了 person1 和 person2娇斩。因為這兩個例子內(nèi)部都有鏈接指向 Person.prototype仁卷,所以結(jié)果都返回 true。
ECMAScript 的 Object 類型有一個方法叫 Object.getPrototypeOf()犬第,返回參數(shù)的內(nèi)部特性[[Prototype]]的值五督。例如:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "Nicholas"
第一行代碼簡單確認了 Object.getPrototypeOf()返回的對象就是傳入對象的原型對象。第二行代碼則取得了原型對象上 name 屬性的值瓶殃,即"Nicholas"。使用 Object.getPrototypeOf()可以方便地取得一個對象的原型副签,而這在通過原型實現(xiàn)繼承時顯得尤為重要(本章后面會介紹)
Object 類型還有一個 setPrototypeOf()方法遥椿,可以向?qū)嵗乃接刑匦訹[Prototype]]寫入一個新值基矮。這樣就可以重寫一個對象的原型繼承關(guān)系:
let biped = {
numLegs: 2
};
let person = {
name: 'Matt'
};
Object.setPrototypeOf(person, biped);
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
警告 Object.setPrototypeOf()可能會嚴重影響代碼性能。Mozilla 文檔說得很清楚:“在所有瀏覽器和 JavaScript 引擎中冠场,修改繼承關(guān)系的影響都是微妙且深遠的家浇。這種影響并不僅是執(zhí)行 Object.setPrototypeOf()語句那么簡單,而是會涉及所有訪問了那些修改過[[Prototype]]的對象的代碼碴裙「直”
為避免使用 Object.setPrototypeOf()可能造成的性能下降,可以通過 Object.create()來創(chuàng)建一個新對象舔株,同時為其指定原型:
let biped = {
numLegs: 2
};
let person = Object.create(biped);
person.name = 'Matt';
console.log(person.name); // Matt
console.log(person.numLegs); // 2
console.log(Object.getPrototypeOf(person) === biped); // true
2.原型層級
在通過對象訪問屬性時莺琳,會按照這個屬性的名稱開始搜索。搜索開始于對象實例本身载慈。如果在這個實例上發(fā)現(xiàn)了給定的名稱惭等,則返回該名稱對應(yīng)的值。如果沒有找到這個屬性办铡,則搜索會沿著指針進入原型對象辞做,然后在原型對象上找到屬性后,再返回對應(yīng)的值寡具。
因此秤茅,在調(diào)用 person1.sayName()時,會發(fā)生兩步搜索童叠。首先框喳,JavaScript 引擎會問:“person1 實例有 sayName 屬性嗎?”答案是沒有拯钻。然后帖努,繼續(xù)搜索并問:“person1 的原型有 sayName 屬性嗎?”答案是有粪般。于是就返回了保存在原型上的這個函數(shù)拼余。在調(diào)用 person2.sayName()時,會發(fā)生同樣的搜索過程亩歹,而且也會返回相同的結(jié)果匙监。這就是原型用于在多個對象實例間共享屬性和方法的原理。
雖然可以通過實例讀取原型對象上的值小作,但不可能通過實例重寫這些值亭姥。如果在實例上添加了一個與原型對象中同名的屬性,那就會在實例上創(chuàng)建這個屬性顾稀,這個屬性會遮住原型對象上的屬性达罗。下面看一個例子:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Greg";
console.log(person1.name); // "Greg",來自實例
console.log(person2.name); // "Nicholas",來自原型
在這個例子中粮揉,person1 的 name 屬性遮蔽了原型對象上的同名屬性巡李。雖然 person1.name 和person2.name 都返回了值,但前者返回的是"Greg"(來自實例)扶认,后者返回的是"Nicholas"(來自原型)侨拦。當 console.log()訪問 person1.name 時,會先在實例上搜索個屬性辐宾。因為這個屬性在實例上存在狱从,所以就不會再搜索原型對象了。而在訪問 person2.name 時叠纹,并沒有在實例上找到這個屬性季研,所以會繼續(xù)搜索原型對象并使用定義在原型上的屬性。
只要給對象實例添加一個屬性吊洼,這個屬性就會遮蔽(shadow)原型對象上的同名屬性训貌,也就是雖然不會修改它,但會屏蔽對它的訪問冒窍。即使在實例上把這個屬性設(shè)置為 null递沪,也不會恢復(fù)它和原型的聯(lián)系。不過综液,使用 delete 操作符可以完全刪除實例上的這個屬性款慨,從而讓標識符解析過程能夠繼續(xù)搜索原型對象。
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Greg";
console.log(person1.name); // "Greg"谬莹,來自實例
console.log(person2.name); // "Nicholas"檩奠,來自原型
delete person1.name;
console.log(person1.name); // "Nicholas",來自原型
這個修改后的例子中使用 delete 刪除了 person1.name附帽,這個屬性之前以"Greg"遮蔽了原型上的同名屬性埠戳。然后原型上 name 屬性的聯(lián)系就恢復(fù)了,因此再訪問 person1.name 時蕉扮,就會返回原型對象上這個屬性的值整胃。
hasOwnProperty()方法用于確定某個屬性是在實例上還是在原型對象上。這個方法是繼承自 Object的喳钟,會在屬性存在于調(diào)用它的對象實例上時返回 true屁使,如下面的例子所示:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty("name")); // false
person1.name = "Greg";
console.log(person1.name); // "Greg",來自實例
console.log(person1.hasOwnProperty("name")); // true
console.log(person2.name); // "Nicholas"奔则,來自原型
console.log(person2.hasOwnProperty("name")); // false
delete person1.name;
console.log(person1.name); // "Nicholas"蛮寂,來自原型
console.log(person1.hasOwnProperty("name")); // false
在這個例子中,通過調(diào)用 hasOwnProperty()能夠清楚地看到訪問的是實例屬性還是原型屬性易茬。調(diào)用 person1.hasOwnProperty("name")只在重寫 person1 上 name 屬性的情況下才返回 true酬蹋,表明此時 name 是一個實例屬性,不是原型屬性。圖 8-2 形象地展示了上面例子中各個步驟的狀態(tài)除嘹。(為簡單起見写半,圖中省略了 Person 構(gòu)造函數(shù)。)
3.原型和 in 操作符
有兩種方式使用 in 操作符:單獨使用和在 for-in 循環(huán)中使用尉咕。在單獨使用時,in 操作符會在可以通過對象訪問指定屬性時返回 true璃岳,無論該屬性是在實例上還是在原型上年缎。來看下面的例子:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let 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
在上面整個例子中,name 隨時可以通過實例或通過原型訪問到犁柜。因此洲鸠,調(diào)用"name" in persoon1時始終返回 true,無論這個屬性是否在實例上馋缅。如果要確定某個屬性是否存在于原型上扒腕,則可以像下面這樣同時使用 hasOwnProperty()和 in 操作符:
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
只要通過對象可以訪問,in 操作符就返回 true萤悴,而 hasOwnProperty()只有屬性存在于實例上時才返回 true瘾腰。因此,只要 in 操作符返回 true 且 hasOwnProperty()返回 false覆履,就說明該屬性是一個原型屬性蹋盆。來看下面的例子:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person = new Person();
console.log(hasPrototypeProperty(person, "name")); // true
person.name = "Greg";
console.log(hasPrototypeProperty(person, "name")); // false
在這里,name 屬性首先只存在于原型上硝全,所以 hasPrototypeProperty()返回 true栖雾。而在實例上重寫這個屬性后,實例上也有了這個屬性伟众,因此 hasPrototypeProperty()返回 false析藕。即便此時原型對象還有 name 屬性,但因為實例上的屬性遮蔽了它赂鲤,所以不會用到噪径。
在 for-in 循環(huán)中使用 in 操作符時,可以通過對象訪問且可以被枚舉的屬性都會返回数初,包括實例屬性和原型屬性找爱。遮蔽原型中不可枚舉([[Enumerable]]特性被設(shè)置為 false)屬性的實例屬性也會在 for-in 循環(huán)中返回,因為默認情況下開發(fā)者定義的屬性都是可枚舉的泡孩。
要獲得對象上所有可枚舉的實例屬性车摄,可以使用 Object.keys()方法。這個方法接收一個對象作為參數(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);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName"
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"
這里变屁,keys 變量保存的數(shù)組中包含"name"、"age"意狠、"job"和"sayName"粟关。這是正常情況下通過for-in 返回的順序。而在 Person 的實例上調(diào)用時环戈,Object.keys()返回的數(shù)組中只包含"name"和"age"兩個屬性闷板。
如果想列出所有實例屬性,無論是否可以枚舉院塞,都可以使用 Object.getOwnPropertyNames():
let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // "[constructor,name,age,job,sayName]"
注意遮晚,返回的結(jié)果中包含了一個不可枚舉的屬性 constructor。Object.keys()和 Object. getOwnPropertyNames()在適當?shù)臅r候都可用來代替 for-in 循環(huán)拦止。
在 ECMAScript 6 新增符號類型之后县遣,相應(yīng)地出現(xiàn)了增加一個 Object.getOwnPropertyNames()的兄弟方法的需求,因為以符號為鍵的屬性沒有名稱的概念汹族。因此萧求,Object.getOwnProperty-Symbols()方法就出現(xiàn)了,這個方法與 Object.getOwnPropertyNames()類似鞠抑,只是針對符號而已:
let k1 = Symbol('k1'),
k2 = Symbol('k2');
let o = {
[k1]: 'k1',
[k2]: 'k2'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
4. 屬性枚舉順序
for-in 循環(huán)饭聚、Object.keys()、Object.getOwnPropertyNames()搁拙、Object.getOwnPropertySymbols()以及 Object.assign()在屬性枚舉順序方面有很大區(qū)別秒梳。for-in 循環(huán)和 Object.keys()的枚舉順序是不確定的,取決于 JavaScript 引擎箕速,可能因瀏覽器而異酪碘。
Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign()的枚舉順序是確定性的盐茎。先以升序枚舉數(shù)值鍵兴垦,然后以插入順序枚舉字符串和符號鍵。在對象字面量中定義的鍵以它們逗號分隔的順序插入字柠。
let k1 = Symbol('k1'),
k2 = Symbol('k2');
let o = {
1: 1,
first: 'first',
[k1]: 'sym2',
second: 'second',
0: 0
};
o[k2] = 'sym2';
o[3] = 3;
o.third = 'third';
o[2] = 2;
console.log(Object.getOwnPropertyNames(o));
// ["0", "1", "2", "3", "first", "second", "third"]
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
八探越、對象迭代
在 JavaScript 有史以來的大部分時間內(nèi),迭代對象屬性都是一個難題窑业。ECMAScript 2017 新增了兩個靜態(tài)方法钦幔,用于將對象內(nèi)容轉(zhuǎn)換為序列化的——更重要的是可迭代的——格式。這兩個靜態(tài)方法Object.values()和 Object.entries()接收一個對象常柄,返回它們內(nèi)容的數(shù)組鲤氢。Object.values()返回對象值的數(shù)組搀擂,Object.entries()返回鍵/值對的數(shù)組。下面的示例展示了這兩個方法:
const o = {
foo: 'bar',
baz: 1,
qux: {}
};
console.log(Object.values(o));
// ["bar", 1, {}]
console.log(Object.entries((o)));
// [["foo", "bar"], ["baz", 1], ["qux", {}]]
九卷玉、重寫原型
有讀者可能注意到了哨颂,在前面的例子中,每次定義一個屬性或方法都會把 Person.prototype 重寫一遍相种。為了減少代碼冗余威恼,也為了從視覺上更好地封裝原型功能,直接通過一個包含所有屬性和方法的對象字面量來重寫原型成為了一種常見的做法蚂子,如下面的例子所示:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
在這個例子中沃测,Person.prototype 被設(shè)置為等于一個通過對象字面量創(chuàng)建的新對象。最終結(jié)果是一樣的食茎,只有一個問題:這樣重寫之后,Person.prototype 的 constructor 屬性就不指向 Person了馏谨。在創(chuàng)建函數(shù)時别渔,也會創(chuàng)建它的 prototype 對象,同時會自動給這個原型的 constructor 屬性賦值惧互。而上面的寫法完全重寫了默認的 prototype 對象哎媚,因此其 constructor 屬性也指向了完全不同的新對象(Object 構(gòu)造函數(shù)),不再指向原來的構(gòu)造函數(shù)喊儡。雖然 instanceof 操作符還能可靠地返回值拨与,但我們不能再依靠 constructor 屬性來識別類型了,如下面的例子所示:
let 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仍然對Object和Person都返回true买喧。但constructor屬性現(xiàn)在等于Object而不是 Person 了。如果 constructor 的值很重要匆赃,則可以像下面這樣在重寫原型對象時專門設(shè)置一下它的值:
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
這次的代碼中特意包含了 constructor 屬性淤毛,并將它設(shè)置為 Person,保證了這個屬性仍然包含恰當?shù)闹怠?/p>
但要注意算柳,以這種方式恢復(fù) constructor 屬性會創(chuàng)建一個[[Enumerable]]為 true 的屬性低淡。而原生 constructor 屬性默認是不可枚舉的。因此瞬项,如果你使用的是兼容 ECMAScript 的 JavaScript 引擎蔗蹋,那可能會改為使用 Object.defineProperty()方法來定義 constructor 屬性:
function Person() {}
Person.prototype = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
// 恢復(fù) constructor 屬性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
十、原型的動態(tài)性
因為從原型上搜索值的過程是動態(tài)的囱淋,所以即使實例在修改原型之前已經(jīng)存在猪杭,任何時候?qū)υ蛯ο笏龅男薷囊矔趯嵗戏从吵鰜怼O旅媸且粋€例子:
let friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
};
friend.sayHi(); // "hi"绎橘,沒問題胁孙!
以上代碼先創(chuàng)建一個 Person 實例并保存在 friend 中唠倦。然后一條語句在 Person.prototype 上添加了一個名為 sayHi()的方法。雖然 friend 實例是在添加方法之前創(chuàng)建的涮较,但它仍然可以訪問這個方法稠鼻。之所以會這樣,主要原因是實例與原型之間松散的聯(lián)系狂票。在調(diào)用 friend.sayHi()時候齿,首先會從這個實例中搜索名為 sayHi 的屬性。在沒有找到的情況下闺属,運行時會繼續(xù)搜索原型對象慌盯。因為實例和原型之間的鏈接就是簡單的指針,而不是保存的副本掂器,所以會在原型上找到 sayHi 屬性并返回這個屬性保存的函數(shù)亚皂。
雖然隨時能給原型添加屬性和方法,并能夠立即反映在所有對象實例上国瓮,但這跟重寫整個原型是兩回事灭必。實例的[[Prototype]]指針是在調(diào)用構(gòu)造函數(shù)時自動賦值的,這個指針即使把原型修改為不同的對象也不會變乃摹。重寫整個原型會切斷最初原型與構(gòu)造函數(shù)的聯(lián)系禁漓,但實例引用的仍然是最初的原型。記住孵睬,實例只有指向原型的指針播歼,沒有指向構(gòu)造函數(shù)的指針。來看下面的例子:
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName() {
console.log(this.name);
}
};
friend.sayName(); // 錯誤
在這個例子中掰读,Person 的新實例是在重寫原型對象之前創(chuàng)建的秘狞。在調(diào)用 friend.sayName()的時候,會導(dǎo)致錯誤磷支。這是因為 firend 指向的原型還是最初的原型谒撼,而這個原型上并沒有 sayName 屬性。
十一雾狈、原生對象原型
原型模式之所以重要廓潜,不僅體現(xiàn)在自定義類型上,而且還因為它也是實現(xiàn)所有原生引用類型的模式善榛。所有原生引用類型的構(gòu)造函數(shù)(包括 Object辩蛋、Array、String 等)都在原型上定義了實例方法移盆。比如悼院,數(shù)組實例的 sort()方法就是 Array.prototype 上定義的,而字符串包裝對象的 substring()方法也是在 String.prototype 上定義的咒循,如下所示:
console.log(typeof Array.prototype.sort); // "function"
console.log(typeof String.prototype.substring); // "function"
通過原生對象的原型可以取得所有默認方法的引用据途,也可以給原生類型的實例定義新的方法绞愚。可以像修改自定義對象原型一樣修改原生對象原型颖医,因此隨時可以添加方法位衩。比如,下面的代碼就給 String原始值包裝類型的實例添加了一個 startsWith()方法:
String.prototype.startsWith = function (text) {
return this.indexOf(text) === 0;
};
let msg = "Hello world!";
console.log(msg.startsWith("Hello")); // true
如果給定字符串的開頭出現(xiàn)了調(diào)用 startsWith()方法的文本熔萧,那么該方法會返回 true糖驴。因為這個方法是被定義在 String.prototype 上,所以當前環(huán)境下所有的字符串都可以使用這個方法佛致。msg是個字符串贮缕,在讀取它的屬性時,后臺會自動創(chuàng)建 String 的包裝實例俺榆,從而找到并調(diào)用 startsWith()方法感昼。
注意 盡管可以這么做,但并不推薦在產(chǎn)品環(huán)境中修改原生對象原型罐脊。這樣做很可能造成誤會抑诸,而且可能引發(fā)命名沖突(比如一個名稱在某個瀏覽器實現(xiàn)中不存在,在另一個實現(xiàn)中卻存在)爹殊。另外還有可能意外重寫原生的方法。推薦的做法是創(chuàng)建一個自定義的類奸绷,繼承原生類型梗夸。
十二、原型的問題
原型模式也不是沒有問題号醉。首先反症,它弱化了向構(gòu)造函數(shù)傳遞初始化參數(shù)的能力,會導(dǎo)致所有實例默認都取得相同的屬性值畔派。雖然這會帶來不便铅碍,但還不是原型的最大問題。原型的最主要問題源自它的共享特性线椰。
我們知道胞谈,原型上的所有屬性是在實例間共享的,這對函數(shù)來說比較合適憨愉。另外包含原始值的屬性也還好烦绳,如前面例子中所示,可以通過在實例上添加同名屬性來簡單地遮蔽原型上的屬性配紫。真正的問題來自包含引用值的屬性径密。來看下面的例子:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: "Software Engineer",
friends: ["Shelby", "Court"],
sayName() {
console.log(this.name);
}
};
let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
這里,Person.prototype 有一個名為 friends 的屬性躺孝,它包含一個字符串數(shù)組享扔。然后這里創(chuàng)建了兩個 Person 的實例底桂。person1.friends 通過 push 方法向數(shù)組中添加了一個字符串。由于這個friends 屬性存在于 Person.prototype 而非 person1 上惧眠,新加的這個字符串也會在(指向同一個數(shù)組的)person2.friends 上反映出來籽懦。如果這是有意在多個實例間共享數(shù)組,那沒什么問題锉试。但一般來說猫十,不同的實例應(yīng)該有屬于自己的屬性副本。這就是實際開發(fā)中通常不單獨使用原型模式的原因呆盖。
在第三版的JS紅寶書中拖云,提出組合使用構(gòu)造函數(shù)模式和原型模式來解決這個問題。構(gòu)造函數(shù)用于定義實例屬性应又,原型模式用于定義方法和共享的屬性宙项。另外,這種混合模式還支持向構(gòu)造函數(shù)傳遞參數(shù)株扛,可謂是集兩種模式之長尤筐。
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby","Court"];
}
Person.prototype = {
constructor: Person,
sayName() {
console.log(this.name);
}
};
let person1 = new Person("Nicholas",29,"software Engineer");
let person2 = new Person("Greg",27,"doctor");
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court"
console.log(person1.friends === person2.friends); // false
console.log(person1.sayName === person2.sayName);//true