您當前的位置:首頁 > 舞蹈

鍵值對在Javascript如何儲存 —— Object vs Map vs WeakMap

作者:由 FreewheelLee 發表于 舞蹈時間:2020-09-27

很多業務場景下,我們需要儲存一些鍵值對方便之後的檢索、查詢、遍歷等。

比如,我們有一個員工物件的陣列,我們希望能快速根據員工的姓名查到對應的員工物件,與其每次遍歷整個陣列對比每個物件的name屬性,不如重新構造一個鍵值對資料結構,鍵是姓名而值是員工物件。

鍵值對在Javascript如何儲存 —— Object vs Map vs WeakMap

而事實上,每個員工物件裡的屬性名和屬性值也是一對對的鍵值對

鍵值對在Javascript如何儲存 —— Object vs Map vs WeakMap

Object

因此,不管你是不是注意到了,JavaScript 物件是存放鍵值對的最常用、最簡單、最原始的方案。

const tom = {

name: ‘tom’,

age: 25,

gender: ‘male’,

job: ‘developer’

}

可以用以下方法獲取所有“鍵”(即物件的屬性)

for (let key in tom) {

if (tom。hasOwnProperty(key)) { // 排除 prototype chain 上的屬性

console。log(key);

}

}

Object。keys() 提供了更便捷的方式

const keys = Object。keys(tom);

for (let i = 0; i < keys。length; i++) {

console。log(keys[i]);

}

然而使用 JavaScript物件 存放鍵值對,有如下明顯的幾個缺點:

缺點一:

物件中的“鍵”(屬性)都必須是字串

—— 非字串的鍵都會被轉換成字串

(物件中還可以存放 Symbol 鍵,由於意圖完全不同,這邊不做討論,有興趣可以看我另一篇文章 FreewheelLee:談談我對ES6 Symbol的理解 )

const obj = {};

const obj2 = {};

const tom = {

[obj]: ‘good’,

[obj2]: ‘bad’,

9: ‘secret’,

name: ‘tom’,

}

const keys = Object。keys(tom);

for (let i = 0; i < keys。length; i++) {

console。log(keys[i]);

console。log(typeof keys[i]); // 輸出都是 string

console。log(tom[keys[i]]);

}

輸出的結果:

9

string

secret

[object Object]

string

bad

name

string

tom

上面的測試有2個有意思的點:

1。所有 typeof keys[i] 的值都是 string,包括 9 和 obj

2。tom “理論上應該”有兩個屬性是物件(obj 和 obj2)但遍歷結果只有一個 [object Object],且屬性值為 bad ;且當我們嘗試直接獲取屬性 obj 的值時,驚訝地發現仍然是 bad

console。log(tom[obj]); // 仍然輸出 bad

即 obj2 覆蓋掉了 obj (讀者可以繼續思考為什麼)

缺點二:如果物件涉及繼承(即 子類物件),需要考慮父類(prototype chain上)的屬性

缺點三:JavaScript 物件無法直接獲得鍵值對的數量,即沒有 size / length 屬性或者方法

“缺點”四:如果物件裡有方法,也一樣會被遍歷出來,例:

const tom = {

name: ‘tome’,

age: 25,

gender: ‘male’,

job: ‘developer’,

work(){

console。log(“coding”);

}

};

const keys = Object。keys(tom);

for (let i = 0; i < keys。length; i++) {

console。log(keys[i]);

}

// 會輸出

name

age

gender

job

work

(這一點是不是缺點有待商榷,畢竟在 JavaScript 中函式也是一等公民)

小結:

當使用JavaScript物件存放鍵值對時,只適合非常簡單的場景 —— 畢竟物件的設計初衷不是讓你存放鍵值對的。

Map

ES6 之後,JavaScript 添加了 Map 特性 —— 這是一個專門用來存放鍵值對的資料結構。

簡單看看如何使用 Map 相關的基礎API

const contacts = new Map()

contacts。set(‘Jessie’, {phone: “213-555-1234”, address: “123 N 1st Ave”})

contacts。has(‘Jessie’) // true

contacts。get(‘Hilary’) // undefined

contacts。set(‘Hilary’, {phone: “617-555-4321”, address: “321 S 2nd St”})

contacts。get(‘Jessie’) // {phone: “213-555-1234”, address: “123 N 1st Ave”}

contacts。delete(‘Raymond’) // false

contacts。delete(‘Jessie’) // true

console。log(contacts。size) // 1

這些API非常直觀易懂。

再看看如何遍歷一個 Map

// 使用 for 。。。 of 語法

const myMap = new Map()

myMap。set(0, ‘zero’)

myMap。set(1, ‘one’)

for (let [key, value] of myMap) {

console。log(key + ‘ = ’ + value)

}

// 0 = zero

// 1 = one

for (let key of myMap。keys()) {

console。log(key)

}

// 0

// 1

for (let value of myMap。values()) {

console。log(value)

}

// zero

// one

for (let [key, value] of myMap。entries()) {

console。log(key + ‘ = ’ + value)

}

// 0 = zero

// 1 = one

// 使用 forEach 方法

myMap。forEach(function(value, key) {

console。log(key + ‘ = ’ + value)

})

// 0 = zero

// 1 = one

Map 的

鍵沒有型別限制

,比如 物件、函式,也不會做任何隱式轉換。

const map = new Map();

const key1 = {};

const key2 = {};

const key3 = function (){};

map。set(key1, ‘one’)

map。set(key2, ‘two’);

map。set(key3, ‘three’);

console。log(map。get(key1)); // one

console。log(map。get(key2)); // two

console。log(map。get(key3)); // three

更有意思的是 Map 跟 二維陣列 可以便捷地相互轉換

const kvArray = [[‘key1’, ‘value1’], [‘key2’, ‘value2’]]

// 將 二維陣列轉換成 Map

const myMap = new Map(kvArray)

myMap。get(‘key1’) // returns “value1”

// 使用 Array。from() 將 Map 轉換成 二維陣列

console。log(Array。from(myMap)) // 跟 kvArray 一模一樣

// 使用 spread syntax 也能把 Map 轉換成 二維陣列

console。log([。。。myMap])

使用 構造器就能複製一個 Map

let original = new Map([

[1, ‘one’]

])

let clone = new Map(original)

console。log(clone。get(1)) // one

console。log(original === clone) // false (淺比較)

Map 之間的合併和覆蓋

const first = new Map([

[1, ‘one’],

[2, ‘two’],

[3, ‘three’],

])

const second = new Map([

[1, ‘uno’],

[2, ‘dos’]

])

// 使用 spread syntax 合併兩個 Map, 後面同名的key對應的值會覆蓋掉前面的值

// 其實原理就是利用 Map 和 二維陣列 的轉換

const merged = new Map([。。。first, 。。。second])

console。log(merged。get(1)) // uno

console。log(merged。get(2)) // dos

console。log(merged。get(3)) // three

Map 的缺點:

目前發現的一個小缺點是沒有便利的 API 可以直接將 Map 轉換成 JSON ,解決方案只能是先將 Map 轉換 成JavaScript Object 再 轉換成 JSON

function strMapToObj(strMap) {

let obj = Object。create(null);

for (let [k, v] of strMap) {

obj[k] = v;

}

return obj;

}

function strMapToJson(strMap) {

return JSON。stringify(strMapToObj(strMap));

}

let original = new Map([

[‘one’, 1],

[‘two’, 2],

])

console。log(strMapToJson(original)); // {“one”:1,“two”:2}

小結:

可以看到 Map 相對 JavaScript Object 在存放 鍵值對 方面專業了許多,提供了大量便利的API。

WeakMap

Java 同學看到這個詞估計很親切,因為 Java API 中有個 WeakHashMap。事實上,它們的確是類似的。 JavaScript 的 WeakMap 也是用於解決記憶體洩露問題。

如果 Map 的鍵是個JavaScript 物件,當外部丟失了這個物件的引用時,Map內部也始終引用著這個物件,垃圾回收器就無法回收這個物件,造成記憶體洩露。

而 WeakMap 都是使用弱引用(“weak” references)指向鍵物件,不會阻止垃圾回收器回收,就能避免洩露的發生。

WeakMap 的多數API跟 Map 類似,但有2個比較明顯的不同是:

1。 由於使用了弱引用,

WeakMap 不能被遍歷,也無法獲得當前所有的鍵和值

2。 WeakMap 的

鍵 只能是 JavaScript 物件型別 ,

值則沒有任何限制(包括函式)

最後分享一下 業界的一個利用WeakMap隱藏物件私有資料/實現的有趣的模式

const privates = new WeakMap();

function Public() {

const me = {

// Private data goes here

};

privates。set(this, me);

}

Public。prototype。method = function () {

const me = privates。get(this);

// Do stuff with private data in `me`。。。

};

module。exports = Public;

所有私有的資料和函式都放在 WeakMap 裡

所有例項的屬性/方法 和 prototype 上暴露的屬性/方法 都是公開的;其餘的都無法被外部訪問到,因為 privates 並沒有被模組匯出。

因為使用了 WeakMap 也避免了記憶體洩露的問題

總結

本文介紹了在 JavaScript 存放鍵值對的三種方案,是否對你有幫助和啟發呢?歡迎點贊、喜歡、收藏三連!也可以在評論區留言分享你的經驗和技巧。

參考連結:

Map

WeakMap

Hiding Implementation Details with ECMAScript 6 WeakMaps

標簽: log  map  const  console  keys