【拓展】未來(lái)的JavaScript記錄與元組
編者按:本文譯者李松峰,資深技術(shù)圖書譯者,翻譯出版過(guò)40余部技術(shù)及交互設(shè)計(jì)專著,現(xiàn)任360奇舞團(tuán)Web前端開發(fā)資深專家,360前端技術(shù)委員會(huì)委員、W3C AC代表。
Dr. Axel Rauschmayer最近撰文介紹了還處于Stage1階段的兩個(gè)JavaScript新特性:記錄和元組。
記錄和元組是一個(gè)新提案(Record & Tuple,https://github.com/tc39/proposal-record-tuple),建議為JavaScript增加兩個(gè)復(fù)合原始類型:
記錄(Record),是不可修改的按值比較的對(duì)象
元組(Tuple),是不可修改的按值比較的數(shù)組
什么是按值比較
當(dāng)前,JavaScript只有在比較原始值(如字符串)時(shí)才會(huì)按值比較(比較內(nèi)容):
>?'abc'?===?'abc'
true
但在比較對(duì)象時(shí),則是按標(biāo)識(shí)比較(by identity),因此對(duì)象只與自身嚴(yán)格相等:
>?{x:?1,?y:?4}?===?{x:?1,?y:?4}
false
>?['a',?'b']?===?['a',?'b']
false
“記錄和元組”的提案就是為了讓我們可以創(chuàng)建按值比較的復(fù)合類型值。
比如,在對(duì)象字面量前面加一個(gè)井號(hào)(#),就可以創(chuàng)建一個(gè)記錄。而記錄是一個(gè)按值比較的復(fù)合值,且不可修改:
>?#{x:?1,?y:?4}?===?#{x:?1,?y:?4}
true
如果在數(shù)組字面量前面加一個(gè)#,就可以創(chuàng)建一個(gè)元組,也就是可以按值比較且不可修改的數(shù)組:
>?#['a',?'b']?===?#['a',?'b']
true
按值比較的復(fù)合值就叫復(fù)合原始值或者復(fù)合原始類型。
記錄和元組是原始類型
使用typeof可以看出來(lái),記錄和元組都是原始類型:
>?typeof?#{x:?1,?y:?4}
'record'
>?typeof?#['a',?'b']
'tuple'
記錄和元組的內(nèi)容有限制
記錄:
鍵必須是字符串
值必須是原始值(包括記錄和元組)
元組:
元素必須是原始值(包括記錄和元組)
把對(duì)象轉(zhuǎn)換為記錄和元組
>?Record({x:?1,?y:?4})
#{x:?1,?y:?4}
>?Tuple.from(['a',?'b'])
#['a',?'b']
注意:這些都是淺層轉(zhuǎn)換。如果值樹結(jié)構(gòu)中有任何節(jié)點(diǎn)不是原始值,Record()和Tuple.from()會(huì)拋出異常。
使用記錄
const?record?=?#{x:?1,?y:?4};
// 訪問(wèn)屬性
assert.equal(record.y,?4);
// 解構(gòu)
const?{x}?=?record;
assert.equal(x,?1);
// 擴(kuò)展
assert.ok(
??#{...record,?x:?3,?z:?9}?===?#{x:?3,?y:?4,?z:?9});
使用元組
const?tuple?=?#['a',?'b'];
// 訪問(wèn)元素
assert.equal(tuple[1],?'b');
// 解構(gòu)(元組是可迭代對(duì)象)
const?[a]?=?tuple;
assert.equal(a,?'a');
// 擴(kuò)展
assert.ok(
??#[...tuple,?'c']?===?#['a',?'b',?'c']);
// 更新
assert.ok(
??tuple.with(0,?'x')?===?#['x',?'b']);
為什么按值比較的值不可修改
某些數(shù)據(jù)結(jié)構(gòu)(比如散列映射和搜索樹)有槽位,其中鍵的保存位置根據(jù)它們的值來(lái)確定。如果鍵的值改變了,那這個(gè)鍵通常必須放到不同的槽位。這就是為什么在JavaScript中可以用作鍵的值:
要么按值比較且不可修改(原始值)
要么按標(biāo)識(shí)比較且可修改(對(duì)象)
復(fù)合原始值的好處
復(fù)合原始值有如下好處。
深度比較對(duì)象,這是一個(gè)內(nèi)置操作,可以通過(guò)如===來(lái)調(diào)用。
共享值:如果對(duì)象是可修改的,為了安全共享就需要深度復(fù)制它的一個(gè)副本。而對(duì)于不可修改的值,就可以直接共享。
數(shù)據(jù)的非破壞性更新:如果要修改復(fù)合值,由于一切都是不可修改的,所以就要?jiǎng)?chuàng)建一個(gè)可修改的副本,然后就可以放心地重用不必修改的部分。
在Map和Set等數(shù)據(jù)結(jié)構(gòu)中使用:因?yàn)閮蓚€(gè)內(nèi)容相同的復(fù)合原始值在這門語(yǔ)言的任何地方(包括作為Map的鍵和作為Set的元素)都被認(rèn)為嚴(yán)格相等,所以映射和集合成會(huì)變得更有用。
接下來(lái)演示這些好處。
示例:集合與映射變得更有用
通過(guò)集合去重
有了復(fù)合原始值,即使是復(fù)合值(不是原始值那樣的原子值)也可以去重:
>?[...new?Set([#[3,4],?#[3,4],?#[5,-1],?#[5,-1]])]
[#[3,4],?#[5,-1]]
如果是數(shù)組就辦不到了:
>?[...new?Set([[3,4],?[3,4],?[5,-1],?[5,-1]])]
[[3,4],?[3,4],?[5,-1],?[5,-1]]
映射的復(fù)合鍵
因?yàn)閷?duì)象是按標(biāo)識(shí)比較的,所以在(非弱)映射中用對(duì)象作為鍵幾乎沒什么用:
const?m?=?new?Map();
m.set({x:?1,?y:?4},?1);
m.set({x:?1,?y:?4},?2);
assert.equal(m.size,?2)
如果使用復(fù)合原始值就不一樣了:下面行(A)創(chuàng)建的映射會(huì)保存地址(記錄)到人名的映射。
const?persons?=?[
??#{
????name:?'Eddie',
????address:?#{
??????street:?'1313 Mockingbird Lane',
??????city:?'Mockingbird Heights',
????},
??},
??#{
????name:?'Dawn',
????address:?#{
??????street:?'1630 Revello Drive',
??????city:?'Sunnydale',
????},
??},
??#{
????name:?'Herman',
????address:?#{
??????street:?'1313 Mockingbird Lane',
??????city:?'Mockingbird Heights',
????},
??},
??#{
????name:?'Joyce',
????address:?#{
??????street:?'1630 Revello Drive',
??????city:?'Sunnydale',
????},
??},
];
const?addressToNames?=?new?Map();?// (A)
for?(const?person?of?persons)?{
??if?(!addressToNames.has(person.address))?{
????addressToNames.set(person.address,?new?Set());
??}
??addressToNames.get(person.address).add(person.name);
}
assert.deepEqual(
??// Convert the Map to an Array with key-value pairs,
??// so that we can compare it via assert.deepEqual().
??[...addressToNames],
??[
????[
??????#{
????????street:?'1313 Mockingbird Lane',
????????city:?'Mockingbird Heights',
??????},
??????new?Set(['Eddie',?'Herman']),
????],
????[
??????#{
????????street:?'1630 Revello Drive',
????????city:?'Sunnydale',
??????},
??????new?Set(['Dawn',?'Joyce']),
????],
??]);
示例:有效地深度相等
使用復(fù)合屬性值處理對(duì)象
在下面的例子中,我們使用數(shù)組的方法.filter()(行(B))提取了地址等于address(行(A))的所有條目 。
const?persons?=?[
??#{
????name:?'Eddie',
????address:?#{
??????street:?'1313 Mockingbird Lane',
??????city:?'Mockingbird Heights',
????},
??},
??#{
????name:?'Dawn',
????address:?#{
??????street:?'1630 Revello Drive',
??????city:?'Sunnydale',
????},
??},
??#{
????name:?'Herman',
????address:?#{
??????street:?'1313 Mockingbird Lane',
??????city:?'Mockingbird Heights',
????},
??},
??#{
????name:?'Joyce',
????address:?#{
??????street:?'1630 Revello Drive',
??????city:?'Sunnydale',
????},
??},
];
const?address?=?#{?// (A)
??street:?'1630 Revello Drive',
??city:?'Sunnydale',
};
assert.deepEqual(
??persons.filter(p?=>?p.address?===?address),?// (B)
??[
????#{
??????name:?'Dawn',
??????address:?#{
????????street:?'1630 Revello Drive',
????????city:?'Sunnydale',
??????},
????},
????#{
??????name:?'Joyce',
??????address:?#{
????????street:?'1630 Revello Drive',
????????city:?'Sunnydale',
??????},
????},
??]);
對(duì)象變了嗎?
在處理緩存的數(shù)據(jù)(如下面例子中的previousData)時(shí),內(nèi)置深度相等可以讓我們有效地檢查數(shù)據(jù)是否發(fā)生了變化。
let?previousData;
function?displayData(data)?{
??if?(data?===?previousData)?return;
??// ···
}
displayData(#['Hello',?'world']);?// 顯示
displayData(#['Hello',?'world']);?// 不顯示
測(cè)試
多數(shù)測(cè)試框架都支持深度相等,以檢查某個(gè)計(jì)算是否產(chǎn)生了預(yù)期的結(jié)果。例如,Node.js內(nèi)置的assert模塊有一個(gè)函數(shù)叫deepEqual()。有了復(fù)合原始值,就可以直接斷言:
function?invert(color)?{
??return?#{
????red:?255?-?color.red,
????green:?255?-?color.green,
????blue:?255?-?color.blue,
??};
}
assert.ok(
??invert(#{red:?255,?green:?153,?blue:?51})
????===?#{red:?0,?green:?102,?blue:?204});
新語(yǔ)法的優(yōu)缺點(diǎn)
新語(yǔ)法的一個(gè)缺點(diǎn)是字符#已經(jīng)在很多地方被占用了(比如私有字段),另外非數(shù)字字母字符多少顯得有點(diǎn)神秘??梢钥纯聪旅娴睦樱?/p>
const?della?=?#{
??name:?'Della',
??children:?#[
????#{
??????name:?'Huey',
????},
????#{
??????name:?'Dewey',
????},
????#{
??????name:?'Louie',
????},
??],
};
優(yōu)點(diǎn)是這個(gè)語(yǔ)法比較簡(jiǎn)潔。對(duì)于一個(gè)常用的結(jié)構(gòu),當(dāng)然越簡(jiǎn)單越好。此外,一旦熟悉了這個(gè)語(yǔ)法之后,神秘感自然就會(huì)越來(lái)越淡。
除了特殊的字面量語(yǔ)法,還可以使用工廠函數(shù):
const?della?=?Record({
??name:?'Della',
??children:?Tuple([
????Record({
??????name:?'Huey',
????}),
????Record({
??????name:?'Dewey',
????}),
????Record({
??????name:?'Louie',
????}),
??]),
});
如果JavaScript支持Tagged Collection Literals(https://github.com/zkat/proposal-collection-literals,已撤銷),這個(gè)語(yǔ)法還可能有所改進(jìn):
const?della?=?Record!{
??name:?'Della',
??children:?Tuple![
????Record!{
??????name:?'Huey',
????},
????Record!{
??????name:?'Dewey',
????},
????Record!{
??????name:?'Louie',
????},
??],
};
唉,即便使用更短的名字,結(jié)果看起來(lái)還是有點(diǎn)亂:
const?R?=?Record;
const?T?=?Tuple;
const?della?=?R!{
??name:?'Della',
??children:?T![
????R!{
??????name:?'Huey',
????},
????R!{
??????name:?'Dewey',
????},
????R!{
??????name:?'Louie',
????},
??],
};
JSON與記錄和元組
JSON.stringify()把記錄當(dāng)成對(duì)象,把元組當(dāng)成數(shù)組(遞歸)。
JSON.parseImmutable與JSON.parse()類似,但返回記錄而非對(duì)象,返回元組而非數(shù)組(遞歸)。
未來(lái):類的實(shí)例會(huì)按值比較嗎?
相比對(duì)象和數(shù)組,我其實(shí)更喜歡使用類作為一個(gè)數(shù)據(jù)容器。因?yàn)樗梢园衙痔砑拥綄?duì)象上。為此,我希望將來(lái)會(huì)有一種類,它的實(shí)例不可修改且按值比較。
假如我們還可以深度、非破壞性地更新那些包含由值類型的類產(chǎn)生的對(duì)象的數(shù)據(jù),那就更好了。
擴(kuò)展閱讀
共享可修改狀態(tài)的問(wèn)題及如何避免:https://exploringjs.com/deep-js/ch_shared-mutable-state.html
