Js大部分历史时期都只存在一种集合类型,也就是数组类型。数组在 JS 中的使用正如其他语言的数组一样,但缺少更多类型的集合导致数组也经常被当作队列与栈来使用。数组只使用了数值型的索引,而如果非数值型的索引是必要的,开发者便会使用非数组的对象。这种技巧引出了非数组对象的定制实现,即 Set 与 Map 。
Set:
Set概述
Set结构类似于数组,但是没有重复结构
Set会自动移除重复的值,因此可以用来过滤数组中的重复结构
Set内的对象是强引用
Set的构造:
a) let set = new Set([1, 2, 3, 4]);
Set 构造器实际上可以接收任意可迭代对象作为参数。能使用数组是因为它们默认就是可迭代的,Set与Map也是一样,Set构造器会使用迭代器来提取参数中的值
b) let set = new Set();
set.add(1);
set.add(2);
set.add(2);
//如果add对相同的值进行了多次调用,那么那么在第一次之后的调用实际上会被忽略
set.add(3);
set.add('3');
// Set 不会使用强制类型转换来判断值是否重复。这意味着 Set 可以同时包含数值 3 与 字符串 "3" ,将它们都作为相对独立的项.
Set判断是否重复使用了Object.is() 方法,唯一的例外是 +0 与 -0 在 Set 中被判断为是相等的
c) 还可以向Set添加多个值相同的对象,他们不会被合并为同一项。
let set = new Set();
let key1 = {};
let key2 = {};
set.add(key1);
set.add(key2);
Set的属性及方法:
1、属性
Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数。
2、方法
Set的方法分为两类:操作方法 和 遍历方法
2.1、操作方法:
• add(value):添加某个值,返回 Set 结构本身。
因为返回set本身,所以可以写成:
set.add(1).add(2).add(3)
• delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
set.delete(2) ; // true
• has(value):返回一个布尔值,表示该值是否为Set的成员。
set.has(2) ; // false
• clear():清除所有成员,没有返回值。
2.2、遍历方法:
forEach():使用回调函数遍历每个成员
es5给数组添加了forEach()方法,使得更容易处理数组中的每一项,没有返回值
对于数组来说forEach的三个参数:
arr[].forEach(function(value,index,array){//do})
value数组中的当前项, index当前项的索引, array原始数组
但是对于Set来说:
let set2 = new Set([{'a':1}, {'b':2}]);
set2.forEach(function(value, key, ownerSet) {
console.log(ownerSet === set2);
console.log(value === key);
});
key 与 value 总是相同的,同时 ownerSet 也始终等于 set 。此代码输出:
true
true
true
true
如果想在回调函数中使用当前的this,还可以在forEach中传入this作为第二个参数
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);
本例中,在processor.process方法中的set调用了forEach方法,并传递了this作为第二个参数,这样便能正确的调用到this.output()方法
或者也可以使用箭头函数来达到相同的效果,无需传入this,只需将上边的process改写成:
process(dataSet) {
dataSet.forEach((value) => this.output(value));
}
keys()、values()和entries()
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
keys方法、values方法、entries方法返回的都是遍历器对象,由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
这就意味着,可以省略values方法,直接用for…of循环遍历Set
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red
// green
// blue
Set常用场景
Set最经典的应用就是数组排重,
Set的内部实现
Set的对象通过指针指向的内存中的地址来做比较,所以要判断对象是否相等,只能通过遍历去判断,这其中包含了很多边际情况。所以set没有去判断这些,只要往里面扔一个对象,那么就是一个新的元素
Set与其他数据结构的互相转换
1、 Set与数组的转换
1.1、 Set转数组
使用扩展运算符可以很简单的将Set转成数组
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
let array = [...set];
console.log(array); // [1,2,3,4,5]
该示例很好的说明了Set的一个重要的功能,数组排重,当然也很好的展示了,如何从Set转成数组的过程。
注意,这种转变是生成了一个新的数组对象原来的Set依然存在。
1.2、 数组转Set
如最初的构造示例:
let set = new Set([1,4,5,6,7]);
2、 Set与其他格式互换
WeakSet:
主要写自身特性和与Set的区别?
验证:外部赋空看内部是否变化?
外部回收后?
由于 Set 类型存储对象引用的方式,它也可以被称为 Strong Set 。对象存储在 Set 的一个实例中时,实际上相当于把对象存储在变量中。只要对于 Set 实例的引用仍然存在,所存储的对象就无法被垃圾回收机制回收,从而无法释放内存。
当 JS 代码在网页中运行,同时你想保持与 DOM 元素的联系,在该元素可能被其他脚本移除的情况下,你应当不希望自己的代码保留对该 DOM 元素的最后一个引用(这种情况被称为内存泄漏)
为了缓解这个问题, ES6 也包含了 Weak Set ,该类型只允许存储对象弱引用,而不能存储基本类型的值。对象的弱引用在它自己成为该对象的唯一引用时,不会阻止垃圾回收
WeakSet的构造:
WeakSet的属性和方法:
WeakSet 结构有以下三个方法。
WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
WeakSet的特性
- 对于 WeakSet 的实例,若调用 add() 方法时传入了非对象的参数,就会抛出错误(
has() 或 delete() 则会在传入了非对象的参数时返回 false );
- Weak Set 不可迭代,因此不能被用在 for-of 循环中;
- Weak Set 无法暴露出任何迭代器(例如 keys() 与 values() 方法),因此没有任何编
程手段可用于判断 Weak Set 的内容;
- Weak Set 没有 forEach() 方法;
- Weak Set 没有 size 属性。
WeakSet之所以不可遍历是由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
WeakSet常用场景
Weak Set 看起来功能有限,而这对于正确管理内存而言是必要的。一般来说,若只想追踪对象的引用,应当使用 Weak Set 而不是正规 Set
如果Set中引用了不再需要的大型对象,如已经从DOM树中删除的DOM元素,那么其回收代价是昂贵的
Map:
Map概述
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。
Map的构造
ES6 的 Map 类型是键值对的有序列表,而键和值都可以是任意类型。键的比较使用的是Object.is() ,因此你能将 5 与 "5" 同时作为键,因为它们类型不同。这与使用对象属性
作为键的方式(指的是用对象来模拟 Map )截然不同,因为对象的属性会被强制转换为字符串。
set方法构造
你可以调用 set() 方法并给它传递一个键与一个关联的值,来给 Map 添加项;此后使用键名来调用 get() 方法便能提取对应的值。例如:
let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2016);
console.log(map.get("title")); // "Understanding ES6"
console.log(map.get("year")); // 2016
数组构造
依然与 Set 类似,你能将数组传递给 Map 构造器,以便使用数据来初始化一个 Map 。该数组中的每一项也必须是数组,内部数组的首个项会作为键,第二项则为对应值。因此整个Map 就被这些双项数组所填充。例如:
let map = new Map([["name", "Nicholas"], ["age", 25]]);
console.log(map.has("name")); // true
console.log(map.get("name")); // "Nicholas"
console.log(map.size); // 2
通过构造器中的初始化, "name" 与 "age" 这两个键就被添加到 map 变量中。虽然由数组构成的数组看起来有点奇怪,这对于准确表示键来说却是必要的:因为键允许是任意数据类型,将键存储在数组中,是确保它们在被添加到 Map 之前不会被强制转换为其他类型的唯一方法。
Map构造函数接受数组作为参数,实际上执行的是下面的算法。
const items = [
["name", "Nicholas"]
["age", 25] ]
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当做Map构造函数的参数。这就是说,Set和Map都可以用来生成新的Map
其他构造
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
上面代码中,我们分别使用 Set 对象和 Map 对象,当作Map构造函数的参数,结果都生成了新的 Map 对象。
Map的属性和方法
1、属性
Map 同样拥有 size 属性,用于指明包含了多少个键值对。以下代码用不同方式使用了这三种方法以及 size 属性:
2方法
2.1、操作方法
2.11、set(key, value)
set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
const m = new Map();
m.set('edition', 6) // 键是字符串
m.set(262, 'standard') // 键是数值
m.set(undefined, 'nah') // 键是 undefined
set方法返回的是当前的Map对象,因此可以采用链式写法。
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
2.12、get(key)
get方法读取key对应的键值,如果找不到key,返回undefined。
const m = new Map();
const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数
m.get(hello) // Hello ES6!
2.13、has、delete、clear
Map 与 Set 共享了几个方法,这是有意的,允许你使用相似的方式来与 Map 及 Set 进行交互。以下三个方法在 Map 与 Set 上都存在:
2.13.1、has(key)
has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition') // true
m.has('years') // false
m.has(262) // true
m.has(undefined) // true
2.13.2、delete(key)
delete方法删除某个键,返回true。如果删除失败,返回false。
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined) // true
m.delete(undefined)
m.has(undefined) // false
2.13.4、clear()
clear方法清除所有成员,没有返回值。
let map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
map.clear()
map.size // 0
2.2、遍历方法
2.2.1遍历器生成函数
• keys():返回键名的遍历器。
• values():返回键值的遍历器。
• entries():返回所有成员的遍历器
需要特别注意的是,Map 的遍历顺序就是插入顺序。
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
2.2.2、forEach
Map 的 forEach() 方法类似于 Set 与数组的同名方法,它接受一个能接收三个参数的回调函数:
- Map 中下个位置的值;
- 该值所对应的键;
- 目标 Map 自身。
回调函数的这些参数更紧密契合了数组 forEach() 方法的行为,即:第一个参数是值、第二个参数则是键(数组中的键是数值索引)。此处有个示例:
let map = new Map([
["name", "Nicholas"],
["age", 25]
]);
map.forEach(function(value, key, ownerMap) {
console.log(key + " " + value);
console.log(ownerMap === map);
});
forEach() 的回调函数输出了传给它的信息。其中 value 与 key 被直接输出, ownerMap
与 map 进行了比较,说明它们是相等的。这就输出了:
name Nicholas
true
age 25
true
Map的内部实现
Map与其他数据结构的相互装换
Map转数组:
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
数组转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
对象转为 Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
WeakMap:
WeakMap的特性
WeakMap与Map的区别有两点。
首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。
const e1 = document.getElementById('foo');
const e2 = document.getElementById('bar');
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素'],
];
上面代码中,e1和e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arr对e1和e2的引用。
一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1和e2占用的内存。
// 不需要 e1 和 e2 的时候
// 必须手动删除引用
arr [0] = null;
arr [1] = null;
上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
WeakMap的构造:
ES6 的 WeakMap 类型是键值对的无序列表,其中键必须是非空的对象,值则允许是任意类型。 WeakMap 的接口与 Map 的非常相似
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2
// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"
WeakMap的属性和方法:
WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。
const wm = new WeakMap();
// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined
WeakMap常用场景
Weak Map 的最佳用武之地,就是在浏览器中创建一个关联到特定 DOM 元素的对象。例如,某些用在网页上的 JS 库会维护一个自定义对象,用于引用该库所使用的每一个 DOM 元素,并且其映射关系会存储在内部的对象缓存中。
该方法的困难之处在于:如何判断一个 DOM 元素已不复存在于网页中,以便该库能移除此元素的关联对象。若做不到,该库就会继续保持对 DOM 元素的一个无效引用,并造成内存泄漏。使用 Weak Map 来追踪 DOM 元素,依然允许将自定义对象关联到每个 DOM 元素,而在此对象所关联的 DOM 元素不复存在时,它就会在 Weak Map 中被自动销毁。
必须注意的是, Weak Map 的键才是弱引用,而值不是。在 Weak Map 的值中存储对象会阻止垃圾回收,即使该对象的其他引用已全都被移除。
当决定是要使用 Weak Map 还是使用正规 Map 时,首要考虑因素在于你是否只想使用对象类型的键。如果你打算这么做,那么最好的选择就是 Weak Map 。因为它能确保额外数据在不再可用后被销毁,从而能优化内存使用并规避内存泄漏。
要记住 Weak Map 只为它们的内容提供了很小的可见度,因此你不能使用 forEach() 方法、size 属性或 clear() 方法来管理其中的项。如果你确实需要一些检测功能,那么正规 Map会是更好的选择,只是一定要确保留意内存的使用。
//map和set 比较的方法是不是一样
NaN 和 +0 -0
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。