Set 、WeakSet、Map、WeakMap用法

本文深入解析ES6新增的数据结构Set、WeakSet、Map和WeakMap,介绍它们的特性和使用方法,包括实例属性、操作方法、遍历方法及初始化过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Set

ES6提供了新的数据结构 —— Set 。它类似于数组,但是成员的值都是唯一的,没有重复。Set 本身是一个构造函数,用来生成Set数据结构

[Set结构的实例属性]

  • Set.Prototype.constructor: 构造函数,默认就是Set函数
  • Set.prototypr.size: 返回Set实例的成员总数

Set实例的方法分为两大类: 操作方法(用于操作数据)和遍历方法(用于遍历成员)。

[Set 实例的操作方法]

  • add(value): 添加某个值,返回Set结果本身
  • delete(value): 删除某个值,返回一个布尔值,表示删除是否成功
    + has(value): 返回一个布尔值,表示参数是否为Set成员
    + clear(value): 清除所有成员,没有返回值

[创建Set集合、add添加元素]

调用new Set()创建Set实例集合,调用add()方法向集合中添加元素。访问属性size,得到集合中元素数量

    let set = new Set();
    set.add(5);
    set.add('5');
    console.log(set.size);// 2

在Set集合中,不会对所存元素强制的类型转换,数字5和字符串‘5’可以作为两个独立元素存在.

Set 内部判断两个值是否相同使用的算法是“same-value equality”,它类似于精确相等运算符(===),主要的区别是NaN等于本身,而精确相等运算符认为NaN不等于自身。

let set  = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
console.log(set);// Set(1) {NaN}

另外两个对象总是不想等的,它们之间彼此保持独立

let set  = new Set();
set.add({});
console.log(set.size);// 1
set.add({});
console.log(set.size);// 2

Set结构不会添加重复的值:

 const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(function (item) {
    s.add(item);
})
for(let i of s) {
    console.log(i);
}
//  2 3 5 4

上面的代码用add方法向Set结构加入成员,结构表明Set结构不会添加重复的值。

[Set初始化]

Set函数可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化.

// 例1:

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

// 例2:
function divs() {
    return [...document.querySelectorAll('div')];
}
const set  = new Set(divs());
console.log(set.size);// 3

上面两个例子中Set函数分别接受数组和一个类似数组的对象作为参数。

[注意]:

Set集合不能像访问数组那样直接通过索引访问集合中的元素。如果需要,最好先将Set集合装换成一个数组。

方法就是像上例一样([…new Set(array)]),将数组传入Set的构造函数中,在使用扩展运算符(…)将Set集合转为数组。

另外,这也展示了一种数组去重的方法:

扩展运算符与Set结构相结合就可以去除数组的重复元素

[has()检测元素]

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

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

[delete()和clear()移除元素]

调用delete()方法可以移除Set集合中的某一个元素,调用clear()方法会移除集合中的所有元素

let set  = new Set();
set.add(5);
set.add('5');
console.log(set.has(5));// true
set.delete(5);
console.log(set.has(5));// false
console.log(set.size);// 1
set.clear();
console.log(set.size);// 0

[遍历方法]

Set实例有4个遍历方法:

  • Keys(): 返回键名的遍历器
  • values(): 返回键值的遍历器
  • entries(): 返回键值对的遍历器
  • forEach(): 使用回调函数遍历每个成员

[注意]:

Set 的遍历顺序就是插入顺序。

[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方法

console.log( 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

[forEach()]

Set 结构实例的forEach方法用于对每个成员执行某种操作,没有返回值。

let set  = new Set(['a', 'b', 'c']);
set.forEach(function(value, key, set) {
    console.log(value, key, set);
} )
//a a Set(3){'a','b','c'}
//b b Set(3){'a','b','c'}
//c c Set(3){'a','b','c'}

上面的代码说明,forEach方法的参数是一个处理函数。该函数的参数依次为键值、键名、集合本身.

在Set集合的forEach()方法中,也可以有第二个参数this,表示绑定的this对象。

    let set  = new Set([1, 2]);
    let obj = {
       output(value) {
           console.log(value);
       },
       process(dataSet) {
           dataSet.forEach(function(value){
               this.output(value);
           }, this);
       }
   }
    obj.process(set);
   // 1
   // 2

上面的代码中,obj.process()方法调用了Set集合的forEach()方法并将this传入作为回调函数的this值,从而 this.output()方法可以正确的调用obj.output()方法。

这里可以使用箭头函数,这样就不需要将this作为第二个 参数传入回调函数中。

    let set  = new Set([1, 2]);
    let obj = {
       output(value) {
           console.log(value);
       },
       process(dataSet) {
           dataSet.forEach( value => this.output(value));
       }
   }
    obj.process(set);

WeakSet

WeakSet结构与Set类似,也是不重复的值的集合。但它与Set有两个区别:

  • WeakSet 的成员只能是对象,而不能是其他类型的值
    let ws = new WeakSet();
    ws.add(1); // TypeError: Invalid value used in weak set
    ws.add(Symbol(1));// TypeError: Invalid value used in weak set
  • WeakSet中的对象都是弱引用,即如果其他对象都不在引用该对象,那么垃圾回收机制会自动回收该对象所占用的空间。但在Set中,只要Set实例存在引用,垃圾回收机制就不能释放该对象的内存空间,即Set类型可以看作是一个强引用。

在Set中:

let set = new Set();
let key = {
    value:1
};
set.add(key);
console.log(set.size);// 1
// 取消原始引用
key = null;
console.log(set.size);// 1
// 重新获得原始引用
key = [...set][0];   
console.log(key);// {value: 1}

外部设置key = null,而Set内部并没有取消引用。

在WeakSet中:

let ws = new WeakSet();
let key = [1, 2];
ws.add(key);
console.log(ws.has(key));// true
key = null;
console.log(ws.has(key));// false

外部设置key = null,WeakSet内部的引用也取消

[创建WeakSet集合]

WeakSet是一个构造函数,可以使用new命令创建WeakSet集合。集合支持3个方法:add()、has()和delete()

  • WeakSet.prototype.add(value): 向WeakSet实例添加一个新成员
  • WeakSet.prototype.delete(value): 清除WeakSet实例的指定成员
  • WeakSet.prototype.has(value): 返回一个布尔值,表示某个值是否在WeakSet实例中。
    let set  = new WeakSet()
    let key = {};
    set.add(key);
    console.log(set.has(key));// true
    set.delete(key);
    console.log(set.has(key)); // false

作为构造函数,WeakSet集合使用方式与Set集合类似,可以接受一个数组或类似数组的对象作为参数。实际上, 任何具有iterable接口的对象都可以作为WeakSet的参数。该数组的所有成员都会自动成为WeakSet实例的成员。

const a = [[1, 2],[3, 4]];
let ws = new WeakSet(a);
console.log(ws);// WeakSet { Array(2), Array(2)}

上面的代码中,a是一个数组,它的两个元素都是数组。将a作为WeakSet构造函数的参数,a的成员会自动成为WeakSet的成员。

[注意]: 成为WeakSet实例成员的是a数组的成员,而不是a本身。因为weakSet的成员只能是对象。

[与Set的区别]

WeakSet是弱引用,在其内部有多少个成员取决于垃圾回收机制有没有运行, 运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此es6规定weakSet是不可遍历的。

  • 在WeakSet的实例中,如果向add()、has()和delete()这三个方法传入非对象参数都会导致程序报错
  • WeakSet集合是不可遍历的,所以不能被用于for-of循环
    + WeakSet集合不暴露任何迭代器
    + WeakSet没有size属性,也没有forEach方法

虽然WeakSet集合的功能受限,但它能够正确处理内存中的数据。如果只需要跟踪对象引用,更应该使用WeakSet集合而不是普通的Set集合.

Map

js的对象,本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它带来了很大的限制。

为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串 —— 值”的对应,Map结构提供了“值 —— 值”的对应,是一种更完善的Hash结构实现。如果需要“键值对”的数据结构,Map比
Object更合适。

[注意]:

Map集合中将 +0 和-0视为相等。

[Map实例的操作方法]

  • Map实例支持size属性: 返回Map结构中的成员数目数量
  • set(key, value)方法: 设置Key所对应的键值,然后返回整个Map结构
  • get(key)方法: 读取key对应的键值,如果找不到key,返回undefined
    + has(key)方法: 返回一个布尔值,表示某个键是否在Map结构中
    + delete(key)方法: 删除某个键,返回true,删除失败返回false
    + clear()方法: 清除所有成员,没有返回值

[创建Map集合、使用操作方法]

    let map = new Map();
    map.set('title', 'Es6');
    map.set('year', 2017);
    console.log(map.get('title'))// Es6
    console.log(map.get('year'))// 2017

在对象中,无法用对象作为属性的键名。但是在Map集合中,却可以这样做:

    let map = new Map();
    key1 = {};
    key2 = {};
    map.set(key1, 1);
    map.set(key2, 42);
    console.log(map.get(key1));// 1
    console.log(map.get(key2));// 42

[使用操作方法]

let map = new Map();
map.set('name', 'xiaoqi');
map.set('age', 20);
console.log(map.size);// 2
console.log(map.has('name'));// true
console.log(map.get('name'));// xiaoqi
console.log(map.has('age'));// true
console.log(map.get('age'));// 20
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.has('age'));// false
console.log(map.size);// 0

[接受数组初始化Map集合]

作为构造函数,Map也接受数组作为参数初始化一个Map集合,这一点也与Set集合相似。数组中的每一个元素都是一个子数组, 子数组中包含一个键值对的键名与键值两个元素。

    let map = new Map([['name', 'xiaoqi'], ['age', 20]]);
    console.log(map.has('name'));// true
    console.log(map.get('name'));// xiaoqi
    console.log(map.has('age'));// true
    console.log(map.get('age'));// 20
    console.log(map.size);// 2

[同名键碰撞]

Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这样就解决了同名键碰撞的问题。

const map = new Map();
map.set(['a'], 555);
console.log( map.get(['a']));// undefined

上面的代码的set与get方法,表面是针对同一个键,但实际上这是两个值,内存地址不一样,因此get方法无法读取该键,返回undefined。

const map = new Map();
let k1 = ['a'];
let k2 = ['a'];
map.set(k1, 111).set(k2, 222);
console.log(map.get(k1));// 111
console.log(map.get(k2));// 222

上面代码中,变量k1和k2的值一样,但是它们在Map结构中被视为两个键

[Map实例的遍历方法]

Map 结构原生提供三个遍历器生成函数和一个遍历方法:

  • keys(): 返回键名的遍历器
    + values(): 返回键值的遍历器
    + entries(): 返回所有成员的遍历器
    + forEach():遍历Map的所有成员

[注意]:

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) {
    console.log(key, value);
}
 // "F" "no"
// "T" "yes"

Map结构的默认遍历器接口(Symbol iterable属性)就是entries方法

console.log( map[Symbol iterable] === map.entries )// true

[转为数组]

Map结构转为数组,比较快速的方法是使用扩展运算符(…)

    const map = new Map([
    [1, 'one'], 
    [2, 'two'], 
    [3, 'three']
]);
console.log( [...map.keys()] );// [1, 2, 3]

console.log( [...map.values()]);// ['one', 'two', 'three']

console.log( [...map.entries()])// [[1, 'one'], [2, 'two'], [3, 'three']]

console.log([...map]);// [[1, 'one'], [2, 'two'], [3, 'three']]

结合数组的map方法、filter方法,可以实现Map的遍历和过滤

  const map0 = new Map();
    map0.set(1, 'a');
    map0.set(2, 'b');
    map0.set(3, 'c');

    const map1 = new Map(
        [...map0].filter(([k, v]) => k < 3)
    );
   console.log(map1);// Map(2) {1 => "a", 2 => "b"}

   const map2 = new Map(
       [...map0].map(([k,v]) => [k * 2, '_' + v])
   );
   console.log(map2);// Map(3) {2 => "_a", 4 => "_b", 6 => "_c"}

[forEach()]

Map还有个forEach方法,与数组的forEach方法类似,也可以实现遍历

const map = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three']
]);
map.forEach((value, key, map) => {
    //one  1  Map(3) {1 => "one", 2 => "two", 3 => "three"}
    //two  2  Map(3) {1 => "one", 2 => "two", 3 => "three"}
    //three  3  Map(3) {1 => "one", 2 => "two", 3 => "three"}
    console.log(value, key, map);
})

[注意]:

遍历过程中,Map会按照键值对插入Map集合的顺序将相应信息传入forEach()方法的回调函数中。

forEach方法还可以接受第二个参数,用来绑定this

const reporter = {
    report: function (key, value) {
        console.log('key: %s, Value: %s', key,value);
    }
};
map.forEach(function(value, key, map) {
    this.report(key, value);
},reporter);
// key: 1, Value: one
//  key: 2, Value: two
//  key: 3, Value: three

上面的代码中,forEach方法的回调函数的this,就指向reporter。

WeakMap

WeakMap与Map结构类似,也用于生成键值对的集合。

[WeakMap与Map的区别]

  • WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

  • WeakMap的键名所指向的对象不计入垃圾回收机制。它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此所有引用的对象的其他引用都被清除,垃圾回收站就会释放该对象所占用的内存。

  • WeakMap没有遍历操作(即没有key()、values()和entries()方法),也没有size属性。

  • WeakMap无法清空,即不支持clear()方法。

[使用WeakMap集合]

Es6中的WeakMap类型是一种存储着许多键值对的无序列表,列表的键名必须是非null类型的对象,键名对应的值可以是任意类型。通过set()方法添加数据,通过get()方法获取数据。

    let wm = new WeakMap();
    let element = document.querySelector('.element');
     wm.set(element, 'Original');
     let value = wm.get(element);
    // 移除元素
    element.parentElement.removeChild(element);
    element = null;
    // 该WeakMap 在此处为空

这个示例中存储了一个键值对,键名element 是一个DOM元素,其对应的值是一个字符串,将DOM元素传入get()方法即可获取之前存取过的值,如果随后从document对象中移除并将引用这个元素的变量为null,那么WeakMap集合中的数据也会被同步清除。

[WeakMap实例的方法]

WeakMap只支持4个方法: get()、set() 、has()、delete()

let wm = new WeakMap();
let element = document.querySelector('.element');
wm.set(element, 'Original');
console.log(map.has(element));// true
console.log(map.get(element));// "Original"
map.delete(element);
console.log(map.has(element));// false
console.log(map.get(element))// undefined

[WeakMap集合的初始化]

WeakMap集合的初始化过程与Map集合类似,调用WeakMap构造函数并传入一个数组容器,容器包含其他数组,每个数组有两个元素构成;第一个元素是键名,传入的值必须是非null的对象;第二个元素是这个键对应的值。(可以是任意类型)

let key1 = {},
    key2 = {},
    wm = new WeakMap([[key1, 'hello'],[key2, 42]]);
console.log(wm.has(key1));// true
console.log(wm.get(key1)); // "hello"
console.log(wm.has(key2));// true
console.log(wm.get(key2)); // 42

[WeakMap用途]

  • 存储DOM元素

WeakMap应用的典型场合就是DOM节点作为键名:

  let myElement = document.getElementById('logo');
  let myweakMap =  new WeakMap();
  myweakMap.set(myElement, {timesClicked: 0});
	myElement.addEventListener('click', function () {
    let logoData = myweakMap.get(myElement);
    logoData.timesClicked++;
	}, false);

上面的代码中,myElement是一个DOM节点,每当发生click事件,就更新一下状态。将这个状态作为键值放在WeakMap里,对应的键名就是myElement。一旦这个DOM节点删除,该状态就会自动消失,不存在内存泄露风险。

进步一说,注册监听事件的listener对象,就很适合用WeakMap实现:

  const listener = new WeakMap();
 listener.set(element1, handler1);
 listener.set(element2, handler2);

 element1.addEventListener('click', listener.get(element1), false);
 element2.addEventListener('click', listener.get(element2), false);

上面代码中,监听函数放在WeakMap里面。一旦DOM对象消失,跟它绑定的监听函数也会自动消失

  • 部署私有属性

WeakMap的另外一个用处是部署私有属性

const _counter = new WeakMap();
const _action = new WeakMap();
class CountDown {
    constructor(counter, action) {
        _counter.set(this, counter);
        _action.set(this, action);
    }
    dec() {
        let counter = _counter.get(this);
         if(counter < 1) return;
         counter --;
         _counter.set(this, counter);
         if(counter === 0) {
             _action.get(this)();
         }
    }
    
}
const c = new  CountDown(2 , () => console.log('down'))
c.dec();
c.dec();// down

上面的代码中,CountDown类的两个内部属性 —— _counter和_action是实例的弱引用,如果删除实例,它们也会随之消失,不会造成内存泄漏。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

crazy的蓝色梦想

如果对你有帮助,就鼓励我一下吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值