JavaScript 數(shù)據(jù)處理 - 映射表篇
作者:邊城
來源:SegmentFault ?思否社區(qū)?
JavaScript 的常用數(shù)據(jù)集合有列表 (Array) 和映射表 (Plain Object)。列表已經(jīng)講過了,這次來講講映射表。
由于 JavaScript 的動態(tài)特性,其對象本身就是一個映射表,對象的「屬性名?屬性值」就是映射表中的「鍵?值」。為了便于把對象當(dāng)作映射表來使用,JavaScript 甚至允許屬性名不是標(biāo)識符 —— 任意字符串都可以作為屬性名。當(dāng)然非標(biāo)識符屬性名只能使用?[]?來訪問,不能使用 . 號訪問。
使用?[]?訪問對象屬性更契合映射表的訪問形式,所以在把對象當(dāng)作映射表使用時,通常會使用?[]?訪問表元素。這個時候?[]?中的內(nèi)容稱為“鍵”,訪問操作存取的是“值”。因此,映射表元素的基本結(jié)構(gòu)稱為“鍵值對”。、
在 JavaScript 對象中,鍵允許有三種類型:number、string 和 symbol。
number 類型的鍵主要是用作數(shù)組索引,而數(shù)組也可以認(rèn)為是特殊的映射表,其鍵通常是連續(xù)的自然數(shù)。不過在映射表訪問過程中,number 類型的鍵會被轉(zhuǎn)成 string 類型來使用。
symbol 類型的鍵用得比較少,一般都是按規(guī)范使用一些特殊的 Symbol 鍵,比如 Symbol.iterator。symbol 類型的鍵通常會用于較為嚴(yán)格的訪問控制,在使用 Object.keys()?和 Object.entries()?訪問相關(guān)元素時,會忽略掉鍵類型是 symbol 類型的元素。
一、CRUD
創(chuàng)建對象映射表直接使用?{ }?定義 Object Literal 就行,基本技能,不用詳述。但需要注意的是?{ }?在 JavaScript 也用于封裝代碼塊,所以把 Object Literal 用于表達(dá)式時往往需要使用一對小括號把它包裹起來,就像這樣:({ })。在使用箭頭函數(shù)表達(dá)式直接返回一個對象的時候尤其需要注意這一點。
對映射表元素的增、改、查都用?[]?運算符。
如果想判斷某個屬性是否存在,有人習(xí)慣用?!!map[key]?,或者?map[key] === undefined?來判斷。使用前者要注意 JavaScript 假值的影響;使用后者則要注意有可能值本身就是?undefined。如果想準(zhǔn)確地判斷是否存在某個鍵,應(yīng)該使用?in?運算符:
const?a?=?{?k1:?undefined?};
console.log(a["k1"]?!==?undefined);??//?false
console.log("k1"?in?a);??????????????//?true
console.log(a["k2"]?!==?undefined);??//?false
console.log("k2"?in?a);??????????????//?false
類似地,要刪除一個鍵也不是將其值改變?yōu)?undefined?或者?null,而是使用?delete?運算符:
const?a?=?{?k1:?"v1",?k2:?"v2",?k3:?"v3"?};
a["k1"]?=?undefined;
delete?a["k2"];
console.dir(a);?//?{?k1:?undefined,?k3:?'v3'?}
使用?delete a["k2"]?操作后?a?的?k2?屬性不復(fù)存在。
注
上述兩個示例中,由于 k1、k2、k3 都是合法標(biāo)識符,ESLint 可能會報違反 dot-notation 規(guī)則。這種情況下可以關(guān)閉此規(guī)則,或者改用 . 號訪問(由團(tuán)隊決定處理方式)。
二、映射表中的列表
映射表可以看作是鍵值對的列表,所以映射表可以轉(zhuǎn)換成鍵值對列表來處理。
鍵值對用英語一般稱為 key value pair 或 entry,Java 中用?Map.Entry
在 JavaScript 中,可以使用?Object.entries(it)?來得到一個由?[鍵, 值]?形成的鍵值對列表。
const?obj?=?{?a:?1,?b:?2,?c:?3?};
console.log(Object.entries(obj));
//?[?[?'a',?1?],?[?'b',?2?],?[?'c',?3?]?]
映射表除了有 entry 列表之外,還可以把鍵和值分開,得到單獨的鍵列表,或者值列表。要得到一個對象的鍵列表,使用?Object.keys(obj)?靜態(tài)方法;相應(yīng)的要得到值列表使用?Object.values(obj)?靜態(tài)方法。
const?obj?=?{?a:?1,?b:?2,?c:?3?};
console.log(Object.keys(obj));??????//?[?'a',?'b',?'c'?]
console.log(Object.values(obj));????//?[?1,?2,?3?]
三、遍歷映射表
既然映射表可以看作鍵值對列表,也可以單獨取得鍵或值的列表,那么遍歷映射表的方法也比較多。
最基本的方法就是用?for?循環(huán)。不過需要注意的是,由于映射表通常不帶序號(索引號),不能通過普通的?for(;;)?循環(huán)來遍歷,而是需要使用 for each 來遍歷。不過有意思的是,for...in?可以用于會遍歷映射表所有的 Key;但在映射表上使用?for...of?會出錯,因為對象“is not iterable”(不可迭代,或不可遍歷)。
const?obj?=?{?a:?1,?b:?2,?c:?3?};
for?(let?key?in?obj)?{
????console.log(`${key}?=?${obj[key]}`);???//?拿到?key?之后通過?obj[key]?來取值
}
//?a?=?1
//?b?=?2
//?c?=?3
既然映射表可以單獨拿到鍵集和值集,所以在遍歷的處理上會比較靈活。但是通常情況下我們一般都會同時使用鍵和值,所以在實際使用中,比較常用的是對映射表的所有 entry 進(jìn)行遍歷:
Object.entries(obj)
????.forEach(([key,?value])?=>?console.log(`${key}?=?${value}`));
四、從列表到映射表
前面兩個小節(jié)都是在講映射表怎么轉(zhuǎn)成列表。反過來,要從列表生成映射表呢?
要從列表生成映射表,最基本的操作是生成一個空映射表,然后遍歷列表,從每個元素中去取到“鍵”和“值”,將它們添加到映射表中,比如下面這個示例:
const?items?=?[
????{?name:?"size",?value:?"XL"?},
????{?name:?"color",?value:?"中國藍(lán)"?},
????{?name:?"material",?value:?"滌綸"?}
];
function?toObject(specs)?{
????return?specs.reduce((obj,?spec)?=>?{
????????obj[spec.name]?=?spec.value;
????????return?obj;
????},?{});
}
console.log(toObject(items));
//?{?size:?'XL',?color:?'中國藍(lán)',?material:?'滌綸'?}
這是常規(guī)操作。注意到?Object?還提供了一個?fromEntries()?靜態(tài)方法,只要我們準(zhǔn)備好鍵值對列表,使用?Object.fromEntries()?就能快速得到相應(yīng)的對象:
function?toObject(specs)?{
????return?Object.fromEntries(
????????specs.map(({?name,?value?})?=>?[name,?value])
????);
}
五、一個小小的應(yīng)用案例
數(shù)據(jù)處理過程中,列表和映射表之間往往需要相互轉(zhuǎn)換以達(dá)到較為易讀的代碼或更好的性能。本文前面的內(nèi)容已經(jīng)講到了轉(zhuǎn)換的兩個關(guān)鍵方法:
Object.entries()?把映射表轉(zhuǎn)換成鍵值對列表 Object.fromEntries()?從鍵值對列表生成映射表
提出問題:
[
?{?"id":?1,?"parentId":?0,?"label":?"第?1?章"?},
?{?"id":?2,?"parentId":?1,?"label":?"第?1.1?節(jié)"?},
?{?"id":?3,?"parentId":?2,?"label":?"第?1.2?節(jié)"?},
?{?"id":?4,?"parentId":?0,?"label":?"第?2?章"?},
?{?"id":?5,?"parentId":?4,?"label":?"第?2.1?節(jié)"?},
?{?"id":?6,?"parentId":?4,?"label":?"第?2.2?節(jié)"?},
?{?"id":?7,?"parentId":?5,?"label":?"第?2.1.1?點"?},
?{?"id":?8,?"parentId":?5,?"label":?"第?2.1.2?點"?}
]
在已生成的樹中查找某個節(jié)點本身是個復(fù)雜的過程,不管是用遞歸通過深度遍歷查找,還是用隊列通過廣度遍歷查找,都需要寫相對復(fù)雜的算法,也比較耗時;
對于列表所有節(jié)點順序,如果不能保證子節(jié)點在父節(jié)點之后,處理的復(fù)雜度會大大增加。
const?nodeMap?=?Object.fromEntries(
????nodes.map(node?=>?[node.id,?node])
);
六、映射表的拆分
映射表本身不支持拆分,但是我們可以按照一定規(guī)則從中選擇一部分鍵值對出來,組成新的映射表,達(dá)到拆分的目的。這個過程就是?Object.entries()??filter()???Object.fromEntries()。比如,希望把某配置對象中所有帶下劃線前綴的屬性剔除掉:
const?options?=?{?_t1:?1,?_t2:?2,?_t3:?3,?name:?"James",?title:?"Programmer"?};
const?newOptions?=?Object.fromEntries(
????Object.entries(options).filter(([key])?=>?!key.startsWith("_"))
);
//?{?name:?'James',?title:?'Programmer'?}
提出問題:
async?function?asyncDoIt(options)?{
????const?success?=?options.success;
????const?fail?=?options.fail;
????delete?options.success;
????delete?options.fail;
????try?{
????????const?result?=?await?callNewProcess(options);
????????success?.(result);
????}?catch?(e)?{
????????fail?.(e);
????}
}
const?{?success,?fail?}?=?options;
const?{?success,?fail,?...opts?}?=?options;
async?function?asyncDoIt({?success,?fail,?...options?}?=?{})?{
????//?TODO?try?{?...?}?catch?(e)?{?...?}
}
七、合并映射表
Object.assign() 展開運算符
提出問題
const?defaultOptions?=?{
????a:?1,?b:?2,?c:?3,?d:?4
};
function?doSomthing(options)?{
????options?=?Object.assign({},?defaultOptions,?options);
????//?TODO?使用?options
}
const?defaultOptions?=?Object.freeze({
//?????????????????????^^^^^^^^^^^^^^
????a:?1,?b:?2,?c:?3,?d:?4
});
options?=?{?...defaultOptions,?...options?};
function?fetchSomething(url,?options)?{
????options?=?{
????????...defaultOptions,
????????...options,
????????url,????????//?鍵和變量同名時可以簡寫
????????more:?"hi"??//?普通的?Object?Literal?屬性寫法
????};
????//?TODO?使用?options
}
const?t1?=?{?a:?{?x:?1?}?};
const?t2?=?{?a:?{?y:?2?}?};
const?r?=?Object.assign({},?t1,?t2);????//?{?a:?{?y:?2?}?}
八、Map 類
添加/修改,使用?set()?方法;
通過鍵取值,使用?get()?方法;
根據(jù)鍵刪除,使用?delete()?方法,還有一個?clear()?直接清空映射表;
has()?訪求用來判斷是否存在某個鍵值對;
size?屬性可以拿到 entry 數(shù),不像 Plain Object 需要用?Object.entries(map).length?來獲取;
entries(),keys()?和?values()?方法用來獲取 entry、鍵、值的列表,但結(jié)果不是數(shù)組,而是 Iterator;
還有個?forEach()?方法直接用來遍歷,處理函數(shù)不接收整個 entry (即?([k, v])),而是分離的?(value, key, map)。
總結(jié)

