JavaScript 中的 物件導向程式設計 (OOP) 有好幾層:
每一層都只依賴於前一層,讓你可以逐步學習 JavaScript OOP。第 1 層和第 2 層形成一個簡單的核心,當你對較為複雜的第 3 層和第 4 層感到困惑時,可以隨時回頭參考。
粗略來說,JavaScript 中的所有物件都是從字串到值的對應(字典)。物件中的(金鑰、值)項目稱為屬性。屬性的金鑰永遠都是文字字串。屬性的值可以是任何 JavaScript 值,包括函式。 方法 是其值為函式的屬性。
屬性有以下三種種類
[[Prototype]]
會保留物件的原型,並可透過 Object.getPrototypeOf()
讀取。JavaScript 的 物件文字 讓你可以直接建立 純粹物件(Object
的直接實例)。下列程式碼使用物件文字將物件指定給變數 jane
。物件有兩個屬性:name
和 describe
。 describe
是方法:
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
// (1)
},
// (2)
};
this
來參照目前的物件(也稱為方法呼叫的 接收器)。你可能會認為物件 僅 是從字串到值的對應。但它們不只如此:它們是真正的通用物件。例如,你可以使用物件之間的繼承(請參閱 第 2 層:物件之間的原型關係),而且可以保護物件不被變更。直接建立物件的能力是 JavaScript 的傑出功能之一:你可以從具體物件開始(不需要類別!)並在稍後加入抽象化。例如,建構函式 是物件的工廠(如 第 3 層:建構函式—實例的工廠 所述),與其他語言中的類別大致類似。
點運算子提供一個簡潔的語法來存取屬性。屬性鍵必須是識別碼(請參閱合法識別碼)。如果您想要讀取或寫入具有任意名稱的屬性,您需要使用方括號運算子(請參閱方括號運算子 ([]):透過計算鍵存取屬性)。
本節中的範例使用下列物件
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
點運算子讓您可以「取得」屬性(讀取其值)。以下是幾個範例
> jane.name // get property `name` 'Jane' > jane.describe // get property `describe` [Function]
取得不存在的屬性會傳回undefined
> jane.unknownProperty undefined
您可以使用指定運算子 (=
) 來設定透過點符號參照的屬性值。例如:
> jane.name = 'John'; // set property `name` > jane.describe() 'Person named John'
如果屬性尚未存在,設定它會自動建立它。如果屬性已存在,設定它會變更其值。
delete
運算子讓您可以從物件中完全移除屬性(整個鍵值對)。例如:
> var obj = { hello: 'world' }; > delete obj.hello true > obj.hello undefined
如果您僅將屬性設定為 undefined
,屬性仍然存在,而物件仍然包含其鍵:
> var obj = { foo: 'a', bar: 'b' }; > obj.foo = undefined; > Object.keys(obj) [ 'foo', 'bar' ]
如果您刪除屬性,其鍵也會消失
> delete obj.foo true > Object.keys(obj) [ 'bar' ]
delete
僅影響物件的直接(「自有」,非繼承)屬性。其原型不會受到影響(請參閱刪除繼承屬性)。
請謹慎使用delete
運算子。如果建構函式建立的執行個體的「形狀」不會變更(大致上:沒有移除或新增任何屬性),大多數現代 JavaScript 引擎會最佳化其效能。刪除屬性會妨礙該最佳化。
delete
如果屬性為自有屬性,但無法刪除,則傳回 false
。在所有其他情況下,它傳回 true
。以下是幾個範例。
作為準備,我們建立一個可以刪除的屬性,以及另一個無法刪除的屬性 (透過描述子取得和定義屬性 說明 Object.defineProperty()
)
var
obj
=
{};
Object
.
defineProperty
(
obj
,
'canBeDeleted'
,
{
value
:
123
,
configurable
:
true
});
Object
.
defineProperty
(
obj
,
'cannotBeDeleted'
,
{
value
:
456
,
configurable
:
false
});
delete
傳回 false
,表示無法刪除的自有屬性
> delete obj.cannotBeDeleted false
delete
在所有其他情況下傳回 true
> delete obj.doesNotExist true > delete obj.canBeDeleted true
delete
傳回 true
,即使它沒有變更任何內容(繼承的屬性永遠不會被移除)
> delete obj.toString true > obj.toString // still there [Function: toString]
雖然您無法使用 保留字(例如 var
和 function
)作為變數名稱,但您可以將它們用作屬性金鑰:
> var obj = { var: 'a', function: 'b' }; > obj.var 'a' > obj.function 'b'
數字可以用作物件文字中的屬性金鑰,但它們會被解釋為字串。點運算子只能存取金鑰為識別碼的屬性。因此,您需要方括號運算子(在以下範例中顯示)來存取金鑰為數字的屬性:
> var obj = { 0.7: 'abc' }; > Object.keys(obj) [ '0.7' ] > obj['0.7'] 'abc'
物件文字也允許您使用任意字串(既不是識別碼也不是數字)作為屬性金鑰,但您必須加上引號。同樣地,您需要方括號運算子來存取屬性值
> var obj = { 'not an identifier': 123 }; > Object.keys(obj) [ 'not an identifier' ] > obj['not an identifier'] 123
點運算子適用於固定的屬性金鑰,而方括號運算子允許您透過表達式來參照屬性。
方括號運算子讓 您透過表達式計算屬性的金鑰:
> var obj = { someProperty: 'abc' }; > obj['some' + 'Property'] 'abc' > var propKey = 'someProperty'; > obj[propKey] 'abc'
這也允許您存取金鑰不是識別碼的屬性
> var obj = { 'not an identifier': 123 }; > obj['not an identifier'] 123
請注意,方括號運算子會將其內部強制轉換為字串。例如
> var obj = { '6': 'bar' }; > obj[3+3] // key: the string '6' 'bar'
呼叫方法的方式 與您預期的一樣:
> var obj = { myMethod: function () { return true } }; > obj['myMethod']() true
設定屬性 與點運算子類似:
> var obj = {}; > obj['anotherProperty'] = 'def'; > obj.anotherProperty 'def'
刪除屬性也類似於點運算子:
> var obj = { 'not an identifier': 1, prop: 2 }; > Object.keys(obj) [ 'not an identifier', 'prop' ] > delete obj['not an identifier'] true > Object.keys(obj) [ 'prop' ]
這不是一個常見的用例,但有時你需要將一個任意值轉換為一個物件。 Object()
,用作函式(而不是建構函式),提供該服務。它會產生以下結果:
值 | 結果 |
(不帶任何參數呼叫) |
|
|
|
|
|
布林值 |
|
數字 |
|
字串 |
|
物件 |
|
以下是一些範例
> Object(null) instanceof Object true > Object(false) instanceof Boolean true > var obj = {}; > Object(obj) === obj true
以下函式檢查 value
是否為物件:
function
isObject
(
value
)
{
return
value
===
Object
(
value
);
}
請注意,如果 value
不是物件,前一個函式會建立一個物件。你可以透過 typeof
來實作相同的功能,而不用這麼做(請參閱 陷阱:typeof null)。
你也可以呼叫 Object
作為建構函式,它會產生與呼叫它作為函式相同的結果:
> var obj = {}; > new Object(obj) === obj true > new Object(123) instanceof Number true
當你呼叫函式時,this
總是一個(隱式)參數:
即使正常函式不需要 this
,它仍然存在作為一個特殊變數,其值總是全域物件(瀏覽器中的 window
;請參閱 全域物件)
> function returnThisSloppy() { return this } > returnThisSloppy() === window true
this
總是 undefined
> function returnThisStrict() { 'use strict'; return this } > returnThisStrict() === undefined true
this
參照呼叫方法的物件
> var obj = { method: returnThisStrict }; > obj.method() === obj true
在方法的情況下,this
的值稱為接收者 的方法呼叫。
請記住,函式也是物件。 因此,每個函式都有自己的方法。本節中介紹了其中的三個方法,並有助於呼叫函式。這三個方法在後續各節中用於解決呼叫函式的某些陷阱。接下來的範例都參照以下物件 jane
:
var
jane
=
{
name
:
'Jane'
,
sayHelloTo
:
function
(
otherName
)
{
'use strict'
;
console
.
log
(
this
.
name
+
' says hello to '
+
otherName
);
}
};
第一個參數是 this
在呼叫函式中擁有的值;其餘參數作為引數傳遞給呼叫函式。以下三個呼叫是等效的:
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
call
(
jane
,
'Tarzan'
);
var
func
=
jane
.
sayHelloTo
;
func
.
call
(
jane
,
'Tarzan'
);
對於第二次呼叫,您需要重複 jane
,因為 call()
不知道您如何取得它所呼叫的函式。
第一個參數 是 this
在呼叫函式中所具有的值;第二個參數是一個提供呼叫引數的陣列。以下三個呼叫是等效的:
jane
.
sayHelloTo
(
'Tarzan'
);
jane
.
sayHelloTo
.
apply
(
jane
,
[
'Tarzan'
]);
var
func
=
jane
.
sayHelloTo
;
func
.
apply
(
jane
,
[
'Tarzan'
]);
對於第二次呼叫,您需要重複 jane
,因為 apply()
不知道您如何取得它所呼叫的函式。
適用於建構函式的 apply() 說明如何將 apply()
與建構函式搭配使用。
這個方法 執行 部分函式應用,意即它會建立一個新的函式,以以下方式呼叫 bind()
的接收器:this
的值是 thisValue
,而引數從 arg1
開始,直到 argN
,接著是新函式的引數。換句話說,新函式在呼叫原始函式時會將其引數附加到 arg1, ..., argN
。我們來看一個範例:
function
func
()
{
console
.
log
(
'this: '
+
this
);
console
.
log
(
'arguments: '
+
Array
.
prototype
.
slice
.
call
(
arguments
));
}
var
bound
=
func
.
bind
(
'abc'
,
1
,
2
);
陣列方法 slice
用於將 arguments
轉換為陣列,這是記錄它所必要的(此操作在 類似陣列的物件和一般方法 中說明)。bound
是新的函式。以下是互動
> bound(3) this: abc arguments: 1,2,3
以下三個 sayHelloTo
呼叫都是等效的
jane
.
sayHelloTo
(
'Tarzan'
);
var
func1
=
jane
.
sayHelloTo
.
bind
(
jane
);
func1
(
'Tarzan'
);
var
func2
=
jane
.
sayHelloTo
.
bind
(
jane
,
'Tarzan'
);
func2
();
讓我們假設 JavaScript 有一個三點運算子 (...
),它會將陣列轉換為實際參數。 這樣的運算子允許您將 Math.max()
(請參閱 其他函式)與陣列搭配使用。在這種情況下,以下兩個表達式將會等效
Math
.
max
(...[
13
,
7
,
30
])
Math
.
max
(
13
,
7
,
30
)
對於函式,您可以透過 apply()
達到三點運算子的效果
> Math.max.apply(null, [13, 7, 30]) 30
三點運算子對於建構函式來說也很有意義
new
Date
(...[
2011
,
11
,
24
])
// Christmas Eve 2011
唉,這裡 apply()
無法運作,因為它只協助函式或方法呼叫,不協助建構函式呼叫。
我們可以在 兩個步驟中 模擬 apply()
。
透過方法呼叫將參數傳遞給 Date
(它們不在陣列中,但將會在陣列中)
new
(
Date
.
bind
(
null
,
2011
,
11
,
24
))
前述程式碼使用 bind()
建立沒有參數的建構函式,並透過 new
呼叫它。
使用 apply()
將陣列傳遞給 bind()
。由於 bind()
是方法呼叫,因此我們可以使用 apply()
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
,
2011
,
11
,
24
]))
前述陣列包含 null
,接著是 arr
的元素。我們可以使用 concat()
建立它,方法是將 null
加到 arr
的前面
var
arr
=
[
2011
,
11
,
24
];
new
(
Function
.
prototype
.
bind
.
apply
(
Date
,
[
null
].
concat
(
arr
)))
前述手動解決方法的靈感來自 Mozilla 發布的 函式庫方法。以下是經過稍微編輯的版本
if
(
!
Function
.
prototype
.
construct
)
{
Function
.
prototype
.
construct
=
function
(
argArray
)
{
if
(
!
Array
.
isArray
(
argArray
))
{
throw
new
TypeError
(
"Argument must be an array"
);
}
var
constr
=
this
;
var
nullaryFunc
=
Function
.
prototype
.
bind
.
apply
(
constr
,
[
null
].
concat
(
argArray
));
return
new
nullaryFunc
();
};
}
以下是使用中的方法
> Date.construct([2011, 11, 24]) Sat Dec 24 2011 00:00:00 GMT+0100 (CET)
替代前述方法的作法是透過 Object.create()
建立未初始化的執行個體,然後透過 apply()
呼叫建構函式(作為函式)。這表示您實際上重新實作了 new
營運子(省略了一些檢查)
Function
.
prototype
.
construct
=
function
(
argArray
)
{
var
constr
=
this
;
var
inst
=
Object
.
create
(
constr
.
prototype
);
var
result
=
constr
.
apply
(
inst
,
argArray
);
// (1)
// Check: did the constructor return an object
// and prevent `this` from being the result?
return
result
?
result
:
inst
;
};
前述程式碼不適用於大多數內建建構函式,這些建構函式在作為函式呼叫時,總是會產生新的執行個體。換句話說,第 (1) 行的步驟並未依需求設定 inst
。
如果您從物件中提取方法,它會再次變成真正的函式。它與物件的連線會中斷,而且通常不再正常運作。舉例來說,以下物件 counter
:
var
counter
=
{
count
:
0
,
inc
:
function
()
{
this
.
count
++
;
}
}
提取 inc
並呼叫它(作為函式!)會失敗
> var func = counter.inc; > func() > counter.count // didn’t work 0
以下是說明:我們已將 counter.inc
的值呼叫為函式。因此,this
是全域物件,而且我們已執行 window.count++
。不存在 window.count
,而且它是 undefined
。對它套用 ++
營運子會將它設定為 NaN
> count // global variable NaN
如果方法 inc()
處於嚴格模式,您會收到警告
> counter.inc = function () { 'use strict'; this.count++ }; > var func2 = counter.inc; > func2() TypeError: Cannot read property 'count' of undefined
原因是當我們呼叫嚴格模式函式 func2
時,this
是 undefined
,導致錯誤。
感謝 bind()
,我們可以確保 inc
不會失去與 counter
的連線
> var func3 = counter.inc.bind(counter); > func3() > counter.count // it worked! 1
在 JavaScript 中,有許多函式和方法接受回呼。瀏覽器的範例是 setTimeout()
和事件處理。如果我們傳入 counter.inc
作為回呼,它也會作為函式呼叫,導致剛才描述的相同問題。為了說明這個現象,我們使用一個簡單的回呼呼叫函式:
function
callIt
(
callback
)
{
callback
();
}
透過 callIt
執行 counter.count
會觸發警告(由於嚴格模式)
> callIt(counter.inc) TypeError: Cannot read property 'count' of undefined
與之前一樣,我們透過 bind()
修復問題
> callIt(counter.inc.bind(counter)) > counter.count // one more than before 2
每次呼叫 bind()
都會建立一個新函式。當您註冊和取消註冊回呼(例如,對於事件處理)時,這會有後果。您需要將註冊的值儲存在某個地方,並將其用於取消註冊。
您經常在 JavaScript 中巢狀函式定義,因為函式可以是參數(例如,回呼),而且它們可以在函式表達式中就地建立。當方法包含一個常規函式,而且您想在後者中存取前者的 this
時,這會造成問題,因為方法的 this
會被常規函式的 this
隱藏(它甚至不會使用自己的 this
)。在以下範例中,(1) 處的函式嘗試存取 (2) 處的方法 this
:
var
obj
=
{
name
:
'Jane'
,
friends
:
[
'Tarzan'
,
'Cheeta'
],
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
// (1)
console
.
log
(
this
.
name
+
' knows '
+
friend
);
// (2)
}
);
}
};
這會失敗,因為 (1) 處的函式有自己的 this
,這裡是 undefined
> obj.loop(); TypeError: Cannot read property 'name' of undefined
有三個方法可以解決這個問題。
我們將 this
指派給一個變數,它不會在巢狀函式內被覆蓋
loop
:
function
()
{
'use strict'
;
var
that
=
this
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
that
.
name
+
' knows '
+
friend
);
});
}
以下是互動
> obj.loop(); Jane knows Tarzan Jane knows Cheeta
我們可以使用 bind()
來提供 callback 給 this
的固定值,也就是方法的 this
(第 (1) 行):
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
this
.
name
+
' knows '
+
friend
);
}.
bind
(
this
));
// (1)
}
針對 forEach()
的特定解決方法(請參閱 檢查方法)是在 callback 之後提供第二個參數,它會變成 callback 的 this
:
loop
:
function
()
{
'use strict'
;
this
.
friends
.
forEach
(
function
(
friend
)
{
console
.
log
(
this
.
name
+
' knows '
+
friend
);
},
this
);
}
兩個物件之間的原型關係與繼承有關:每個物件都可以有另一個物件作為其原型。然後前者物件會繼承其所有原型的屬性。物件透過內部屬性 [[Prototype]]
指定其原型。每個物件都有此屬性,但它可以是 null
。由 [[Prototype]]
屬性連接的物件鏈稱為 原型鏈(圖 17-1)。
要了解基於原型的(或 原型)繼承如何運作,我們來看一個範例(使用發明的語法來指定 [[Prototype]]
屬性)
var
proto
=
{
describe
:
function
()
{
return
'name: '
+
this
.
name
;
}
};
var
obj
=
{
[[
Prototype
]]
:
proto
,
name
:
'obj'
};
物件 obj
從 proto
繼承屬性 describe
。它還有一個所謂的 自有(非繼承的、直接)屬性 name
。
obj
繼承屬性 describe
;您可以存取 它,就像物件本身有該屬性一樣:
> obj.describe [Function]
每當您透過 obj
存取屬性時,JavaScript 會從該物件開始搜尋,並繼續其原型、原型的原型,以此類推。這就是為什麼我們可以透過 obj.describe
存取 proto.describe
。原型鏈表現得好像它是單一物件。當您呼叫方法時,這種錯覺會被維持:this
的值始終是開始搜尋方法的物件,而不是找到方法的物件。這允許方法存取原型鏈的所有屬性。例如
> obj.describe() 'name: obj'
在 describe()
內部,this
是 obj
,這允許方法存取 obj.name
。
在 原型鏈中,物件中的屬性會 覆寫 「較後」物件中具有相同金鑰的屬性:會先找到前者屬性。它會隱藏後者屬性,後者屬性無法再被存取。舉例來說,我們在 obj
中覆寫方法 proto.describe()
:
> obj.describe = function () { return 'overridden' }; > obj.describe() 'overridden'
這類似於在基於類別的語言中方法的覆寫方式。
原型非常適合在物件之間共用資料:多個物件取得相同的原型,原型包含所有共用屬性。 讓我們來看一個範例。物件 jane
和 tarzan
都包含相同的方法 describe()
。這是我們希望透過共用來避免的事情:
var
jane
=
{
name
:
'Jane'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
tarzan
=
{
name
:
'Tarzan'
,
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
兩個物件都是人。它們的 name
屬性不同,但我們可以讓它們共用 describe
方法。我們透過建立一個稱為 PersonProto
的共用原型,並將 describe
放入其中來做到這一點(圖 17-2)。
以下程式碼建立物件 jane
和 tarzan
,它們共用原型 PersonProto
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
以下是互動方式
> jane.describe() Person named Jane > tarzan.describe() Person named Tarzan
這是常見的模式:資料存在於原型鏈的第一個物件中,而方法存在於較後的物件中。JavaScript 的原型繼承風格旨在支援此模式:設定屬性只會影響原型鏈中的第一個物件,而取得屬性則會考慮整個鏈(請參閱 設定和刪除只會影響自己的屬性)。
到目前為止,我們假裝你可以從 JavaScript 存取內部屬性 [[Prototype]]
。但語言不允許你這麼做。相反地,有函式用於讀取原型和建立具有給定原型的物件。
此 呼叫:
Object
.
create
(
proto
,
propDescObj
?
)
建立一個原型為 proto
的物件。另外,可以透過描述子 (在 屬性描述子 中說明) 加入屬性。在以下範例中,物件 jane
取得原型 PersonProto
和可變屬性 name
,其值為 'Jane'
(透過屬性描述子指定)
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
Object
.
create
(
PersonProto
,
{
name
:
{
value
:
'Jane'
,
writable
:
true
}
});
以下是互動
> jane.describe() 'Person named Jane'
但你經常只建立一個空物件,然後手動加入屬性,因為描述子很冗長
var
jane
=
Object
.
create
(
PersonProto
);
jane
.
name
=
'Jane'
;
此方法 呼叫:
Object
.
getPrototypeOf
(
obj
)
傳回 obj
的原型。繼續前一個範例
> Object.getPrototypeOf(jane) === PersonProto true
此 語法:
Object
.
prototype
.
isPrototypeOf
(
obj
)
檢查方法的接收者是否為 obj
的 (直接或間接) 原型。換句話說:接收者和 obj
是否在同一個原型鏈中,而且 obj
是否在接收者之前?例如
> var A = {}; > var B = Object.create(A); > var C = Object.create(B); > A.isPrototypeOf(C) true > C.isPrototypeOf(A) false
下列函式會反覆運算物件 obj
的屬性鏈。它會傳回第一個具有 propKey
鍵的自有屬性的物件,或如果沒有這樣的物件,則傳回 null
:
function
getDefiningObject
(
obj
,
propKey
)
{
obj
=
Object
(
obj
);
// make sure it’s an object
while
(
obj
&&
!
{}.
hasOwnProperty
.
call
(
obj
,
propKey
))
{
obj
=
Object
.
getPrototypeOf
(
obj
);
// obj is null if we have reached the end
}
return
obj
;
}
在前述程式碼中,我們以一般的方式呼叫 方法 Object.prototype.hasOwnProperty
(請參閱 一般方法:從原型借用方法).
某些 JavaScript 引擎有一個特殊屬性,用於取得和設定物件的原型:__proto__
。它讓語言可以直接存取 [[Prototype]]
:
> var obj = {}; > obj.__proto__ === Object.prototype true > obj.__proto__ = Array.prototype > Object.getPrototypeOf(obj) === Array.prototype true
關於 __proto__
,您需要知道下列幾件事
__proto__
的發音為「dunder proto」,是「double underscore proto」的縮寫。這個發音是借用自 Python 程式語言(正如 Ned Batchelder 在 2006 年所建議)。在 Python 中,使用雙底線的特殊變數相當常見。
__proto__
並非 ECMAScript 5 標準的一部分。因此,如果您希望程式碼符合該標準,並在目前的 JavaScript 引擎中可靠執行,就不得使用它。
__proto__
的支援,它也會成為 ECMAScript 6 的一部分。
下列表示式會檢查引擎是否支援 __proto__
作為特殊屬性
Object
.
getPrototypeOf
({
__proto__
:
null
})
===
null
只有在取得屬性時,才會考慮物件的完整原型鏈。設定和刪除會略過繼承,只會影響自己的屬性。
設定屬性會建立一個自己的屬性,即使存在一個繼承的屬性具有相同的鍵。例如,假設有下列原始碼:
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
從 proto
繼承 foo
> obj.foo 'a' > obj.hasOwnProperty('foo') false
設定 foo
會得到預期的結果
> obj.foo = 'b'; > obj.foo 'b'
不過,我們建立了一個自己的屬性,並沒有變更 proto.foo
> obj.hasOwnProperty('foo') true > proto.foo 'a'
其理由在於,原型屬性是讓多個物件共用的。這種方法讓我們可以非破壞性地「變更」它們,只會影響目前的物件。
您只能刪除自己的屬性。讓我們再建立一個物件 obj
,其原型為 proto
:
var
proto
=
{
foo
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
刪除繼承的屬性 foo
沒有任何效果
> delete obj.foo true > obj.foo 'a'
如需有關 delete
算子的更多資訊,請參閱 刪除屬性。
如果您想要變更繼承的屬性,您必須先找到擁有它的物件(請參閱尋找定義屬性的物件),然後對該物件執行變更。例如,讓我們刪除先前範例中的屬性 foo
> delete getDefiningObject(obj, 'foo').foo; true > obj.foo undefined
true
或 false
的 旗標。可列舉性通常不重要,而且通常可以忽略(請參閱可列舉性:最佳實務)。
您可以列出自有屬性鍵、列出所有可列舉屬性鍵,以及檢查屬性是否存在。下列小節會說明如何執行這些動作。
您可以列出所有自有 屬性鍵,或只列出可列舉的屬性鍵:
Object.getOwnPropertyNames(obj)
會傳回 obj
的所有自有屬性的鍵。
Object.keys(obj)
會傳回 obj
的所有 可列舉自有屬性的鍵。 請注意,屬性通常是可列舉的(請參閱 可列舉性:最佳實務),因此您可以使用 Object.keys()
,特別是對於您所建立的物件。
如果您想要列出物件的所有屬性(包括自有屬性和繼承屬性),則您有兩個選項。
選項 1 是使用迴圈
for
(
«
variable
»
in
«
object
»
)
«
statement
»
來反覆運算 object
的所有可列舉屬性的鍵。請參閱for-in 以取得更詳盡的說明。
選項 2 是自己實作一個函式,以反覆運算所有屬性(不只是可列舉屬性)。例如
function
getAllPropertyNames
(
obj
)
{
var
result
=
[];
while
(
obj
)
{
// Add the own property names of `obj` to `result`
result
=
result
.
concat
(
Object
.
getOwnPropertyNames
(
obj
));
obj
=
Object
.
getPrototypeOf
(
obj
);
}
return
result
;
}
您可以檢查物件是否具有屬性,或檢查屬性是否直接存在於物件內部:
propKey in obj
obj
具有鍵為 propKey
的屬性,則傳回 true
。此測試包含繼承屬性。
Object.prototype.hasOwnProperty(propKey)
this
) 具有鍵為 propKey
的 自有 (非繼承) 屬性,則傳回 true
。避免直接在物件上呼叫 hasOwnProperty()
,因為它可能會被覆寫 (例如,鍵為 hasOwnProperty
的自有屬性)
> var obj = { hasOwnProperty: 1, foo: 2 }; > obj.hasOwnProperty('foo') // unsafe TypeError: Property 'hasOwnProperty' is not a function
相反地,最好以一般方式呼叫它 (請參閱 一般方法:從原型借用方法)
> Object.prototype.hasOwnProperty.call(obj, 'foo') // safe true > {}.hasOwnProperty.call(obj, 'foo') // shorter true
var
proto
=
Object
.
defineProperties
({},
{
protoEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
protoEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
var
obj
=
Object
.
create
(
proto
,
{
objEnumTrue
:
{
value
:
1
,
enumerable
:
true
},
objEnumFalse
:
{
value
:
2
,
enumerable
:
false
}
});
Object.defineProperties()
在 透過描述子取得和定義屬性 中說明,但它的運作方式應該很明顯: proto
具有自有屬性 protoEnumTrue
和 protoEnumFalse
,而 obj
具有自有屬性 objEnumTrue
和 objEnumFalse
(並繼承 proto
的所有屬性)。
請注意,物件 (例如前一個範例中的 proto
) 通常至少具有原型 Object.prototype
(其中定義了標準方法,例如 toString()
和 hasOwnProperty()
)
> Object.getPrototypeOf({}) === Object.prototype true
在 屬性相關操作 中,可列舉性只會影響 for-in
迴圈 和 Object.keys()
(它也會影響 JSON.stringify()
,請參閱 JSON.stringify(value, replacer?, space?))。
for-in
迴圈會反覆運算所有可列舉屬性的鍵,包括繼承的屬性 (請注意, Object.prototype
的所有不可列舉屬性都不會顯示)
> for (var x in obj) console.log(x); objEnumTrue protoEnumTrue
Object.keys()
會傳回所有自有 (非繼承) 可列舉屬性的鍵
> Object.keys(obj) [ 'objEnumTrue' ]
如果您要取得所有自有屬性的鍵,您需要使用 Object.getOwnPropertyNames()
> Object.getOwnPropertyNames(obj) [ 'objEnumTrue', 'objEnumFalse' ]
只有 for-in
迴圈(請參閱前一個範例) 和 in
算子會考量繼承:
> 'toString' in obj true > obj.hasOwnProperty('toString') false > obj.hasOwnProperty('objEnumFalse') true
要反覆處理屬性金鑰:
將 for-in
與 hasOwnProperty()
結合使用,其方式如 for-in 中所述。這甚至適用於較舊的 JavaScript 引擎。例如
for
(
var
key
in
obj
)
{
if
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
key
))
{
console
.
log
(
key
);
}
}
將 Object.keys()
或 Object.getOwnPropertyNames()
與 forEach()
陣列反覆處理結合使用
var
obj
=
{
first
:
'John'
,
last
:
'Doe'
};
// Visit non-inherited enumerable keys
Object
.
keys
(
obj
).
forEach
(
function
(
key
)
{
console
.
log
(
key
);
});
要反覆處理屬性值或 (金鑰、值) 成對
ECMAScript 5 讓您撰寫方法,其呼叫看起來就像您正在取得或設定屬性。這表示屬性是虛擬的,而非儲存空間。例如,您可以禁止設定屬性,並在讀取屬性時,總是計算傳回的值。
下列 範例使用物件文字來定義屬性 foo
的設定器和取得器:
var
obj
=
{
get
foo
()
{
return
'getter'
;
},
set
foo
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
};
以下是互動
> obj.foo = 'bla'; setter: bla > obj.foo 'getter'
指定取得器和設定器的另一種方式是透過屬性描述符(請參閱 屬性描述符)。下列程式碼定義與前一個文字相同的物件
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
// object with property descriptors
foo
:
{
// property descriptor
get
:
function
()
{
return
'getter'
;
},
set
:
function
(
value
)
{
console
.
log
(
'setter: '
+
value
);
}
}
}
);
取得器和設定器會從原型繼承:
> var proto = { get foo() { return 'hello' } }; > var obj = Object.create(proto); > obj.foo 'hello'
屬性屬性和屬性描述符是進階主題。您通常不需要知道它們如何運作。
在本節中,我們將探討屬性的內部結構:
屬性的所有狀態,包括其資料及其元資料,都儲存在 屬性 中。它們是屬性所擁有的欄位,就像物件擁有屬性一樣。屬性金鑰通常以雙中括號撰寫。屬性對於一般屬性和存取器(取得器和設定器)來說很重要。
下列屬性是特定於一般屬性
[[Value]]
儲存屬性的值,其資料。
[[Writable]]
儲存一個布林值,表示屬性的值是否可以變更。
[[Get]]
儲存取得器,一個在讀取屬性時呼叫的函式。此函式會計算讀取存取的結果。
[[Set]]
儲存設定器,一個在將屬性設定為某個值時呼叫的函式。此函式會接收該值作為參數。
所有屬性都具有下列 屬性:
[[Enumerable]]
儲存一個布林值。將屬性設為不可列舉會讓它在某些運算中隱藏(請參閱 屬性的反覆運算和偵測)。
[[Configurable]]
儲存一個布林值。如果它是 false
,您就無法刪除屬性、變更其任何屬性([[Value]]
除外),或將它從資料屬性轉換為存取器屬性,反之亦然。換句話說,[[Configurable]]
控制屬性元資料的可寫性。此規則有一個例外—JavaScript 允許您將不可設定的屬性從可寫變更為唯讀,原因是 歷史因素;陣列的 length
屬性一直都是可寫且不可設定的。沒有這個例外,您就無法凍結(請參閱 凍結)陣列。
如果您未指定屬性,將使用下列預設值:
屬性鍵 | 預設值 |
|
|
|
|
|
|
|
|
|
|
|
|
當您透過屬性描述子建立屬性時,這些預設值非常重要(請參閱下列章節)。
屬性描述子是一種用於以程式化方式處理屬性的資料結構。它是一個編碼屬性屬性的物件。描述子的每個屬性都對應到一個屬性。例如,下列是值為 123 的唯讀屬性的描述子:
{
value
:
123
,
writable
:
false
,
enumerable
:
true
,
configurable
:
false
}
您可以透過存取器達成相同的目標,也就是不可變性。然後,描述子如下所示
{
get
:
function
()
{
return
123
},
enumerable
:
true
,
configurable
:
false
}
定義屬性會產生不同的結果,視屬性是否已存在而定
如果屬性不存在,請建立一個新屬性,其屬性由描述子指定。如果屬性在描述子中沒有對應的屬性,請使用預設值。預設值由屬性名稱的意義決定。它們與透過指定建立屬性時所使用的值相反(然後,屬性可寫、可列舉且可組態)。例如:
> var obj = {}; > Object.defineProperty(obj, 'foo', { configurable: true }); > Object.getOwnPropertyDescriptor(obj, 'foo') { value: undefined, writable: false, enumerable: false, configurable: true }
我通常不依賴預設值,而是明確指出所有屬性,以完全清楚。
如果屬性已存在,請根據描述子指定的方式更新屬性的屬性。如果屬性在描述子中沒有對應的屬性,請不要變更它。以下是一個範例(延續前一個範例)
> Object.defineProperty(obj, 'foo', { writable: true }); > Object.getOwnPropertyDescriptor(obj, 'foo') { value: undefined, writable: true, enumerable: false, configurable: true }
下列操作讓您透過屬性描述子取得和設定屬性的屬性:
Object.getOwnPropertyDescriptor(obj, propKey)
傳回鍵為propKey
的obj
的自有(非繼承)屬性的描述子。如果沒有這樣的屬性,將傳回undefined
> Object.getOwnPropertyDescriptor(Object.prototype, 'toString') { value: [Function: toString], writable: true, enumerable: false, configurable: true } > Object.getOwnPropertyDescriptor({}, 'toString') undefined
Object.defineProperty(obj, propKey, propDesc)
建立或變更 obj
的屬性,其金鑰為 propKey
,其屬性透過 propDesc
指定。傳回已修改的物件。例如
var
obj
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
123
,
enumerable
:
true
// writable: false (default value)
// configurable: false (default value)
});
Object.defineProperties(obj, propDescObj)
var
obj
=
Object
.
defineProperties
({},
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
Object.create(proto, propDescObj?)
首先,建立一個原型為 proto
的物件。然後,如果已指定選用參數 propDescObj
,則以與 Object.defineProperties
相同的方式新增屬性。最後,傳回結果。例如,下列程式碼片段產生與前一個片段相同的結果
var
obj
=
Object
.
create
(
Object
.
prototype
,
{
foo
:
{
value
:
123
,
enumerable
:
true
},
bar
:
{
value
:
'abc'
,
enumerable
:
true
}
});
若要建立物件的完全相同副本,您需要做好兩件事:
下列函式執行此類複製
function
copyObject
(
orig
)
{
// 1. copy has same prototype as orig
var
copy
=
Object
.
create
(
Object
.
getPrototypeOf
(
orig
));
// 2. copy has all of orig’s properties
copyOwnPropertiesFrom
(
copy
,
orig
);
return
copy
;
}
屬性會透過此函式從 orig
複製到 copy
。
function
copyOwnPropertiesFrom
(
target
,
source
)
{
Object
.
getOwnPropertyNames
(
source
)
// (1)
.
forEach
(
function
(
propKey
)
{
// (2)
var
desc
=
Object
.
getOwnPropertyDescriptor
(
source
,
propKey
);
// (3)
Object
.
defineProperty
(
target
,
propKey
,
desc
);
// (4)
});
return
target
;
};
涉及的步驟如下
source
所有自有屬性金鑰的陣列。
target
中建立自有屬性。
請注意,此函式與 Underscore.js 函式庫中的函式 _.extend()
非常類似。
下列兩個作業非常類似:
defineProperty()
和 defineProperties()
定義屬性(請參閱透過描述取得和定義屬性)。
=
指定屬性。
不過,有一些細微的差異
指定屬性 prop
表示變更現有屬性。程序如下:
prop
是 setter(自有或繼承),則呼叫該 setter。
prop
是唯讀的(自己的或繼承的),則擲出例外(在嚴格模式下)或不執行任何動作(在寬鬆模式下)。下一節會更詳細地說明這個(有點意外的)現象。
prop
是自己的(且可寫入),則變更該屬性的值。
prop
,或它是繼承的且可寫入。在這兩種情況下,定義一個可寫入、可設定和可列舉的自己的屬性 prop
。在後一種情況下,我們剛剛覆寫了一個繼承的屬性(非破壞性地變更它)。在前一種情況下,會自動定義一個遺失的屬性。這種自動定義是有問題的,因為在指定中出現的錯字可能很難偵測。
如果一個物件 obj
從一個原型繼承一個屬性 foo
,而 foo
是唯讀的,則無法指定 obj.foo
:
var
proto
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
'a'
,
writable
:
false
});
var
obj
=
Object
.
create
(
proto
);
obj
從 proto
繼承唯讀屬性 foo
。在寬鬆模式下,設定屬性不會造成任何影響
> obj.foo = 'b'; > obj.foo 'a'
在嚴格模式下,您會得到一個例外
> (function () { 'use strict'; obj.foo = 'b' }()); TypeError: Cannot assign to read-only property 'foo'
這符合指定會變更繼承的屬性,但是非破壞性的概念。如果一個繼承的屬性是唯讀的,您會想要禁止所有變更,即使是非破壞性的變更。
請注意,您可以透過定義一個自己的屬性來規避這個保護(請參閱前一個小節,以了解定義和指定之間的差異)
> Object.defineProperty(obj, 'foo', { value: 'b' }); > obj.foo 'b'
一般的 規則是系統建立的屬性是不可列舉的,而使用者建立的屬性是可列舉的:
> Object.keys([]) [] > Object.getOwnPropertyNames([]) [ 'length' ] > Object.keys(['a']) [ '0' ]
這對於內建實例原型的函式特別適用
> Object.keys(Object.prototype) [] > Object.getOwnPropertyNames(Object.prototype) [ hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString' ]
可列舉性的主要目的是告訴 for-in
迴圈它應該忽略哪些屬性。正如我們剛剛在查看內建建構函式的實例時所看到的,任何非使用者建立的項目都會對 for-in
隱藏起來。
受到可列舉性影響的唯一作業是
for-in
迴圈
Object.keys()
(列出自己的屬性金鑰)
JSON.stringify()
(JSON.stringify(value, replacer?, space?))
以下是一些要記住的最佳實務
for-in
迴圈(最佳實務:反覆運算陣列)。
保護物件有三種層級,由弱到強列出如下:
防止擴充的方法:
Object
.
preventExtensions
(
obj
)
讓obj
無法新增屬性。例如
var
obj
=
{
foo
:
'a'
};
Object
.
preventExtensions
(
obj
);
現在在隨意模式中新增屬性會靜默失敗
> obj.bar = 'b'; > obj.bar undefined
而在嚴格模式中會擲回錯誤
> (function () { 'use strict'; obj.bar = 'b' }()); TypeError: Can't add property bar, object is not extensible
不過你仍然可以刪除屬性
> delete obj.foo true > obj.foo undefined
你可以透過以下方式檢查物件是否可擴充
Object
.
isExtensible
(
obj
)
封存的方法:
Object
.
seal
(
obj
)
防止擴充和讓所有屬性都「不可設定」。後者表示屬性的屬性(請參閱屬性屬性和屬性描述子)無法再變更。例如,唯讀屬性會永遠保持唯讀。
以下範例示範封存會讓所有屬性都不可設定
> var obj = { foo: 'a' }; > Object.getOwnPropertyDescriptor(obj, 'foo') // before sealing { value: 'a', writable: true, enumerable: true, configurable: true } > Object.seal(obj) > Object.getOwnPropertyDescriptor(obj, 'foo') // after sealing { value: 'a', writable: true, enumerable: true, configurable: false }
你仍然可以變更屬性foo
> obj.foo = 'b'; 'b' > obj.foo 'b'
但你無法變更它的屬性
> Object.defineProperty(obj, 'foo', { enumerable: false }); TypeError: Cannot redefine property: foo
你可以透過以下方式檢查物件是否已封存
Object
.
isSealed
(
obj
)
凍結的方法:
Object
.
freeze
(
obj
)
它會讓所有屬性都不可寫入,並封存obj
。換句話說,obj
不可擴充,且所有屬性都唯讀,而且無法變更。我們來看一個範例:
var
point
=
{
x
:
17
,
y
:
-
5
};
Object
.
freeze
(
point
);
再一次,你在隨意模式中會得到靜默失敗
> point.x = 2; // no effect, point.x is read-only > point.x 17 > point.z = 123; // no effect, point is not extensible > point { x: 17, y: -5 }
而在嚴格模式中會得到錯誤
> (function () { 'use strict'; point.x = 2 }()); TypeError: Cannot assign to read-only property 'x' > (function () { 'use strict'; point.z = 123 }()); TypeError: Can't add property z, object is not extensible
你可以透過以下方式檢查物件是否已凍結
Object
.
isFrozen
(
obj
)
保護物件是淺層的:它會影響自己的屬性,但不會影響那些屬性的值。例如,考慮以下物件:
var
obj
=
{
foo
:
1
,
bar
:
[
'a'
,
'b'
]
};
Object
.
freeze
(
obj
);
即使你已凍結obj
,它並非完全不可變—你可以變更屬性bar
的(可變)值
> obj.foo = 2; // no effect > obj.bar.push('c'); // changes obj.bar > obj { foo: 1, bar: [ 'a', 'b', 'c' ] }
此外,obj
具有原型Object.prototype
,它也是可變的。
建構函式(簡稱:建構函式)有助於產生在某方面相似的物件。它是一個正常的函式,但它的命名、設定和呼叫方式不同。本節說明建構函式的運作方式。它們對應於其他語言中的類別。
我們已經看過兩個相似的物件範例(在透過原型在物件間共用資料中)
var
PersonProto
=
{
describe
:
function
()
{
return
'Person named '
+
this
.
name
;
}
};
var
jane
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Jane'
};
var
tarzan
=
{
[[
Prototype
]]
:
PersonProto
,
name
:
'Tarzan'
};
物件jane
和tarzan
都被視為「人」,並共用原型物件PersonProto
。我們將那個原型轉換成建構函式Person
,它會建立像jane
和tarzan
這樣的物件。建構函式建立的物件稱為它的實例。這些實例具有與jane
和tarzan
相同的結構,由兩個部分組成
jane
和 tarzan
)。
PersonProto
)。
建構函式是一個透過 new
算子呼叫的函式。依慣例,建構函式的名稱以大寫字母開頭,而一般函式和方法的名稱則以小寫字母開頭。函式本身會設定第 1 部分
function
Person
(
name
)
{
this
.
name
=
name
;
}
在 Person.prototype
中的物件會變成 Person
的所有執行個體的原型。它會提供第 2 部分
Person
.
prototype
.
describe
=
function
()
{
return
'Person named '
+
this
.
name
;
};
讓我們建立並使用 Person
的一個執行個體
> var jane = new Person('Jane'); > jane.describe() 'Person named Jane'
我們可以看到 Person
是個一般函式。它只有在透過 new
呼叫時才會變成建構函式。 new
算子會執行下列步驟:
Person.
prototype
。
Person
會收到該物件作為隱含參數 this
,並新增執行個體屬性。
圖 17-3 顯示執行個體 jane
的樣子。 Person.prototype
的屬性 constructor
會指向建構函式,並在 執行個體的 constructor 屬性 中說明。
instanceof
算子讓我們可以檢查物件是否為特定建構函式的執行個體
> jane instanceof Person true > jane instanceof Date false
如果你要手動實作 new
算子,它大致會如下所示:
function
newOperator
(
Constr
,
args
)
{
var
thisValue
=
Object
.
create
(
Constr
.
prototype
);
// (1)
var
result
=
Constr
.
apply
(
thisValue
,
args
);
if
(
typeof
result
===
'object'
&&
result
!==
null
)
{
return
result
;
// (2)
}
return
thisValue
;
}
在第 (1) 行,您可以看到由建構函式 Constr
建立的執行個體原型是 Constr.prototype
。
第 (2) 行揭露了 new
營運子的另一個功能:您可以從建構函式傳回一個任意物件,而它會變成 new
營運子的結果。如果您想要建構函式傳回子建構函式的執行個體,這會很有用(在 從建構函式傳回任意物件 中提供了一個範例)。
不幸的是,術語 原型 在 JavaScript 中使用 模稜兩可:
一個物件可以是另一個物件的原型
> var proto = {}; > var obj = Object.create(proto); > Object.getPrototypeOf(obj) === proto true
在前面的範例中,proto
是 obj
的原型。
prototype
的值
每個建構函式 C
都有一個 prototype
屬性,它會參照一個物件。該物件會變成 C
的所有執行個體的原型:
> function C() {} > Object.getPrototypeOf(new C()) === C.prototype true
通常,上下文會清楚說明指的是哪一個原型。如果需要消除歧義,那麼我們只能使用 原型 來描述物件之間的關係,因為這個名稱已經透過 getPrototypeOf
和 isPrototypeOf
進入標準函式庫。因此,我們需要為 prototype
屬性參照的物件找到一個不同的名稱。一種可能性是 建構函式原型,但這是有問題的,因為建構函式也有原型
> function Foo() {} > Object.getPrototypeOf(Foo) === Function.prototype true
因此,執行個體原型 是最佳的 選項。
預設情況下,每個函式 C
都包含一個執行個體原型物件 C.prototype
,其屬性 constructor
指回 C
:
> function C() {} > C.prototype.constructor === C true
由於 constructor
屬性是由每個執行個體從原型繼承而來的,因此您可以使用它來取得執行個體的建構函式
> var o = new C(); > o.constructor [Function: C]
在 下列 catch
子句中,我們會根據捕捉到的例外狀況的建構式採取不同的動作:
try
{
...
}
catch
(
e
)
{
switch
(
e
.
constructor
)
{
case
SyntaxError
:
...
break
;
case
CustomError
:
...
break
;
...
}
}
此方法僅偵測給定建構式的直接實例。相較之下,instanceof
會偵測直接實例和所有子建構式的實例。
例如
> function Foo() {} > var f = new Foo(); > f.constructor.name 'Foo'
並非所有 JavaScript 引擎都支援函式的 name
屬性。
以下是建立新物件 y
的方式,其建構式與現有物件 x
相同
function
Constr
()
{}
var
x
=
new
Constr
();
var
y
=
new
x
.
constructor
();
console
.
log
(
y
instanceof
Constr
);
// true
此技巧對於必須適用於子建構式實例的方法非常實用,而且想要建立與 this
類似的實例。如此一來,您無法使用固定的建構式
SuperConstr
.
prototype
.
createCopy
=
function
()
{
return
new
this
.
constructor
(...);
};
有些繼承函式庫會將超原型指定給子建構式的屬性。例如,YUI 架構會透過 Y.extend
提供子類別化
function
Super
()
{
}
function
Sub
()
{
Sub
.
superclass
.
constructor
.
call
(
this
);
// (1)
}
Y
.
extend
(
Sub
,
Super
);
第 (1) 行的呼叫會運作,因為 extend
會將 Sub.superclass
設定為 Super.prototype
。多虧了 constructor
屬性,您可以將超建構式呼叫為方法。
instanceof
算子(請參閱 instanceof 算子)不依賴 constructor
屬性。
請確定對於每個建構式 C
,都有下列斷言成立:
C
.
prototype
.
constructor
===
C
預設情況下,每個函式 f
都已經有正確設定的 prototype
屬性
> function f() {} > f.prototype.constructor === f true
因此,您應該避免取代此物件,而只新增屬性到其中
// Avoid:
C
.
prototype
=
{
method1
:
function
(...)
{
...
},
...
};
// Prefer:
C
.
prototype
.
method1
=
function
(...)
{
...
};
...
如果您有取代它,您應該手動將正確值指定給 constructor
C
.
prototype
=
{
constructor
:
C
,
method1
:
function
(...)
{
...
},
...
};
請注意,JavaScript 中沒有任何關鍵部分依賴於 constructor
屬性;但設定它是很好的風格,因為它能啟用本節中提到的技術。
instanceof
算子
value
instanceof
Constr
會判斷 value
是否是由建構式 Constr
或子建構式建立的。它會透過檢查 Constr.prototype
是否在 value
的原型鏈中來執行此動作。因此,下列兩個表達式是等效的:
value
instanceof
Constr
Constr
.
prototype
.
isPrototypeOf
(
value
)
以下是一些範例
> {} instanceof Object true > [] instanceof Array // constructor of [] true > [] instanceof Object // super-constructor of [] true > new Date() instanceof Date true > new Date() instanceof Object true
正如預期,instanceof
對於原始值永遠是 false
> 'abc' instanceof Object false > 123 instanceof Number false
最後,如果 instanceof
的右側不是函式,它會擲回例外狀況
> [] instanceof 123 TypeError: Expecting a function in instanceof check
幾乎所有物件都是 Object
的實例,因為 Object.prototype
在其原型鏈中。但也有物件不是這種情況。以下是兩個範例:
> Object.create(null) instanceof Object false > Object.prototype instanceof Object false
前一個物件在 字典模式:沒有原型的物件是更好的映射 中有更詳細的說明。後一個物件是大多數原型鏈的終點(而且它們必須在某處終止)。兩個物件都沒有原型
> Object.getPrototypeOf(Object.create(null)) null > Object.getPrototypeOf(Object.prototype) null
但 typeof
正確地將它們分類為物件
> typeof Object.create(null) 'object' > typeof Object.prototype 'object'
這個陷阱對大多數 instanceof
的使用案例來說並非破壞性的,但你必須知道它。
在網頁瀏覽器中,每個框架和視窗都有自己的 領域,其中有獨立的全球變數。這會阻止 instanceof
對跨越領域的物件運作。要了解原因,請查看以下程式碼:
if
(
myvar
instanceof
Array
)
...
// Doesn’t always work
如果 myvar
是來自不同領域的陣列,則其原型是該領域的 Array.prototype
。因此,instanceof
將找不到 myvar
原型鏈中的目前領域的 Array.prototype
,並會傳回 false
。ECMAScript 5 有函式 Array.isArray()
,它總是運作:
<head>
<script>
function
test
(
arr
)
{
var
iframe
=
frames
[
0
];
console
.
log
(
arr
instanceof
Array
);
// false
console
.
log
(
arr
instanceof
iframe
.
Array
);
// true
console
.
log
(
Array
.
isArray
(
arr
));
// true
}
</script>
</head>
<body>
<iframe
srcdoc=
"<script>window.parent.test([])</script>"
>
</iframe>
</body>
顯然,這也是非內建建構函式的問題。
除了使用 Array.isArray()
之外,還有幾件事可以解決這個問題
postMessage()
方法,它可以將物件複製到另一個領域,而不是傳遞參考。
檢查實例建構函式的名稱(僅適用於支援函式屬性 name
的引擎)
someValue
.
constructor
.
name
===
'NameOfExpectedConstructor'
使用原型屬性將實例標記為屬於類型 T
。有幾種方法可以做到這一點。檢查 value
是否是 T
的實例如下
value.isT()
:T
實例的原型必須從此方法傳回 true
;共用超建構函式應傳回預設值 false
。
'T' in value
:您必須使用其金鑰為 'T'
(或更獨特的名稱)的屬性標記 T
實例的原型。
value.TYPE_NAME === 'T'
:每個相關原型都必須具有 TYPE_NAME
屬性,且其值適當。
本節提供實作建構函式的幾個提示。
如果您在使用建構函式時忘記 new
,您將會將其呼叫為函式,而非建構函式。在隨意模式中,您不會取得實例,而且會建立全域變數。不幸的是,所有這些情況都會在沒有警告的情況下發生:
function
SloppyColor
(
name
)
{
this
.
name
=
name
;
}
var
c
=
SloppyColor
(
'green'
);
// no warning!
// No instance is created:
console
.
log
(
c
);
// undefined
// A global variable is created:
console
.
log
(
name
);
// green
在嚴格模式下,您會得到一個例外
function
StrictColor
(
name
)
{
'use strict'
;
this
.
name
=
name
;
}
var
c
=
StrictColor
(
'green'
);
// TypeError: Cannot set property 'name' of undefined
class
Expression
{
// Static factory method:
public
static
Expression
parse
(
String
str
)
{
if
(...)
{
return
new
Addition
(...);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
}
...
Expression
expr
=
Expression
.
parse
(
someStr
);
在 JavaScript 中,您只要從建構函式傳回您需要的任何物件即可。因此,前述程式碼的 JavaScript 版本會如下所示
function
Expression
(
str
)
{
if
(...)
{
return
new
Addition
(..);
}
else
if
(...)
{
return
new
Multiplication
(...);
}
else
{
throw
new
ExpressionException
(...);
}
}
...
var
expr
=
new
Expression
(
someStr
);
這是個好消息:JavaScript 建構函式不會將您鎖定,因此您可以隨時改變主意,決定建構函式是否應該傳回直接實例或其他內容。
本節說明在大部分情況下,您不應該將資料放入原型屬性中。不過,這項規則有幾個例外。
原型包含多個物件共用的屬性。因此,它們非常適合方法。此外,透過下一個說明的技術,您也可以使用它們為實例屬性提供初始值。稍後我將說明為何不建議這麼做。
建構函式通常會設定實例屬性為初始值。如果其中一個值是預設值,則不需要建立實例屬性。您只需要一個具有相同金鑰的原型屬性,其值為預設值。例如
/**
* Anti-pattern: don’t do this
*
* @param data an array with names
*/
function
Names
(
data
)
{
if
(
data
)
{
// There is a parameter
// => create instance property
this
.
data
=
data
;
}
}
Names
.
prototype
.
data
=
[];
參數 data
是選用的。如果它不存在,實例不會取得屬性 data
,而是繼承 Names.prototype.data
。
這種方法大多數時候都有用:您可以建立 Names
的實例 n
。取得 n.data
會讀取 Names.prototype.data
。設定 n.data
會在 n
中建立新的自有屬性,並保留原型中的共用預設值。我們只會在 變更 預設值(而不是以新值取代它)時遇到問題
> var n1 = new Names(); > var n2 = new Names(); > n1.data.push('jane'); // changes default value > n1.data [ 'jane' ] > n2.data [ 'jane' ]
在前面的範例中,push()
變更了 Names.prototype.data
中的陣列。由於所有沒有自有屬性 data
的實例共用該陣列,n2.data
的初始值也變更了。
根據我們剛剛討論的內容,最好不要共用預設值,並始終建立新的預設值
function
Names
(
data
)
{
this
.
data
=
data
||
[];
}
顯然地,如果共用的預設值是不可變的(就像所有基本型別一樣;請參閱 基本型別值),則不會出現修改共用預設值的問題。但為了保持一致性,最好堅持使用單一的方式設定屬性。我也比較喜歡維持慣用的關注點分離(請參閱 第 3 層:建構函式—實例工廠):建構函式設定實例屬性,而原型包含方法。
ECMAScript 6 會讓這變成更佳的實務,因為建構函式參數可以有預設值,而且您可以透過類別定義原型方法,但不能定義具有資料的原型屬性。
偶爾,建立屬性值會是一個昂貴的作業(在運算或儲存方面)。在這種情況下,您可以依需求建立實例屬性:
function
Names
(
data
)
{
if
(
data
)
this
.
data
=
data
;
}
Names
.
prototype
=
{
constructor
:
Names
,
// (1)
get
data
()
{
// Define, don’t assign
// => avoid calling the (nonexistent) setter
Object
.
defineProperty
(
this
,
'data'
,
{
value
:
[],
enumerable
:
true
,
configurable
:
false
,
writable
:
false
});
return
this
.
data
;
}
};
我們無法透過指定將屬性 data
加入實例,因為 JavaScript 會抱怨缺少設定器(當它只找到取得器時就會這樣)。因此,我們透過 Object.defineProperty()
加入它。請參閱 屬性:定義與指定 來檢閱定義與指定之間的差異。在第 (1) 行中,我們確保屬性 constructor
已正確設定(請參閱 實例的 constructor 屬性)。
顯然地,這會花費相當多的工作,因此您必須確定它值得這麼做。
如果同一個屬性(相同鍵、相同語意,通常不同值)存在於多個原型中,它稱為多型。然後,透過實例讀取屬性的結果會透過該實例的原型動態決定。未多型使用的原型屬性可以替換為變數(這能更佳反映其非多型性質)。
例如,你可以將常數儲存在原型屬性中,並透過this
存取它
function
Foo
()
{}
Foo
.
prototype
.
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
this
.
FACTOR
;
};
此常數非多型。因此,你也可以透過變數存取它
// This code should be inside an IIFE or a module
function
Foo
()
{}
var
FACTOR
=
42
;
Foo
.
prototype
.
compute
=
function
(
x
)
{
return
x
*
FACTOR
;
};
以下是多型原型屬性的範例 ,其中包含不可變資料。透過原型屬性標記建構函式的實例,你可以將它們與不同建構函式的實例區分開來:
function
ConstrA
()
{
}
ConstrA
.
prototype
.
TYPE_NAME
=
'ConstrA'
;
function
ConstrB
()
{
}
ConstrB
.
prototype
.
TYPE_NAME
=
'ConstrB'
;
由於多型「標籤」TYPE_NAME
,你可以區分 ConstrA
和 ConstrB
的實例,即使它們跨越領域(然後 instanceof
無法運作;請參閱 陷阱:跨越領域(框架或視窗))。
JavaScript 沒有 專用的方法來管理物件的私密資料。本節將說明三種解決此限制的方法:
此外,我將說明如何透過 IIFE 保持全域資料私密。
當建構函式 被呼叫時,會建立兩件事:建構函式的實例和環境(請參閱 環境:管理變數)。實例將由建構函式初始化。環境包含建構函式的參數和區域變數。在建構函式內建立的每個函式(包括方法)都會保留對環境的參考,也就是建立它的環境。由於這個參考,它將永遠可以存取環境,即使建構函式已完成。函式和環境的這種組合稱為封閉(封閉:函式與其建立範圍保持連線)。因此,建構函式的環境是獨立於實例的資料儲存,而且只因為這兩者同時建立而與實例相關。為了正確連接它們,我們必須有存在於兩個世界的函式。使用 Douglas Crockford 的術語,實例可以有與其關聯的三種類型值(請參閱 圖 17-4)
以下各節會更詳細地說明每種類型的值。
請記住,給定一個建構函式 Constr
,有兩種 公開 的屬性,所有人都可以存取。首先,原型屬性 儲存在 Constr.prototype
中,並由所有實例共用。 原型屬性通常是方法:
Constr
.
prototype
.
publicMethod
=
...;
其次,實例屬性 是每個實例獨有的。 它們在建構函式中新增,通常包含資料(而非方法):
function
Constr
(...)
{
this
.
publicData
=
...;
...
}
建構函式的環境包含參數和局部變數。 它們只能從建構函式內部存取,因此對實例而言是私人的:
function
Constr
(...)
{
...
var
that
=
this
;
// make accessible to private functions
var
privateData
=
...;
function
privateFunction
(...)
{
// Access everything
privateData
=
...;
that
.
publicData
=
...;
that
.
publicMethod
(...);
}
...
}
私人資料非常安全,無法從外部存取,因此原型方法無法存取它。 但是,離開建構函式後,你還能如何使用它呢?答案是 特權方法:在建構函式中建立的函式會新增為實例方法。這表示一方面它們可以存取私人資料;另一方面,它們是公開的,因此原型方法可以看到它們。換句話說,它們充當私人資料和公開資料(包括原型方法)之間的仲介:
function
Constr
(...)
{
...
this
.
privilegedMethod
=
function
(...)
{
// Access everything
privateData
=
...;
privateFunction
(...);
this
.
publicData
=
...;
this
.
publicMethod
(...);
};
}
以下是使用 Crockford 隱私模式實作的 StringBuilder
:
function
StringBuilder
()
{
var
buffer
=
[];
this
.
add
=
function
(
str
)
{
buffer
.
push
(
str
);
};
this
.
toString
=
function
()
{
return
buffer
.
join
(
''
);
};
}
// Can’t put methods in the prototype!
以下是互動
> var sb = new StringBuilder(); > sb.add('Hello'); > sb.add(' world!'); > sb.toString() ’Hello world!’
以下是使用 Crockford 隱私模式時需要考慮的一些重點:
對於大多數非安全關鍵應用程式,隱私更像是對 API 用戶的提示:「您不需要看到這個。」這是封裝的主要好處,也就是隱藏複雜性。即使在幕後進行更多操作,您只需要了解 API 的公開部分。命名慣例的理念是讓用戶透過標記屬性的金鑰來了解隱私。前置底線通常用於此目的。
讓我們重新撰寫先前的StringBuilder
範例,以便將緩衝區保存在屬性_buffer
中,該屬性是私人的,但僅根據慣例
function
StringBuilder
()
{
this
.
_buffer
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
.
_buffer
.
push
(
str
);
},
toString
:
function
()
{
return
this
.
_buffer
.
join
(
''
);
}
};
以下是透過標記屬性金鑰進行隱私的優缺點
使用私有屬性命名慣例的一個問題是鍵可能衝突(例如,建構函式的鍵與子建構函式的鍵衝突,或 mixin 的鍵與建構函式的鍵衝突)。你可以使用較長的鍵來降低此類衝突發生的機率,例如,包含建構函式的名稱。然後,在前述情況中,私有屬性 _buffer
會稱為 _StringBuilder_buffer
。如果此類鍵對你來說太長,你可以選擇實體化它,將它儲存在變數中:
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
我們現在透過 this[KEY_BUFFER]
存取私有資料
var
StringBuilder
=
function
()
{
var
KEY_BUFFER
=
'_StringBuilder_buffer'
;
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
constructor
:
StringBuilder
,
add
:
function
(
str
)
{
this
[
KEY_BUFFER
].
push
(
str
);
},
toString
:
function
()
{
return
this
[
KEY_BUFFER
].
join
(
''
);
}
};
return
StringBuilder
;
}();
我們在 StringBuilder
周圍包裝一個 IIFE,讓常數 KEY_BUFFER
保持在區域,且不會污染全域命名空間。
實體化屬性鍵讓你可以在鍵中使用 UUID(通用唯一識別碼)。例如,透過 Robert Kieffer 的 node-uuid
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
KEY_BUFFER
在每次執行程式碼時都有不同的值。例如,它可能如下所示
_StringBuilder_buffer_110ec58a-a0f2-4ac4-8393-c866d813b8d1
具有 UUID 的長鍵幾乎不可能發生鍵衝突。
此小節說明如何透過 IIFE(請參閱 透過 IIFE 介紹新的範圍),讓全域資料對單例物件、建構函式和方法保持私有。這些 IIFE 會建立新的環境(請參閱 環境:管理變數),這是你放置私有資料的地方。
你不需要建構函式將物件與環境中的私有資料關聯起來。以下範例顯示如何使用 IIFE 達到相同的目的,方法是將它包裝在單例物件周圍:
var
obj
=
function
()
{
// open IIFE
// public
var
self
=
{
publicMethod
:
function
(...)
{
privateData
=
...;
privateFunction
(...);
},
publicData
:
...
};
// private
var
privateData
=
...;
function
privateFunction
(...)
{
privateData
=
...;
self
.
publicData
=
...;
self
.
publicMethod
(...);
}
return
self
;
}();
// close IIFE
有些全域資料僅與建構函式和原型方法相關。透過將 IIFE 包裝在兩者周圍,您可以將其隱藏在公開檢視中。 具有具體化金鑰的屬性中的私密資料提供了一個範例:建構函式StringBuilder
及其原型方法使用常數KEY_BUFFER
,其中包含屬性金鑰。該常數儲存在 IIFE 的環境中
var
StringBuilder
=
function
()
{
// open IIFE
var
KEY_BUFFER
=
'_StringBuilder_buffer_'
+
uuid
.
v4
();
function
StringBuilder
()
{
this
[
KEY_BUFFER
]
=
[];
}
StringBuilder
.
prototype
=
{
// Omitted: methods accessing this[KEY_BUFFER]
};
return
StringBuilder
;
}();
// close IIFE
請注意,如果您使用模組系統(請參閱第 31 章),您可以透過將建構函式加上方法放入模組中,使用更簡潔的程式碼來達到相同的目的。
有時您只需要針對單一方法使用全域資料。您可以透過將其放入您用來包裝方法的 IIFE 環境中,讓它保持私密。例如:
var
obj
=
{
method
:
function
()
{
// open IIFE
// method-private data
var
invocCount
=
0
;
return
function
()
{
invocCount
++
;
console
.
log
(
'Invocation #'
+
invocCount
);
return
'result'
;
};
}()
// close IIFE
};
以下是互動
> obj.method() Invocation #1 'result' > obj.method() Invocation #2 'result'
在本節中,我們將探討如何從建構函式繼承:給定建構函式Super
,我們如何撰寫一個新的建構函式Sub
,它具備Super
的所有功能,以及它自己的部分功能?很遺憾,JavaScript 沒有內建機制來執行此任務。因此,我們必須進行一些手動工作。
圖 17-5說明了這個概念:子建構函式Sub
應具備Super
的所有屬性(原型屬性和執行個體屬性),以及它自己的屬性。因此,我們對於Sub
的樣貌有粗略的概念,但不知道如何實現。有幾件事我們需要弄清楚,我將在下面說明
instanceof
運作:如果 sub
是 Sub
的實例,我們也希望 sub instanceof Super
為真。
Sub
中的 Super
方法之一。
Super
的方法之一,我們可能需要從 Sub
呼叫原始方法。
實例屬性會設定在 建構函式本身,因此繼承超建構函式的實例屬性涉及呼叫該建構函式:
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Super
.
call
(
this
,
prop1
,
prop2
);
// (1)
this
.
prop3
=
prop3
;
// (2)
this
.
prop4
=
prop4
;
// (3)
}
當 Sub
透過 new
呼叫時,它的隱式參數 this
參照一個新的實例。它首先將該實例傳遞給 Super
(1),它會新增其實例屬性。之後,Sub
設定其自己的實例屬性 (2,3)。訣竅是不透過 new
呼叫 Super
,因為那會建立一個新的超實例。相反地,我們將 Super
作為函式呼叫,並將目前的 (子) 實例作為 this
的值傳入。
共用屬性(例如方法)會保留在實例原型中。因此,我們需要找到一個方法,讓 Sub.prototype
繼承所有 Super.prototype
的屬性。解決方案是讓 Sub.prototype
的原型為 Super.prototype
。
是的,JavaScript 的術語在此令人困惑。如果您感到迷失,請參閱 術語:兩個原型,它會說明它們的差異。
這是達成此目的的程式碼
Sub
.
prototype
=
Object
.
create
(
Super
.
prototype
);
Sub
.
prototype
.
constructor
=
Sub
;
Sub
.
prototype
.
methodB
=
...;
Sub
.
prototype
.
methodC
=
...;
Object.create()
會產生一個新的物件,其原型為 Super.prototype
。之後,我們會新增 Sub
的方法。如 實例的建構函式屬性 中所說明,我們也需要設定屬性 constructor
,因為我們已經取代了原始實例原型,而它具有正確的值。
圖 17-6 顯示 Sub
和 Super
現在的關聯方式。Sub
的結構確實類似我在 圖 17-5 中所繪製的內容。此圖表未顯示實例屬性,這些屬性是由圖表中提到的函式呼叫所設定的。
「確保 instanceof
運作」表示 Sub
的每個實例也必須是 Super
的實例。圖 17-7 顯示 Sub
的實例 subInstance
的原型鏈的樣子:它的第一個原型是 Sub.prototype
,而第二個原型是 Super.prototype
。
我們從一個較簡單的問題開始:subInstance
是 Sub
的實例嗎?是的,因為以下兩個斷言是等效的(後者可被視為前者的定義)
subInstance
instanceof
Sub
Sub
.
prototype
.
isPrototypeOf
(
subInstance
)
如前所述,Sub.prototype
是 subInstance
的原型之一,因此兩個斷言都是正確的。類似地,subInstance
也是 Super
的實例,因為以下兩個斷言成立
subInstance
instanceof
Super
Super
.
prototype
.
isPrototypeOf
(
subInstance
)
我們覆寫 Super.prototype
中的方法,方法名稱相同,新增到 Sub.prototype
中。 methodB
是個範例,在 圖 17-7 中,我們可以看到它的運作方式:尋找 methodB
會從 subInstance
開始,找到 Sub.prototype.methodB
,在 Super.prototype.methodB
之前。
要了解超呼叫,您需要知道術語 主物件。 方法的主物件是擁有屬性的物件,其值是方法。例如, Sub.prototype.methodB
的主物件是 Sub.prototype
。超呼叫方法 foo
涉及三個步驟:
foo
的方法。
this
呼叫該方法。理由是超方法必須與目前方法使用相同的執行個體;它必須能夠存取相同的執行個體屬性。
因此,子方法的程式碼如下所示。它超呼叫它自己,呼叫它已覆寫的方法
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Super
.
prototype
.
methodB
.
call
(
this
,
x
,
y
);
// (1)
return
this
.
prop3
+
' '
+
superResult
;
}
在 (1) 處讀取超呼叫的一種方式如下:直接參照超方法,並使用目前的 this
呼叫它。但是,如果我們將它分成三部分,我們會找到上述步驟
Super.prototype
:在 Super.prototype
中開始搜尋,它是 Sub.prototype
的原型(目前方法 Sub.prototype.methodB
的主物件)。
methodB
:尋找名稱為 methodB
的方法。
call(this, ...)
:呼叫在先前步驟中找到的方法,並維護目前的 this
。
到目前為止,我們總是透過提及超建構函式名稱來參照超方法和超建構函式。這種硬編碼會讓您的程式碼較不靈活。您可以透過將超原型指定給 Sub
的屬性來避免它:
Sub
.
_super
=
Super
.
prototype
;
然後呼叫超級建構函式和超級方法如下所示
function
Sub
(
prop1
,
prop2
,
prop3
,
prop4
)
{
Sub
.
_super
.
constructor
.
call
(
this
,
prop1
,
prop2
);
this
.
prop3
=
prop3
;
this
.
prop4
=
prop4
;
}
Sub
.
prototype
.
methodB
=
function
(
x
,
y
)
{
var
superResult
=
Sub
.
_super
.
methodB
.
call
(
this
,
x
,
y
);
return
this
.
prop3
+
' '
+
superResult
;
}
設定 Sub._super
通常由一個實用函式處理,該函式也會將子原型連接到超級原型。例如
function
subclasses
(
SubC
,
SuperC
)
{
var
subProto
=
Object
.
create
(
SuperC
.
prototype
);
// Save `constructor` and, possibly, other methods
copyOwnPropertiesFrom
(
subProto
,
SubC
.
prototype
);
SubC
.
prototype
=
subProto
;
SubC
.
_super
=
SuperC
.
prototype
;
};
此程式碼使用輔助函式 copyOwnPropertiesFrom()
,其顯示和說明於 複製物件。
將「子類別」視為動詞: SubC
子類別 SuperC
。此類實用函式可以減輕建立子建構函式的部分負擔:手動執行的項目較少,而且從未重複提到超級建構函式的名稱。下列範例示範它如何簡化程式碼。
作為具體範例,我們假設建構函式 Person
已存在:
function
Person
(
name
)
{
this
.
name
=
name
;
}
Person
.
prototype
.
describe
=
function
()
{
return
'Person called '
+
this
.
name
;
};
現在我們要建立建構函式 Employee
作為 Person
的子建構函式。我們手動執行,如下所示
function
Employee
(
name
,
title
)
{
Person
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
=
Object
.
create
(
Person
.
prototype
);
Employee
.
prototype
.
constructor
=
Employee
;
Employee
.
prototype
.
describe
=
function
()
{
return
Person
.
prototype
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
以下是互動
> var jane = new Employee('Jane', 'CTO'); > jane.describe() Person called Jane (CTO) > jane instanceof Employee true > jane instanceof Person true
前一節的實用函式 subclasses()
讓 Employee
的程式碼稍微簡單一些,並避免硬式編碼超級建構函式 Person
function
Employee
(
name
,
title
)
{
Employee
.
_super
.
constructor
.
call
(
this
,
name
);
this
.
title
=
title
;
}
Employee
.
prototype
.
describe
=
function
()
{
return
Employee
.
_super
.
describe
.
call
(
this
)
+
' ('
+
this
.
title
+
')'
;
};
subclasses
(
Employee
,
Person
);
內建建構函式使用本節所述的相同子類別方法。例如, Array
是 Object
的子建構函式。因此, Array
執行個體的原型鏈如下所示:
> var p = Object.getPrototypeOf > p([]) === Array.prototype true > p(p([])) === Object.prototype true > p(p(p([]))) === null true
在 ECMAScript 5 和 Object.create()
之前,一個常用的解決方案是透過呼叫超級建構函式來建立子原型:
Sub
.
prototype
=
new
Super
();
// Don’t do this
在 ECMAScript 5 中不建議這樣做。原型將擁有 Super
的所有執行個體屬性,但它沒有用途。因此,最好使用前述模式(包含 Object.create()
)。
幾乎所有物件在原型鏈中都有 Object.prototype
:
> Object.prototype.isPrototypeOf({}) true > Object.prototype.isPrototypeOf([]) true > Object.prototype.isPrototypeOf(/xyz/) true
下列小節說明 Object.prototype
為其原型提供的方法。
以下兩個方法用於將物件轉換為原始值:
Object.prototype.toString()
傳回物件的字串表示形式:
> ({ first: 'John', last: 'Doe' }.toString()) '[object Object]' > [ 'a', 'b', 'c' ].toString() 'a,b,c'
Object.prototype.valueOf()
這是將物件轉換為數字的首選方式。預設實作傳回 this
> var obj = {}; > obj.valueOf() === obj true
valueOf
會被包裝建構函式覆寫以傳回包裝的原始值
> new Number(7).valueOf() 7
轉換為數字和字串(無論是隱式或明確)建立在轉換為原始值之上(詳細資訊請參閱 演算法:ToPrimitive()—將值轉換為原始值)。這就是為什麼您可以使用上述兩個方法配置這些轉換。 valueOf()
是轉換為數字的首選
> 3 * { valueOf: function () { return 5 } } 15
toString()
是轉換為字串的首選
> String({ toString: function () { return 'ME' } }) 'Result: ME'
轉換為布林值無法設定;物件總是會被視為 true
(請參閱 轉換為布林值)。
此方法 傳回物件的特定語言環境字串表示形式。預設實作會呼叫 toString()
。大多數引擎不會支援此方法的範圍超過此範圍。但是,許多現代引擎支援的 ECMAScript 國際化 API(請參閱 ECMAScript 國際化 API)會為幾個內建建構函式覆寫它。
下列方法有助於原型繼承 和屬性:
Object.prototype.isPrototypeOf(obj)
如果接收器是 obj
的原型鏈的一部分,則傳回 true
> var proto = { }; > var obj = Object.create(proto); > proto.isPrototypeOf(obj) true > obj.isPrototypeOf(obj) false
Object.prototype.hasOwnProperty(key)
如果 this
擁有一個其金鑰為 key
的屬性,則傳回 true
。擁有表示屬性存在於物件本身中,而不是存在於其原型之一中。
您通常應該以一般方式(而非直接方式)呼叫此方法,特別是在您無法靜態得知其屬性的物件上。原因和方法說明於 屬性的反覆運算和偵測
> var proto = { foo: 'abc' }; > var obj = Object.create(proto); > obj.bar = 'def'; > Object.prototype.hasOwnProperty.call(obj, 'foo') false > Object.prototype.hasOwnProperty.call(obj, 'bar') true
Object.prototype.propertyIsEnumerable(propKey)
如果接收器具有具有鍵 propKey
的屬性,則傳回 true
,該屬性可列舉,否則傳回 false
:
> var obj = { foo: 'abc' }; > obj.propertyIsEnumerable('foo') true > obj.propertyIsEnumerable('toString') false > obj.propertyIsEnumerable('unknown') false
有時,實例原型具有對比它們繼承的物件更實用的方法。本節說明如何使用原型的函式,而不用從原型繼承。例如,實例原型 Wine.prototype
具有函式 incAge()
:
function
Wine
(
age
)
{
this
.
age
=
age
;
}
Wine
.
prototype
.
incAge
=
function
(
years
)
{
this
.
age
+=
years
;
}
互動如下
> var chablis = new Wine(3); > chablis.incAge(1); > chablis.age 4
函式 incAge()
可用於具有屬性 age
的任何物件。我們如何對非 Wine
實例的物件呼叫它?讓我們看看前面的函式呼叫
chablis
.
incAge
(
1
)
實際上有兩個參數
chablis
是函式呼叫的接收器,透過 this
傳遞給 incAge
。
1
是參數,透過 years
傳遞給 incAge
。
我們無法用任意物件取代前者,接收器必須是 Wine
的實例。否則,找不到函式 incAge
。但前面的函式呼叫等於(參閱 呼叫函式同時設定 this:call()、apply() 和 bind())
Wine
.
prototype
.
incAge
.
call
(
chablis
,
1
)
使用前面的模式,我們可以將物件設為接收器(call
的第一個參數),該物件不是 Wine
的實例,因為接收器不用於尋找函式 Wine.prototype.incAge
。在以下範例中,我們將函式 incAge()
套用至物件 john
> var john = { age: 51 }; > Wine.prototype.incAge.call(john, 3) > john.age 54
可以用這種方式的函式稱為泛型函式;它必須準備好 this
不是「其」建構函式的實例。因此,並非所有函式都是泛型的;ECMAScript 語言規範明確指出哪些函式是泛型的(請參閱 所有泛型函式的清單)。
以泛型方式呼叫函式相當冗長:
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
您可以透過空物件字面值 {}
建立的 Object
實例存取 hasOwnProperty
,以縮短此程式碼
{}.
hasOwnProperty
.
call
(
obj
,
'propKey'
)
類似地,下列兩個表達式是等效的
Array
.
prototype
.
join
.
call
(
str
,
'-'
)
[].
join
.
call
(
str
,
'-'
)
此模式的優點在於它較不冗長。但它也較不具自明性。效能不應成為問題(至少長期而言),因為引擎可以靜態地判斷文字不應建立物件。
以下是 通用方法的使用範例:
使用 apply()
(請參閱 Function.prototype.apply(thisValue, argArray))將陣列推入(而非個別元素;請參閱 新增和移除元素(破壞性))
> var arr1 = [ 'a', 'b' ]; > var arr2 = [ 'c', 'd' ]; > [].push.apply(arr1, arr2) 4 > arr1 [ 'a', 'b', 'c', 'd' ]
此範例說明如何將陣列轉換為參數,而非從其他建構函數借用方法。
將陣列方法 join()
套用至字串(非陣列)
> Array.prototype.join.call('abc', '-') 'a-b-c'
將陣列方法 map()
套用至字串:[17]
> [].map.call('abc', function (x) { return x.toUpperCase() }) [ 'A', 'B', 'C' ]
以通用方式使用 map()
比使用 split('')
更有效率,後者會建立中間陣列
> 'abc'.split('').map(function (x) { return x.toUpperCase() }) [ 'A', 'B', 'C' ]
將字串方法套用至非字串。 toUpperCase()
會將接收器轉換為字串,並將結果轉換為大寫
> String.prototype.toUpperCase.call(true) 'TRUE' > String.prototype.toUpperCase.call(['a','b','c']) 'A,B,C'
在一般物件上使用通用陣列方法,可讓您深入了解其運作方式
在偽陣列上呼叫陣列方法
> var fakeArray = { 0: 'a', 1: 'b', length: 2 }; > Array.prototype.join.call(fakeArray, '-') 'a-b'
瞭解陣列方法如何轉換它視為陣列的物件
> var obj = {}; > Array.prototype.push.call(obj, 'hello'); 1 > obj { '0': 'hello', length: 1 }
JavaScript 中有些物件感覺像是陣列,但實際上並非如此。 這表示它們雖然有索引存取和 length
屬性,但沒有任何陣列方法(forEach()
、push
、concat()
等)。這很不幸,但正如我們所見,通用陣列方法可提供解決方法。類陣列物件的範例包括:
特殊變數 arguments
(請參閱 依索引取得所有參數:特殊變數 arguments),它是一個重要的類陣列物件,因為它是 JavaScript 中的基本組成部分。 arguments
看起來像陣列
> function args() { return arguments } > var arrayLike = args('a', 'b'); > arrayLike[0] 'a' > arrayLike.length 2
但沒有任何陣列方法可用
> arrayLike.join('-') TypeError: object has no method 'join'
這是因為 arrayLike
不是 Array
的實例(而且 Array.prototype
不在原型鏈中)
> arrayLike instanceof Array false
瀏覽器 DOM 節點清單,由 document.getElementsBy*()
(例如 getElementsByTagName()
)、document.forms
等傳回
> var elts = document.getElementsByTagName('h3'); > elts.length 3 > elts instanceof Array false
字串,也是類陣列
> 'abc'[1] 'b' > 'abc'.length 3
術語 類陣列 也可視為通用陣列方法與物件之間的合約。物件必須符合特定需求;否則,方法無法對其運作。需求如下
類陣列物件的元素必須可透過方括號和從 0 開始的整數索引來存取。所有方法都需要讀取權限,而有些方法額外需要寫入權限。請注意,所有物件都支援這種索引:括號中的索引會轉換為字串,並用作尋找屬性值的關鍵字
> var obj = { '0': 'abc' }; > obj[0] 'abc'
length
屬性,其值為其元素的數量。有些方法需要 length
是可變的(例如 reverse()
)。長度不可變的值(例如字串)無法與這些方法搭配使用。
下列模式 可用於處理類陣列物件:
將類陣列物件轉換為陣列
var
arr
=
Array
.
prototype
.
slice
.
call
(
arguments
);
方法 slice()
(請參閱 串接、切片、合併(非破壞性))在沒有任何參數的情況下,會建立類陣列接收者的副本
var
copy
=
[
'a'
,
'b'
].
slice
();
若要反覆處理類陣列物件的所有元素,可以使用簡單的 for
迴圈
function
logArgs
()
{
for
(
var
i
=
0
;
i
<
arguments
.
length
;
i
++
)
{
console
.
log
(
i
+
'. '
+
arguments
[
i
]);
}
}
但您也可以借用 Array.prototype.forEach()
function
logArgs
()
{
Array
.
prototype
.
forEach
.
call
(
arguments
,
function
(
elem
,
i
)
{
console
.
log
(
i
+
'. '
+
elem
);
});
}
在兩種情況下,互動如下所示
> logArgs('hello', 'world'); 0. hello 1. world
下列 清單包含所有通用方法,如 ECMAScript 語言規格中所述:
Array.prototype
(請參閱 陣列原型方法)
concat
every
filter
forEach
indexOf
join
lastIndexOf
map
pop
push
reduce
reduceRight
reverse
shift
slice
some
sort
splice
toLocaleString
toString
unshift
Date.prototype
(請參閱 日期原型方法)
toJSON
Object.prototype
(請參閱 所有物件的方法)
Object
方法都是自動泛用的,必須適用於所有物件。)
String.prototype
(請參閱 字串原型方法)
charAt
charCodeAt
concat
indexOf
lastIndexOf
localeCompare
match
replace
search
slice
split
substring
toLocaleLowerCase
toLocaleUpperCase
toLowerCase
toUpperCase
trim
由於 JavaScript 沒有內建的映射資料結構,物件通常用作從字串到值的映射。唉呀,那比看起來更常出錯。本節說明此任務中涉及的三個陷阱。
讀取屬性的操作可以 區分為兩種:
讀取物件當作映射的項目時,您需要在這些類型的操作之間仔細選擇。要了解原因,請考慮以下範例
var
proto
=
{
protoProp
:
'a'
};
var
obj
=
Object
.
create
(
proto
);
obj
.
ownProp
=
'b'
;
obj
是具有單一自有屬性的物件,其原型是 proto
,它也有一個自有屬性。 proto
的原型是 Object.prototype
,就像所有由物件文字建立的物件一樣。因此,obj
從 proto
和 Object.
prototype
繼承屬性。
我們希望將 obj
解釋為具有單一項目的映射
ownProp: 'b'
也就是說,我們想要忽略繼承的屬性,只考慮自有屬性。讓我們看看哪些讀取操作會以這種方式詮釋 obj
,哪些不會。請注意,對於物件作為映射,我們通常想要使用儲存在變數中的任意屬性鍵。這排除了點表示法。
in
算子檢查物件 是否具有具有給定鍵的屬性,但它會考慮繼承的屬性:
> 'ownProp' in obj // ok true > 'unknown' in obj // ok false > 'toString' in obj // wrong, inherited from Object.prototype true > 'protoProp' in obj // wrong, inherited from proto true
我們需要檢查以忽略繼承的屬性。 hasOwnProperty()
會執行我們想要的操作
> obj.hasOwnProperty('ownProp') // ok true > obj.hasOwnProperty('unknown') // ok false > obj.hasOwnProperty('toString') // ok false > obj.hasOwnProperty('protoProp') // ok false
我們可以使用哪些操作來尋找 obj
的所有鍵,同時尊重我們將其解釋為映射? for-in
看起來可能有效。但是,唉,它沒有:
> for (propKey in obj) console.log(propKey) ownProp protoProp
它會考慮繼承的可列舉屬性。 Object.prototype
的所有屬性都沒有顯示在這裡的原因是它們都是不可列舉的。
相反, Object.keys()
僅列出自有屬性
> Object.keys(obj) [ 'ownProp' ]
此方法僅傳回可列舉的自有屬性; ownProp
已透過指定新增,因此預設為可列舉。如果您想要列出所有自有屬性,您需要使用 Object.getOwnPropertyNames()
。
對於讀取屬性的值,我們只能在點運算子與方括號運算子之間進行選擇。我們無法使用前者,因為我們有任意鍵,儲存在變數中。這讓我們只剩下方括號運算子,它會考慮繼承的屬性:
> obj['toString'] [Function: toString]
這不是我們想要的。沒有內建的操作可以僅讀取自有屬性,但您可以輕鬆地自己實作一個
function
getOwnProperty
(
obj
,
propKey
)
{
// Using hasOwnProperty() in this manner is problematic
// (explained and fixed later)
return
(
obj
.
hasOwnProperty
(
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
使用該函式,繼承的屬性 toString
會被忽略
> getOwnProperty(obj, 'toString') undefined
函式 getOwnProperty()
在 obj
上呼叫方法 hasOwnProperty()
。通常,這很好
> getOwnProperty({ foo: 123 }, 'foo') 123
但是,如果您新增一個鍵為 hasOwnProperty
的屬性到 obj
,則該屬性會覆寫方法 Object.prototype.hasOwnProperty()
,而 getOwnProperty()
會停止運作
> getOwnProperty({ hasOwnProperty: 123 }, 'foo') TypeError: Property 'hasOwnProperty' is not a function
您可以透過直接參照 hasOwnProperty()
來解決此問題。這避免透過 obj
來尋找它
function
getOwnProperty
(
obj
,
propKey
)
{
return
(
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
?
obj
[
propKey
]
:
undefined
);
}
我們已呼叫 hasOwnProperty()
為泛型 (請參閱 泛型方法:從原型借用方法)。
在許多 JavaScript 引擎中,屬性 __proto__
(請參閱 特殊屬性 __proto__) 是特殊的:取得它會擷取物件的原型,而設定它會變更物件的原型。這就是為什麼物件無法將對應資料儲存在其金鑰為 '__proto__'
的屬性中。如果您想要允許對應金鑰 '__proto__'
,您必須在將其用作屬性金鑰之前對其進行跳脫:
function
get
(
obj
,
key
)
{
return
obj
[
escapeKey
(
key
)];
}
function
set
(
obj
,
key
,
value
)
{
obj
[
escapeKey
(
key
)]
=
value
;
}
// Similar: checking if key exists, deleting an entry
function
escapeKey
(
key
)
{
if
(
key
.
indexOf
(
'__proto__'
)
===
0
)
{
// (1)
return
key
+
'%'
;
}
else
{
return
key
;
}
}
我們還需要跳脫 '__proto__'
(等) 的跳脫版本,以避免衝突;也就是說,如果我們將金鑰 '__proto__'
跳脫為 '__proto__%'
,那麼我們也需要跳脫金鑰 '__proto__%'
,這樣它才不會取代 '__proto__'
項目。這就是第 (1) 行中所發生的情況。
Mark S. Miller 在 電子郵件 中提到了此陷阱的實際影響
認為此練習是學術性的,且不會出現在實際系統中嗎?正如在支援執行緒中所觀察到的,直到最近,在所有非 IE 瀏覽器上,如果您在新的 Google 文件開頭輸入「__proto__」,您的 Google 文件就會當掉。這被追蹤到將物件當成字串對應使用時出現的錯誤。
您可以建立一個沒有原型的物件 如下所示:
var
dict
=
Object
.
create
(
null
);
此類物件是一個比一般物件更好的對應 (字典),這就是為什麼此模式有時稱為 dict 模式 (dict 代表 dictionary)。讓我們先檢查一般物件,然後找出為什麼沒有原型的物件是更好的對應。
通常,您在 JavaScript 中建立的每個物件至少在它的原型鏈中具有 Object.prototype
。 Object.prototype
的原型是 null
,因此大多數原型鏈都會在此結束
> Object.getPrototypeOf({}) === Object.prototype true > Object.getPrototypeOf(Object.prototype) null
沒有原型的物件具有兩個優點,可用作對應
in
算子來偵測屬性是否存在,並使用方括號來讀取屬性。 __proto__
將會被停用。在 ECMAScript 6 中,如果 Object.prototype
不在物件的原型鏈中,特殊屬性 __proto__
將會被停用。你可以預期 JavaScript 引擎會慢慢地轉移到這個行為,但目前還不是很常見。
唯一的缺點是,你將會失去 Object.prototype
所提供的服務。例如,一個字典物件無法再自動轉換成字串了
> console.log('Result: '+obj) TypeError: Cannot convert object to primitive value
但這並不是真正的缺點,因為直接呼叫字典物件的方法本來就不安全。
使用字典模式進行快速修改,並作為函式庫的基礎。在(非函式庫)生產程式碼中,函式庫比較好,因為你可以確定避免所有陷阱。下一節列出幾個這樣的函式庫。
使用物件當作地圖有很多應用。如果所有屬性金鑰在靜態時(在開發時間)已知,那麼你只需要確定忽略繼承,而且只查看自己的屬性。如果可以使用任意金鑰,你應該轉向函式庫以避免本節提到的陷阱。以下是兩個範例:
本節是一個快速參考,提供更詳細說明的指標。
物件文字(請參閱 物件文字)
var
jane
=
{
name
:
'Jane'
,
'not an identifier'
:
123
,
describe
:
function
()
{
// method
return
'Person named '
+
this
.
name
;
},
};
// Call a method:
console
.
log
(
jane
.
describe
());
// Person named Jane
點運算子(.)(請參閱 點運算子 (.): 存取屬性透過固定金鑰)
obj
.
propKey
obj
.
propKey
=
value
delete
obj
.
propKey
方括弧運算子([])(請參閱 方括弧運算子 ([]): 存取屬性透過計算金鑰)
obj
[
'propKey'
]
obj
[
'propKey'
]
=
value
delete
obj
[
'propKey'
]
取得和設定原型(請參閱 取得和設定原型)
Object
.
create
(
proto
,
propDescObj
?
)
Object
.
getPrototypeOf
(
obj
)
Object
.
keys
(
obj
)
Object
.
getOwnPropertyNames
(
obj
)
Object
.
prototype
.
hasOwnProperty
.
call
(
obj
,
propKey
)
propKey
in
obj
取得和定義屬性透過描述符(請參閱 取得和定義屬性透過描述符)
Object
.
defineProperty
(
obj
,
propKey
,
propDesc
)
Object
.
defineProperties
(
obj
,
propDescObj
)
Object
.
getOwnPropertyDescriptor
(
obj
,
propKey
)
Object
.
create
(
proto
,
propDescObj
?
)
Object
.
preventExtensions
(
obj
)
Object
.
isExtensible
(
obj
)
Object
.
seal
(
obj
)
Object
.
isSealed
(
obj
)
Object
.
freeze
(
obj
)
Object
.
isFrozen
(
obj
)
Object
.
prototype
.
toString
()
Object
.
prototype
.
valueOf
()
Object
.
prototype
.
toLocaleString
()
Object
.
prototype
.
isPrototypeOf
(
obj
)
Object
.
prototype
.
hasOwnProperty
(
key
)
Object
.
prototype
.
propertyIsEnumerable
(
propKey
)