乐趣区

关于程序员:快速了解-ES6-的Map与WeakMap

在 ECMAScript 6 之前,能够应用 Object 的对象属性作为键,再应用属性援用值的形式来实现 key/value 存储;而 ECMAScript 6 标准中,新增了 MapWeakMap 两种汇合类型来存储 key/value

Map

ECMAScript 6 新增的 Map 汇合能够应用 newMap 构造函数创立一个空映射:

const map = new Map();

也能够在创立映射时,就将键值对依照迭代程序插入到新映射实例中:

// 应用嵌套数组初始化映射
const m1 = new Map([["name", "小赵"],
    ["age", 12],
    ["sex", "男"]
]);
alert(m1.size); // 3

// 应用自定义迭代器初始化映射
const m2 = new Map({[Symbol.iterator]: function*() {yield ["name", "小赵"];
        yield ["age", 12];
        yield ["sex", "男"];
    }
});
alert(m2.size); // 3

// 映射期待的键 / 值对,无论是否提供
const m3 = new Map([[]]);
alert(m3.has(undefined)); // true
alert(m3.get(undefined)); // undefined

API

初始化之后,在想要往映射表中增加 key/value,就须要应用 set() 办法:

const map = new Map();
map.set("name", "小赵");
map.set("age", 24);
map.set("sex", "男");

然而,set() 办法返回的是 Map 实例,因而能够初始化能够如下申明:

const map = new Map().set("name", "小赵");
map.set("age", 24).set("sex", "男");

寄存进去的 key/value 能够通过 get()has() 办法进行查问,has() 办法用于判断 key 是否存在 Map 中,而 get() 用于获取 key 对应的 value

console.log(map.has("score"));    // false
console.log(map.get("name"));    // "小赵"

能够通过 size 属性获取 Mapkey/value 的数量:

console.log(map.size);    // 3

如果要删除 Map 中的 key/value,能够应用 delete()clear() 办法,delete() 办法用于删除指定的 key,而 clear() 用于革除 Map 中的所有 key/value

map.delete("age");
console.log(map.size);    // 2
map.clear();
console.log(map.size);    // 0

如果须要以插入程序返回 Map 中每个元素的 [key, value] 数组,能够应用 entries() 办法或者 Symbol.iterator 属性获取 Map 中的迭代器。

const m = new Map([["name", "小赵"],
    ["age", 12],
    ["sex", "男"]
]);
console.log(m.entries === m[Symbol.iterator]); // true
for (let kv of m.entries()) {console.log(kv);
}
// ['name', '小赵']
// ['age', 12]
// ['sex', '男']
for (let kv of m[Symbol.iterator]()) {console.log(kv);
}
// ['name', '小赵']
// ['age', 12]
// ['sex', '男']
for (let [k, v] of m.entries()) {console.log(k + "=" + v);
}
// name = 小赵
// age = 12
// sex = 男 

因为 entries() 是默认迭代器,能够间接对 Map 实例应用扩大操作,把 Map 转换成数组:

const m = new Map([["name", "小赵"],
    ["age", 12],
    ["sex", "男"]
]);
console.log([...m]); // [[ 'name', '小赵'], ['age', 12], ['sex', '男'] ]

也能够应用 forEach(callback, opt_thisArg) 办法并传入回调,顺次迭代每个 key/value。传入的回调接管可选的第二个参数,这个参数用于重写回调外部 this 的值:

m.forEach((val, key) => console.log(`${key} -> ${val}`));
// name -> 小赵
// age -> 12
// sex -> 男 

keys()values() 别离返回以插入程序生成 keyvalue 的迭代器:

for (let k of m.keys()) {console.log(k);
}
// name
// age
// sex
for (let v of m.values()) {console.log(v);
}
// 小赵
// 12
// 男 

键和值在迭代器遍历时是能够批改的,但映射外部的援用则无奈批改。当然,这并不障碍批改作为键或值的对象外部的属性,因为这样并不影响它们在映射实例中的身份:

const m1 = new Map([["name", "小赵"]
]);
// 作为键的字符串原始值是不能批改的
for (let key of m1.keys()) {
    key = 10010;
    console.log(key); // 10010
    console.log(m1.get("name")); // 小赵
}
const key2 = {id: 1};
const m2 = new Map([[key2, "小新"]
]);
// 批改了作为键的对象的属性,但对象在映射外部依然援用雷同的值
for (let key of m2.keys()) {
    key.id = 10086;
    console.log(key); // {id: 10086}
    console.log(m2.get(key2)); // 小新
}
console.log(key2); // {id: 10086}

键的相等性

Map 中,键的比拟是基于 someValueZero 算法:

  • NaN 是与 NaN 相等的(尽管 NaN !== NaN),剩下所有其它的值是依据 === 运算符的后果判断是否相等。
  • 在目前的 ECMAScript 标准中,-0+0 被认为是相等的,只管这在晚期的草案中并不是这样的。

与 Object 比拟

MapObject 相似,都容许按 key 存取 value、删除 key、检测 key 是否绑定了 value。但 MapObject 也在内存和性能上的确存在显著的差异。

通常存储单个 key/value 所占用的内存会随着 key 的数量线性减少,也会受不同浏览器的影响,然而对于规定大小的内存,Map 能够比 Object 多存储 key/value;而要波及大量插入,专为存储 key/value 设计的 Map,它的性能更佳。

对于 Map 而言,存储 Mapkey 能够是任意值,而且 key 的程序是由插入的程序决定的;delete() 办法也比插入和查找要更快。

Objectkey 只能是 StringSymbol,而且插入的程序是无序的;而且在 Object 中删除属性,会呈现一些伪删除,包含把属性值设置为 undefinednull

WeakMap

WeakMap 也是 ECMAScript 6 新增的映射表,而且还是 Map 的子集。然而 WeakMap 寄存的 key 必须是对象类型,而且还是 “弱映射”,这会使得 key 所指的值没有其余中央援用的时候,它就会 GC 回收;value 能够是任意类型。

WeakMapMap 的 API 根本都雷同,然而 WeakMap 中的键只能是 Object 或者继承自 Object 的类型,尝试应用非对象设置键会抛出 TypeError,值的类型没有限度。

WeakMap 曾经弃用 clear() 办法,但能够通过创立一个空的 WeakMap 并替换原对象来实现。

let wm = new WeakMap();
vm.set("name", "小钱").set("age", 24).set("sex", "男");

// 清空 WeakMap
vm = new WeakMap();

弱键

WeakMap 中的 "weak" 示意的是 key 存在,key/value 就会存在 WeakMap 中,并当作 value 的援用,不会被 GC 回收;而 key 不存在时,不会阻止 GC 回收。

const wm = new WeakMap();
wm.set({}, "weak value");

set() 办法初始化了一个新对象并将它用作一个字符串的键。因为没有指向这个对象的其余援用,所以当这行代码执行实现后,这个对象键就会被当作垃圾回收。而后,这个键 / 值对就从弱映射中隐没了,使其成为一个空映射。在这个例子中,因为值也没有被援用,所以这对键 / 值被毁坏当前,值自身也会成为垃圾回收的指标。再看一个略微不同的例子:

const wm = new WeakMap();
const container = {key: {}
};
wm.set(container.key, "weak val");
function removeReference() {container.key = null;}

这一次,container 对象保护着一个 container.key 的援用,因而这个对象键不会被 GC 回收。不过,如果调用了 removeReference(),就会捣毁 key 的最初一个援用,就会被 GC 回收。

不可迭代键

因为 WeakMap 中的 key/value 任何时候都可能被销毁,也没有提供 entries()keysvalues 等可迭代的办法,也没有提供 clear() 一次性革除所有 key/value 的办法。

之所以限度只能用对象作为 key,是为了保障只有通过 key 的援用能力获得值。如果容许原始值,那就没方法辨别初始化时应用的字符串字面量和初始化之后应用的一个相等的字符串了。

利用

因为 WeakMapkey 的弱援用,因而能够在相应的许多方面利用它。

公有变量

公有变量是不会被外界拜访到的,不能被多个实例共享,因而能够应用 WeakMap 来实现。如下所示:

const wm = new WeakMap();
class User {constructor(id) {this.idProperty = Symbol('id');
        this.setId(id);
    }
    setPrivate(property, value) {const privateMembers = wm.get(this) || {};
        privateMembers[property] = value;
        wm.set(this, privateMembers);
    }
    getPrivate(property) {return wm.get(this)[property];
   }
    setId(id) {this.setPrivate(this.idProperty, id);
    }
    getId() {return this.getPrivate(this.idProperty);
    }
}
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456
// 并不是真正公有的
alert(wm.get(user)[user.idProperty]); // 456

对于下面的实现,内部只须要拿到对象实例的援用和弱映射,就能够获得 “公有” 变量了。为了防止这种拜访,能够用一个闭包把 WeakMap 包装起来,这样就能够把弱映射与外界齐全隔离开了:

const User = (() => {const wm = new WeakMap();
    class User {constructor(id) {this.idProperty = Symbol('id');
            this.setId(id);
        }
        setPrivate(property, value) {const privateMembers = wm.get(this) || {};
            privateMembers[property] = value;
            wm.set(this, privateMembers);
        }
        getPrivate(property) {return wm.get(this)[property];
        }
        setId(id) {this.setPrivate(this.idProperty, id);
        }
        getId(id) {return this.getPrivate(this.idProperty);
        }
    }
    return User;
})();
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456

这样,拿不到弱映射中的健,也就无奈获得弱映射中对应的值。尽管这避免了后面提到的拜访,但整个代码也齐全陷入了 ES6 之前的闭包公有变量模式。

DOM 节点元数据

应用 WeakMap 实例存储的 DOM 元素时在被革除时,对应的 WeakMap 记录会被 GC 回收,因而非常适合保留关联元数据。来看上面这个例子,其中应用了惯例的 Map

const m = new Map();
const lb = document.querySelector('#login');
// 给这个节点关联一些元数据
m.set(lb, {disabled: true});

假如在下面的代码执行后,页面被 JavaScript 扭转了,原来的登录按钮从 DOM 树中被删掉了。但因为映射中还保留着按钮的援用,所以对应的 DOM 节点依然会勾留在内存中,除非明确将其从映射中删除或者等到映射自身被销毁。

如果这里应用的是弱映射,如以下代码所示,那么当节点从 DOM 树中被删除后,垃圾回收程序就能够立刻开释其内存(假如没有其余中央援用这个对象):

const wm = new WeakMap();
const lb = document.querySelector('#login');
// 给这个节点关联一些元数据
wm.set(lb, {disabled: true});

更多内容请关注公众号「 海人为记

退出移动版