前言
Dr. Axel Rauschmayer最近撰文介紹了還處於Stage1階段的兩個JavaScript新特性:記錄和元組。
記錄和元組是一個新提案(Record & Tuple,
https://github.com/tc39/proposal-record-tuple),建議為JavaScript增加兩個複合原始類型:
- 記錄(Record),是不可修改的按值比較的對象
- 元組(Tuple),是不可修改的按值比較的數組
什麼是按值比較
當前,JavaScript只有在比較原始值(如字符串)時才會按值比較(比較內容):
> 'abc' === 'abc'
true但在比較對象時,則是按標識比較(by identity),因此對象只與自身嚴格相等:
> {x: 1, y: 4} === {x: 1, y: 4}
false
> ['a', 'b'] === ['a', 'b']
false“記錄和元組”的提案就是為了讓我們可以創建按值比較的複合類型值。
比如,在對象字面量前面加一個井號(#),就可以創建一個記錄。而記錄是一個按值比較的複合值,且不可修改:
> #{x: 1, y: 4} === #{x: 1, y: 4}
true如果在數組字面量前面加一個#,就可以創建一個元組,也就是可以按值比較且不可修改的數組:
> #['a', 'b'] === #['a', 'b']
true按值比較的複合值就叫複合原始值或者複合原始類型。
記錄和元組是原始類型
使用typeof可以看出來,記錄和元組都是原始類型:
> typeof #{x: 1, y: 4}
'record'
> typeof #['a', 'b']
'tuple'記錄和元組的內容有限制
- 記錄:
- 鍵必須是字符串
- 值必須是原始值(包括記錄和元組)
- 元組:
- 元素必須是原始值(包括記錄和元組)
把對象轉換為記錄和元組
> Record({x: 1, y: 4})
#{x: 1, y: 4}
> Tuple.from(['a', 'b'])
#['a', 'b']注意:這些都是淺層轉換。如果值樹結構中有任何節點不是原始值,Record()和Tuple.from()會拋出異常。
使用記錄
const record = #{x: 1, y: 4};
// 訪問屬性
assert.equal(record.y, 4);
// 解構
const {x} = record;
assert.equal(x, 1);
// 擴展
assert.ok(
#{...record, x: 3, z: 9} === #{x: 3, y: 4, z: 9});使用元組
const tuple = #['a', 'b'];
// 訪問元素
assert.equal(tuple[1], 'b');
// 解構(元組是可迭代對象)
const [a] = tuple;
assert.equal(a, 'a');
// 擴展
assert.ok(
#[...tuple, 'c'] === #['a', 'b', 'c']);
// 更新
assert.ok(
tuple.with(0, 'x') === #['x', 'b']);為什麼按值比較的值不可修改
某些數據結構(比如散列映射和搜索樹)有槽位,其中鍵的保存位置根據它們的值來確定。如果鍵的值改變了,那這個鍵通常必須放到不同的槽位。這就是為什麼在JavaScript中可以用作鍵的值:
- 要麼按值比較且不可修改(原始值)
- 要麼按標識比較且可修改(對象)
複合原始值的好處
複合原始值有如下好處。
- 深度比較對象,這是一個內置操作,可以通過如===來調用。
- 共享值:如果對象是可修改的,為了安全共享就需要深度複製它的一個副本。而對於不可修改的值,就可以直接共享。
- 數據的非破壞性更新:如果要修改複合值,由於一切都是不可修改的,所以就要創建一個可修改的副本,然後就可以放心地重用不必修改的部分。
- 在Map和Set等數據結構中使用:因為兩個內容相同的複合原始值在這門語言的任何地方(包括作為Map的鍵和作為Set的元素)都被認為嚴格相等,所以映射和集合成會變得更有用。
接下來演示這些好處。
示例:集合與映射變得更有用
通過集合去重
有了複合原始值,即使是複合值(不是原始值那樣的原子值)也可以去重:
> [...new Set([#[3,4], #[3,4], #[5,-1], #[5,-1]])]
[#[3,4], #[5,-1]]如果是數組就辦不到了:
> [...new Set([[3,4], [3,4], [5,-1], [5,-1]])]
[[3,4], [3,4], [5,-1], [5,-1]]映射的複合鍵
因為對象是按標識比較的,所以在(非弱)映射中用對象作為鍵幾乎沒什麼用:
const m = new Map();
m.set({x: 1, y: 4}, 1);
m.set({x: 1, y: 4}, 2);
assert.equal(m.size, 2)如果使用複合原始值就不一樣了:下面行(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 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']),
],
]);示例:有效地深度相等
使用複合屬性值處理對象
在下面的例子中,我們使用數組的方法.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',
},
},
]);對象變了嗎?
在處理緩存的數據(如下面例子中的previousData)時,內置深度相等可以讓我們有效地檢查數據是否發生了變化。
let previousData;
function displayData(data) {
if (data === previousData) return;
// ···
}
displayData(#['Hello', 'world']); // 顯示
displayData(#['Hello', 'world']); // 不顯示測試
多數測試框架都支持深度相等,以檢查某個計算是否產生了預期的結果。例如,Node.js內置的assert模塊有一個函數叫deepEqual()。有了複合原始值,就可以直接斷言:
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});新語法的優缺點
新語法的一個缺點是字符#已經在很多地方被佔用了(比如私有字段),另外非數字字母字符多少顯得有點神秘。可以看看下面的例子:
const della = #{
name: 'Della',
children: #[
#{
name: 'Huey',
},
#{
name: 'Dewey',
},
#{
name: 'Louie',
},
],
};優點是這個語法比較簡潔。對於一個常用的結構,當然越簡單越好。此外,一旦熟悉了這個語法之後,神秘感自然就會越來越淡。
除了特殊的字面量語法,還可以使用工廠函數:
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,已撤銷),這個語法還可能有所改進:
const della = Record!{
name: 'Della',
children: Tuple![
Record!{
name: 'Huey',
},
Record!{
name: 'Dewey',
},
Record!{
name: 'Louie',
},
],
};唉,即便使用更短的名字,結果看起來還是有點亂:
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()把記錄當成對象,把元組當成數組(遞歸)。
- JSON.parseImmutable與JSON.parse()類似,但返回記錄而非對象,返回元組而非數組(遞歸)。
未來:類的實例會按值比較嗎?
相比對象和數組,我其實更喜歡使用類作為一個數據容器。因為它可以把名字添加到對象上。為此,我希望將來會有一種類,它的實例不可修改且按值比較。
假如我們還可以深度、非破壞性地更新那些包含由值類型的類產生的對象的數據,那就更好了。
原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/231176.html
微信掃一掃
支付寶掃一掃