類別(在下一章中說明)是 ECMAScript 6 中主要的新 OOP 功能。不過,它也包含物件文字的新功能和 Object
中的新工具方法。本章將說明這些功能。
Object
中的新方法
Object
的新方法
Object.assign(target, source_1, source_2, ···)
Object.getOwnPropertySymbols(obj)
Object.setPrototypeOf(obj, proto)
__proto__
__proto__
__proto__
__proto__
的魔法
__proto__
的支援
__proto__
的發音是「dunder proto」__proto__
的建議
Symbol.hasInstance
(方法)Symbol.toPrimitive
(方法)Symbol.toStringTag
(字串)Symbol.unscopables
(物件)super
嗎?方法定義
const
obj
=
{
myMethod
(
x
,
y
)
{
···
}
};
屬性值簡寫
const
first
=
'Jane'
;
const
last
=
'Doe'
;
const
obj
=
{
first
,
last
};
// Same as:
const
obj
=
{
first
:
first
,
last
:
last
};
計算屬性鍵
const
propKey
=
'foo'
;
const
obj
=
{
[
propKey
]
:
true
,
[
'b'
+
'ar'
]
:
123
};
這個新的語法也可以用於方法定義
const
obj
=
{
[
'h'
+
'ello'
]()
{
return
'hi'
;
}
};
console
.
log
(
obj
.
hello
());
// hi
計算屬性鍵的主要用例是讓符號容易用作屬性鍵。
Object
中的新方法 Object
最重要的新方法是 assign()
。傳統上,這個功能在 JavaScript 世界中稱為 extend()
。與這個經典操作運作方式相反,Object.assign()
僅考慮自己的(非繼承的)屬性。
const
obj
=
{
foo
:
123
};
Object
.
assign
(
obj
,
{
bar
:
true
});
console
.
log
(
JSON
.
stringify
(
obj
));
// {"foo":123,"bar":true}
在 ECMAScript 5 中,方法是其值為函式的屬性
var
obj
=
{
myMethod
:
function
(
x
,
y
)
{
···
}
};
在 ECMAScript 6 中,方法仍然是函式值屬性,但現在有一個更簡潔的方法來定義它們
const
obj
=
{
myMethod
(
x
,
y
)
{
···
}
};
Getter 和 setter 繼續像在 ECMAScript 5 中那樣工作(注意它們在語法上與方法定義有多麼相似)
const
obj
=
{
get
foo
()
{
console
.
log
(
'GET foo'
);
return
123
;
},
set
bar
(
value
)
{
console
.
log
(
'SET bar to '
+
value
);
// return value is ignored
}
};
讓我們使用 obj
> obj.foo
GET foo
123
> obj.bar = true
SET bar to true
true
還有一種簡潔定義其值為產生器函式的屬性的方法
const obj = {
* myGeneratorMethod() {
···
}
};
此程式碼等於
const obj = {
myGeneratorMethod: function* () {
···
}
};
屬性值簡寫讓您縮寫物件文字中屬性的定義:如果指定屬性值的變數名稱也是屬性鍵,則可以省略該鍵。如下所示。
const
x
=
4
;
const
y
=
1
;
const
obj
=
{
x
,
y
};
最後一行等於
const
obj
=
{
x
:
x
,
y
:
y
};
屬性值簡寫與解構一起使用效果很好
const
obj
=
{
x
:
4
,
y
:
1
};
const
{
x
,
y
}
=
obj
;
console
.
log
(
x
);
// 4
console
.
log
(
y
);
// 1
屬性值簡寫的一個用例是多個回傳值(在解構章節中說明)。
請記住,設定屬性時有兩種指定鍵的方法。
obj.foo = true;
obj['b'+'ar'] = 123;
在物件文字中,在 ECMAScript 5 中,你只有選項 #1。ECMAScript 6 另外提供選項 #2(A 行)
const
obj
=
{
foo
:
true
,
[
'b'
+
'ar'
]
:
123
};
這個新的語法也可以用於方法定義
const
obj
=
{
[
'h'
+
'ello'
]()
{
return
'hi'
;
}
};
console
.
log
(
obj
.
hello
());
// hi
計算屬性金鑰的主要使用案例是符號:你可以定義一個公開符號,並將其用作永遠唯一的特殊屬性金鑰。一個突出的範例是儲存在 Symbol.iterator
中的符號。如果一個物件有一個具有該金鑰的方法,它就會變成 可迭代的:該方法必須傳回一個迭代器,而 for-of
迴圈等建構使用該迭代器來迭代物件。以下程式碼示範它是如何運作的。
const
obj
=
{
*
[
Symbol
.
iterator
]()
{
// (A)
yield
'hello'
;
yield
'world'
;
}
};
for
(
const
x
of
obj
)
{
console
.
log
(
x
);
}
// Output:
// hello
// world
obj
是可迭代的,因為 產生器方法定義 從 A 行開始。
Object
的新方法 Object.assign(target, source_1, source_2, ···)
此方法會將來源合併到目標中:它會修改 target
,首先將 source_1
的所有可列舉的 自己的(非繼承的)屬性複製到其中,然後是 source_2
的所有自己的屬性,依此類推。最後,它會傳回目標。
const
obj
=
{
foo
:
123
};
Object
.
assign
(
obj
,
{
bar
:
true
});
console
.
log
(
JSON
.
stringify
(
obj
));
// {"foo":123,"bar":true}
讓我們更仔細地看看 Object.assign()
如何運作
Object.assign()
認識字串和符號作為屬性金鑰。Object.assign()
會忽略繼承的屬性和不可列舉的屬性。
const
value
=
source
[
propKey
];
這表示如果來源有一個金鑰為 propKey
的 getter,那麼它將會被呼叫。因此,由 Object.assign()
建立的屬性是資料屬性,它不會將 getter 傳輸到目標。
target
[
propKey
]
=
value
;
因此,如果目標有一個金鑰為 propKey
的 setter,那麼它將會使用 value
被呼叫。
以下是你可以複製 所有 自己的屬性(不只是可列舉的屬性)的方法,同時正確地傳輸 getter 和 setter,而且不會在目標上呼叫 setter
function
copyAllOwnProperties
(
target
,
...
sources
)
{
for
(
const
source
of
sources
)
{
for
(
const
key
of
Reflect
.
ownKeys
(
source
))
{
const
desc
=
Object
.
getOwnPropertyDescriptor
(
source
,
key
);
Object
.
defineProperty
(
target
,
key
,
desc
);
}
}
return
target
;
}
請參閱「Speaking JavaScript」中的「屬性屬性和屬性描述符」章節,以取得更多關於屬性描述符(如 Object.getOwnPropertyDescriptor()
和 Object.defineProperty()
所使用)的資訊。
Object.assign()
不適用於移動方法 一方面,您無法移動使用 super
的方法:此類方法具有內部插槽 [[HomeObject]]
,將其繫結到建立它的物件。如果您透過 Object.assign()
移動它,它將繼續參照原始物件的 super 屬性。詳細說明請參閱 類別章節中的部分。
另一方面,如果您將物件文字建立的方法移至類別的原型,則可列舉性會錯誤。前者方法皆可列舉(否則 Object.assign()
無法看到它們),但原型通常只有不可列舉的方法。
Object.assign()
的使用案例 讓我們來看幾個使用案例。
this
您可以在建構函式中使用 Object.assign()
將屬性新增至 this
class
Point
{
constructor
(
x
,
y
)
{
Object
.
assign
(
this
,
{
x
,
y
});
}
}
Object.assign()
也可用於填入遺失屬性的預設值。在以下範例中,我們有一個具有屬性預設值的物件 DEFAULTS
和一個具有資料的物件 options
。
const
DEFAULTS
=
{
logLevel
:
0
,
outputFormat
:
'html'
};
function
processContent
(
options
)
{
options
=
Object
.
assign
({},
DEFAULTS
,
options
);
// (A)
···
}
在 A 行,我們建立一個新的物件,將預設值複製到其中,然後將 options
複製到其中,覆寫預設值。Object.assign()
傳回這些作業的結果,我們將其指定給 options
。
另一個使用案例是將方法新增至物件
Object
.
assign
(
SomeClass
.
prototype
,
{
someMethod
(
arg1
,
arg2
)
{
···
},
anotherMethod
()
{
···
}
});
您也可以手動指定函式,但這樣您就沒有好的方法定義語法,而且每次都需要提到 SomeClass.prototype
SomeClass
.
prototype
.
someMethod
=
function
(
arg1
,
arg2
)
{
···
};
SomeClass
.
prototype
.
anotherMethod
=
function
()
{
···
};
Object.assign()
的最後一個使用案例是快速複製物件
function
clone
(
orig
)
{
return
Object
.
assign
({},
orig
);
}
這種複製方式也有點髒,因為它不會保留 orig
的屬性屬性。如果您需要這樣,您必須使用屬性描述符,就像我們實作 copyAllOwnProperties()
時所做的那樣。
如果您希望複製具有與原始物件相同的原型,您可以使用 Object.getPrototypeOf()
和 Object.create()
function
clone
(
orig
)
{
const
origProto
=
Object
.
getPrototypeOf
(
orig
);
return
Object
.
assign
(
Object
.
create
(
origProto
),
orig
);
}
Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols(obj)
擷取 obj
的所有自有(非繼承)符號值屬性金鑰。它補充了 Object.getOwnPropertyNames()
,後者擷取所有字串值自有屬性金鑰。請參閱後面的章節,以取得關於如何遍歷屬性的更多詳細資訊。
嚴格相等運算子 (===
) 以與預期不同的方式處理兩個值。
首先,NaN
不等於它自己。
> NaN === NaN
false
這很不幸,因為它常常會阻止我們偵測 NaN
。
> [0,NaN,2].indexOf(NaN)
-1
其次,JavaScript 有兩個零,但嚴格相等會將它們視為相同的值。
> -0 === +0
true
這麼做通常是件好事。
Object.is()
提供了一種比較值的方式,其比 ===
更精確。它的運作方式如下:
> Object.is(NaN, NaN)
true
> Object.is(-0, +0)
false
其他所有內容都與 ===
相同。
Object.is()
尋找陣列元素 在以下函式 myIndexOf()
中,我們將 Object.is()
與新的 ES6 陣列方法findIndex()
結合使用,以在陣列中尋找 NaN
。
function
myIndexOf
(
arr
,
elem
)
{
return
arr
.
findIndex
(
x
=>
Object
.
is
(
x
,
elem
));
}
const
myArray
=
[
0
,
NaN
,
2
];
myIndexOf
(
myArray
,
NaN
);
// 1
myArray
.
indexOf
(
NaN
);
// -1
如您在最後一行中所見,indexOf()
找不到 NaN
。
Object.setPrototypeOf(obj, proto)
此方法將 obj
的原型設定為 proto
。ECMAScript 5 中的非標準做法(許多引擎支援)是透過指派給特殊屬性 __proto__
來進行。設定原型的建議方式與 ECMAScript 5 中相同:在建立物件時,透過 Object.create()
。這總是會比先建立一個物件,然後設定其原型更快。顯然地,如果您想變更現有物件的原型,這就不管用了。
在 ECMAScript 6 中,屬性的金鑰可以是字串或符號。以下是遍歷物件 obj
的屬性金鑰的五個運算:
Object.keys(obj) : Array<字串>
Object.getOwnPropertyNames(obj) : Array<字串>
Object.getOwnPropertySymbols(obj) : Array<符號>
Reflect.ownKeys(obj) : Array<字串|符號>
for (const key in obj)
ES6 定義了屬性的兩個遍歷順序。
自己的屬性金鑰
Object.assign()
、Object.defineProperties()
、Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Reflect.ownKeys()
可列舉的自己的名稱
for-in
遍歷屬性的順序相同。JSON.parse()
、JSON.stringify()
、Object.keys()
for-in
遍歷屬性的順序未定義。 引用 Allen Wirfs-Brock
從歷史上看,
for-in
順序未定義,瀏覽器實作在它們產生的順序(和其他具體事項)中有所不同。ES5 新增了Object.keys
和它應該與for-in
以相同順序排序金鑰的要求。在 ES5 和 ES6 的開發過程中,考慮定義一個特定的for-in
順序,但由於網路舊版相容性問題和不確定瀏覽器是否願意更改它們目前產生的排序,因此未採用。
即使您透過整數索引存取陣列元素,但規格會將它們視為一般的字串屬性金鑰
const
arr
=
[
'a'
,
'b'
,
'c'
];
console
.
log
(
arr
[
'0'
]);
// 'a'
// Operand 0 of [] is coerced to string:
console
.
log
(
arr
[
0
]);
// 'a'
整數索引只有兩種特殊情況:它們會影響陣列的length
,而且在列出屬性金鑰時,它們會優先列出。
粗略地說,整數索引是一個字串,如果轉換成 53 位元的非負整數再轉回來,會得到相同的數值。因此
'10'
和 '2'
是整數索引。'02'
不是整數索引。將它轉換成整數再轉回來,會得到不同的字串 '2'
。'3.141'
不是整數索引,因為 3.141 不是整數。進一步閱讀
下列程式碼示範「自有屬性金鑰」的遍歷順序
const
obj
=
{
[
Symbol
(
'first'
)]
:
true
,
'02'
:
true
,
'10'
:
true
,
'01'
:
true
,
'2'
:
true
,
[
Symbol
(
'second'
)]
:
true
,
};
Reflect
.
ownKeys
(
obj
);
// [ '2', '10', '02', '01',
// Symbol('first'), Symbol('second') ]
說明
'2'
和 '10'
是整數索引,會優先列出,並以數值順序排序(不是字典順序)。'02'
和 '01'
是正常的字串金鑰,會接著列出,並以加入 obj
的順序顯示。Symbol('first')
和 Symbol('second')
是符號,會最後列出,並以加入 obj
的順序顯示。Tab Atkins Jr. 的回答
因為至少對於物件來說,所有實作都使用大致相同的順序(符合目前的規格),而且許多程式碼無意間寫成依賴於該順序,如果以不同的順序列舉,就會中斷。由於瀏覽器必須實作這個特定順序才能與網路相容,因此將其指定為需求。
曾討論過在 Maps/Sets 中打破這個順序,但這麼做需要我們指定一個程式碼不可能依賴的順序;換句話說,我們必須強制順序是隨機的,而不能只是未指定。這被認為太費力,而且建立順序相當有價值(例如,請參閱 Python 中的 OrderedDict),因此決定讓 Maps 和 Sets 與 Objects 相符。
規格中下列部分與本節相關
[[OwnPropertyKeys]]
由 Reflect.ownKeys()
及其他方法使用。EnumerableOwnNames
由 Object.keys()
及其他方法使用。[[Enumerate]]
由 for-in
使用。有兩種類似的方式可以將屬性 prop
加入物件 obj
obj.prop = 123
Object.defineProperty(obj, 'prop', { value: 123 })
有三個情況指派不會建立自有屬性 prop
– 即使它尚未存在
prop
。然後,指派會在嚴格模式中導致 TypeError
。prop
的設定器。然後,會呼叫該設定器。prop
的取得器。然後,會在嚴格模式中擲出 TypeError
。此情況類似於第一個情況。這些情況都不會阻止 Object.defineProperty()
建立自有屬性。下一個章節會更詳細地探討情況 #3。
如果物件 obj
繼承了唯讀屬性 prop
,則無法指派給該屬性
const
proto
=
Object
.
defineProperty
({},
'prop'
,
{
writable
:
false
,
configurable
:
true
,
value
:
123
,
});
const
obj
=
Object
.
create
(
proto
);
obj
.
prop
=
456
;
// TypeError: Cannot assign to read-only property
這類似於繼承屬性的運作方式,該屬性具有取得器,但沒有設定器。這符合將指派視為變更繼承屬性的值。它以非破壞性的方式進行:原始值不會被修改,而是被新建立的自有屬性覆寫。因此,繼承的唯讀屬性和繼承的沒有設定器的屬性都會阻止透過指派進行變更。不過,您可以透過定義屬性來強制建立自有屬性
const
proto
=
Object
.
defineProperty
({},
'prop'
,
{
writable
:
false
,
configurable
:
true
,
value
:
123
,
});
const
obj
=
Object
.
create
(
proto
);
Object
.
defineProperty
(
obj
,
'prop'
,
{
value
:
456
});
console
.
log
(
obj
.
prop
);
// 456
__proto__
屬性 __proto__
(發音為「dunder proto」)已存在於大多數 JavaScript 引擎中一段時間。本節說明它在 ECMAScript 6 之前是如何運作的,以及 ECMAScript 6 有哪些變更。
對於本節,如果您知道原型鏈是什麼,將會有幫助。如有必要,請參閱「Speaking JavaScript」中的章節「第 2 層:物件之間的原型關係」。
__proto__
JavaScript 中的每個物件都會開始一個包含一個或多個物件的鏈,稱為原型鏈。每個物件都透過內部插槽 [[Prototype]]
(如果沒有繼承者,則為 null
)指向其繼承者,即其原型。該插槽稱為內部,因為它只存在於語言規範中,且無法直接從 JavaScript 存取。在 ECMAScript 5 中,取得物件 obj
的原型 p
的標準方法是
var
p
=
Object
.
getPrototypeOf
(
obj
);
沒有標準方法可以變更現有物件的原型,但您可以建立一個具有給定原型 p
的新物件 obj
var
obj
=
Object
.
create
(
p
);
__proto__
很久以前,Firefox 取得了非標準屬性 __proto__
。由於其普及性,其他瀏覽器最終也複製了該功能。
在 ECMAScript 6 之前,__proto__
以模糊的方式運作
var
obj
=
{};
var
p
=
{};
console
.
log
(
obj
.
__proto__
===
p
);
// false
obj
.
__proto__
=
p
;
console
.
log
(
obj
.
__proto__
===
p
);
// true
> var obj = {};
> '__proto__' in obj
false
__proto__
建立 Array
的子類別 __proto__
之所以變得普及的主要原因,是因為它啟用了在 ES5 中建立 Array
的子類別 MyArray
的唯一方法:Array 實例是無法透過一般建構函式建立的特殊物件。因此,使用了以下技巧
function
MyArray
()
{
var
instance
=
new
Array
();
// exotic object
instance
.
__proto__
=
MyArray
.
prototype
;
return
instance
;
}
MyArray
.
prototype
=
Object
.
create
(
Array
.
prototype
);
MyArray
.
prototype
.
customMethod
=
function
(
···
)
{
···
};
ES6 中的子類別 的運作方式與 ES5 中不同,並支援開箱即用的內建子類別。
__proto__
在 ES5 中有問題 主要問題是 __proto__
混用兩個層級:物件層級(一般屬性,持有資料)和元層級。
如果你不小心將 __proto__
當成一般屬性(物件層級!)來使用,用來儲存資料,你會遇到麻煩,因為這兩個層級會衝突。情況會更複雜,因為在 ES5 中你必須將物件當成映射來使用,因為它沒有內建的資料結構可用於此目的。映射應該可以持有任意鍵,但你無法在物件當映射使用時使用鍵 '__proto__'
。
理論上,你可以使用符號取代特殊名稱 __proto__
來解決問題,但將元機制完全分開(如透過 Object.getPrototypeOf()
所做的那樣)是最好的方法。
__proto__
由於 __proto__
獲得廣泛支援,因此決定將其行為標準化為 ECMAScript 6。然而,由於其有問題的性質,它被新增為已棄用的功能。這些功能位於 ECMAScript 規格的附錄 B 中,其說明如下
當 ECMAScript 主機是網路瀏覽器時,需要此附錄中定義的 ECMAScript 語言語法和語意。如果 ECMAScript 主機不是網路瀏覽器,此附錄的內容為規範性但為選用。
JavaScript 有許多不受歡迎的功能,但網路上的大量程式碼需要這些功能。因此,網路瀏覽器必須實作這些功能,但其他 JavaScript 引擎則不必。
為了說明 __proto__
背後的魔法,ES6 中引入了兩個機制
Object.prototype.__proto__
實作的 getter 和 setter。'__proto__'
視為用於指定所建立物件原型的一個特殊運算子。Object.prototype.__proto__
ECMAScript 6 允許透過儲存在 Object.prototype
中的 getter 和 setter 來取得和設定屬性 __proto__
。如果你要手動實作它們,大致上會像這樣
Object
.
defineProperty
(
Object
.
prototype
,
'__proto__'
,
{
get
()
{
const
_thisObj
=
Object
(
this
);
return
Object
.
getPrototypeOf
(
_thisObj
);
},
set
(
proto
)
{
if
(
this
===
undefined
||
this
===
null
)
{
throw
new
TypeError
();
}
if
(
!
isObject
(
this
))
{
return
undefined
;
}
if
(
!
isObject
(
proto
))
{
return
undefined
;
}
const
status
=
Reflect
.
setPrototypeOf
(
this
,
proto
);
if
(
!
status
)
{
throw
new
TypeError
();
}
return
undefined
;
},
});
function
isObject
(
value
)
{
return
Object
(
value
)
===
value
;
}
__proto__
如果 __proto__
出現在物件文字中未加引號或加引號的屬性金鑰中,則由該文字建立的物件的原型會設定為屬性值
> Object.getPrototypeOf({ __proto__: null })
null
> Object.getPrototypeOf({ '__proto__': null })
null
使用字串值 '__proto__'
作為計算屬性金鑰不會變更原型,而是會建立一個自有屬性
> const obj = { ['__proto__']: null };
> Object.getPrototypeOf(obj) === Object.prototype
true
> Object.keys(obj)
[ '__proto__' ]
__proto__
的魔法 __proto__
在 ECMAScript 6 中,如果您定義自有屬性 __proto__
,則不會觸發任何特殊功能,而 getter/setter Object.prototype.__proto__
會被覆寫
const
obj
=
{};
Object
.
defineProperty
(
obj
,
'__proto__'
,
{
value
:
123
})
Object
.
keys
(
obj
);
// [ '__proto__' ]
console
.
log
(
obj
.
__proto__
);
// 123
Object.prototype
的物件 __proto__
getter/setter 是透過 Object.prototype
提供的。因此,原型鏈中沒有 Object.prototype
的物件也沒有 getter/setter。在以下程式碼中,dict
是此類物件的範例,它沒有原型。因此,__proto__
現在就像任何其他屬性一樣運作
>
const
dict
=
Object
.
create
(
null
);
>
'__proto__'
in
dict
false
>
dict
.
__proto__
=
'abc'
;
>
dict
.
__proto__
'abc'
__proto__
和 dict 物件 如果您想使用物件作為字典,最好讓它沒有原型。這就是為什麼沒有原型的物件也稱為dict 物件。在 ES6 中,您甚至不必跳脫 dict 物件的屬性金鑰 '__proto__'
,因為它不會觸發任何特殊功能。
__proto__
作為物件文字中的運算子,讓您可以更簡潔地建立 dict 物件
const
dictObj
=
{
__proto__
:
null
,
yes
:
true
,
no
:
false
,
};
請注意,在 ES6 中,您通常應該偏好內建資料結構 Map
而非 dict 物件,特別是在金鑰沒有固定的情況下。
__proto__
和 JSON 在 ES6 之前,JavaScript 引擎可能會發生以下情況
> JSON.parse('{"__proto__": []}') instanceof Array
true
由於 __proto__
在 ES6 中是 getter/setter,因此 JSON.parse()
運作良好,因為它定義屬性,而不是指定屬性 (如果實作正確,舊版的 V8 會指定)。
JSON.stringify()
也不會受到 __proto__
的影響,因為它只考慮自有屬性。名稱為 __proto__
的自有屬性的物件運作良好
> JSON.stringify({['__proto__']: true})
'{"__proto__":true}'
__proto__
的支援 對 ES6 風格 __proto__
的支援因引擎而異。請參閱 kangax 的 ECMAScript 6 相容性表格,以取得現狀資訊
以下兩個區段說明如何以程式方式偵測引擎是否支援這兩種 __proto__
。
__proto__
作為 getter/setter getter/setter 的簡單檢查
var
supported
=
{}.
hasOwnProperty
.
call
(
Object
.
prototype
,
'__proto__'
);
更精密的檢查
var
desc
=
Object
.
getOwnPropertyDescriptor
(
Object
.
prototype
,
'__proto__'
);
var
supported
=
(
typeof
desc
.
get
===
'function'
&&
typeof
desc
.
set
===
'function'
);
__proto__
作為物件文字中的運算子 您可以使用以下檢查
var
supported
=
Object
.
getPrototypeOf
({
__proto__
:
null
})
===
null
;
__proto__
發音為「dunder proto」 在 Python 中,以雙底線括住名稱是常見做法,以避免元資料(例如 __proto__
)和資料(使用者定義的屬性)之間的名稱衝突。這種做法在 JavaScript 中永遠不會變得普遍,因為它現在有符號來達成這個目的。然而,我們可以參考 Python 社群,了解如何發音雙底線。
Ned Batchelder 建議 以下發音
在 Python 中編程的尷尬之處:有很多雙底線。例如,語法糖底下的標準方法名稱有
__getattr__
、建構函式是__init__
、內建運算子可以使用__add__
來覆寫,等等。[…]我對雙底線的問題是,它很難發音。您如何發音
__init__
?「底線底線 init 底線底線」?「底底 init 底底」?只說「init」似乎遺漏了某些重要的東西。我有一個解決方案:雙底線應發音為「dunder」。因此,
__init__
是「dunder init dunder」,或僅為「dunder init」。
因此,__proto__
發音為「dunder proto」。這種發音方式被採用的機率很高,JavaScript 創造者 Brendan Eich 就使用這種發音。
__proto__
的建議 ES6 將 __proto__
從模糊的東西轉變成容易理解的東西,這一點很好。
然而,我仍然建議不要使用它。它實際上是一個已棄用的功能,且不屬於核心標準的一部分。您不能依賴它存在於必須在所有引擎上執行的程式碼中。
更多建議
Object.getPrototypeOf()
取得物件的原型。Object.create()
來建立具有給定原型的新物件。避免使用 Object.setPrototypeOf()
,它會在許多引擎上降低效能。__proto__
作為物件文字中的運算子。它對於示範原型繼承和建立字典物件很有用。但是,先前提到的警告仍然適用。可列舉性是物件屬性的屬性。本節說明它在 ECMAScript 6 中如何運作。我們先來探討什麼是屬性。
每個物件都有零個或更多個屬性。每個屬性都有金鑰和三個或更多個屬性,稱為儲存屬性資料的槽(換句話說,屬性本身很像 JavaScript 物件或像資料庫中的具有欄位的記錄)。
ECMAScript 6 支援下列屬性(ES5 也是如此)
enumerable
:將此屬性設定為 false
會將屬性隱藏在某些運算中。configurable
:將此屬性設定為 false
會防止對屬性進行多項變更(除了 value
之外的屬性無法變更、屬性無法刪除等)。value
:儲存屬性的值。writable
:控制是否可以變更屬性的值。get
:儲存取得器(函式)。set
:儲存設定器(函式)。您可以透過 Object.getOwnPropertyDescriptor()
來擷取屬性的屬性,它會傳回屬性作為 JavaScript 物件
>
const
obj
=
{
foo
:
123
}
;
>
Object
.
getOwnPropertyDescriptor
(
obj
,
'foo'
)
{
value
:
123
,
writable
:
true
,
enumerable
:
true
,
configurable
:
true
}
本節說明屬性 enumerable
在 ES6 中如何運作。所有其他屬性以及如何變更屬性說明在「Speaking JavaScript」中的「屬性屬性和屬性描述符」一節中。
ECMAScript 5
for-in
迴圈:遍歷自有和繼承的可列舉屬性的字串金鑰。Object.keys()
:傳回可列舉的自有屬性的字串金鑰。JSON.stringify()
:僅將具有字串金鑰的可列舉自有屬性字串化。ECMAScript 6
Object.assign()
:只複製可列舉的自身屬性(字串鍵和符號鍵皆視為可列舉)。for-in
是唯一內建運算,其中可列舉性對繼承屬性至關重要。所有其他運算只適用於自身屬性。
遺憾的是,可列舉性是一個相當獨特的特性。本節說明其數個使用案例,並主張除了避免舊有程式碼中斷外,其用途有限。
for-in
迴圈隱藏屬性 for-in
迴圈會遍歷物件的所有可列舉屬性,包括自身和繼承的屬性。因此,enumerable
屬性用於隱藏不應遍歷的屬性。這就是 ECMAScript 1 中引入可列舉性的原因。
不可列舉屬性出現在語言中的下列位置
prototype
屬性都是不可列舉的
> const desc = Object.getOwnPropertyDescriptor.bind(Object);
> desc(Object.prototype, 'toString').enumerable
false
prototype
屬性都是不可列舉的
> desc(class {foo() {}}.prototype, 'foo').enumerable
false
length
不可列舉,這表示 for-in
只會遍歷索引。(不過,如果您透過指派新增屬性,這可以輕易變更,這會讓它可列舉。)
> desc([], 'length').enumerable
false
> desc(['a'], '0').enumerable
true
讓所有這些屬性不可列舉的主要原因是將它們(尤其是繼承的屬性)隱藏起來,避免使用 for-in
迴圈或 $.extend()
(以及同時複製繼承和自身屬性的類似運算;請參閱下一節)的舊有程式碼。ES6 中應避免這兩種運算。隱藏它們可確保舊有程式碼不會中斷。
在複製屬性時,有兩個重要的歷史先例會考量可列舉性
Object.extend(destination, source)
const
obj1
=
Object
.
create
({
foo
:
123
});
Object
.
extend
({},
obj1
);
// { foo: 123 }
const
obj2
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
123
,
enumerable
:
false
});
Object
.
extend
({},
obj2
)
// {}
$.extend(target, source1, source2, ···)
會將 source1
等的自身和繼承的可列舉屬性複製到 target
的自身屬性中。
const
obj1
=
Object
.
create
({
foo
:
123
});
$
.
extend
({},
obj1
);
// { foo: 123 }
const
obj2
=
Object
.
defineProperty
({},
'foo'
,
{
value
:
123
,
enumerable
:
false
});
$
.
extend
({},
obj2
)
// {}
使用這種方式複製屬性的問題
標準庫中唯一不可列舉的實例屬性是陣列的屬性 length
。但是,由於該屬性會透過其他屬性神奇地更新自身,因此只需要隱藏該屬性。您無法為自己的物件建立這種神奇的屬性(除非使用 Proxy)。
Object.assign()
在 ES6 中,Object.assign(target, source_1, source_2, ···)
可用於將來源合併到目標中。會考慮來源的所有可列舉自有屬性(也就是說,鍵可以是字串或符號)。Object.assign()
使用「取得」操作從來源讀取值,並使用「設定」操作將值寫入目標。
關於列舉性,Object.assign()
延續了 Object.extend()
和 $.extend()
的傳統。引用 Yehuda Katz
Object.assign 會為所有已在流通的 extend() API 鋪平道路。我們認為,在這些情況下不複製可列舉方法的先例,足以讓 Object.assign 具有這種行為。
換句話說:Object.assign()
是在考慮從 $.extend()
(和類似函式)升級的路徑下建立的。它的方法比 $.extend
更簡潔,因為它忽略了繼承的屬性。
如果您將屬性設為不可列舉,則 Object.keys()
和 for-in
迴圈將無法再看到它。關於這些機制,該屬性是私有的。
但是,這種方法有幾個問題
JSON.stringify()
中隱藏自有屬性 JSON.stringify()
輸出時不包含不可列舉的屬性。因此,你可以使用可列舉性來決定哪些自有屬性應該匯出到 JSON。這個用例類似於將屬性標記為私有的前一個用例。但它也有所不同,因為這更多關於匯出,而且適用略有不同的考量。例如:物件可以從 JSON 完全重建嗎?
指定物件應如何轉換為 JSON 的另一種方法是使用 toJSON()
const
obj
=
{
foo
:
123
,
toJSON
()
{
return
{
bar
:
456
};
},
};
JSON
.
stringify
(
obj
);
// '{"bar":456}'
我發現 toJSON()
比可列舉性更簡潔,適用於目前的用例。它也給你更多控制權,因為你可以匯出物件上不存在的屬性。
一般來說,較短的名稱表示只考慮可列舉的屬性
Object.keys()
忽略不可列舉的屬性Object.getOwnPropertyNames()
列出所有屬性名稱然而,Reflect.ownKeys()
偏離了該規則,它忽略可列舉性並傳回所有屬性的鍵。此外,從 ES6 開始,做出了以下區別
因此,Object.keys()
現在更好的名稱應該是 Object.names()
。
在我看來,可列舉性只適合於隱藏 for-in
迴圈和 $.extend()
(以及類似操作)的屬性。這兩個都是舊功能,你應該在新的程式碼中避免使用它們。至於其他用例
toJSON()
方法比可列舉性更強大且明確。我不確定可列舉性未來的最佳策略是什麼。如果在 ES6 中,我們開始假裝它不存在(除了讓原型屬性不可列舉,這樣舊程式碼才不會中斷),我們最終可能已經可以棄用可列舉性。然而,Object.assign()
考慮可列舉性與該策略相反(但它這樣做是有正當理由的,向後相容性)。
在我自己的 ES6 程式碼中,我沒有使用可列舉性,除了(隱含地)對於其 prototype
方法不可列舉的類別。
最後,在使用互動式命令列時,我偶爾會錯過一個操作,該操作傳回物件的所有屬性鍵,而不仅仅是自有的(Reflect.ownKeys
)。這樣的操作將提供物件內容的良好概觀。
本節說明如何使用下列眾所周知的符號作為屬性金鑰自訂基本語言操作
Symbol.hasInstance
(方法)C
自訂 x instanceof C
的行為。Symbol.toPrimitive
(方法)Symbol.toStringTag
(字串)Object.prototype.toString()
呼叫以計算物件 obj
的預設字串描述。
'[object '
+
obj
[
Symbol
.
toStringTag
]
+
']'
Symbol.unscopables
(物件)with
陳述式中隱藏一些屬性。Symbol.hasInstance
(方法) 物件 C
可透過金鑰為 Symbol.hasInstance
的方法自訂 instanceof
運算子的行為,該方法具有下列簽章
[
Symbol
.
hasInstance
](
potentialInstance
:
any
)
x instanceof C
在 ES6 中的工作方式如下
C
不是物件,則擲回 TypeError
。C[Symbol.hasInstance](x)
,將結果強制轉換為布林值並傳回。C
必須可呼叫,C.prototype
在 x
的原型鏈中,等等)。標準函式庫中唯一具有此金鑰的方法為
Function.prototype[Symbol.hasInstance]()
這是所有函式(包括類別)預設使用的 instanceof
實作。 引用規格
此屬性為不可寫入且不可設定,以防止竄改,而這可能會用於在全域公開繫結函式的目標函式。
之所以會發生竄改,是因為傳統的 instanceof
演算法 OrdinaryHasInstance()
會在遇到繫結函式時將 instanceof
套用至目標函式。
由於此屬性為唯讀,因此您無法使用賦值來覆寫它,如前所述。
舉例來說,我們實作一個物件 ReferenceType
,其「實例」都是物件,不只是 Object
的實例(因此在原型鏈中具有 Object.prototype
)。
const
ReferenceType
=
{
[
Symbol
.
hasInstance
](
value
)
{
return
(
value
!==
null
&&
(
typeof
value
===
'object'
||
typeof
value
===
'function'
));
}
};
const
obj1
=
{};
console
.
log
(
obj1
instanceof
Object
);
// true
console
.
log
(
obj1
instanceof
ReferenceType
);
// true
const
obj2
=
Object
.
create
(
null
);
console
.
log
(
obj2
instanceof
Object
);
// false
console
.
log
(
obj2
instanceof
ReferenceType
);
// true
Symbol.toPrimitive
(方法) Symbol.toPrimitive
讓物件自訂如何將其強制轉換(自動轉換)為基本值。
許多 JavaScript 作業會將值強制轉換為它們需要的類型。
*
) 將其運算元強制轉換為數字。new Date(year, month, date)
將其參數強制轉換為數字。parseInt(string , radix)
將其第一個參數強制轉換為字串。以下是值最常強制轉換的類型
true
給真值,false
給假值。物件永遠為真值(即使是 new Boolean(false)
)。null
→ 0
、true
→ 1
、'123'
→ 123
等)。null
→ 'null'
、true
→ 'true'
、123
→ '123'
等)。b
透過 new Boolean(b)
、數字 n
透過 new Number(n)
等)。因此,對於數字和字串,第一步是確保值是任何一種基本值。這是由規格內部作業 ToPrimitive()
處理的,它有三個模式
預設模式僅由下列使用
==
)+
)new Date(value)
(只有一個參數!)如果值是基本值,則 ToPrimitive()
已完成。否則,該值是物件 obj
,它會轉換為基本值,如下所示
obj.valueOf()
是基本值,則傳回其結果。否則,如果 obj.toString()
是基本值,則傳回其結果。否則,擲回 TypeError
。toString()
,再呼叫 valueOf()
。可以透過提供物件一個具有下列簽章的方法,來覆寫這個正常演算法
[
Symbol
.
toPrimitive
](
hint
:
'default'
|
'string'
|
'number'
)
在標準函式庫中,有兩個這樣的函式
Symbol.prototype[Symbol.toPrimitive](hint)
可防止呼叫 toString()
(會擲回例外)。Date.prototype[Symbol.toPrimitive](hint)
這個方法實作的行為與預設演算法不同。引用規格:「日期物件在內建 ECMAScript 物件中獨樹一格,因為它們將 'default'
視為等同於 'string'
。所有其他內建 ECMAScript 物件將 'default'
視為等同於 'number'
。」下列程式碼示範強制轉換如何影響物件 obj
。
const
obj
=
{
[
Symbol
.
toPrimitive
](
hint
)
{
switch
(
hint
)
{
case
'number'
:
return
123
;
case
'string'
:
return
'str'
;
case
'default'
:
return
'default'
;
default
:
throw
new
Error
();
}
}
};
console
.
log
(
2
*
obj
);
// 246
console
.
log
(
3
+
obj
);
// '3default'
console
.
log
(
obj
==
'default'
);
// true
console
.
log
(
String
(
obj
));
// 'str'
Symbol.toStringTag
(字串) 在 ES5 及更早版本中,每個物件都有內部自有屬性 [[Class]]
,其值暗示其類型。您無法直接存取它,但其值是 Object.prototype.toString()
傳回字串的一部分,這就是為什麼該方法用於類型檢查,作為 typeof
的替代方案。
在 ES6 中,不再有內部槽 [[Class]]
,而且不建議使用 Object.prototype.toString()
進行類型檢查。為了確保該方法的向後相容性,引入了金鑰為 Symbol.toStringTag
的公開屬性。您可以說它取代了 [[Class]]
。
Object.prototype.toString()
現在的運作方式如下
this
轉換為物件 obj
。obj
的 toString 標籤 tst
。'[object ' + tst + ']'
。下列表格顯示各種物件的預設值。
值 | toString 標籤 |
---|---|
未定義 |
'Undefined' |
null |
'Null' |
陣列物件 | 'Array' |
字串物件 | 'String' |
參數 |
'參數' |
可呼叫的物件 | '函式' |
錯誤物件 | '錯誤' |
布林物件 | '布林' |
數字物件 | '數字' |
日期物件 | '日期' |
正規表示式物件 | '正規表示式' |
(否則) | '物件' |
左欄位中的大部分檢查都是透過查看內部插槽來執行。例如,如果物件有內部插槽 [[Call]]
,則它可呼叫。
下列互動示範預設的 toString 標籤。
>
Object
.
prototype
.
toString
.
call
(
null
)
'
[
object
Null
]
'
>
Object
.
prototype
.
toString
.
call
([])
'
[
object
Array
]
'
>
Object
.
prototype
.
toString
.
call
({})
'
[
object
Object
]
'
>
Object
.
prototype
.
toString
.
call
(
Object
.
create
(
null
))
'
[
object
Object
]
'
如果物件有 (自己的或繼承的) 屬性,其金鑰為 Symbol.toStringTag
,則其值會覆寫預設的 toString 標籤。例如
>
({}.
toString
())
'
[
object
Object
]
'
>
({[
Symbol
.
toStringTag
]
:
'
Foo
'
}.
toString
())
'
[
object
Foo
]
'
使用者定義類別的執行個體取得預設的 toString 標籤 (物件)
class
Foo
{
}
console
.
log
(
new
Foo
().
toString
());
// [object Object]
覆寫預設值的一個選項是透過 getter
class
Bar
{
get
[
Symbol
.
toStringTag
]()
{
return
'Bar'
;
}
}
console
.
log
(
new
Bar
().
toString
());
// [object Bar]
在 JavaScript 標準函式庫中,有下列自訂的 toString 標籤。沒有全域名稱的物件會用百分比符號引起來 (例如:%TypedArray%
)。
JSON[Symbol.toStringTag]
→ 'JSON'
Math[Symbol.toStringTag]
→ 'Math'
M
:M[Symbol.toStringTag]
→ 'Module'
ArrayBuffer.prototype[Symbol.toStringTag]
→ 'ArrayBuffer'
DataView.prototype[Symbol.toStringTag]
→ 'DataView'
Map.prototype[Symbol.toStringTag]
→ 'Map'
Promise.prototype[Symbol.toStringTag]
→ 'Promise'
Set.prototype[Symbol.toStringTag]
→ 'Set'
get %TypedArray%.prototype[Symbol.toStringTag]
→ 'Uint8Array'
等。WeakMap.prototype[Symbol.toStringTag]
→ 'WeakMap'
WeakSet.prototype[Symbol.toStringTag]
→ 'WeakSet'
%MapIteratorPrototype%[Symbol.toStringTag]
→ 'Map Iterator'
%SetIteratorPrototype%[Symbol.toStringTag]
→ 'Set Iterator'
%StringIteratorPrototype%[Symbol.toStringTag]
→ 'String Iterator'
Symbol.prototype[Symbol.toStringTag]
→ 'Symbol'
Generator.prototype[Symbol.toStringTag]
→ 'Generator'
GeneratorFunction.prototype[Symbol.toStringTag]
→ 'GeneratorFunction'
所有金鑰為 Symbol.toStringTag
的內建屬性都有下列屬性描述
{
writable
:
false
,
enumerable
:
false
,
configurable
:
true
,
}
如前所述,您無法使用指定來覆寫這些屬性,因為它們是唯讀的。
Symbol.unscopables
(物件) Symbol.unscopables
讓物件可以隱藏某些屬性,使其不會出現在 with
陳述式中。
這樣做的原因是,它允許 TC39 在不中斷舊程式碼的情況下,將新方法新增到 Array.prototype
。請注意,目前的程式碼很少使用 with
,而嚴格模式和 ES6 模組(它們隱含地處於嚴格模式)中禁止使用 with
。
為什麼新增方法到 Array.prototype
會中斷使用 with
的程式碼(例如廣泛部署的 Ext JS 4.2.1)?請看以下程式碼。如果使用陣列呼叫 foo()
,則 Array.prototype.values
屬性的存在會中斷 foo()
。
function
foo
(
values
)
{
with
(
values
)
{
console
.
log
(
values
.
length
);
// abc (*)
}
}
Array
.
prototype
.
values
=
{
length
:
'abc'
};
foo
([]);
在 with
陳述式內,values
的所有屬性都會變成局部變數,甚至會隱藏 values
本身。因此,如果 values
有一個 values
屬性,則第 * 行的陳述式會記錄 values.values.length
,而不是 values.length
。
Symbol.unscopables
在標準函式庫中只使用一次
Array.prototype[Symbol.unscopables]
with
陳述式中):copyWithin
、entries
、fill
、find
、findIndex
、keys
、values
super
嗎? 可以!詳細說明請參閱 類別章節。