ES6学习笔记6--Set与Map

  |  

前言

Set个Map类型


7



ES5中的Set和Map

在ES5中,开发者使用对象属性来模拟Set和Map

1
2
3
4
5
let set = Object.create(null);
set.foo = true;
if(set.foo) {

}

本例中的set变量是一个原型null的对象,确保在此对象没有继承属性。使用对象的属性作为需要检查的唯一值在ES5中是很常用的方法。当一个属性被添加到set对象时,它的值也被设为true,因此条件判断语句就可以简单判断出该值是否存在。

使用对象模拟Set与模拟Map之间唯一真正的区别是所存储的值。Map皴法的是数据

变通方法的问题

尽管在简单情况下将对象作为Set与Map来使用都是可行的,但是一旦接触到对象属性的局限性,此方式就会遇到更多麻烦。例如,由于对象属性名的类型必须是字符串,你就必须保证任意两个键不能被转换为相同的字符串

1
2
3
let map = Object.create(null);
map[5] = "foo";
console.log(map["5"]); //"foo"

数值类型的键会在内部被转换为字符串,因此map[“5”]与map[5]实际上引用了同一个属性。当想将数值和字符串都作为键来使用时,这种内部转换就会引起问题。而若使用对象作为键,就会出现另一个问题

1
2
3
4
5
let map = Object.create(null),
key1 = {},
key2 = {};
map[key1] = "foo";
console.log(map[key2]); //"foo"

由于对象的属性名只能是字符串,key1与key2都均会被转换成”[object Object]”。

将对象转换为默认的字符串表现形式,使得对象很难被当作Map的键来使用

ES6的Set

ES6新增了Set类型,这是一种无重复值的有序列表。Set允许对他包含的数据进行快速访问,从而增加了一个追踪离散值的更有效方式

创建Set并添加项目

Set使用new Set()来创建,而调用add()方法就能向Set中添加项目,检查size属性还能查看其中包含有多少项:

1
2
3
4
let set = new Set();
set.add(5);
set.add("5");
console.log(set.size); // 2

Set不会使用强制类型转换来判断值是否重复。Set内部的比较使用Object.is()方法,来判断两个值是否相等,唯一例外的是+0与-0在Set中被判断为是相等的。除此之外还可以向Set中添加多个对象,它们不会被合并为同一项

1
2
3
4
5
6
let set = new Set(),
key1 = {},
key2 = {};
set.add(key1);
set.add(key2);
console.log(set.size);

如果add()方法用相同值进行多次调用,那么在第一次之后的调用实际上会被忽略

可以使用数组来初始化一个Set,并且Set构造器会确保不重复地使用这些值

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

set构造器实际上可以接收任意可迭代对象作为参数。Set构造器会使用迭代器来提取参数中的值(数组就是可迭代的)

可以使用has()方法来测试某个值是否存在于Set中

1
2
3
4
5
let set = new Set();
set.add(5);
set.add("6");
console.log(set.has(5)); // true
console.log(set.has(6)); // false

移除值

可以使用delete()方法来移除单个值,或调用clear()方法来将所有值从Set中移除

1
2
3
4
5
6
7
let set = new Set();
set.add(5);
set.add("6");
set.delete(5);
console.log(set.size); // 1
set.clear();
console.log(set.size); //0

Set上的forEach()方法

Set类型也添加了数组中的forEach()方法,其工作方式相同,forEach()方法会被传递一个回调方法,该回调接收三个参数:

  • Set中下个位置的值
  • 与第一个参数相同的值
  • 目标Set自身

Set版本的forEach()与数组的不同就在于它的第一第二个参数相同,因为Set没有键,但是为了让forEach()方法的统一,所以让回调函数前两个参数相等。

除了回调函数的参数差异外,其它与在数组上的没有区别

1
2
3
4
5
let set = new Set([1, 2]);
set.forEach(function(value, key, ownerSet) {
console.log(key + " " + value);
console.log(ownerSet === set);
})

与使用数组相同,如果想在回调函数中使用this,可以给forEach()传入一个this值作为第二个参数

1
2
3
4
5
6
7
8
9
10
11
12
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参数

1
2
3
process(dataSet) {
dataSet.forEach((value) => this.output(value));
}

将Set转换为数组

Set虽然能非常好地追踪值,但是却无法像数组那样用索引来直接访问某个值,所以我们可以将Set转换为数组。转换也很简单,可以使用扩展运算符

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

Set在初始化时载入了一个包含重复值的数组,Set清除了重复值后使用扩展运算符将自身的项放到一个新数组中

Weak Set

由于Set类型存放对象采取的是存放引用的方式,所以它可以被成为Strong Set。只要对于Set实例的引用仍然村反,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存

1
2
3
4
5
6
7
let set = new Set();
let key = {};
set.add(key);
console.log(set.size); // 1
key = null
console.log(set.size); // 1
ket = [...set][0];

ES6包含Weak Set来解决上述问题,Weak Set只允许存储对象的弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收

创建Weak Set

Weak Set使用WeakSet构造器来创建,包含add、has以及delete方法,可以给构造器传入一个可迭代对象来初始化Weak Set的值。

Weak Set和Set之间最大的差异在于弱引用

1
2
3
4
5
6
let set = new WeakSet(),
key = {};
set.add(key);
console.log(set.has(key));// true
key = null;
console.log(set..has(key));// false

Weak Set的特征:

  • Weak Set的实例,如果调用add()方法传入非对象参数,就会抛出错误,has()和delete()在传入非对象参数时返回false
  • Weak Set 不可迭代,不能被用于for-of循环
  • Weak Set无法暴露出任何迭代器,因此没有任何编程手段可用于判断Weak Set的内容
  • Weak Set没有forEach()方法
  • Weak Set没有size属性

ES6中的Map

ES6的Map类型是键值对的有序列表,键和值都可以是任意类型。键的内部比较使用Object.is方法,所以数字和字符并不会成为相同的键。

可以通过调用set()方法并传递一个键和值作为参数来给Map添加项。可以使用get()方法并传入键名来获得对应的值

1
2
3
4
let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2016);
console.log(map.get("title")); // "Understanding ES6"

Map的方法

Map同样包含has()、delete()和clear()方法。

Map同样包含size属性用以表明包含了多少个键值对

Map的初始化

同样可以使用数组来初始化Map,但是这里的数组与Set的数组不同。该数组的每一项都必须是数组,数组的第一项会作为键名,第二项会成为对应的值

1
let map = new Map([["name", "Nicholas"], ["age", 25]]);

Map上的forEach方法

Map的forEach()方法类似于Set的同名方法,它接收一个三参数的回调函数,三个参数分别是:

  • Map中下一个位置的值
  • 该值对应的键
  • 目标Map本省
1
2
3
4
5
let map = new Map([["name", "Nicholas"], ["age", 25]]);
map.forEach(function(value, key, ownerMap) {
console.log(key + " " + value);
console.log(ownerMap === map);
})

同样可以给回调函数指定第二个参数this

Weak Map

在Weak Map中,所有的键都必须是对象,而且这些对象都是弱引用,不阻止来及回收。注意,键必须是对象,值不一定是

使用Weak Map

ES6的WeakMap类型是键值对的无序列表,其中的键必须是非空的对象,值允许是任意类型。WeakMap同样使用set和get方法

1
2
3
4
5
6
7
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, 'Original');
let value = map.get(element);
console.log(value); // "Original"
element.parentNode.removeChild(element);
element = null;

在这个例子中,element键是一个DOM元素,用于存储一个有关联的字符串值。当DOM元素从文档树中移除时,将引用它的变量设置为null,对应的数据也就会在Weak Map中被移除。

类似于Weak Set,Weak Map没有size属性,没有任何办法可以确定Weak Map是否为空。当其它引用被移除后,就没办法使用get()方法来提取对应的值。Weak Map已经切断了对该值的所有访问。

Weak Map同样可以使用二维数组来初始化

Weak Map的方法

Weak Map只有两个附加方法,has()和delete()。clear()方法并不存在,应为无法枚举Weak Map

对象的私有数据

很多人认为Weak Map的主要用于在于关联数据和DOM元素。但是除此之外Weak Map还有很多其它用法,其中一个就是给对象设置私有数据。

在ES5中我们能创建的最接近于私有的数据,只能采用IIFE的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}());
var test1 = new Person("test1");
var test2 = new Person("test2");
//test1._id = 0;
//test2._id = 1

这个例子使用立即执行函数包裹Person的定义,并在其中定义了两个私有属性:privateData和privateId。当Person构造器被调用时,会创建一个不可枚举、不可配置、不可写入的_id属性。接下来在privateData对象中创建一个与实例ID对应的入口,并存放name的值。

随后在Person的原型上定义getName()方法,使用this._id作为privateData的键来提取值。

由于privateData无法从IIFE外部访问,实际的数据可以理解为私有的安全的,但是this._id在privateData上依然公开。

但是这种模式的最大确定就是privateData的数据将永远存在在内存中,对象实例被销毁时没有任何方法可以获得或操作该值。

但是,这个问题我们可以用WeakMap来解决

1
2
3
4
5
6
7
8
9
10
let Person(function() {
let privateData = new WeakMap();
function Person(name) {
privateData.set(this, {name: name});
}
Person.prototype.getName = function() {
return privateData.get(this).name
}
return Person;
}());

这个例子我们使用Weak Map而不是对象来存储私有数据,且不再使用_id这样额外的变量,而是直接使用实例对象本身(也就是里面的this)作为键,所以当实例对象被销毁时,WeakMap中对应的键值对也就别销毁,这样就不会出现额外的内存占用。

WeakMap的用法与局限性

当我们只想使用对象类型的键时,应该首先考虑使用WeakMap。因为他能确保额外数据不再可能后被销毁,从而优化内存使用并且避免内存泄露。

当然如果不可避免的要使用到非对象键,那么我们就只能使用Map。

文章目录
  1. 1. ES5中的Set和Map
  2. 2. 变通方法的问题
  3. 3. ES6的Set
    1. 3.1. 创建Set并添加项目
    2. 3.2. 移除值
    3. 3.3. Set上的forEach()方法
    4. 3.4. 将Set转换为数组
    5. 3.5. Weak Set
  4. 4. ES6中的Map
    1. 4.1. Map的方法
    2. 4.2. Map的初始化
    3. 4.3. Map上的forEach方法
    4. 4.4. Weak Map
    5. 4.5. 对象的私有数据
|