Skip to content

深入理解 ES6 #7-Set 集合和 Map 集合

🏷️ 《深入理解 ES6》

ES6 新标准中将 Set 集合和 Map 集合添加到 JS 中。

ES5 中 Set 集合和 Map 集合

在 ES5 中,开发者们用对象属性来模拟这两种集合。

js
var set = Object.create(null);

set.foo = true;

// 检查属性是否存在
if (set.foo) {
    // ...
}

模拟 Map 集合同上例类似。

如果只是简单的应用上面的方法基本上能满足需求,但是如果碰到对象属性名的限制,就会产生一些问题。

js
var map = Object.create(null);
map[5] = "foo";
console.log(map["5"]); // "foo"
console.log(map); // {5: "foo"}

虽然 map 中存储的属性键名是数值型的 5,但 map["5"] 引用的是同一个属性。

用对象作为键名也有类似的问题。

js
var map = Object.create(null),
    key1 = {},
    key2 = {};
map[key1] = "foo";
console.log(map[key2]); // "foo"
console.log(map); // {[object Object]: "foo"}

上例中 map[key1]map[key2] 也引用了同一个值。因为 key1key2 会被转换为对象的默认字符串 "[object Object]"

ES6 中的 Set 集合

ES6 中新增的 Set 集合是一种有序列表,其中还有一些相互独立的非重复值,通过 Set 集合可以快速访问其中的数据,更有效地追踪各种离散值。

创建 Set 集合并添加元素

js
let set = new Set();
set.add(5);
set.add("5");

console.log(set.size); // 2
console.log(set); // Set(2) {5, "5"}

Set 集合中添加对象

js
let set = new Set(),
    key1 = {},
    key2 = {};

set.add(key1);
set.add(key2);

console.log(set.size); // 2
console.log(set); // Set(2) {{…}, {…}}

如果多次传入相同的值,后面的调用实际上会被忽略。

js
let set = new Set();
set.add(5);
set.add("5");
set.add(5); // 重复 - 本次调用直接被忽略

console.log(set.size); // 2
console.log(set); // Set(2) {5, "5"}

使用数组来初始化 Set 集合,并且同时可以出去重复的元素。

js
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
console.log(set.size); // 5
console.log(set); // Set(5) {1, 2, 3, 4, 5}

NOTE

实际上,Set 构造函数可以接受所有可迭代对象作为参数,数组、Set 集合、Map 集合都是可迭代的,因而都可以作为 Set 构造函数的参数使用;构造函数通过迭代器从参数中提取值。

通过 has() 方法可以检测 Set 集合中是否存在某个值

js
let set = new Set();
set.add(5);
set.add("5");
console.log(set.has(5)); // true
console.log(set.has(6)); // false

移除元素

delete() 方法可以移除 Set 集合中的某一个元素;

clear() 方法可以移除集合中所有元素。

js
let set = new Set();
set.add(5);
set.add("5");

console.log(set.has(5)); // true
console.log(set.size); // 2

set.delete(5);

console.log(set.has(5)); // false
console.log(set.size); // 1

set.clear();

console.log(set.has("5")); // false
console.log(set.size); // 0

Set 集合的 forEach() 方法

forEach() 方法的回调函数接受以下 3 个参数:

  1. Set 集合中下一次索引的位置
  2. 与第一个参数一样的值
  3. 被遍历的 Set 集合本身

需要注意的是这里的第一和第二个参数是一样的。这是为了和数组和 Map 集合的 forEach() 方法统一。

js
let set = new Set([1, 2]);

set.forEach(function(value, key, ownerSet) {
    console.log(key + " " + value);
    console.log(ownerSet === set);
});

// 输出结果:
// 1 1
// 5 true
// 4 2 2
// 5 true

如果需要在回调函数中使用 this 引用,则要将 this 作为第二个参数传入 forEach() 函数。

js
let set = new Set([1, 2]);

let processor = {
    output(value) {
        console.log(value);
    },
    process(dataSet) {
        dataSet.forEach(function(value) {
            this.output(value);
        }, this);
    }
};

processor.process(set);

这里可以使用箭头函数,这样就可以不用再将 this 传入 forEach() 方法了。

js
let set = new Set([1, 2]);

let processor = {
    output(value) {
        console.log(value);
    },
    process(dataSet) {
        dataSet.forEach(value => this.output(value));
    }
};

processor.process(set);

将 Set 集合转换为数组

js
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
    array = [...set];
console.log(array); // [1, 2, 3, 4, 5]

使用 Set 集合实现数组的去重

js
function eliminateDuplicates(items) {
    return [...new Set(items)];
}

let numbers = [1, 2, 3, 3, 3, 4, 5],
    noDuplicates = eliminateDuplicates(numbers);

console.log(noDuplicates);

Weak Set 集合

弱引用的 Set 集合。用法同 Set 集合基本一样。

js
let set = new WeakSet(),
    key = {};

// 向集合中添加对象
set.add(key);

console.log(set.has(key)); // true

set.delete(key);

console.log(set.has(key)); // false

两种 Set 类型的主要区别

Weak Set 保存的是对象值得弱引用。

当 Weak Set 中的某个对象的所有强引用都被移除的时候,该对象也会自动从 Weak Set 中移除。

js
let set = new WeakSet(),
    key = {};

// 向集合中添加对象
set.add(key);

console.log(set.has(key)); // true

// 移除对象 key 的最后一个强引用(Weak Set 中的引用也会自动移除)
key = null;

Weak Set 集合和普通 Set 集合的差别

  1. 在 WeakSet 的实例中,如果向 add()has()delete() 这 3 个方法传入非对象参数都会导致程序报错;
  2. Weak Set 集合不可迭代,所以不能被用于 for-of 循环;
  3. Weak Set 集合不暴露任何迭代器(例如 keys()values() 方法),所以无法通过程序本身来检测其中的内容;
  4. Weak Set 集合不支持 forEach() 方法;
  5. Weak Set 集合不支持 size 属性。

Weak Set 集合的功能看似受限,其实这是为了让它能够正确的处理内存中数据。

总之,如果你只需要跟踪对象引用,你更应该使用 Weak Set 集合而不是 Set 集合。

ES6 中的 Map 集合

ES6 中的 Map 类型是一种储存着许多键值对的有序列表,其中键名和对应的值支持所有的数据类型。

键名的等价判断是通过 Object.is() 方法实现的。

js
let map = new Map();
map.set("site", "liujiajia.me");
map.set("year", 2017);

console.log(map.get("site")); // "liujiajia.me"
console.log(map.get("year")); // 2017

使用对象作为键名

js
let map = new Map,
    key1 = {},
    key2 = {};

map.set(key1, 9);
map.set(key2, 32);

console.log(map.get(key1)); // 9
console.log(map.get(key2)); // 32

Map 集合支持的方法

Map 集合和 Set 集合有如下 3 个通用方法:

  • has(key)
    检测指定的键名在 Map 集合中是否存在

  • delete(key)
    从 Map 集合中移除指定键名及其对应的值

  • clear()
    移除 Map 集合中所有的键值对

js
let map = new Map();
map.set("name", "JiaJia");
map.set("age", 32);

console.log(map.has("name")); // true
console.log(map.get("name")); // "JiaJia"
console.log(map.has("age")); // true
console.log(map.get("age")); // 32
console.log(map.size); // 2

map.delete("name");
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.size); // 1

map.clear();
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.has("age")); // false
console.log(map.get("age")); // undefined
console.log(map.size); // 0

Map 集合的初始化方法

可以传入一个数组来初始化 Map 集合。

数组中的每一个元素都是一个子数组,子数组中包含一个键值对的键名和值两个元素。

js
let map = new Map([["name", "JiaJia"], ["age", 32]]);

console.log(map.has("name")); // true
console.log(map.get("name")); // "JiaJia"
console.log(map.has("age")); // true
console.log(map.get("age")); // 32
console.log(map.size); // 2

Map 集合的 forEach() 方法

和数组的 forEach() 方法类似,回调函数都接受 3 个参数:

  • Map 集合中下一次索引的位置
  • 值对应的键名
  • Map 集合本身
js
let map = new Map([["name", "JiaJia"], ["age", 32]]);

map.forEach(function(value, key, ownerMap) {
    console.log(key + " " + value);
    console.log(ownerMap === map);
});
// 执行结果:
// name JiaJia
// true
// age 32
// true

同 Set 集合一样,可以指定 forEach() 方法的第二个参数作为回调函数的 this 值。

Weak Map 集合

Weak Map 是弱引用的 Map 集合,也用于存储对象的弱引用。

  • Weak Map 集合中的键名必须是对象;

  • 只有键名保存的是弱引用,键名对应的值如果是个对象,则保存的是该对象的强引用。

Weak Map 最大的用途是保存 Web 页面中的 DOM 元素。

js
let map = new WeakMap(),
    element = document.querySelector(".element");

map.set(element, "Original");

let value = map.get(element);
console.log(value); // "Original"

// 移除 element 元素
element.parentNode.removeChild(element);
element = null;

// 此时 Weak Map 集合为空

私有对象数据

Weak Map 还可以用于存储对象实例的私有数据。

在 ES6 中所有属性都是公开的。如果想存储一些只对对象开放的数据,则需要一些创造力。

下例使用约定作为私有属性:

js
function Person(name) {
    this._name = name;
}

Person.prototype.getName = function() {
    return this._name;
};

看似是只允许通过 getName() 方法获取 name 属性,但其实仍然可以通过给实例的 _name 属性赋值来更改该属性值。

js
let user = new Person("JiaJia")
console.log(user.getName()); // "JiaJia"
user._name = "Dlph";
console.log(user.getName()); // "Dlph"

在 ES5 中,可以通过下面这种模式创建一个对象接近真正的私有数据。

js
var Person = (function() {
    var privateData = {},
        privateId = 0;
    
    function Person(name) {
        Object.defineProperty(this, "_id", { value: privateId++ });

        privateData[this._id] = {
            name: name
        };
    }

    Person.prototype.getName = function() {
        return privateData[this._id].name;
    }

    return Person;
}());

let user1 = new Person("JiaJia");
console.log(user1); // Person {_id: 0}
console.log(user1.getName()); // "JiaJia"

var user2 = new Person("Dlph");
console.log(user2); // Person {_id: 1}
console.log(user2.getName()); // "Dlph"

上例中的 privateDataprivateId 变量被隐藏了起来,在外面无法查看及更改。

该方法最大的问题是,如果不主动管理,由于无法获知对象实例何时被销毁,因此 privateData 中的数据就永远不会消失。而使用 Weak Map 集合就可以解决该问题。

js
let Person = (function() {
    let privateData = new WeakMap();
    
    function Person(name) {
        privateData.set(this, { name });
    }

    Person.prototype.getName = function() {
        return privateData.get(this).name;
    }

    return Person;
}());

let user1 = new Person("JiaJia");
console.log(user1); // Person {}
console.log(user1.getName()); // "JiaJia"

let user2 = new Person("Dlph");
console.log(user2); // Person {}
console.log(user2.getName()); // "Dlph"

user1 = null;
user2 = null;

使用 Weak Map 来存储私有数据,只要对象实例被销毁,相关信息也会被销毁,从而保证了信息的私有性。

Weak Map 集合的使用方式及使用限制

当要在 Weak Map 和 普通 Map 集合之间进行选择时,如果只用对象作为集合的键名,那么 Weak Map 是最好的选择。

相对于 Map 集合而言,Weak Map 集合对用户的可见度更低,其不支持通过 forEach() 方法、size 属性及 clear() 方法来管理集合中的元素。

如果只想使用非对象名作为键名,那么普通的 Map 集合是唯一的选择。