阮一峰es6要点总结——Set与Map数据结构

笔记,完全摘录自阮一峰

set数据结构

ES6提供的,新的数据结构,类似于数组。

特点:成员值唯一,没有重复的值

  • 生成set数据结构
    使用Set()构造函数,可接受一个数组,类数组对象作为参数。

  • Set实例的方法和属性
    属性

  • Set.prototype.constructor:指向构造函数Set

  • Set.prototype.size:返回set实例的成员总数

方法——操作方法(操作数据)/遍历方法(遍历成员)

  • 操作方法
    • add(value):添加value,并返回Set结构本身
    • delete(value):删除value,返回布尔值,表示是否删除成功
    • has(value):返回布尔值,表示value是否为set的成员
    • clear():清除所有成员
  • 遍历操作
  • key():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
  • forEach():使用回调函数遍历每个成员

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

由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

Set结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。

  • Set内部的值相等判断
    使用“Same-value equality”算法,类似全等===,区别是,NaN等于自身。

  • 应用

1.利用Set结构的独特特性,实现数组去重

// 去除数组的重复成员
[...new Set(array)]

2.实现并集、交集、差集

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

3.在遍历中,改变原Set结构

// 方法一,利用原Set结构映射出一个新的结构,然后赋值给原来的Set结构
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6
// 方法二,利用`Array.from()`
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6

WeakSet

特点

  • 成员唯一,且必须是对象。对象均为弱引用

  • 弱引用性:如果没有其他对象引用WeakSet中的对象,那么垃圾回收机制会自动回收该对象占用的内存。

  • 由于弱引用性,导致WeakSet的成员不可遍历(因为无法确保遍历成员的存在)。

用法

构造函数WeakSet(),可传入数组,或类数组等具有iterable接口的对象。

注意:成为WeakSet的成员的是参数的成员,而不是参数本身,所以

var b = [3, 4];
var ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)

数组b的成员是数字,不符合WeakSet的特性,会报错。其他情况的参数也一样。

方法

  • WeakSet.prototype.add(value):添加新成员
  • WeakSet.prototype.delete(value):清除指定成员
  • WeakSet.prototype.has(value):返回布尔值,表示某个值是否存在。

应用
存储dom节点,不用担心,这些节点从文档移除是,造成内存泄漏

举例:闭包保存下一个dom节点,当dom节点删除时候,由于还存在引用,无法被回收。使用WeakSet就可以解决这个问题。

另一个例子

const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method () {
    if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
    }
  }

上面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用WeakSet的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。

Map

ObjectMap均为Hash结构(键值对集合)

区别
Object键名只能是字符串形式,Map的键名可以是任意类型(包括对象)。

总的来说,Map是一种更完善的Hash结构的实现。如果你需要“键值对”的数据结构,Map比Object更合适。

构造函数
可传入一个数组参数。



var items = [
  ['name', '张三'],
  ['title', 'Author']
];

var map = new Map(items) //等价于下边的

var map = new Map();
items.forEach(([key, value]) => map.set(key, value));

特性

  • Map键名可以是任意类型
  • Map键名为对象类型时,与内存地址绑定,如果内存地址不同,就视为两个键
var map = new Map();

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

 var k1 = ['a'];
 var k2 = ['a'];

 map
.set(k1, 111)
.set(k2, 222);

 map.get(k1) // 111
 map.get(k2) // 222
  • Map键名为简单类型(数值,字符串,布尔值),只要两个值严格相等(===),就认为是一个键。
  • NaN被视为同一个键,0-0被视为同一个键
  • 相同键多次赋值,后覆盖前

实例的属性和方法

  • size属性
    返回Map成员总数

  • set(key, value)
    设置键值对,返回Map本身,所以可以链式

  • get(key)
    读取key对应的值,找不到返回undefined

  • has(key)
    返回布尔值,表示是否存在

  • delete(key)
    删除某键,返回布尔值,表示是否成功删除

  • clear()
    清空所有成员,无返回值。

  • 遍历方法

  • keys()

  • values()

  • entries()

  • forEach(),接受第二个参数,绑定回调函数的this

需要特别注意的是,Map的遍历顺序就是插入顺序。
基本同Set
Map结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

与其他数据结构的互相转换

  • Map-->Array
    使用扩展运算符(...)
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
  • Array-->Map
new Map([[true, 7], [{foo: 3}, ['abc']]])
// Map {true => 7, Object {foo: 3} => ['abc']}
  • Map-->Object
    键名转成字符串就行
function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

 let myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
  • Object-->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})
// [ [ 'yes', true ], [ 'no', false ] ]
  • Map-->JSON

两种情况
一.键名全是字符串的,调用上边的,Map-->Object

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-->Array

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

一.正常情况

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes":true,"no":false}')
// Map {'yes' => true, 'no' => false}

二.特殊情况,整个json就是一个数组,每项都是两成员数组

unction jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}

WeakMap

WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制

WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

一个例子

var wm = new WeakMap();
var element = document.querySelector(".element");
wm.set(element, "Original");
wm.get(element) // "Original"
element.parentNode.removeChild(element);
element = null;
wm.get(element) // undefined

与Map的区别

  • 没有遍历操作
  • 没有size
  • 无法清空

这些都是因为WeakMap的键不被计入引用,被垃圾回收机制忽略。

WeakMap的方法

  • get
  • set
  • has
  • delete

WeakMap应用的典型例子

例子一

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 节点删除,该状态就会自动消失,不存在内存泄漏风险。

例子二
部署私有属性

let _counter = new WeakMap();
let _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)();
    }
  }
}

let c = new Countdown(2, () => console.log('DONE'));

c.dec()
c.dec()
// DONE

上面代码中,Countdown类的两个内部属性_counter_action,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。

推荐阅读更多精彩内容