前言
本文作為對(duì)本書(shū)的一些知識(shí)點(diǎn)的收集
正文
1. LHS
和 RHS
當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時(shí)進(jìn)行
LHS
查詢酥泛,出現(xiàn)在右側(cè)時(shí)進(jìn)行RHS
查詢;講得再準(zhǔn)確一點(diǎn)芋哭,RHS
查詢與簡(jiǎn)單地查找某個(gè)變量的值別無(wú)二致袁辈,而LHS
查詢則是試圖找到變量的容器本身挨措;從這個(gè)角度說(shuō)汗洒,RHS
并不是真正意義上的 “賦值操作的右側(cè)” 议纯,更準(zhǔn)確地說(shuō)是 “非左側(cè)”;可以將RHS
理解成retrieve his source value
(取到它的源值)溢谤,這意味著 “得到某某的值”
2. ReferenceError
和 TypeError
ReferenceError
同作用域判別失敗相關(guān)瞻凤,而TypeError
則代表作用域判別成功了,但是對(duì)
結(jié)果的操作是非法或不合理的世杀。
3. 不成功的 RHS
引用會(huì)導(dǎo)致拋出 ReferenceError
異常阀参。不成功的 LHS
引用會(huì)導(dǎo)致自動(dòng)隱式地創(chuàng)建一個(gè)全局變量(非嚴(yán)格模式下),該變量使用 LHS
引用的目標(biāo)作為標(biāo)識(shí)符瞻坝,或者拋
出 ReferenceError
異常(嚴(yán)格模式下)结笨。
4. 小測(cè)驗(yàn)答案
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
// 1. 找出所有的 LHS 查詢(這里有 3 處!)
c = ..;湿镀、a = 2(隱式變量分配)、b = ..
// 2. 找出所有的 RHS 查詢(這里有 4 處7ズ丁)
foo(2..勉痴、= a;、a ..树肃、.. b
5. 個(gè)不推薦使用 eval(..)
和 with
的原因
會(huì)被嚴(yán)格模式所影響(限制)蒸矛。with 被完全禁止,而在保留核心功能的前提下,間接或非安全地使用 eval(..) 也被禁止了雏掠。
JavaScript 引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)的性能優(yōu)化斩祭。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置乡话,才能在執(zhí)行過(guò)程中快速找到標(biāo)識(shí)符摧玫。
但如果引擎在代碼中發(fā)現(xiàn)了 eval(..) 或 with,它只能簡(jiǎn)單地假設(shè)關(guān)于標(biāo)識(shí)符位置的判斷都是無(wú)效的绑青,因?yàn)闊o(wú)法在詞法分析階段明確知道 eval(..) 會(huì)接收到什么代碼诬像,這些代碼會(huì)如何對(duì)作用域進(jìn)行修改,也無(wú)法知道傳遞給 with 用來(lái)創(chuàng)建新詞法作用域的對(duì)象的內(nèi)容到底是什么闸婴。
最悲觀的情況是如果出現(xiàn)了 eval(..) 或 with坏挠,所有的優(yōu)化可能都是無(wú)意義的,因此最簡(jiǎn)單的做法就是完全不做任何優(yōu)化邪乍。
如果代碼中大量使用 eval(..) 或 with降狠,那么運(yùn)行起來(lái)一定會(huì)變得非常慢。無(wú)論引擎多聰明庇楞,試圖將這些悲觀情況的副作用限制在最小范圍內(nèi)榜配,也無(wú)法避免如果沒(méi)有這些優(yōu)化,代碼會(huì)運(yùn)行得更慢這個(gè)事實(shí)姐刁。
6. eval(..) 和 with 小結(jié)
JavaScript 中有兩個(gè)機(jī)制可以“欺騙”詞法作用域:eval(..) 和 with芥牌。前者可以對(duì)一段包含一個(gè)或多個(gè)聲明的“代碼”字符串進(jìn)行演算,并借此來(lái)修改已經(jīng)存在的詞法作用域(在運(yùn)行時(shí))聂使。后者本質(zhì)上是通過(guò)將一個(gè)對(duì)象的引用當(dāng)作作用域來(lái)處理壁拉,將對(duì)象的屬性當(dāng)作作用域中的標(biāo)識(shí)符來(lái)處理,從而創(chuàng)建了一個(gè)新的詞法作用域(同樣是在運(yùn)行時(shí))柏靶。
這兩個(gè)機(jī)制的副作用是引擎無(wú)法在編譯時(shí)對(duì)作用域查找進(jìn)行優(yōu)化弃理,因?yàn)橐嬷荒苤?jǐn)慎地認(rèn)為這樣的優(yōu)化是無(wú)效的。使用這其中任何一個(gè)機(jī)制都將導(dǎo)致代碼運(yùn)行變慢屎蜓。不要使用它們痘昌。
7. 函數(shù)聲明和函數(shù)表達(dá)式
區(qū)分函數(shù)聲明和表達(dá)式最簡(jiǎn)單的方法是看 function 關(guān)鍵字出現(xiàn)在聲明中的位
置(不僅僅是一行代碼,而是整個(gè)聲明中的位置)炬转。如果 function 是聲明中
的第一個(gè)詞辆苔,那么就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式扼劈。
8. 在 JavaScript 中被稱為模塊的模式
function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
9. 當(dāng)只需要一個(gè)實(shí)例時(shí)驻啤,可以對(duì)上面這個(gè)模式進(jìn)行簡(jiǎn)單的改進(jìn)來(lái)實(shí)現(xiàn)單例模式:
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
10. 現(xiàn)代的模塊機(jī)制
大多數(shù)模塊依賴加載器 / 管理器本質(zhì)上都是將這種模塊定義封裝進(jìn)一個(gè)友好的 API。這里
并不會(huì)研究某個(gè)具體的庫(kù)荐吵,為了宏觀了解我會(huì)簡(jiǎn)單地介紹一些核心概念:
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
這段代碼的核心是
modules[name] = impl.apply(impl, deps)
骑冗。為了模塊的定義引入了包裝
函數(shù)(可以傳入任何依賴)赊瞬,并且將返回值,也就是模塊的 API贼涩,儲(chǔ)存在一個(gè)根據(jù)名字來(lái)管理的模塊列表中巧涧。
下面展示了如何使用它來(lái)定義模塊:
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase() );
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(bar.hello( "hippo" )); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
"foo" 和 "bar" 模塊都是通過(guò)一個(gè)返回公共 API 的函數(shù)來(lái)定義的。"foo" 甚至接受 "bar" 的
示例作為依賴參數(shù)遥倦,并能相應(yīng)地使用它
它們符合前面列出的模塊模式的兩個(gè)特點(diǎn):為函數(shù)定義引入包裝函數(shù)谤绳,并保證它的返回值和模塊的 API 保持一致。
11. 使用 new
來(lái)調(diào)用函數(shù)谊迄,或者說(shuō)發(fā)生構(gòu)造函數(shù)調(diào)用的原理
- 舉例來(lái)說(shuō)闷供,思考一下
Number(..)
作為構(gòu)造函數(shù)時(shí)的行為,ES5.1
中這樣描述它:
15.7.2 Number 構(gòu)造函數(shù)
當(dāng) Number 在 new 表達(dá)式中被調(diào)用時(shí)统诺,它是一個(gè)構(gòu)造函數(shù):它會(huì)初始化新創(chuàng)建的對(duì)象歪脏。
- 所以,包括內(nèi)置對(duì)象函數(shù)(比如
Number(..)
粮呢,詳情請(qǐng)查看第 3 章)在內(nèi)的所有函數(shù)都可以用new
來(lái)調(diào)用婿失,這種函數(shù)調(diào)用被稱為構(gòu)造函數(shù)調(diào)用。
這里有一個(gè)重要但是非常細(xì)微的區(qū)別:實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”啄寡,只有對(duì)于函數(shù)的“構(gòu)造調(diào)用”豪硅。 - 使用 new 來(lái)調(diào)用函數(shù),或者說(shuō)發(fā)生構(gòu)造函數(shù)調(diào)用時(shí)挺物,會(huì)自動(dòng)執(zhí)行下面的操作懒浮。
- 創(chuàng)建(或者說(shuō)構(gòu)造)一個(gè)全新的對(duì)象。
- 這個(gè)新對(duì)象會(huì)被執(zhí)行 [[ 原型 ]] 連接识藤。
- 這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的 this砚著。
- 如果函數(shù)沒(méi)有返回其他對(duì)象,那么 new 表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象痴昧。
12. this
綁定的優(yōu)先級(jí)
- 根據(jù)優(yōu)先級(jí)來(lái)判斷函數(shù)在某個(gè)調(diào)用位置應(yīng)用的是哪條規(guī)則稽穆。可以按照下面的順序來(lái)進(jìn)行判斷:
- 函數(shù)是否在 new 中調(diào)用(new 綁定)赶撰?如果是的話 this 綁定的是新創(chuàng)建的對(duì)象舌镶。
var bar = new foo()
- 函數(shù)是否通過(guò) call、apply(顯式綁定)或者硬綁定調(diào)用豪娜?如果是的話餐胀,this 綁定的是
指定的對(duì)象。
- 函數(shù)是否通過(guò) call、apply(顯式綁定)或者硬綁定調(diào)用豪娜?如果是的話餐胀,this 綁定的是
var bar = foo.call(obj2)
- 函數(shù)是否在某個(gè)上下文對(duì)象中調(diào)用(隱式綁定)瘤载?如果是的話骂澄,this 綁定的是那個(gè)上下文對(duì)象。
var bar = obj1.foo()
- 如果都不是的話惕虑,使用默認(rèn)綁定坟冲。如果在嚴(yán)格模式下,就綁定到 undefined溃蔫,否則綁定到全局對(duì)象健提。
var bar = foo()
- 在某些場(chǎng)景下
this
的綁定行為會(huì)出乎意料- 如果你把
null
或者undefined
作為this
的綁定對(duì)象傳入call
、apply
或者bind
伟叛,這些值在調(diào)用時(shí)會(huì)被忽略私痹,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:
function foo() { console.log( this.a ); } var a = 2; foo.call( null ); // 2
- 如果你把
13. 創(chuàng)建一個(gè)空對(duì)象最簡(jiǎn)單的方法
如果函數(shù)并不關(guān)心
this
的話,你
仍然需要傳入一個(gè)占位值统刮,使用這個(gè)方法比null
更安全紊遵,它就是一個(gè)空的非委托的對(duì)象
// 因?yàn)?? 表示 “希望 this 是空”,這比 null 的含義更清楚侥蒙。不過(guò)你可以用任何喜歡的名字來(lái)命名 DMZ(demilitarized zone) 對(duì)象
var ? = Object.create(null)
Object.create(null)
和{}
很 像暗膜, 但 是 并 不 會(huì) 創(chuàng) 建Object.prototype
這個(gè)委托,所以它比{}
“更空”:
- 代碼示例:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 我們的 DMZ 空對(duì)象
var ? = Object.create( null );
// 把數(shù)組展開(kāi)成參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind( ?, 2 );
bar( 3 ); // a:2, b:3
14. this
全面解析 小結(jié)
-
如果要判斷一個(gè)運(yùn)行中函數(shù)的
this
綁定鞭衩,就需要找到這個(gè)函數(shù)的直接調(diào)用位置学搜。找到之后就可以順序應(yīng)用下面這四條規(guī)則來(lái)判斷this
的綁定對(duì)象。- 由
new
調(diào)用论衍?綁定到新創(chuàng)建的對(duì)象瑞佩。
- 由
- 由
call
或者apply
(或者bind
)調(diào)用?綁定到指定的對(duì)象坯台。
- 由
- 由上下文對(duì)象調(diào)用炬丸?綁定到那個(gè)上下文對(duì)象。
- 默認(rèn):在嚴(yán)格模式下綁定到
undefined
蜒蕾,否則綁定到全局對(duì)象稠炬。
- 默認(rèn):在嚴(yán)格模式下綁定到
一定要注意,有些調(diào)用可能在無(wú)意中使用默認(rèn)綁定規(guī)則滥搭。如果想“更安全”地忽略
this
綁定酸纲,你可以使用一個(gè)DMZ
對(duì)象,比如? = Object.create(null)
瑟匆,以保護(hù)全局對(duì)象闽坡。ES6
中的箭頭函數(shù)并不會(huì)使用四條標(biāo)準(zhǔn)的綁定規(guī)則,而是根據(jù)當(dāng)前的詞法作用域來(lái)決定this
愁溜,具體來(lái)說(shuō)疾嗅,箭頭函數(shù)會(huì)繼承外層函數(shù)調(diào)用的this
綁定(無(wú)論this
綁定到什么)。這其實(shí)和ES6
之前代碼中的 self = this 機(jī)制一樣冕象。
15. 類型
- 對(duì)象是
JavaScript
的基礎(chǔ)代承。在JavaScript
中一共有六種主要類型(術(shù)語(yǔ)是“語(yǔ)言類型”):- string
- number
- boolean
- null
- undefined
- object
注意,簡(jiǎn)單基本類型(
string
渐扮、boolean
论悴、number
掖棉、null
和undefined
)本身并不是對(duì)象。
null
有時(shí)會(huì)被當(dāng)作一種對(duì)象類型膀估,但是這其實(shí)只是語(yǔ)言本身的一個(gè)bug
幔亥,即對(duì)null
執(zhí)行typeof null
時(shí)會(huì)返回字符串 "object"。實(shí)際上察纯,null
本身是基本類型帕棉。原理是這樣的,不同的對(duì)象在底層都表示為二進(jìn)制饼记,在
JavaScript
中二進(jìn)制前三位都為 0 的話會(huì)被判斷為object
類型香伴,null
的二進(jìn)制表示是全 0,自然前三位也是 0具则,所以執(zhí)行typeof
時(shí)會(huì)返回“object”即纲。
有一種常見(jiàn)的錯(cuò)誤說(shuō)法是“JavaScript 中萬(wàn)物皆是對(duì)象”,這顯然是錯(cuò)誤的乡洼。
實(shí)際上崇裁,
JavaScript
中有許多特殊的對(duì)象子類型,我們可以稱之為復(fù)雜基本類型束昵。
函數(shù)就是對(duì)象的一個(gè)子類型(從技術(shù)角度來(lái)說(shuō)就是“可調(diào)用的對(duì)象”)拔稳。
JavaScript
中的函數(shù)是“一等公民”,因?yàn)樗鼈儽举|(zhì)上和普通的對(duì)象一樣(只是可以調(diào)用)锹雏,所以可以像操作其他對(duì)象一樣操作函數(shù)(比如當(dāng)作另一個(gè)函數(shù)的參數(shù))巴比。
數(shù)組也是對(duì)象的一種類型,具備一些額外的行為礁遵。數(shù)組中內(nèi)容的組織方式比一般的對(duì)象要稍微復(fù)雜一些轻绞。
16. 內(nèi)置對(duì)象
-
JavaScript
中還有一些對(duì)象子類型,通常被稱為內(nèi)置對(duì)象佣耐。有些內(nèi)置對(duì)象的名字看起來(lái)和簡(jiǎn)單基礎(chǔ)類型一樣政勃,不過(guò)實(shí)際上它們的關(guān)系更復(fù)雜。- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
JavaScript
中兼砖,這些內(nèi)置對(duì)象實(shí)際上只是一些內(nèi)置函數(shù)奸远。這些內(nèi)置函數(shù)可以當(dāng)作構(gòu)造函數(shù)(由new
產(chǎn)生的函數(shù)調(diào)用)來(lái)使用,從而可以構(gòu)造一個(gè)對(duì)應(yīng)子類型的新對(duì)象讽挟。舉例來(lái)說(shuō):
var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false
var strObject = new String( "I am a string" );
typeof strObject; // "object"
strObject instanceof String; // true
// 檢查 sub-type 對(duì)象
Object.prototype.toString.call( strObject ); // [object String]
從代碼中可以看到懒叛,
strObject
是由String
構(gòu)造函數(shù)創(chuàng)建的一個(gè)對(duì)象。
原始值 "I am a string" 并不是一個(gè)對(duì)象耽梅,它只是一個(gè)字面量薛窥,并且是一個(gè)不可變的值。
如果要在這個(gè)字面量上執(zhí)行一些操作眼姐,比如獲取長(zhǎng)度诅迷、訪問(wèn)其中某個(gè)字符等佩番,那需要將其轉(zhuǎn)換為 String
對(duì)象。
var strPrimitive = "I am a string";
console.log( strPrimitive.length ); // 13
console.log( strPrimitive.charAt( 3 ) ); // "m"
使用以上兩種方法竟贯,我們都可以直接在字符串字面量上訪問(wèn)屬性或者方法答捕,之所以可以這樣做,是因?yàn)橐孀詣?dòng)把字面量轉(zhuǎn)換成
String
對(duì)象屑那,所以可以訪問(wèn)屬性和方法。
同樣的事也會(huì)發(fā)生在數(shù)值字面量上艘款,如果使用類似42.359.toFixed(2)
的方法持际,引擎會(huì)把對(duì)象 42 轉(zhuǎn)換成new Number(42)
。對(duì)于布爾字面量來(lái)說(shuō)也是如此哗咆。
null
和undefined
沒(méi)有對(duì)應(yīng)的構(gòu)造形式蜘欲,它們只有文字形式。相反晌柬,Date
只有構(gòu)造姥份,沒(méi)有文字形式。
對(duì)于
Object
年碘、Array
澈歉、Function
和RegExp
(正則表達(dá)式)來(lái)說(shuō),無(wú)論使用文字形式還是構(gòu)造形式屿衅,它們都是對(duì)象埃难,不是字面量。在某些情況下涤久,相比用文字形式創(chuàng)建對(duì)象涡尘,構(gòu)造形式可以提供一些額外選項(xiàng)。由于這兩種形式都可以創(chuàng)建對(duì)象响迂,所以我們首選更簡(jiǎn)單的文字形式考抄。建議只在需要那些額外選項(xiàng)時(shí)使用構(gòu)造形式。
Error
對(duì)象很少在代碼中顯式創(chuàng)建蔗彤,一般是在拋出異常時(shí)被自動(dòng)創(chuàng)建川梅。也可以使用new Error(..)
這種構(gòu)造形式來(lái)創(chuàng)建,不過(guò)一般來(lái)說(shuō)用不著幕与。
17. 內(nèi)容
- 需要強(qiáng)調(diào)的一點(diǎn)是挑势,當(dāng)我們說(shuō)“內(nèi)容”時(shí),似乎在暗示這些值實(shí)際上被存儲(chǔ)在對(duì)象內(nèi)部啦鸣,但是這只是它的表現(xiàn)形式潮饱。在引擎內(nèi)部,這些值的存儲(chǔ)方式是多種多樣的诫给,一般并不會(huì)存在對(duì)象容器內(nèi)部香拉。存儲(chǔ)在對(duì)象容器內(nèi)部的是這些屬性的名稱啦扬,它們就像指針(從技術(shù)角度來(lái)說(shuō)就是引用)一樣,指向這些值真正的存儲(chǔ)位置凫碌。
var myObject = {
a: 2
};
myObject.a; // 2
myObject["a"]; // 2
如果要訪問(wèn) myObject 中 a 位置上的值扑毡,我們需要使用 . 操作符或者 [] 操作符。.a 語(yǔ)法通常被稱為“屬性訪問(wèn)”盛险,["a"] 語(yǔ)法通常被稱為“鍵訪問(wèn)”瞄摊。實(shí)際上它們?cè)L問(wèn)的是同一個(gè)位置,并且會(huì)返回相同的值 2苦掘,所以這兩個(gè)術(shù)語(yǔ)是可以互換的换帜。
這兩種語(yǔ)法的主要區(qū)別在于 . 操作符要求屬性名滿足標(biāo)識(shí)符的命名規(guī)范,而 [".."] 語(yǔ)法可以接受任意
UTF-8/Unicode
字符串作為屬性名鹤啡。舉例來(lái)說(shuō)惯驼,如果要引用名稱為 "SuperFun!"的屬性,那就必須使用 ["Super-Fun!"] 語(yǔ)法訪問(wèn)递瑰,因?yàn)?Super-Fun!
并不是一個(gè)有效的標(biāo)識(shí)符屬性名祟牲。
- 在對(duì)象中,屬性名永遠(yuǎn)都是字符串抖部。如果你使用
string
(字面量)以外的其他值作為屬性名说贝,那它首先會(huì)被轉(zhuǎn)換為一個(gè)字符串。即使是數(shù)字也不例外您朽,雖然在數(shù)組下標(biāo)中使用的的確是數(shù)字狂丝,但是在對(duì)象屬性名中數(shù)字會(huì)被轉(zhuǎn)換成字符串,所以當(dāng)心不要搞混對(duì)象和數(shù)組中數(shù)字的用法:
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
18. 可計(jì)算屬性名
- ES6 增加了可計(jì)算屬性名哗总,可以在文字形式中使用
[]
包裹一個(gè)表達(dá)式來(lái)當(dāng)作屬性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
19. 復(fù)制對(duì)象
- 對(duì)于
JSON
安全(也就是說(shuō)可以被序列化為一個(gè)JSON
字符串并且可以根據(jù)這個(gè)字符串解析出一個(gè)結(jié)構(gòu)和值完全一樣的對(duì)象)的對(duì)象來(lái)說(shuō)几颜,有一種巧妙的復(fù)制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
當(dāng)然,這種方法需要保證對(duì)象是 JSON 安全的讯屈,所以只適用于部分情況蛋哭。
- ES6 定義了
Object.assign(..)
方法來(lái)實(shí)現(xiàn)淺復(fù)制。
Object.assign(..)
方法的第一個(gè)參數(shù)是目標(biāo)對(duì)象涮母,之后還可以跟一個(gè)或多個(gè)源對(duì)象谆趾。它會(huì)遍歷一個(gè)或多個(gè)源對(duì)象的所有可枚舉(enumerable
,參見(jiàn)下面的代碼)的自有鍵(owned key
)并把它們復(fù)制(使用 = 操作符賦值)到目標(biāo)對(duì)象叛本,最后返回目標(biāo)對(duì)象沪蓬,就像這樣:
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
由于 Object.assign(..) 就是使用 = 操作符來(lái)賦值,所
以源對(duì)象屬性的一些特性(比如 writable)不會(huì)被復(fù)制到目標(biāo)對(duì)象来候。
20. 屬性描述符
- 在 ES5 之前跷叉,JavaScript 語(yǔ)言本身并沒(méi)有提供可以直接檢測(cè)屬性特性的方法,比如判斷屬性是否是只讀。但是從 ES5 開(kāi)始云挟,所有的屬性都具備了屬性描述符梆砸。
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
這個(gè)普通的對(duì)象屬性對(duì)應(yīng)的屬性描述符(也被稱為“數(shù)據(jù)描述符”,因?yàn)樗槐4嬉粋€(gè)數(shù)據(jù)值)可不僅僅只是一個(gè) 2园欣。它還包含另外三個(gè)特性:
writable
(可寫)帖世、enumerable
(可枚舉)和configurable
(可配置)。
- 可以使用
Object.defineProperty(..)
來(lái)添加一個(gè)新屬性或者修改一個(gè)已有屬性(如果它是configurable
)并對(duì)特性進(jìn)行設(shè)置沸枯。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
我們使用
defineProperty(..)
給myObject
添加了一個(gè)普通的屬性并顯式指定了一些特性日矫。然而,一般來(lái)說(shuō)你不會(huì)使用這種方式绑榴,除非你想修改屬性描述符搬男。
-
writable
決定是否可以修改屬性的值。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫彭沼!
configurable: true,
enumerable: true
} );
myObject.a = 3;
myObject.a; // 2
如你所見(jiàn),我們對(duì)于屬性值的修改靜默失敱赴!(
silently failed
)了姓惑。如果在嚴(yán)格模式下,這種方法會(huì)出錯(cuò):
"use strict";
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: false, // 不可寫按脚!
configurable: true,
enumerable: true
} );
myObject.a = 3; // TypeError
TypeError 錯(cuò)誤表示我們無(wú)法修改一個(gè)不可寫的屬性于毙。
-
configurable
只要屬性是可配置的,就可以使用defineProperty(..)
方法來(lái)修改屬性描述符:
var myObject = {
a:2
};
myObject.a = 3;
myObject.a; // 3
Object.defineProperty( myObject, "a", {
value: 4,
writable: true,
configurable: false, // 不可配置辅搬!
enumerable: true
} );
myObject.a; // 4
myObject.a = 5;
myObject.a; // 5
Object.defineProperty( myObject, "a", {
value: 6,
writable: true,
configurable: true,
enumerable: true
} ); // TypeError
最后一個(gè)
defineProperty(..)
會(huì)產(chǎn)生一個(gè)TypeError
錯(cuò)誤唯沮,不管是不是處于嚴(yán)格模式,嘗試修改一個(gè)不可配置的屬性描述符都會(huì)出錯(cuò)堪遂。注意:如你所見(jiàn)介蛉,把configurable
修改成false
是單向操作,無(wú)法撤銷溶褪!
要注意有一個(gè)小小的例外:即便屬性是configurable:false
币旧,我們還是可以把writable
的狀態(tài)由true
改為false
,但是無(wú)法由false
改為true
猿妈。
21. 不變性
- 對(duì)象常量(不可修改吹菱、重定義或者刪除)
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
- 禁止擴(kuò)展(禁 止 一 個(gè) 對(duì) 象 添 加 新 屬 性 并 且 保 留 已 有 屬 性)
var myObject = {
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
在非嚴(yán)格模式下,創(chuàng)建屬性 b 會(huì)靜默失敗彭则。在嚴(yán)格模式下鳍刷,將會(huì)拋出 TypeError 錯(cuò)誤。
- 密封
Object.seal(..)
會(huì)創(chuàng)建一個(gè)“密封”的對(duì)象俯抖,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用Object.preventExtensions(..)
并把所有現(xiàn)有屬性標(biāo)記為configurable:false
输瓜。所以,密封之后不僅不能添加新屬性,也不能重新配置或者刪除任何現(xiàn)有屬性(雖然可以修改屬性的值)
- 凍結(jié)
Object.freeze(..)
會(huì)創(chuàng)建一個(gè)凍結(jié)對(duì)象前痘,這個(gè)方法實(shí)際上會(huì)在一個(gè)現(xiàn)有對(duì)象上調(diào)用Object.seal(..)
并把所有“數(shù)據(jù)訪問(wèn)”屬性標(biāo)記為writable:false
凛捏,這樣就無(wú)法修改它們的值。
這個(gè)方法是你可以應(yīng)用在對(duì)象上的級(jí)別最高的不可變性芹缔,它會(huì)禁止對(duì)于對(duì)象本身及其任意直接屬性的修改(不過(guò)就像我們之前說(shuō)過(guò)的坯癣,這個(gè)對(duì)象引用的其他對(duì)象是不受影響的)。
你可以“深度凍結(jié)”一個(gè)對(duì)象最欠,具體方法為示罗,首先在這個(gè)對(duì)象上調(diào)用
Object.freeze(..)
,然后遍歷它引用的所有對(duì)象并在這些對(duì)象上調(diào)用Object.freeze(..)
芝硬。但是一定要小心蚜点,因
為這樣做有可能會(huì)在無(wú)意中凍結(jié)其他(共享)對(duì)象。
22. 存在性
前面我們介紹過(guò)拌阴,如
myObject.a
的屬性訪問(wèn)返回值可能是undefined
绍绘,但是這個(gè)值有可能是屬性中存儲(chǔ)的undefined
,也可能是因?yàn)閷傩圆淮嬖谒苑祷?undefined
迟赃。那么如何區(qū)分這兩種情況呢陪拘?
- 可以在不訪問(wèn)屬性值的情況下判斷對(duì)象中是否存在這個(gè)屬性:
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
in 操作符會(huì)檢查屬性是否在對(duì)象及其 [[Prototype]] 原型鏈中相比之下,
hasOwnProperty(..)
只會(huì)檢查屬性是否在myObject
對(duì)象中纤壁,不會(huì)檢查 [[Prototype]] 鏈左刽。
所有的普通對(duì)象都可以通過(guò)對(duì)于
Object.prototype
的 委 托 來(lái)訪問(wèn)hasOwnProperty(..)
,但是有的對(duì)象可能沒(méi)有連接到Object.prototype
(通過(guò)Object.create(null)
來(lái)創(chuàng)建——參見(jiàn)第 5 章)酌媒。在這種情況下欠痴,形如myObejct.hasOwnProperty(..)
就會(huì)失敗。
這 時(shí) 可 以 使 用 一 種 更 加 強(qiáng) 硬 的 方 法 來(lái) 進(jìn) 行 判 斷:Object.prototype.hasOwnProperty.call(myObject,"a")
秒咨,它借用基礎(chǔ)的hasOwnProperty(..)
方法并把它顯式綁定到 myObject 上喇辽。
看起來(lái)
in
操作符可以檢查容器內(nèi)是否有某個(gè)值,但是它實(shí)際上檢查的是某個(gè)屬性名是否存在拭荤。對(duì)于數(shù)組來(lái)說(shuō)這個(gè)區(qū)別非常重要茵臭,4 in [2, 4, 6]
的結(jié)果并不是你期待的True
,因?yàn)?[2, 4, 6]
這個(gè)數(shù)組中包含的屬性名是0舅世、1旦委、2
,沒(méi)有4
雏亚。
- 枚舉(什么是“可枚舉性”)
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 讓 a 像普通屬性一樣可以枚舉
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 讓 b 不可枚舉
{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) {
console.log( k, myObject[k] );
}
// "a" 2
可以看到缨硝,
myObject.b
確實(shí)存在并且有訪問(wèn)值,但是卻不會(huì)出現(xiàn)在for..in
循環(huán)中(盡管可以通過(guò)in
操作符來(lái)判斷是否存在)罢低。原因是“可枚舉”就相當(dāng)于“可以出現(xiàn)在對(duì)象屬性的遍歷中”查辩。
在數(shù)組上應(yīng)用
for..in
循環(huán)有時(shí)會(huì)產(chǎn)生出人意料的結(jié)果胖笛,因?yàn)檫@種枚舉不僅會(huì)包含所有數(shù)值索引,還會(huì)包含所有可枚舉屬性宜岛。最好只在對(duì)象上應(yīng)用for..in
循環(huán)长踊,如果要遍歷數(shù)組就使用傳統(tǒng)的for
循環(huán)來(lái)遍歷數(shù)值索引。
- 也可以通過(guò)另一種方式來(lái)區(qū)分屬性是否可枚舉:
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 讓 a 像普通屬性一樣可以枚舉
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 讓 b 不可枚舉
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
propertyIsEnumerable(..)
會(huì)檢查給定的屬性名是否直接存在于對(duì)象中(而不是在原型鏈上)并且滿足enumerable:true
萍倡。
Object.keys(..)
會(huì)返回一個(gè)數(shù)組身弊,包含所有可枚舉屬性
Object.getOwnPropertyNames(..)
會(huì)返回一個(gè)數(shù)組,包含所有屬性列敲,無(wú)論它們是否可枚舉阱佛。
in
和hasOwnProperty(..)
的區(qū)別在于是否查找 [[Prototype]] 鏈,然而戴而,Object.keys(..)
和Object.getOwnPropertyNames(..)
都只會(huì)查找對(duì)象直接包含的屬性凑术。
(目前)并沒(méi)有內(nèi)置的方法可以獲取
in
操作符使用的屬性列表(對(duì)象本身的屬性以及 [[Prototype]] 鏈中的所有屬性,參見(jiàn)第 5 章)所意。不過(guò)你可以遞歸遍歷某個(gè)對(duì)象的整條 [[Prototype]] 鏈并保存每一層中使用Object.keys(..)
得到的屬性列表——只包含可枚舉屬性淮逊。
23. 遍歷
- 對(duì)于數(shù)值索引的數(shù)組來(lái)說(shuō),可以使用標(biāo)準(zhǔn)的 for 循環(huán)來(lái)遍歷值:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
}
// 1 2 3
這實(shí)際上并不是在遍歷值扶踊,而是遍歷下標(biāo)來(lái)指向值壮莹,如
myArray[i]
。
-
ES5
中增加了一些數(shù)組的輔助迭代器姻檀,包括forEach(..)
、every(..)
和some(..)
涝滴。每種輔助迭代器都可以接受一個(gè)回調(diào)函數(shù)并把它應(yīng)用到數(shù)組的每個(gè)元素上绣版,唯一的區(qū)別就是它們對(duì)于回調(diào)函數(shù)返回值的處理方式不同。-
forEach(..)
會(huì)遍歷數(shù)組中的所有值并忽略回調(diào)函數(shù)的返回值 -
every(..)
會(huì)一直運(yùn)行直到回調(diào)函數(shù)返回false
(或者“假”值) -
some(..)
會(huì)一直運(yùn)行直到回調(diào)函數(shù)返回true
(或者
“真”值)
every(..)
和some(..)
中特殊的返回值和普通for
循環(huán)中的break
語(yǔ)句類似歼疮,它們會(huì)提前終止遍歷使用
for..in
遍歷對(duì)象是無(wú)法直接獲取屬性值的杂抽,因?yàn)樗鼘?shí)際上遍歷的是對(duì)象中的所有可枚舉屬性,你需要手動(dòng)獲取屬性值韩脏。遍歷數(shù)組下標(biāo)時(shí)采用的是數(shù)字順序(
for
循環(huán)或者其他迭代器)缩麸,但是遍歷對(duì)象屬性時(shí)的順序是不確定的部服,在不同的JavaScript
引擎中可能不一樣弄匕。因此奥额,在不同的環(huán)境中需要保證一致性時(shí)焚志,一定不要相信任何觀察到的順序先巴,它們是不可靠的丸升。 -
ES6
增加了一種用來(lái)遍歷數(shù)組的for..of
循環(huán)語(yǔ)法(如果對(duì)象本身定義了迭代器的話也可以遍歷對(duì)象):
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
for..of
循環(huán)首先會(huì)向被訪問(wèn)對(duì)象請(qǐng)求一個(gè)迭代器對(duì)象粗仓,然后通過(guò)調(diào)用迭代器對(duì)象的next()
方法來(lái)遍歷所有返回值譬挚。數(shù)組有內(nèi)置的
@@iterator
空民,因此for..of
可以直接應(yīng)用在數(shù)組上刃唐。我們使用內(nèi)置的@@iterator
來(lái)手動(dòng)遍歷數(shù)組,看看它是怎么工作的:
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
我們使用
ES6
中的符號(hào)Symbol.iterator
來(lái)獲取對(duì)象的@@iterator
內(nèi)部屬性。之前我們簡(jiǎn)單介紹過(guò)符(Symbol
画饥,參見(jiàn) 3.3.1 節(jié))衔瓮,跟這里的原理是相同的。引用類似iterator
的特殊屬性時(shí)要使用符號(hào)名抖甘,而不是符號(hào)包含的值热鞍。此外,雖然看起來(lái)很像一個(gè)對(duì)象单山,但是@@iterator
本身并不是一個(gè)迭代器對(duì)象碍现,而是一個(gè)返回迭代器對(duì)象的函數(shù)——這點(diǎn)非常精妙并且非常重要。
如你所見(jiàn)米奸,調(diào)用迭代器的
next()
方法會(huì)返回形式為{ value: .. , done: .. }
的值昼接,value
是當(dāng)前的遍歷值,done
是一個(gè)布爾值悴晰,表示是否還有可以遍歷的值慢睡。
注意,和值“3”一起返回的是done:false
铡溪,乍一看好像很奇怪漂辐,你必須再調(diào)用一次next()
才能得到done:true
,從而確定完成遍歷棕硫。這個(gè)機(jī)制和ES6
中發(fā)生器函數(shù)的語(yǔ)義相關(guān)髓涯,不過(guò)已經(jīng)超出了我們的討論范圍。
和數(shù)組不同哈扮,普通的對(duì)象沒(méi)有內(nèi)置的
@@iterator
纬纪,所以無(wú)法自動(dòng)完成for..of
遍歷。之所以要這樣做滑肉,有許多非常復(fù)雜的原因包各,不過(guò)簡(jiǎn)單來(lái)說(shuō),這樣做是為了避免影響未來(lái)的對(duì)象類型靶庙。
當(dāng)然问畅,你可以給任何想遍歷的對(duì)象定義 @@iterator,舉例來(lái)說(shuō):
var myObject = {
a: 2,
b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
} );
// 手動(dòng)遍歷 myObject
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// 用 for..of 遍歷 myObject
for (var v of myObject) {
console.log( v );
}
// 2
// 3
我們使用
Object.defineProperty(..)
定義了我們自己的@@iterator
(主要是為了讓它不可枚舉)六荒,不過(guò)注意护姆,我們把符號(hào)當(dāng)作可計(jì)算屬性名。此外掏击,也可以直接在定義對(duì)象時(shí)進(jìn)行聲明签则,比如var myObject = { a:2, b:3, [Symbol.iterator]: function() { /* .. */ } }
。
- 一個(gè)“無(wú)限”迭代器铐料,它永遠(yuǎn)不會(huì)“結(jié)束”并且總會(huì)返回一個(gè)新值(隨機(jī)數(shù))
var randoms = {
[Symbol.iterator]: function() {
return {
next: function() {
return { value: Math.random() };
}
};
}
};
var randoms_pool = [];
for (var n of randoms) {
randoms_pool.push( n );
// 防止無(wú)限運(yùn)行渐裂!
if (randoms_pool.length === 100) break;
}
24. 混合對(duì)象 “類” 小結(jié)
類是一種設(shè)計(jì)模式豺旬。許多語(yǔ)言提供了對(duì)于面向類軟件設(shè)計(jì)的原生語(yǔ)法。
JavaScript
也有類似的語(yǔ)法柒凉,但是和其他語(yǔ)言中的類完全不同族阅。
類意味著復(fù)制。
傳統(tǒng)的類被實(shí)例化時(shí)膝捞,它的行為會(huì)被復(fù)制到實(shí)例中坦刀。類被繼承時(shí),行為也會(huì)被復(fù)制到子類中蔬咬。
多態(tài)(在繼承鏈的不同層次名稱相同但是功能不同的函數(shù))看起來(lái)似乎是從子類引用父類鲤遥,但是本質(zhì)上引用的其實(shí)是復(fù)制的結(jié)果。
JavaScript
并不會(huì)(像類那樣)自動(dòng)創(chuàng)建對(duì)象的副本林艘。
混入模式(無(wú)論顯式還是隱式)可以用來(lái)模擬類的復(fù)制行為盖奈,但是通常會(huì)產(chǎn)生丑陋并且脆弱的語(yǔ)法,比如顯式偽多態(tài)(
OtherObj.methodName.call(this, ...)
)狐援,這會(huì)讓代碼更加難懂并且難以維護(hù)钢坦。
此外,顯式混入實(shí)際上無(wú)法完全模擬類的復(fù)制行為啥酱,因?yàn)閷?duì)象(和函數(shù)爹凹!別忘了函數(shù)也是對(duì)象)只能復(fù)制引用,無(wú)法復(fù)制被引用的對(duì)象或者函數(shù)本身镶殷。忽視這一點(diǎn)會(huì)導(dǎo)致許多問(wèn)題禾酱。
25. 原型 -- [[Prototype]]
使用
for..in
遍歷對(duì)象時(shí)原理和查找[[Prototype]]
鏈類似,任何可以通過(guò)原型鏈訪問(wèn)到(并且是enumerable
绘趋,參見(jiàn)第 3 章)的屬性都會(huì)被枚舉宇植。使用in
操作符來(lái)檢查屬性在對(duì)象中是否存在時(shí),同樣會(huì)查找對(duì)象的整條原型鏈(無(wú)論屬性是否可枚舉):
var anotherObject = {
a:2
};
// 創(chuàng)建一個(gè)關(guān)聯(lián)到 anotherObject 的對(duì)象
var myObject = Object.create( anotherObject );
myObject.a; // 2
26. 原型 -- Object.prototype
所有普通的
[[Prototype]]
鏈最終都會(huì)指向內(nèi)置的Object.prototype
埋心。由于所有的“普通”(內(nèi)置,不是特定主機(jī)的擴(kuò)展)對(duì)象都“源于”(或者說(shuō)把[[Prototype]]
鏈的頂端設(shè)置為)這個(gè)Object.prototype
對(duì)象忙上,所以它包含JavaScript
中許多通用的功能拷呆。
有 些 功 能 你 應(yīng) 該 已 經(jīng) 很 熟 悉 了, 比 如 說(shuō).toString()
和.valueOf()
疫粥, 第 3 章 還 介 紹過(guò).hasOwnProperty(..)
茬斧。稍后我們還會(huì)介紹.isPrototypeOf(..)
,這個(gè)你可能不太熟悉梗逮。
27. 原型 - “類”函數(shù)
new Foo()
會(huì)生成一個(gè)新對(duì)象(我們稱之為a
)项秉,這個(gè)新對(duì)象的內(nèi)部鏈接[[Prototype]]
關(guān)聯(lián)的是Foo.prototype
對(duì)象。
最后我們得到了兩個(gè)對(duì)象慷彤,它們之間互相關(guān)聯(lián)娄蔼,就是這樣怖喻。我們并沒(méi)有初始化一個(gè)類,實(shí)際上我們并沒(méi)有從“類”中復(fù)制任何行為到一個(gè)對(duì)象中岁诉,只是讓兩個(gè)對(duì)象互相關(guān)聯(lián)锚沸。
實(shí)際上,絕大多數(shù)JavaScript
開(kāi)發(fā)者不知道的秘密是涕癣,new Foo()
這個(gè)函數(shù)調(diào)用實(shí)際上并沒(méi)有直接創(chuàng)建關(guān)聯(lián)哗蜈,這個(gè)關(guān)聯(lián)只是一個(gè)意外的副作用。new Foo()
只是間接完成了我們的目標(biāo):一個(gè)關(guān)聯(lián)到其他對(duì)象的新對(duì)象坠韩。
28. 技術(shù)
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個(gè)新原型對(duì)象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
Object(..)
并沒(méi)有“構(gòu)造”a1
距潘,對(duì)吧?看起來(lái)應(yīng)該是Foo()
“構(gòu)造”了它只搁。大部分開(kāi)發(fā)者都認(rèn)為是Foo()
執(zhí)行了構(gòu)造工作音比,但是問(wèn)題在于,如果你認(rèn)為 “constructor” 表示“由……構(gòu)造”的話须蜗,a1.constructor
應(yīng)該是Foo
硅确,但是它并不是Foo
!
到底怎么回事明肮?a1
并沒(méi)有.constructor
屬性菱农,所以它會(huì)委托[[Prototype]]
鏈上的Foo.prototype
。但是這個(gè)對(duì)象也沒(méi)有.constructor
屬性(不過(guò)默認(rèn)的Foo.prototype
對(duì)象有這個(gè)屬性J凉馈)循未,所以它會(huì)繼續(xù)委托,這次會(huì)委托給委托鏈頂端的Object.prototype
秫舌。這個(gè)對(duì)象有.constructor
屬性的妖,指向內(nèi)置的Object(..)
函數(shù)。
- 當(dāng)然足陨,你可以給
Foo.prototype
添加一個(gè).constructor
屬性嫂粟,不過(guò)這需要手動(dòng)添加一個(gè)符合正常行為的不可枚舉(參見(jiàn)第 3 章)屬性。
舉例來(lái)說(shuō):
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個(gè)新原型對(duì)象
// 需要在 Foo.prototype 上“修復(fù)”丟失的 .constructor 屬性
// 新對(duì)象屬性起到 Foo.prototype 的作用
// 關(guān)于 defineProperty(..)墨缘,參見(jiàn)第 3 章
Object.defineProperty( Foo.prototype, "constructor" , {
enumerable: false,
writable: true,
configurable: true,
value: Foo // 讓 .constructor 指向 Foo
} );
修復(fù)
.constructor
需要很多手動(dòng)操作星虹。所有這些工作都是源于把 “constructor” 錯(cuò)誤地理解為“由……構(gòu)造”,這個(gè)誤解的代價(jià)實(shí)在太高了镊讼。
實(shí)際上宽涌,對(duì)象的
.constructor
會(huì)默認(rèn)指向一個(gè)函數(shù),這個(gè)函數(shù)可以通過(guò)對(duì)象的.prototype
引用蝶棋⌒读粒“constructor” 和 “prototype” 這兩個(gè)詞本身的含義可能適用也可能不適用。最好的辦法是記住這一點(diǎn) “constructor 并不表示被構(gòu)造”玩裙。
.constructor
并不是一個(gè)不可變屬性兼贸。它是不可枚舉(參見(jiàn)上面的代碼)的段直,但是它的值是可寫的(可以被修改)。此外寝受,你可以給任意[[Prototype]]
鏈中的任意對(duì)象添加一個(gè)名為constructor
的屬性或者對(duì)其進(jìn)行修改坷牛,你可以任意對(duì)其賦值。
- ES6 添加了輔助函數(shù)
Object.setPrototypeOf(..)
很澄,可以用標(biāo)準(zhǔn)并且可靠的方法來(lái)修
改關(guān)聯(lián)京闰。
我們來(lái)對(duì)比一下兩種把Bar.prototype
關(guān)聯(lián)到Foo.prototype
的方法:
// ES6 之前需要拋棄默認(rèn)的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開(kāi)始可以直接修改現(xiàn)有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
如果忽略掉
Object.create(..)
方法帶來(lái)的輕微性能損失(拋棄的對(duì)象需要進(jìn)行垃圾回收),它實(shí)際上比ES6
及其之后的方法更短而且可讀性更高甩苛。不過(guò)無(wú)論如何蹂楣,這是兩種完全不同的語(yǔ)法。
29. 原型 - 小結(jié)
如果要訪問(wèn)對(duì)象中并不存在的一個(gè)屬性讯蒲,
[[Get]]
操作(參見(jiàn)第 3 章)就會(huì)查找對(duì)象內(nèi)部[[Prototype]]
關(guān)聯(lián)的對(duì)象痊土。這個(gè)關(guān)聯(lián)關(guān)系實(shí)際上定義了一條“原型鏈”(有點(diǎn)像嵌套的作用域鏈),在查找屬性時(shí)會(huì)對(duì)它進(jìn)行遍歷墨林。
所有普通對(duì)象都有內(nèi)置的
Object.prototype
赁酝,指向原型鏈的頂端(比如說(shuō)全局作用域),如果在原型鏈中找不到指定的屬性就會(huì)停止旭等。toString()
酌呆、valueOf()
和其他一些通用的功能都存在于Object.prototype
對(duì)象上,因此語(yǔ)言中所有的對(duì)象都可以使用它們搔耕。
關(guān)聯(lián)兩個(gè)對(duì)象最常用的方法是使用
new
關(guān)鍵詞進(jìn)行函數(shù)調(diào)用隙袁,在調(diào)用的 4 個(gè)步驟(第 2章)中會(huì)創(chuàng)建一個(gè)關(guān)聯(lián)其他對(duì)象的新對(duì)象。4 個(gè)步驟:
創(chuàng)建(或者說(shuō)構(gòu)造)一個(gè)全新的對(duì)象弃榨。
這個(gè)新對(duì)象會(huì)被執(zhí)行 [[ 原型 ]] 連接菩收。
這個(gè)新對(duì)象會(huì)綁定到函數(shù)調(diào)用的 this。
如果函數(shù)沒(méi)有返回其他對(duì)象鲸睛,那么 new 表達(dá)式中的函數(shù)調(diào)用會(huì)自動(dòng)返回這個(gè)新對(duì)象娜饵。
使用
new
調(diào)用函數(shù)時(shí)會(huì)把新對(duì)象的.prototype
屬性關(guān)聯(lián)到“其他對(duì)象”。帶new
的函數(shù)調(diào)用通常被稱為“構(gòu)造函數(shù)調(diào)用”官辈,盡管它們實(shí)際上和傳統(tǒng)面向類語(yǔ)言中的類構(gòu)造函數(shù)不一樣箱舞。
雖然這些
JavaScript
機(jī)制和傳統(tǒng)面向類語(yǔ)言中的“類初始化”和“類繼承”很相似,但
是JavaScript
中的機(jī)制有一個(gè)核心區(qū)別钧萍,那就是不會(huì)進(jìn)行復(fù)制,對(duì)象之間是通過(guò)內(nèi)部的[[Prototype]]
鏈關(guān)聯(lián)的政鼠。
出于各種原因风瘦,以“繼承”結(jié)尾的術(shù)語(yǔ)(包括“原型繼承”)和其他面向?qū)ο蟮男g(shù)語(yǔ)都無(wú)
法幫助你理解JavaScript
的真實(shí)機(jī)制(不僅僅是限制我們的思維模式)。
相比之下公般,“委托”是一個(gè)更合適的術(shù)語(yǔ)万搔,因?yàn)閷?duì)象之間的關(guān)系不是復(fù)制而是委托胡桨。
30. 行為委托
- 面向?qū)ο箫L(fēng)格的代碼示例
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
子類
Bar
繼承了父類Foo
,然后生成了b1
和b2
兩個(gè)實(shí)例瞬雹。b1
委托了Bar.prototype
昧谊,后者委托了Foo.prototype
。
- 對(duì)象關(guān)聯(lián)風(fēng)格的代碼示例
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();