翻回看過的書线衫,整理筆記凿可,方便溫故而知新。這是一本很不錯的書授账,分為兩部分枯跑,第一部分主要講解了作用域惨驶、閉包,第二部分主要講解this敛助、對象原型等知識點(diǎn)粗卜。
第一部分 作用域和閉包
第二部分 this和對象原型
第1章 關(guān)于this
1.1 為什么要用this
this 提供了一種更優(yōu)雅的方式來隱式“傳遞”一個對象引用,因此可以將 API 設(shè)計得更加簡潔并且易于復(fù)用辜腺。
1.2 誤解
1.2.1 指向自身
人們很容易把 this 理解成指向函數(shù)自身休建。
function foo(num) {
console.log( "foo: " + num );
// 記錄 foo 被調(diào)用的次數(shù)
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo 被調(diào)用了多少次?
console.log( foo.count ); // 0 -- WTF?
函數(shù)內(nèi)部代碼this.count 中的 this 并不是指向那個函數(shù)對象评疗。
// 具名函數(shù)测砂,在它內(nèi)部可以使用 foo 來引用自身。
function foo() {
foo.count = 4; // foo 指向它自身
}
// 回調(diào)函數(shù)沒有名稱標(biāo)識符(這種函數(shù)被稱為匿名函數(shù))百匆,因此無法從函數(shù)內(nèi)部引用自身砌些。
setTimeout( function(){
// 匿名(沒有名字的)函數(shù)無法指向自身
}, 10 );
方法一:(回避了 this 的問題,并且完全依賴于變量 foo 的詞法作用域)
function foo(num) {
console.log( "foo: " + num );
// 記錄 foo 被調(diào)用的次數(shù)
foo.count++;
}
foo.count=0
var i;
for (i=0; i<10; i++) {
if (i > 5) {
foo( i );
}
console.log( foo.count ); // 4
方法二:(接受了 this加匈,沒有回避它)
function foo(num) {
console.log( "foo: " + num );
// 記錄 foo 被調(diào)用的次數(shù)
// 注意存璃,在當(dāng)前的調(diào)用方式下(參見下方代碼),this 確實(shí)指向 foo
this.count++;
}
foo.count = 0;
var i;
for (i=0; i<10; i++) {
if (i > 5) {
// 使用 call(..) 可以確保 this 指向函數(shù)對象 foo 本身
foo.call( foo, i );
}
}
console.log( foo.count ); // 4
1.2.2 它的作用域
this 在任何情況下都不指向函數(shù)的詞法作用域雕拼。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
- 這段代碼試圖通過 this.bar() 來引用 bar() 函數(shù)纵东。這是絕對不可能成功的。調(diào)用 bar() 最自然的方法是省略前面的 this啥寇,直接使用詞法引用標(biāo)識符偎球。
- 還試圖使用 this 聯(lián)通 foo() 和 bar() 的詞法作用域,從而讓bar() 可以訪問 foo() 作用域里的變量 a辑甜。這是不可能實(shí)現(xiàn)的衰絮,你不能使用 this 來引用一個詞法作用域內(nèi)部的東西。
1.3 this到底是什么
this 是在運(yùn)行時進(jìn)行綁定的磷醋,并不是在編寫時綁定猫牡,它的上下文取決于函數(shù)調(diào)用時的各種條件。
當(dāng)一個函數(shù)被調(diào)用時邓线,會創(chuàng)建一個活動記錄(有時候也稱為執(zhí)行上下文)淌友。這個記錄會包含函數(shù)在哪里被調(diào)用(調(diào)用棧)、函數(shù)的調(diào)用方法褂痰、傳入的參數(shù)等信息亩进。this 就是記錄的其中一個屬性。
1.4 小結(jié)
this 既不指向函數(shù)自身也不指向函數(shù)的詞法作用域缩歪!
this 實(shí)際上是在函數(shù)被調(diào)用時發(fā)生的綁定归薛,它指向什么完全取決于函數(shù)在哪里被調(diào)用。
第2章 this全面解析
調(diào)用位置就是函數(shù)在代碼中被調(diào)用的位置(而不是聲明的位置)。
綁定規(guī)則
默認(rèn)綁定
獨(dú)立函數(shù)調(diào)用主籍∠捌叮可以把這條規(guī)則看作是無法應(yīng)用其他規(guī)則時的默認(rèn)規(guī)則。
- 如果使用嚴(yán)格模式(strict mode)千元,那么全局對象將無法使用默認(rèn)綁定苫昌。
- 只有 foo() 運(yùn)行在非 strict mode 下時,默認(rèn)綁定才能綁定到全局對象幸海;嚴(yán)格模式下與 foo()的調(diào)用位置無關(guān)
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
隱式綁定
調(diào)用位置是否有上下文對象祟身,或者說是否被某個對象擁有或者包含。
當(dāng)函數(shù)引用有上下文對象時物独,隱式綁定規(guī)則會把函數(shù)調(diào)用中的 this 綁定到這個上下文對象袜硫。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
隱式丟失
一個最常見的 this 綁定問題就是被隱式綁定的函數(shù)會丟失綁定對象,也就是說它會應(yīng)用默認(rèn)綁定挡篓,從而把 this 綁定到全局對象或者 undefined 上婉陷。
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
// 雖然 bar 是 obj.foo 的一個引用,但是實(shí)際上官研,它引用的是 foo 函數(shù)本身
// 因此此時的bar() 其實(shí)是一個不帶任何修飾的函數(shù)調(diào)用秽澳,因此應(yīng)用了默認(rèn)綁定。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函數(shù)別名戏羽!
var a = "oops, global"; // a 是全局對象的屬性
bar(); // "oops, global"
顯式綁定
可以使用函數(shù)的 call(..) 和apply(..) 方法担神。
如果你傳入了一個原始值(字符串類型、布爾類型或者數(shù)字類型)來當(dāng)作 this 的綁定對象始花,這個原始值會被轉(zhuǎn)換成它的對象形式(也就是 new String(..)杏瞻、new Boolean(..) 或者new Number(..))。這通常被稱為“裝箱”衙荐。
顯式綁定仍然無法解決我們之前提出的丟失綁定問題。
但是顯式綁定的一個變種可以解決這個問題——硬綁定
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬綁定的 bar 不可能再修改它的 this
bar.call( window ); // 2
內(nèi)部手動調(diào)用了 foo.call(obj)浮创,因此強(qiáng)制把 foo 的 this 綁定到了 obj忧吟。無論之后如何調(diào)用函數(shù) bar,它總會手動在 obj 上調(diào)用 foo斩披。這種綁定是一種顯式的強(qiáng)制綁定溜族,因此我們稱之為硬綁定。
硬綁定的典型應(yīng)用場景就是:
(1)創(chuàng)建一個包裹函數(shù)垦沉,傳入所有的參數(shù)并返回接收到的所有值煌抒。
(2)創(chuàng)建一個可以重復(fù)使用的輔助函數(shù)。
由于硬綁定是一種非常常用的模式厕倍,所以在 ES5 中提供了內(nèi)置的方法Function.prototype.bind
寡壮,它的用法如下:
// bind(..) 會返回一個硬編碼的新函數(shù),它會把參數(shù)設(shè)置為 this 的上下文并調(diào)用原始函數(shù)。
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
第三方庫的許多函數(shù)况既,以及 JavaScript 語言和宿主環(huán)境中許多新的內(nèi)置函數(shù)这溅,都提供了一個可選的參數(shù),通常被稱為“上下文”(context)棒仍,其作用和 bind(..) 一樣悲靴,確保你的回調(diào)函數(shù)使用指定的 this。這些函數(shù)實(shí)際上就是通過 call(..) 或者 apply(..) 實(shí)現(xiàn)了顯式綁定莫其,
new綁定
- JavaScript 中 new 的機(jī)制實(shí)際上和面向類的語言完全不同癞尚。
- 在 JavaScript 中,構(gòu)造函數(shù)只是一些使用 new 操作符時被調(diào)用的函數(shù)。它們并不會屬于某個類而钞,也不會實(shí)例化一個類仍秤。實(shí)際上,它們甚至都不能說是一種特殊的函數(shù)類型临燃,它們只是被 new 操作符調(diào)用的普通函數(shù)而已。
- 實(shí)際上并不存在所謂的“構(gòu)造函數(shù)”烙心,只有對于函數(shù)的“構(gòu)造調(diào)用”膜廊。
使用 new 來調(diào)用函數(shù),或者說發(fā)生構(gòu)造函數(shù)調(diào)用時淫茵,會自動執(zhí)行下面的操作爪瓜。
- 創(chuàng)建(或者說構(gòu)造)一個全新的對象。
- 這個新對象會被執(zhí)行 [[ 原型 ]] 連接匙瘪。
- 這個新對象會綁定到函數(shù)調(diào)用的 this铆铆。
- 如果函數(shù)沒有返回其他對象,那么 new 表達(dá)式中的函數(shù)調(diào)用會自動返回這個新對象丹喻。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 來調(diào)用 foo(..) 時薄货,我們會構(gòu)造一個新對象并把它綁定到 foo(..) 調(diào)用中的 this上。
*優(yōu)先級
- 函數(shù)是否在 new 中調(diào)用(new 綁定)碍论?如果是的話 this 綁定的是新創(chuàng)建的對象谅猾。
var bar = new foo()
- 函數(shù)是否通過 call、apply(顯式綁定)或者硬綁定調(diào)用鳍悠?如果是的話税娜,this 綁定的是指定的對象。
var bar = foo.call(obj2)
- 函數(shù)是否在某個上下文對象中調(diào)用(隱式綁定)藏研?如果是的話敬矩,this 綁定的是那個上下文對象。
var bar = obj1.foo()
- 如果都不是的話蠢挡,使用默認(rèn)綁定弧岳。如果在嚴(yán)格模式下凳忙,就綁定到 undefined,否則綁定到全局對象缩筛。
var bar = foo()
綁定例外
被忽略的this
如果你把 null 或者 undefined 作為 this 的綁定對象傳入 call消略、apply 或者 bind,這些值在調(diào)用時會被忽略瞎抛,實(shí)際應(yīng)用的是默認(rèn)綁定規(guī)則:
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// 把數(shù)組“展開”成參數(shù)艺演, 傳入null 占位值
foo.apply( null, [2, 3] ); // a:2, b:3
// 使用 bind(..) 進(jìn)行柯里化
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
總是使用 null 來忽略 this 綁定可能產(chǎn)生一些副作用。如果某個函數(shù)確實(shí)使用了this桐臊,那默認(rèn)綁定規(guī)則會把 this 綁定到全局對象胎撤,這將導(dǎo)致不可預(yù)計的后果(比如修改全局對象)。
一種“更安全”的做法是傳入一個空對象断凶,Object.create(null)
// 我們的 DMZ 空對象
var ? = Object.create( null );
// 把數(shù)組展開成參數(shù)
foo.apply( ?, [2, 3] ); // a:2, b:3
間接引用
間接引用最容易在賦值時發(fā)生:
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2
賦值表達(dá)式 p.foo = o.foo 的返回值是目標(biāo)函數(shù)的引用伤提,因此調(diào)用位置是 foo() 而不是p.foo() 或者 o.foo()。根據(jù)我們之前說過的认烁,這里會應(yīng)用默認(rèn)綁定肿男。
軟綁定
硬綁定這種方式可以把 this 強(qiáng)制綁定到指定的對象(除了使用 new時),防止函數(shù)調(diào)用應(yīng)用默認(rèn)綁定規(guī)則却嗡。問題在于舶沛,硬綁定會大大降低函數(shù)的靈活性,使用硬綁定之后就無法使用隱式綁定或者顯式綁定來修改 this窗价。
如果可以給默認(rèn)綁定指定一個全局對象和 undefined 以外的值如庭,那就可以實(shí)現(xiàn)和硬綁定相同的效果,同時保留隱式綁定或者顯式綁定修改 this 的能力撼港。
// 可以通過一種被稱為軟綁定的方法來實(shí)現(xiàn)我們想要的效果:
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this;
// 捕獲所有 curried 參數(shù)
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this
curried.concat.apply( curried, arguments )
);
};
bound.prototype = Object.create( fn.prototype );
return bound;
};
}
下面我們看看 softBind 是否實(shí)現(xiàn)了軟綁定功能:
function foo() {
console.log("name: " + this.name);
}
var obj = { name: "obj" },obj2 = { name: "obj2" },obj3 = { name: "obj3" };
var fooOBJ = foo.softBind( obj );
fooOBJ(); // name: obj
obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2 <---- 看F核!帝牡!
fooOBJ.call( obj3 ); // name: obj3 <---- 看往毡!
setTimeout( obj2.foo, 10 );// name: obj <---- 應(yīng)用了軟綁定
可以看到,軟綁定版本的 foo() 可以手動將 this 綁定到 obj2 或者 obj3 上靶溜,但如果應(yīng)用默認(rèn)綁定卖擅,則會將 this 綁定到 obj。
this詞法
箭頭函數(shù)不使用 this 的四種標(biāo)準(zhǔn)規(guī)則墨技,而是根據(jù)外層(函數(shù)或者全局)作用域來決定 this。
如果你經(jīng)常編寫 this 風(fēng)格的代碼挎狸,但是絕大部分時候都會使用 self = this 或者箭頭函數(shù)來否定 this 機(jī)制扣汪,那你或許應(yīng)當(dāng):
1)只使用詞法作用域并完全拋棄錯誤 this 風(fēng)格的代碼;
2)完全采用 this 風(fēng)格锨匆,在必要時使用 bind(..)崭别,盡量避免使用 self = this 和箭頭函數(shù)冬筒。
當(dāng)然,包含這兩種代碼風(fēng)格的程序可以正常運(yùn)行茅主,但是在同一個函數(shù)或者同一個程序中混合使用這兩種風(fēng)格通常會使代碼更難維護(hù)舞痰,并且可能也會更難編寫。
總結(jié)
如果要判斷一個運(yùn)行中函數(shù)的 this 綁定诀姚,就需要找到這個函數(shù)的直接調(diào)用位置响牛。找到之后就可以順序應(yīng)用下面這四條規(guī)則來判斷 this 的綁定對象。
- 由 new 調(diào)用赫段?綁定到新創(chuàng)建的對象呀打。
- 由 call 或者 apply(或者 bind)調(diào)用?綁定到指定的對象糯笙。
- 由上下文對象調(diào)用贬丛?綁定到那個上下文對象。
- 默認(rèn):在嚴(yán)格模式下綁定到 undefined给涕,否則綁定到全局對象
第3章 對象
語法
對象可以通過兩種形式定義:聲明(文字)形式和構(gòu)造形式豺憔。
// 對象的文字語法大概是這樣:
var myObj = {
key: value
// ...
};
// 構(gòu)造形式大概是這樣:
var myObj = new Object();
myObj.key = value;
構(gòu)造形式和文字形式生成的對象是一樣的。唯一的區(qū)別是够庙,在文字聲明中你可以添加多個鍵 / 值對恭应,但是在構(gòu)造形式中你必須逐個添加屬性。
類型
在 JavaScript 中一共有六種主要類型(術(shù)語是“語言類型”):
string
首启、number
暮屡、boolean
、 null
毅桃、undefined
褒纲、 object
簡單基本類型(string、boolean钥飞、number莺掠、null 和 undefined)本身并不是對象。null 有時會被當(dāng)作一種對象類型读宙,但是這其實(shí)只是語言本身的一個 bug彻秆,即對 null 執(zhí)行typeof null 時會返回字符串 "object"。 實(shí)際上结闸,null 本身是基本類型唇兑。
函數(shù)就是對象的一個子類型。數(shù)組也是對象的一種類型桦锄,具備一些額外的行為扎附。
內(nèi)置對象
JavaScript 中還有一些對象子類型,通常被稱為內(nèi)置對象结耀。有些內(nèi)置對象的名字看起來和簡單基礎(chǔ)類型一樣留夜,不過實(shí)際上它們的關(guān)系更復(fù)雜匙铡。
String
、Number
碍粥、Boolean
鳖眼、Object
、Function
嚼摩、Array
钦讳、Date
、RegExp
低斋、Error
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 對象
Object.prototype.toString.call( strObject ); // [object String]
- 在必要時語言會自動把字符串字面量轉(zhuǎn)換成一個 String 對象蜂厅,引擎自動把字面量轉(zhuǎn)換成 String 對象。類似 42.359.toFixed(2) 的方法膊畴,引擎會把42 轉(zhuǎn)換成 new Number(42)掘猿。對于布爾字面量來說也是如此。
- null 和 undefined 沒有對應(yīng)的構(gòu)造形式唇跨,它們只有文字形式稠通。
- Date 只有構(gòu)造,沒有文字形式买猖。
- 對于 Object改橘、Array、Function 和 RegExp(正則表達(dá)式)來說玉控,無論使用文字形式還是構(gòu)造形式飞主,它們都是對象,不是字面量高诺。
- 由于這兩種形式都可以創(chuàng)建對象碌识,所以我們首選更簡單的文字形式。建議只在需要那些額外選項(xiàng)時使用構(gòu)造形式虱而。
- Error 對象很少在代碼中顯式創(chuàng)建筏餐,一般是在拋出異常時被自動創(chuàng)建。也可以使用 new Error(..) 這種構(gòu)造形式來創(chuàng)建牡拇,不過一般來說用不著魁瞪。
內(nèi)容
- 存儲在對象容器內(nèi)部的是這些屬性的名稱,它們就像指針(從技術(shù)角度來說就是引用)一樣惠呼,指向這些值真正的存儲位置导俘。
- 如果要訪問 myObject 中 a 位置上的值,我們需要使用 . 操作符或者 [] 操作符剔蹋。.a 語法通常被稱為“屬性訪問”旅薄,["a"] 語法通常被稱為“鍵訪問”。[".."] 語法可以接受任意 UTF-8/Unicode 字符串作為屬性名滩租。
- 在對象中赋秀,屬性名永遠(yuǎn)都是字符串。如果你使用 string以外的其他值作為屬性名律想,那它首先會被轉(zhuǎn)換為一個字符串猎莲。即使是數(shù)字也不例外。
- ES6 增加了可計算屬性名技即,可以在文字形式中使用 [] 包裹一個表達(dá)式來當(dāng)作屬性名著洼。
var prefix = "foo";
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]: "world"
};
屬性與方法
由于函數(shù)很容易被認(rèn)為是屬于某個對象,在其他語言中而叼,屬于對象(也被稱為“類”)的函數(shù)通常被稱為“方法”身笤,因此把“屬性訪問”說成是“方法訪問”也就不奇怪了。
從技術(shù)角度來說葵陵,函數(shù)永遠(yuǎn)不會“屬于”一個對象液荸。
無論返回值是什么類型,每次訪問對象的屬性就是屬性訪問脱篙。
function foo() {
console.log( "foo" );
}
var someFoo = foo; // 對 foo 的變量引用
var myObject = {
someFoo: foo
};
foo; // function foo(){..}
someFoo; // function foo(){..}
myObject.someFoo; // function foo(){..}
someFoo 和 myObject.someFoo 只是對于同一個函數(shù)的不同引用娇钱,并不能說明這個函數(shù)是特別的或者“屬于”某個對象。如果 foo() 定義時在內(nèi)部有一個 this 引用绊困,那這兩個函數(shù)引用的唯一區(qū)別就是 myObject.someFoo 中的 this 會被隱式綁定到一個對象文搂。
數(shù)組
- 數(shù)組也支持 [] 訪問形式
- 數(shù)組也是對象,所以雖然每個下標(biāo)都是整數(shù)秤朗,仍然可以給數(shù)組添加屬性
- 雖然添加了命名屬性(無論是通過 . 語法還是 [] 語法)煤蹭,數(shù)組的 length 值并未發(fā)生變化
復(fù)制對象
- 對于 JSON 安全的對象來說,有一種巧妙的復(fù)制方法:
var newObj = JSON.parse( JSON.stringify( someObj ) );
- 相比深復(fù)制取视,淺復(fù)制非常易懂并且問題要少得多硝皂,所以 ES6 定義了 Object.assign(..) 方法來實(shí)現(xiàn)淺復(fù)制。
屬性描述符
var myObject = {
a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
不僅僅只是一個 2贫途。它還包含另外三個特性:writable(可寫)吧彪、enumerable(可枚舉)和 configurable(可配置)。
可以使用 Object.defineProperty(..)來添加一個新屬性或者修改一個已有屬性(如果它是 configurable)并對特性進(jìn)行設(shè)置丢早。
// 一般來說你不會使用這種方式姨裸,除非你想修改屬性描述符。
var myObject = {};
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a; // 2
Writable
writable 決定是否可以修改屬性的值
Configurable
- 只要屬性是可配置的怨酝,就可以使用 defineProperty(..) 方法來修改屬性描述符傀缩。
- configurable 修改成false 是單向操作,無法撤銷农猬!
- 即便屬性是 configurable:false赡艰,我們還是可以把 writable 的狀態(tài)由 true 改為 false,但是無法由 false 改為 true斤葱。
- 除了無法修改慷垮,configurable:false 還會禁止刪除這個屬性
Enumerable
這個描述符控制的是屬性是否會出現(xiàn)在對象的屬性枚舉中揖闸,比如說for..in 循環(huán)。
用戶定義的所有的普通屬性默認(rèn)都是 enumerable料身。
不變性
所有的方法創(chuàng)建的都是淺不變性汤纸,也就是說,它們只會影響目標(biāo)對象和它的直接屬性芹血。如果目標(biāo)對象引用了其他對象(數(shù)組贮泞、對象、函數(shù)等)幔烛,其他對象的內(nèi)容不受影響啃擦,仍然是可變的。
- 對象常量
結(jié)合 writable:false 和 configurable:false 就可以創(chuàng)建一個真正的常量屬性(不可修改饿悬、重定義或者刪除)
var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} );
- 禁止擴(kuò)展
禁止一個對象添加新屬性并且保留已有屬性令蛉,可以使用Object.preventExtensions:
var myObject = {
a:2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; // undefined
- 密封
Object.seal(..) 會創(chuàng)建一個“密封”的對象,這個方法實(shí)際上會在一個現(xiàn)有對象上調(diào)用Object.preventExtensions(..) 并把所有現(xiàn)有屬性標(biāo)記為configurable:false乡恕。
所以言询,密封之后不僅不能添加新屬性,也不能重新配置或者刪除任何現(xiàn)有屬性(雖然可以修改屬性的值)傲宜。 - 凍結(jié)
Object.freeze(..) 會創(chuàng)建一個凍結(jié)對象运杭,這個方法實(shí)際上會在一個現(xiàn)有對象上調(diào)用Object.seal(..) 并把所有“數(shù)據(jù)訪問”屬性標(biāo)記為 writable:false,這樣就無法修改它們的值函卒。這個方法是你可以應(yīng)用在對象上的級別最高的不可變性辆憔。
[[Get]]
var myObject = {
a: 2
};
myObject.a; // 2
- myObject.a 在 myObject 上實(shí)際上是實(shí)現(xiàn)了 [[Get]] 操作(有點(diǎn)像函數(shù)調(diào)
用:[[Get]]()
)。 - 對象默認(rèn)的內(nèi)置 [[Get]] 操作首先在對象中查找是否有名稱相同的屬性报嵌,如果找到就會返回這個屬性的值虱咧。
- 如果沒有找到名稱相同的屬性,按照 [[Get]] 算法的定義會遍歷可能存在的 [[Prototype]] 鏈锚国,也就是原型鏈腕巡。
- 如果都沒有找到名稱相同的屬性,那 [[Get]] 操作會返回值undefined血筑。
- 這種方法和訪問變量時是不一樣的绘沉。如果你引用了一個當(dāng)前詞法作用域中不存在的變量,并不會像對象屬性一樣返回 undefined豺总,而是會拋出一個ReferenceError異常车伞。
[[Put]]
[[Put]] 被觸發(fā)時,實(shí)際的行為取決于許多因素喻喳,包括對象中是否已經(jīng)存在這個屬性(這是最重要的因素)另玖。
如果已經(jīng)存在這個屬性,[[Put]] 算法大致會檢查下面這些內(nèi)容。
- 屬性是否是訪問描述符谦去?如果是并且存在 setter 就調(diào)用 setter慷丽。
- 屬性的數(shù)據(jù)描述符中 writable 是否是 false ?如果是鳄哭,在非嚴(yán)格模式下靜默失敗盈魁,在嚴(yán)格模式下拋出 TypeError 異常。
- 如果都不是窃诉,將該值設(shè)置為屬性的值。
如果對象中不存在這個屬性赤套,[[Put]] 操作會更加復(fù)雜飘痛。(后續(xù)介紹)
Getter和Setter
對象默認(rèn)的 [[Put]] 和 [[Get]] 操作分別可以控制屬性值的設(shè)置和獲取。
在 ES5 中可以使用 getter 和 setter 部分改寫默認(rèn)操作容握。
- getter 是一個隱藏函數(shù)宣脉,會在獲取屬性值時調(diào)用。
- setter 也是一個隱藏函數(shù)剔氏,會在設(shè)置屬性值時調(diào)用塑猖。
- 當(dāng)你給一個屬性定義 getter、setter 或者兩者都有時谈跛,這個屬性會被定義為“訪問描述符”(和“數(shù)據(jù)描述符”相對)羊苟。
- 對于訪問描述符來說,JavaScript 會忽略它們的 value 和writable 特性感憾,取而代之的是關(guān)心 set 和 get(還有configurable 和 enumerable)特性蜡励。
var myObject = {
// 給 a 定義一個 getter
get a() {
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
由于我們只定義了 a 的 getter,所以對 a 的值進(jìn)行設(shè)置時 set 操作會忽略賦值操作阻桅。應(yīng)當(dāng)定義 setter凉倚,和你期望的一樣,setter 會覆蓋單個屬性默認(rèn)的[[Put]](也被稱為賦值)操作嫂沉。通常來說 getter 和 setter 是成對出現(xiàn)的稽寒。
var myObject = {
// 給 a 定義一個 getter
get a() {
return this._a_;
},
// 給 a 定義一個 setter
set a(val) {
this._a_ = val * 2;
}
};
myObject.a = 2;
myObject.a; // 4
存在性
我們可以在不訪問屬性值的情況下判斷對象中是否存在這個屬性:
var myObject = {
a:2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false
- in 操作符會檢查屬性是否在對象及其 [[Prototype]] 原型鏈中。
- hasOwnProperty(..) 只會檢查屬性是否在 myObject 對象中趟章,不會檢查 [[Prototype]] 鏈杏糙。
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.propertyIsEnumerable( "a" ); // true
myObject.propertyIsEnumerable( "b" ); // false
Object.keys( myObject ); // ["a"]
Object.getOwnPropertyNames( myObject ); // ["a", "b"]
- myObject.b 確實(shí)存在并且有訪問值,但是卻不會出現(xiàn)在 for..in 循環(huán)中
- 但可以通過in操作符來判斷是否存在(包括不可枚舉)尤揣,原因是“可枚舉”就相當(dāng)于“可以出現(xiàn)在對象屬性的遍歷中”搔啊。
- propertyIsEnumerable(..) 會檢查給定的屬性名是否直接存在于對象中(而不是在原型鏈上)并且滿足 enumerable:true。
- Object.keys(..) 會返回一個數(shù)組北戏,包含所有可枚舉屬性
- Object.getOwnPropertyNames(..)會返回一個數(shù)組负芋,包含所有屬性,無論它們是否可枚舉。
- in 和 hasOwnProperty(..) 的區(qū)別在于是否查找 [[Prototype]] 鏈旧蛾,然而莽龟,Object.keys(..)和 Object.getOwnPropertyNames(..) 都只會查找對象直接包含的屬性。
在數(shù)組上應(yīng)用 for..in 循環(huán)有時會產(chǎn)生出人意料的結(jié)果锨天,因?yàn)檫@種枚舉不僅會包含所有數(shù)值索引毯盈,還會包含所有可枚舉屬性。最好只在對象上應(yīng)用for..in 循環(huán)病袄,如果要遍歷數(shù)組就使用傳統(tǒng)的 for 循環(huán)來遍歷數(shù)值索引搂赋。
遍歷
for..in 循環(huán)可以用來遍歷對象的可枚舉屬性列表(包括 [[Prototype]] 鏈)。對于數(shù)值索引的數(shù)組來說益缠,可以使用標(biāo)準(zhǔn)的 for 循環(huán)來遍歷值脑奠,這實(shí)際上并不是在遍歷值,而是遍歷下標(biāo)來指向值幅慌。
ES5 中增加了一些數(shù)組的輔助迭代器宋欺,包括 forEach(..)、every(..) 和 some(..)胰伍。
- forEach(..) 會遍歷數(shù)組中的所有值并忽略回調(diào)函數(shù)的返回值齿诞。
- every(..) 會一直運(yùn)行直到回調(diào)函數(shù)返回 false(或者“假”值)。
- some(..) 會一直運(yùn)行直到回調(diào)函數(shù)返回 true(或者“真”值)骂租。
遍歷對象屬性時的順序是不確定的祷杈,在不同的 JavaScript 引擎中可能不一樣。因此渗饮,在不同的環(huán)境中需要保證一致性時吠式,一定不要相信任何觀察到的順序,它們是不可靠的抽米。
ES6 增加了一種用來遍歷數(shù)組的 for..of 循環(huán)語法特占。for..of 循環(huán)首先會向被訪問對象請求一個迭代器對象,然后通過調(diào)用迭代器對象的next() 方法來遍歷所有返回值云茸。數(shù)組有內(nèi)置的 @@iterator是目,因此 for..of 可以直接應(yīng)用在數(shù)組上。和數(shù)組不同标捺,普通的對象沒有內(nèi)置的 @@iterator懊纳,所以無法自動完成 for..of 遍歷。
小結(jié)
- JavaScript 中的對象有字面形式(比如 var a = { .. })和構(gòu)造形式(比如 var a = new Array(..))亡容。字面形式更常用嗤疯。
- 對象就是鍵 / 值對的集合。訪問屬性時闺兢,引擎實(shí)際上會調(diào)用內(nèi)部的默認(rèn) [[Get]] 操作(在設(shè)置屬性值時是 [[Put]])茂缚,[[Get]] 操作會檢查對象本身是否包含這個屬性,如果沒找到的話還會查找 [[Prototype]]鏈。
- 屬性的特性可以通過屬性描述符來控制脚囊,比如 writable 和 configurable龟糕。此外,可以使用Object.preventExtensions(..)悔耘、Object.seal(..) 和 Object.freeze(..) 來設(shè)置對象(及其屬性)的不可變性級別讲岁。
- 屬性不一定包含值——它們可能是具備 getter/setter 的“訪問描述符”。
- for..of會尋找內(nèi)置或者自定義的 @@iterator 對象并調(diào)用它的 next() 方法來遍歷數(shù)據(jù)值衬以。
第4章 混合對象“類”
4.1 類理論
- 面向?qū)ο缶幊虖?qiáng)調(diào)的是數(shù)據(jù)和操作數(shù)據(jù)的行為本質(zhì)上是互相關(guān)聯(lián)的缓艳。好的設(shè)計就是把數(shù)據(jù)以及和它相關(guān)的行為打包(或者說封裝)起來。
- 所有字符串都是 String 類的一個實(shí)例看峻,也就是說它是一個包裹郎任,包含字符數(shù)據(jù)和我們可以應(yīng)用在數(shù)據(jù)上的函數(shù)。
- Car 的定義就是對通用 Vehicle 定義的特殊化备籽。
- 類的另一個核心概念是多態(tài),這個概念是說父類的通用行為可以被子類用更特殊的行為重寫分井。
- 實(shí)際上车猬,相對多態(tài)性 允許我們從重寫行為中引用基礎(chǔ)行為。
- 父類和子類使用相同的方法名來表示特定的行為尺锚,從而讓子類重寫父類
- 類并不是必須的編程基礎(chǔ)珠闰,而是一種可選的代碼抽象。
- JavaScript 中實(shí)際上有類呢瘫辩?簡單來說:不是伏嗜。
- 其他語言中的類和JavaScript中的“類”并不一樣。
- 類是一種設(shè)計模式伐厌。
4.2 類的機(jī)制
如果你想打開一扇門承绸,那就必須接觸真實(shí)的建筑才行——藍(lán)圖只能表示門應(yīng)該在哪,但并不是真正的門挣轨。一個類就是一張藍(lán)圖军熏。為了獲得真正可以交互的對象,我們必須按照類來建造(也可以說實(shí)例化)一個東西卷扮,這個東西通常被稱為實(shí)例荡澎,有需要的話,我們可以直接在實(shí)例上調(diào)用方法并訪問其所有公有數(shù)據(jù)屬性晤锹。這個對象就是類中描述的所有特性的一份副本摩幔。
構(gòu)造函數(shù)
類實(shí)例是由一個特殊的類方法構(gòu)造的,這個方法名通常和類名相同鞭铆,被稱為構(gòu)造函數(shù)或衡。這個方法的任務(wù)就是初始化實(shí)例需要的所有信息(狀態(tài))。
class CoolGuy {
specialTrick = nothing
CoolGuy( trick ) {
specialTrick = trick
}
showOff() {
output( "Here's my trick: ", specialTrick )
}
}
// 我們可以調(diào)用類構(gòu)造函數(shù)來生成一個 CoolGuy 實(shí)例:
Joe = new CoolGuy( "jumping rope" )
Joe.showOff() // 這是我的絕技:跳繩
CoolGuy 類有一個 CoolGuy() 構(gòu)造函數(shù),執(zhí)行 new CoolGuy() 時實(shí)際上調(diào)用的就是它薇宠。類構(gòu)造函數(shù)屬于類偷办,而且通常和類同名。此外澄港,構(gòu)造函數(shù)大多需要用 new 來調(diào)椒涯。
4.3 類的繼承
定義好一個子類之后,相對于父類來說它就是一個獨(dú)立并且完全不同的類回梧。子類會包含父類行為的原始副本废岂,但是也可以重寫所有繼承的行為甚至定義新行為。我們討論的父類和子類并不是實(shí)例狱意。應(yīng)當(dāng)把父類和子類稱為父類 DNA 和子類 DNA湖苞。
class Vehicle {
engines = 1
ignition() {
output( "Turning on my engine." );
}
drive() {
ignition();
output( "Steering and moving forward!" )
}
}
class Car inherits Vehicle {
wheels = 4
drive() {
inherited:drive()
output( "Rolling on all ", wheels, " wheels!" )
}
}
class SpeedBoat inherits Vehicle {
engines = 2
ignition() {
output( "Turning on my ", engines, " engines." )
}
pilot() {
inherited:drive()
output( "Speeding through the water with ease!" )
}
}
Car 重寫了繼承自父類的 drive() 方法,但是之后 Car 調(diào)用了 inherited:drive() 方法详囤,這表明 Car 可以引用繼承來的原始 drive() 方法财骨。這個技術(shù)被稱為多態(tài)或者虛擬多態(tài)。在本例中藏姐,更恰當(dāng)?shù)恼f法是相對多態(tài)隆箩。
多態(tài)
任何方法都可以引用繼承層次中高層的方法(無論高層的方法名和當(dāng)前方法名是否相同)艺糜。之所以說“相對”是因?yàn)槲覀儾⒉粫x想要訪問的絕對繼承層次(或者說類)险胰,而是使用相對引用“查找上一層”。
在 pilot() 中通過相對多態(tài)引用了(繼承來的)Vehicle 中的 drive()茎杂。但是那個 drive() 方法直接通過名字(而不是相對引用)引用了 ignotion() 方法兜材。實(shí)際上它會使用SpeedBoat 的 ignition()理澎。ignition() 方法定義的多態(tài)性取決于你是在哪個類的實(shí)例中引用它。
在子類(而不是它們創(chuàng)建的實(shí)例對象J锕选)中也可以相對引用它繼承的父類糠爬,這種相對引用通常被稱為 super。
子類得到的僅僅是繼承自父類行為的一份副本举庶。子類對繼承到的一個方法進(jìn)行“重寫”秩铆,不會影響父類中的方法,這兩個方法互不影響灯变。
多態(tài)并不表示子類和父類有關(guān)聯(lián)殴玛,子類得到的只是父類的一份副本。類的繼承其實(shí)就是復(fù)制添祸。
多重繼承
有些面向類的語言允許你繼承多個“父類”滚粟。多重繼承意味著所有父類的定義都會被復(fù)制到子類中。
相比之下刃泌,JavaScript 要簡單得多:它本身并不提供“多重繼承”功能凡壤。
4.4 混入
JavaScript 中只有對象署尤,并不存在可以被實(shí)例化的“類”。一個對象并不會被復(fù)制到其他對象亚侠,它們會被關(guān)聯(lián)起來曹体。由于在其他語言中類表現(xiàn)出來的都是復(fù)制行為,因此 JavaScript 開發(fā)者也想出了一個方法來模擬類的復(fù)制行為硝烂,這個方法就是混入箕别。
顯示混入
在許多庫和框架中被稱為extend(..),但是為了方便理解我們稱之為 mixin(..)滞谢。
// 非常簡單的 mixin(..) 例子 :
function mixin( sourceObj, targetObj ) {
for (var key in sourceObj) {
// 只會在不存在的情況下復(fù)制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function() {
console.log( "Turning on my engine." );
},
drive: function() {
this.ignition();
console.log( "Steering and moving forward!" );
}
};
var Car = mixin( Vehicle, {
wheels: 4,
drive: function() {
Vehicle.drive.call( this );
console.log("Rolling on all " + this.wheels + " wheels!");
}
} );
- 在 JavaScript 中不存在類串稀,Vehicle 和 Car 都是對象。
- 從技術(shù)角度來說狮杨,函數(shù)實(shí)際上沒有被復(fù)制母截,復(fù)制的是函數(shù)引用。
- Vehicle.drive.call( this )就是所說的顯式多態(tài)橄教。在之前的偽代碼中對應(yīng)的語句是 inherited:drive()清寇,我們稱之為相對多態(tài)。
- 由于 Car 和Vehicle 中都有 drive() 函數(shù)护蝶,為了指明調(diào)用對象华烟,我們必須使用絕對(而不是相對)引用。我們通過名稱顯式指定 Vehicle 對象并調(diào)用它的 drive() 函數(shù)滓走。
- 如果直接執(zhí)行 Vehicle.drive(),函數(shù)調(diào)用中的 this 會被綁定到 Vehicle 對象而不是Car 對象帽馋,因此搅方,我們會使用 .call(this)來確保 drive() 在 Car 對象的上下文中執(zhí)行。
- 如果函數(shù) Car.drive() 的名稱標(biāo)識符并沒有和 Vehicle.drive() 重疊绽族,就不需要實(shí)現(xiàn)方法多態(tài)姨涡。由于存在標(biāo)識符重疊,所以必須使用更加復(fù)雜的顯式偽多態(tài)方法吧慢。
- 應(yīng)當(dāng)盡量避免使用顯式偽多態(tài)涛漂,因?yàn)檫@樣做往往得不償失。
第二種混入函數(shù)检诗,先進(jìn)行復(fù)制然后對 Car 進(jìn)行特殊化的話匈仗,就可以跳過存在性檢查。不過這種方法并不好用并且效率更低逢慌,所以不如第一種方法常用悠轩。
由于兩個對象引用的是同一個函數(shù),因此這種復(fù)制(或者說混入)實(shí)際上并不能完全模擬面向類的語言中的復(fù)制攻泼。如果你修改了共享的函數(shù)對象火架,那 Vehicle 和 Car 都會受到影響鉴象。
注意,只在能夠提高代碼可讀性的前提下使用顯式混入何鸡,避免使用增加代碼理解難度或者讓對象關(guān)系更加復(fù)雜的模式纺弊。如果使用混入時感覺越來越困難,那或許你應(yīng)該停止使用它了骡男。
寄生繼承
顯式混入模式的一種變體被稱為“寄生繼承”淆游,它既是顯式的又是隱式的:
//“傳統(tǒng)的 JavaScript 類”Vehicle
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function() {
console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
this.ignition();
console.log( "Steering and moving forward!" );
};
function Car() {
var car = new Vehicle(); // 首先,car 是一個 Vehicle
car.wheels = 4; // 接著我們對 car 進(jìn)行定制
var vehDrive = car.drive; // 保存到 Vehicle::drive() 的特殊引用
// 重寫 Vehicle::drive()
car.drive = function() {
vehDrive.call( this );
console.log("Rolling on all " + this.wheels + " wheels!");
}
return car;
}
var myCar = new Car();
myCar.drive();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!
隱式混入
var Something = {
cool: function() {
this.greeting = "Hello World";
this.count = this.count ? this.count + 1 : 1;
}
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
cool: function() {
Something.cool.call( this ); // 隱式把 Something 混入 Another
}
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1(count 不是共享狀態(tài))
通過在構(gòu)造函數(shù)調(diào)用或者方法調(diào)用中使用 Something.cool.call( this )洞翩,我們實(shí)際上“借用”了函數(shù) Something.cool() 并在 Another 的上下文中調(diào)用了它稽犁。最終的結(jié)果是 Something.cool() 中的賦值操作都會應(yīng)用在 Another 對象上而不是Something 對象上。
4.5 小結(jié)
- 類是一種設(shè)計模式骚亿。JavaScript 也有類似的語法已亥,但是和其他語言中的類完全不同。
- 類意味著復(fù)制来屠。
- 傳統(tǒng)的類被實(shí)例化時虑椎,它的行為會被復(fù)制到實(shí)例中。類被繼承時俱笛,行為也會被復(fù)制到子類中捆姜。
- 多態(tài)(在繼承鏈的不同層次名稱相同但是功能不同的函數(shù))看起來似乎是從子類引用父類,但是本質(zhì)上引用的其實(shí)是復(fù)制的結(jié)果迎膜。
- 混入模式(無論顯式還是隱式)可以用來模擬類的復(fù)制行為泥技,但是通常會產(chǎn)生丑陋并且脆弱的語法。
- 顯式混入實(shí)際上無法完全模擬類的復(fù)制行為磕仅,因?yàn)閷ο笾荒軓?fù)制引用珊豹,無法復(fù)制被引用的對象或者函數(shù)本身。
總地來說榕订,在 JavaScript 中模擬類是得不償失的店茶,雖然能解決當(dāng)前的問題,但是可能會埋下更多的隱患劫恒。
第5章 原型
5.1 [[Prototype]]
- JavaScript 中的對象有一個特殊的 [[Prototype]] 內(nèi)置屬性贩幻,其實(shí)就是對于其他對象的引用。幾乎所有的對象在創(chuàng)建時 [[Prototype]] 屬性都會被賦予一個非空的值两嘴。
- 當(dāng)你試圖引用對象的屬性時會觸發(fā)[[Get]] 操作丛楚。
- 對于默認(rèn)的 [[Get]] 操作來說,如果無法在對象本身找到需要的屬性憔辫,就會繼續(xù)訪問對象的 [[Prototype]] 鏈鸯檬。
- for..in 遍歷對象時原理和查找 [[Prototype]] 鏈類似,任何可以通過原型鏈訪問到(并且是 enumerable螺垢,參見第 3 章)的屬性都會被枚舉喧务。使用 in 操作符來檢查屬性在對象中是否存在時赖歌,同樣會查找對象的整條原型鏈(無論屬性是否可枚舉)。
- 所有普通的 [[Prototype]] 鏈最終都會指向內(nèi)置的 Object.prototype功茴。
屬性設(shè)置和屏蔽
myObject.foo = "bar";
給一個對象設(shè)置屬性并不僅僅是添加一個新屬性或者修改已有的屬性值÷耄現(xiàn)在我們完整地講解一下這個過程:
(1)myObject 對象中包含名為 foo 的普通數(shù)據(jù)訪問屬性,只會修改已有的屬性值坎穿。
(2)foo 不是直接存在于 myObject 中展父,[[Prototype]] 鏈就會被遍歷,類似 [[Get]] 操作玲昧。如果原型鏈上找不到 foo栖茉,foo 就會被直接添加到 myObject 上。
(3)foo 存在于原型鏈上層:
①foo為普通數(shù)據(jù)訪問屬性且沒有被標(biāo)記為只讀(writable:false)孵延,會直接在 myObject 中添加一個名為 foo 的新屬性吕漂,它是屏蔽屬性。
②foo被標(biāo)記為只讀(writable:false)尘应,那么無法修改已有屬性或者在 myObject 上創(chuàng)建屏蔽屬性惶凝。不會發(fā)生屏蔽。
③foo是一個 setter犬钢,那就一定會調(diào)用這個 setter苍鲜。foo 不會被添加到(或者說屏蔽于)myObject,也不會重新定義 foo 這個 setter玷犹。
(4)屬性名 foo 既出現(xiàn)在 myObject 中也出現(xiàn)在 myObject 的 [[Prototype]] 鏈上層混滔,那么就會發(fā)生屏蔽,會屏蔽原型鏈上層的所有 foo 屬性歹颓。
var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隱式屏蔽坯屿!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true
++ 操作相當(dāng)于 myObject.a = myObject.a + 1。因此 ++ 操作首先會通過[[Prototype]]查找屬性 a 并從 anotherObject.a 獲取當(dāng)前屬性值 2晴股,然后給這個值加 1愿伴,接著用 [[Put]]將值 3 賦給 myObject 中新建的屏蔽屬性 a肺魁。
5.2 類
JavaScript并沒有類來作為對象的抽象模式或者說藍(lán)圖电湘。JavaScript 中只有對象。
JavaScript是少有的可以不通過類鹅经,直接創(chuàng)建對象的語言寂呛。對象直接定義自己的行為。
5.2.1 "類"函數(shù)
JavaScript 中有一種奇怪的行為一直在被無恥地濫用瘾晃,那就是模仿類贷痪。這種奇怪的“類似類”的行為利用了函數(shù)的一種特殊特性:所有的函數(shù)默認(rèn)都會擁有一個名為 prototype 的公有并且不可枚舉的屬性,它會指向另一個對象蹦误。
function Foo() {
// ...
}
Foo.prototype; // { }
這個對象是在調(diào)用 new Foo()時創(chuàng)建的劫拢,最后會被(有點(diǎn)武斷地)關(guān)聯(lián)到這個“Foo 點(diǎn) prototype”對象上肉津。
function Foo() {
// ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true
調(diào)用 new Foo() 時會創(chuàng)建 a,其中的一步就是給 a 一個內(nèi)部的[[Prototype]] 鏈接舱沧,關(guān)聯(lián)到 Foo.prototype 指向的那個對象妹沙。
在面向類的語言中,類可以被復(fù)制(或者說實(shí)例化)多次熟吏。在 JavaScript 中距糖,并沒有類似的復(fù)制機(jī)制。只能創(chuàng)建多個對象牵寺,它們 [[Prototype]] 關(guān)聯(lián)的是同一個對象悍引。
- new Foo() 會生成一個新對象(我們稱之為 a),這個新對象的內(nèi)部鏈接 [[Prototype]] 關(guān)聯(lián)的是 Foo.prototype 對象帽氓。
- 最后我們得到了兩個對象趣斤,它們之間互相關(guān)聯(lián)。我們并沒有初始化一個類杏节,實(shí)際上我們并沒有從“類”中復(fù)制任何行為到一個對象中唬渗,只是讓兩個對象互相關(guān)聯(lián)。
- new Foo() 這個函數(shù)調(diào)用實(shí)際上并沒有直接創(chuàng)建關(guān)聯(lián)奋渔,這個關(guān)聯(lián)只是一個意外的副作用镊逝。new Foo() 只是間接完成了我們的目標(biāo):一個關(guān)聯(lián)到其他對象的新對象。
- 在 JavaScript 中嫉鲸,我們并不會將一個對象(“類”)復(fù)制到另一個對象(“實(shí)例”)撑蒜,只是將它們關(guān)聯(lián)起來。這個機(jī)制通常被稱為原型繼承玄渗。
- 繼承意味著復(fù)制操作座菠,JavaScript(默認(rèn))并不會復(fù)制對象屬性。相反藤树,JavaScript 會在兩個對象之間創(chuàng)建一個關(guān)聯(lián)浴滴,這樣一個對象就可以通過委托訪問另一個對象的屬性和函數(shù)。委托這個術(shù)語可以更加準(zhǔn)確地描述 JavaScript 中對象的關(guān)聯(lián)機(jī)制岁钓。
- 差異繼承升略。基本原則是在描述對象行為時屡限,使用其不同于普遍描述的特質(zhì)品嚣。
5.2.2 “構(gòu)造函數(shù)”
function Foo() {
// ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; //
到底是什么讓我們認(rèn)為 Foo 是一個“類”呢?其中一個原因是我們看到了關(guān)鍵字 new钧大,另一個原因是翰撑,看起來我們執(zhí)行了類的構(gòu)造函數(shù)方法。
- Foo.prototype 默認(rèn)有一個公有并且不可枚舉的屬性 .constructor啊央,這個屬性引用的是對象關(guān)聯(lián)的函數(shù)眶诈。此外涨醋,通過“構(gòu)造函數(shù)”調(diào)用 new Foo() 創(chuàng)建的對象也有一個 .constructor 屬性,指向“創(chuàng)建這個對象的函數(shù)”逝撬。
- 對于JavaScript 引擎來說首字母大寫沒有任何意義东帅。
- 函數(shù)本身并不是構(gòu)造函數(shù),然而球拦,當(dāng)你在普通的函數(shù)調(diào)用前面加上 new 關(guān)鍵字之后靠闭,就會把這個函數(shù)調(diào)用變成一個“構(gòu)造函數(shù)調(diào)用”。實(shí)際上坎炼,new 會劫持所有普通函數(shù)并用構(gòu)造對象的形式來調(diào)用它愧膀。
- 在 JavaScript 中對于“構(gòu)造函數(shù)”最準(zhǔn)確的解釋是,所有帶 new 的函數(shù)調(diào)用谣光。
- 函數(shù)不是構(gòu)造函數(shù)檩淋,但是當(dāng)且僅當(dāng)使用 new 時,函數(shù)調(diào)用會變成“構(gòu)造函數(shù)調(diào)用”萄金。
5.2.3 技術(shù)
- 看起來 a.constructor === Foo 為真意味著 a 確實(shí)有一個指向 Foo 的.constructor 屬性蟀悦,但是事實(shí)不是這樣。
- 實(shí)際上氧敢,.constructor 引用同樣被委托給了 Foo.prototype日戈,而Foo.prototype.constructor 默認(rèn)指向 Foo。
- a.constructor 只是通過默認(rèn)的 [[Prototype]] 委托指向 Foo孙乖,這和“構(gòu)造”毫無關(guān)系浙炼。
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創(chuàng)建一個新原型對象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
a1 并沒有 .constructor 屬性,所以它會委托 [[Prototype]] 鏈上的 Foo.prototype唯袄。但是這個對象也沒有 .constructor 屬性弯屈,所以它會繼續(xù)委托,這次會委托給委托鏈頂端的 Object.prototype恋拷。這個對象有 .constructor 屬性资厉,指向內(nèi)置的 Object(..) 函數(shù)。
.constructor 并不是一個不可變屬性蔬顾。它是不可枚舉的宴偿,但是它的值是可寫的(可以被修改)。
a1.constructor 是一個非常不可靠并且不安全的引用阎抒。通常來說要盡量避免使用這些引用酪我。
5.3 (原型)繼承
下面這段代碼使用的就是典型的“原型風(fēng)格”:
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
// 我們創(chuàng)建了一個新的 Bar.prototype 對象并關(guān)聯(lián)到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );
// 注意消痛!現(xiàn)在沒有 Bar.prototype.constructor 了
// 如果你需要這個屬性的話可能需要手動修復(fù)一下它
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
調(diào)用Object.create(..) 會憑空創(chuàng)建一個“新”對象并把新對象內(nèi)部的 [[Prototype]] 關(guān)聯(lián)到你指定的對象(本例中是 Foo.prototype)且叁。Bar 會有一個 .prototype 關(guān)聯(lián)到默認(rèn)的對象,但是這個對象并不是我們想要的 Foo.prototype秩伞。因此我們創(chuàng)建了一個新對象并把它關(guān)聯(lián)到我們希望的對象上逞带,直接把原始的關(guān)聯(lián)對象拋棄掉欺矫。
// 并不會創(chuàng)建一個關(guān)聯(lián)到 Bar.prototype 的新對象
Bar.prototype = Foo.prototype;
// 基本上滿足你的需求,但是可能會產(chǎn)生一些副作用(比如寫日志展氓、修改狀態(tài)穆趴、注冊到其他對象、給 this 添加數(shù)據(jù)屬性遇汞,等等)
// 就會影響到 Bar() 的“后代”
Bar.prototype = new Foo();
要創(chuàng)建一個合適的關(guān)聯(lián)對象未妹,我們必須使用 Object.create(..) 而不是使用具有副作用的 Foo(..)。這樣做唯一的缺點(diǎn)就是需要創(chuàng)建一個新對象然后把舊對象拋棄掉空入,不能直接修改已有的默認(rèn)對象络它。
如果能有一個標(biāo)準(zhǔn)并且可靠的方法來修改對象的 [[Prototype]] 關(guān)聯(lián)就好了。ES6 添加了輔助函數(shù) Object.setPrototypeOf(..)歪赢,可以用標(biāo)準(zhǔn)并且可靠的方法來修改關(guān)聯(lián)化戳。
// ES6 之前需要拋棄默認(rèn)的 Bar.prototype
Bar.ptototype = Object.create( Foo.prototype );
// ES6 開始可以直接修改現(xiàn)有的 Bar.prototype
Object.setPrototypeOf( Bar.prototype, Foo.prototype );
檢查一個實(shí)例(JavaScript 中的對象)的繼承祖先(JavaScript 中的委托關(guān)聯(lián))通常被稱為內(nèi)省(或者反射)埋凯。
function Foo() {
// ...
}
Foo.prototype.blah = ...;
var a = new Foo();
我們?nèi)绾瓮ㄟ^內(nèi)省找出 a 的“祖先”(委托關(guān)聯(lián))呢点楼?第一種方法是站在“類”的角度來判斷:
a instanceof Foo; // true
- instanceof 操作符的左操作數(shù)是一個普通的對象,右操作數(shù)是一個函數(shù)白对。instanceof 回答的問題是:在 a 的整條 [[Prototype]] 鏈中是否有指向Foo.prototype 的對象掠廓?
- 如果想判斷兩個對象(比如 a 和 b)之間是否通過 [[Prototype]] 鏈關(guān)聯(lián),只用 instanceof無法實(shí)現(xiàn)甩恼。
// 是第二種判斷 [[Prototype]] 反射的方法却盘,它更加簡潔:
Foo.prototype.isPrototypeOf( a ); // true
// 非常簡單:b 是否出現(xiàn)在 c 的 [[Prototype]] 鏈中?
b.isPrototypeOf( c );
isPrototypeOf(..) 回答的問題是:在 a 的整條 [[Prototype]] 鏈中是否出現(xiàn)過 Foo.prototype 媳拴?這個方法并不需要使用函數(shù)(“類”)黄橘,它直接使用 b 和 c 之間的對象引用來判斷它們的關(guān)系。
獲取一個對象的 [[Prototype]] 鏈:
// 在 ES5 中屈溉,標(biāo)準(zhǔn)的方法是:
Object.getPrototypeOf( a );
// 絕大多數(shù)(不是所有H亍)瀏覽器也支持一種非標(biāo)準(zhǔn)的方法來訪問內(nèi)部 [[Prototype]] 屬性:
a.__proto__ === Foo.prototype; // true
和我們之前說過的 .constructor 一樣,.__proto__
實(shí)際上并不存在于你正在使用的對象中(本例中是 a)子巾。實(shí)際上帆赢,它和其他的常用函數(shù)一樣,存在于內(nèi)置的 Object.prototype 中线梗。
.__proto__
看起來很像一個屬性椰于,但是實(shí)際上它更像一個getter/setter。.__proto__
的實(shí)現(xiàn)大致上是這樣的:
Object.defineProperty( Object.prototype, "__proto__", {
get: function() {
return Object.getPrototypeOf( this );
},
set: function(o) {
// ES6 中的 setPrototypeOf(..)
Object.setPrototypeOf( this, o );
return o;
}
} );
雖然 getter 函數(shù)存在于 Object.prototype 對象中仪搔,但是它的 this 指向?qū)ο?a瘾婿,所以和 Object.getPrototypeOf( a ) 結(jié)果相同。
5.4 對象關(guān)聯(lián)
[[Prototype]] 機(jī)制就是存在于對象中的一個內(nèi)部鏈接,它會引用其他對象偏陪。通常來說抢呆,這個鏈接的作用是:如果在對象上沒有找到需要的屬性或者方法引用,引擎就會繼續(xù)在 [[Prototype]] 關(guān)聯(lián)的對象上進(jìn)行查找笛谦。同理抱虐,如果在后者中也沒有找到需要的引用就會繼續(xù)查找它的[[Prototype]],以此類推饥脑。這一系列對象的鏈接被稱為“原型鏈”恳邀。
5.4.1 創(chuàng)建關(guān)聯(lián)
[[Prototype]] 機(jī)制的意義是什么呢?Object.create(..) 會創(chuàng)建一個新對象并把它關(guān)聯(lián)到我們指定的對象灶轰。我們并不需要類來創(chuàng)建兩個對象之間的關(guān)系轩娶,只需要通過委托來關(guān)聯(lián)對象就足夠了。而Object.create(..) 可以完美地創(chuàng)建我們想要的關(guān)聯(lián)關(guān)系框往。
Object.create(..) 是在 ES5 中新增的函數(shù)鳄抒。
5.4.2 關(guān)聯(lián)關(guān)系是備用
ar anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"
當(dāng)你給開發(fā)者設(shè)計軟件時,假設(shè)要調(diào)用 myObject.cool()椰弊,如果 myObject 中不存在 cool()時這條語句也可以正常工作的話许溅,那你的 API 設(shè)計就會變得很“神奇”,對于未來維護(hù)你軟件的開發(fā)者來說這可能不太好理解秉版。
var anotherObject = {
cool: function() {
console.log( "cool!" );
}
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
this.cool(); // 內(nèi)部委托贤重!
};
myObject.doCool(); // "cool!"
從內(nèi)部來說,我們的實(shí)現(xiàn)遵循的是委托設(shè)計模式清焕,通過 [[Prototype]] 委托到 anotherObject.cool()并蝗。
5.5 小結(jié)
- 如果要訪問對象中并不存在的一個屬性,[[Get]] 操作(參見第 3 章)就會查找對象內(nèi)部[[Prototype]] 關(guān)聯(lián)的對象秸妥。這個關(guān)聯(lián)關(guān)系實(shí)際上定義了一條“原型鏈”(有點(diǎn)像嵌套的作用域鏈)滚停,在查找屬性時會對它進(jìn)行遍歷。
- 所有普通對象都有內(nèi)置的 Object.prototype粥惧,指向原型鏈的頂端键畴。
- 關(guān)聯(lián)兩個對象最常用的方法是使用 new 關(guān)鍵詞進(jìn)行函數(shù)調(diào)用。
- 使用 new 調(diào)用函數(shù)時會把新對象的 .prototype 屬性關(guān)聯(lián)到“其他對象”突雪。帶 new 的函數(shù)調(diào)用通常被稱為“構(gòu)造函數(shù)調(diào)用”起惕。
- JavaScript 中的機(jī)制有一個核心區(qū)別,那就是不會進(jìn)行復(fù)制咏删,對象之間是通過內(nèi)部的[[Prototype]] 鏈關(guān)聯(lián)的惹想。
- 對象之間的關(guān)系不是復(fù)制而是委托。
第6章 行為委托
- [[Prototype]] 機(jī)制就是指對象中的一個內(nèi)部鏈接引用另一個對象督函。
- 如果在第一個對象上沒有找到需要的屬性或者方法引用嘀粱,引擎就會繼續(xù)在 [[Prototype]]關(guān)聯(lián)的對象上進(jìn)行查找激挪。同理,如果在后者中也沒有找到需要的引用就會繼續(xù)查找它的[[Prototype]]草穆,以此類推。這一系列對象的鏈接被稱為“原型鏈”搓译。
- 換句話說悲柱,JavaScript 中這個機(jī)制的本質(zhì)就是對象之間的關(guān)聯(lián)關(guān)系。
面向委托的設(shè)計
類理論
class Task {
id;
// 構(gòu)造函數(shù) Task()
Task(ID) { id = ID; }
outputTask() { output( id ); }
}
class XYZ inherits Task {
label;
// 構(gòu)造函數(shù) XYZ()
XYZ(ID,Label) { super( ID ); label = Label; }
outputTask() { super(); output( label ); }
}
class ABC inherits Task {
// ...
}
委托理論
Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log( this.id ); }
};
// 讓 XYZ 委托 Task
XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...
Task 和 XYZ 并 不 是 類( 或 者 函 數(shù) )些己, 它 們 是 對 象豌鸡。XYZ 通 過 Object.create(..) 創(chuàng)建,它的 [[Prototype]] 委托了 Task 對象段标。
相比于面向類(或者說面向?qū)ο螅┭墓冢視堰@種編碼風(fēng)格稱為“對象關(guān)聯(lián)”。
JavaScript 中就是沒有類似“類”的抽象機(jī)制逼庞。
對象關(guān)聯(lián)風(fēng)格的代碼還有一些不同之處蛇更。
(1)在 [[Prototype]] 委托中最好把狀態(tài)保存在委托者(XYZ、ABC)而不是委托目標(biāo)(Task)上赛糟。
(2)在類設(shè)計模式中派任,我們故意讓父類(Task)和子類(XYZ)中都有 outputTask 方法,這樣就可以利用重寫(多態(tài))的優(yōu)勢璧南。在委托行為中則恰好相反:我們會盡量避免在[[Prototype]] 鏈的不同級別中使用相同的命名掌逛。
(3)setID(..) 方法,由于調(diào)用位置觸發(fā)了 this 的隱式綁定規(guī)則司倚,因此雖然 setID(..) 方法在 Task 中豆混,運(yùn)行時 this 仍然會綁定到 XYZ,這正是我們想要的动知。
- 委托行為意味著某些對象(XYZ)在找不到屬性或者方法引用時會把這個請求委托給另一個對象(Task)皿伺。
- 對象并不是按照父類到子類的關(guān)系垂直組織的,而是通過任意方向的委托關(guān)聯(lián)并排組織的盒粮。
- 在 API 接口的設(shè)計中心傀,委托最好在內(nèi)部實(shí)現(xiàn),不要直接暴露出去拆讯。
你無法在兩個或兩個以上互相(雙向)委托的對象之間創(chuàng)建循環(huán)委托脂男。
互相委托理論上是可以正常工作的,在某些情況下這是非常有用的种呐。之所以要禁止互相委托宰翅,是因?yàn)橐娴拈_發(fā)者們發(fā)現(xiàn)在設(shè)置時檢查(并禁止!)一次無限循環(huán)引用要更加高效爽室,否則每次從對象中查找屬性時都需要進(jìn)行檢查汁讼。
通常來說,JavaScript 規(guī)范并不會控制瀏覽器中開發(fā)者工具對于特定值或者結(jié)構(gòu)的表示方式,瀏覽器和引擎可以自己選擇合適的方式來進(jìn)行解析嘿架,因此瀏覽器和工具的解析結(jié)果并不一定相同瓶珊。
比較思維模型
典型的(“原型”)面向?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();
對象關(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();
同樣利用 [[Prototype]] 把 b1 委托給 Bar 并把 Bar 委托給 Foo,和上一段代碼一模一樣耸彪。我們?nèi)匀粚?shí)現(xiàn)了三個對象之間的關(guān)聯(lián)伞芹。但是非常重要的一點(diǎn)是,這段代碼簡潔了許多蝉娜,我們只是把對象關(guān)聯(lián)起來唱较,并不需要那些既復(fù)雜又令人困惑的模仿類的行為(構(gòu)造函數(shù)、原型以及 new)召川。
JavaScript 機(jī)制有很強(qiáng)的內(nèi)部連貫性南缓。JavaScript 中的函數(shù)之所以可以訪問 call(..)、apply(..) 和 bind(..)荧呐,就是因?yàn)楹瘮?shù)本身是對象汉形。
對象關(guān)聯(lián)風(fēng)格的代碼顯然更加簡潔,因?yàn)檫@種代碼只關(guān)注一件事:對象之間的關(guān)聯(lián)關(guān)系倍阐。
類與對象
class 仍然是通過 [[Prototype]]機(jī)制實(shí)現(xiàn)的获雕。
更好的語法
ES6 的 class 語法可以簡潔地定義類方法。
class Foo {
methodName() { /* .. */ }
}
在 ES6 中 我 們 可 以 在 任 意 對 象 的 字 面 形 式 中 使 用 簡 潔 方 法 聲 明收捣。
var LoginController = {
errors: [],
getUser() { // 媽媽再也不用擔(dān)心代碼里有 function 了届案!
// ...
},
getPassword() {
// ...
}
// ...
};
唯一的區(qū)別是對象的字面形式仍然需要使用“,”來分隔元素,而 class 語法不需要罢艾。
// 使用更好的對象字面形式語法和簡潔方法
var AuthController = {
errors: [],
checkAuth() {
// ...
},
server(url,data) {
// ...
}
// ...
};
// 現(xiàn)在把 AuthController 關(guān)聯(lián)到 LoginController
Object.setPrototypeOf( AuthController, LoginController );
簡潔方法有一個非常小但是非常重要的缺點(diǎn)楣颠。
var Foo = {
bar() { /*..*/ },
baz: function baz() { /*..*/ }
};
// 去掉語法糖之后的代碼如下所示:
var Foo = {
bar: function() { /*..*/ },
baz: function baz() { /*..*/ }
};
由 于 函 數(shù) 對 象 本 身 沒 有 名 稱 標(biāo) 識 符, 所 以 bar() 的 縮 寫 形 式
(function()..)實(shí)際上會變成一個匿名函數(shù)表達(dá)式并賦值給 bar 屬性咐蚯。相比之下童漩,具名函數(shù)表達(dá)式(function baz()..)會額外給 .baz 屬性附加一個詞法名稱標(biāo)識符 baz。
匿名函數(shù)沒有 name 標(biāo)識符春锋,這會導(dǎo)致:
- 調(diào)試棧更難追蹤矫膨;
- 自我引用(遞歸、事件(解除)綁定期奔,等等)更難侧馅;
- 代碼(稍微)更難理解。
簡潔方法沒有第 1 和第 3 個缺點(diǎn)呐萌。
使用簡潔方法時一定要小心這一點(diǎn)馁痴。如果你需要自我引用的話,那最好使用傳統(tǒng)的具名函數(shù)表達(dá)式來定義對應(yīng)的函數(shù)肺孤,不要使用簡潔方法罗晕。
內(nèi)省
內(nèi)省就是檢查實(shí)例的類型济欢。類實(shí)例的自省主要目的是通過創(chuàng)建方式來判斷對象的結(jié)構(gòu)和功能。
function Foo() {
// ...
}
Foo.prototype.something = function(){
// ...
}
var a1 = new Foo();
// 之后
if (a1 instanceof Foo) {
a1.something();
}
因 為 Foo.prototype( 不 是 Foo P≡ā) 在 a1 的 [[Prototype]] 鏈 上法褥, 所 以instanceof 操作(會令人困惑地)告訴我們 a1 是 Foo“類”的一個實(shí)例。從語法角度來說酬屉,instanceof 似乎是檢查 a1 和 Foo 的關(guān)系半等,但是實(shí)際上它想說的是 a1 和 Foo.prototype(引用的對象)是互相關(guān)聯(lián)的。
instanceof 語法會產(chǎn)生語義困惑而且非常不直觀梆惯。
function Foo() { /* .. */ }
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
var b1 = new Bar( "b1" );
// ---------------使用 instanceof 和 .prototype 語義來檢查本例中實(shí)體的關(guān)系
// 讓 Foo 和 Bar 互相關(guān)聯(lián)
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf( Bar.prototype )
=== Foo.prototype; // true
Foo.prototype.isPrototypeOf( Bar.prototype ); // true
// 讓 b1 關(guān)聯(lián)到 Foo 和 Bar
b1 instanceof Foo; // true
b1 instanceof Bar; // true
Object.getPrototypeOf( b1 ) === Bar.prototype; // true
Foo.prototype.isPrototypeOf( b1 ); // true
Bar.prototype.isPrototypeOf( b1 ); // true
你最直觀的想法可能是使用 Bar instanceof Foo酱鸭,但是在 JavaScript 中這是行不通的吗垮,你必須使用 Bar.prototype instanceof Foo垛吗。
還有一種常見但是可能更加脆弱的內(nèi)省模式,鴨子類型”烁登。
if (a1.something) {
a1.something();
}
- 我們并沒有檢查 a1 和委托 something() 函數(shù)的對象之間的關(guān)系怯屉,而是假設(shè)如果 a1 通過了測試 a1.something 的話,那 a1 就一定能調(diào)用.something()(無論這個方法存在于 a1 自身還是委托到其他對象)饵沧。
- ES6 的 Promise 就是典型的“鴨子類型”锨络。如果對象有 then() 方法,ES6 的 Promise 就會認(rèn)為這個對象是“可持續(xù)”(thenable)的狼牺,因此會期望它具有 Promise 的所有標(biāo)準(zhǔn)行為羡儿。
對象關(guān)聯(lián)風(fēng)格代碼,其內(nèi)省更加簡潔是钥。使用對象關(guān)聯(lián)時掠归,所有的對象都是通過 [[Prototype]] 委托互相關(guān)聯(lián),下面是內(nèi)省的方法:
// 讓 Foo 和 Bar 互相關(guān)聯(lián)
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true
// 讓 b1 關(guān)聯(lián)到 Foo 和 Bar
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true
總結(jié)
行為委托認(rèn)為對象之間是兄弟關(guān)系悄泥,互相委托虏冻,而不是父類和子類的關(guān)系。JavaScript 的[[Prototype]] 機(jī)制本質(zhì)上就是行為委托機(jī)制弹囚。