ccforward / cc

Code & Blog
1.59k stars 193 forks source link

40.ES6 笔记四 - Set Map #41

Open ccforward opened 8 years ago

ccforward commented 8 years ago

ES6 笔记四 - Set Map

Set

基本用法

类似数组,但是成员都是唯一的没有重复值。

Set本身是一个构造函数用来生成Set类型的数据

Set可以接受一个数组或者类数组的对象作为参数来初始化

var s = new Set([1,2,3,3,3,4,5])
s.size  // 5
[...s]  // [1,2,3,4,5]

var ss = new Set([...document.querySelectorAll('div')]);
ss.size // div的个数

// 相当于
[...document.querySelectorAll('div')].forEach(div => ss.add(div))

使用 Set 数组去重 [...new Set(array)]

使用 Set 加入值时不会转换类型 5 和 '5' 是两个不同的值。虽然 NaN === NaN 返回 false, 但是在 Set 内部 NaN 是等于自身的。

var set = new Set();
var a = NaN;
var b = NaN;

set.add(a);
set.add(b);

set.size // 1  Set 内部 两个 NaN 是相等的

但是两个对象总是不等的

var set = new Set();

set.add({});
set.add({});

set.size // 2

属性 方法

1. 属性

Array.from 可以将 Set 结构转为数组

var s = new Set([1,2,3,4]);

var arr = Array.from(s);

arr // [1, 2, 3, 4]

数组去重的另一种方法

var unique = function(arr){
    return Array.from(new Set(arr))
}

unique([1,2,2,3,3,4]);

2.2 遍历方法

Set 遍历的顺序就是插入顺序。如果使用Set保存一个回调函数的列表,调用时就能按照添加的顺序来调用。

1) keys(),values(),entries()

key()、values()、entries()返回的都是遍历器对象。因为 Set 没有键名(或者键名和键值是同一个值),所以key()和value()的行为完全一致。

var set = new Set(['aa', 'bb', 'cc']);

for (var item of set.keys()) {
  console.log(item);
}
// aa
// bb
// cc

for (var item of set.values()) {
  console.log(item);
}
// aa
// bb
// cc

for (var item of set.entries()) {
  console.log(item);
}
// ["aa", "aa"]
// ["bb", "bb"]
// ["cc", "cc"]

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

Set.prototype[Symbol.iterator] === Set.prototype.values

那么就可以直接用 for...of 来遍历Set。省略 values 方法。

var set = new Set(['aa', 'bb', 'cc']);

for(s of set) {
    console.log(s);
}
// aa
// bb
// cc
2) forEach()
var set = new Set(['aa', 'bb', 'cc']);

set.forEach((val,key) => console.log(val + ' - sss'))
3) 使用遍历

扩展运算符 (...) 内部使用 for...of 循环,所以也可以是用 Set 结构。

var set = new Set(['a', 'b', 'c']);

var arr = [...set];

// ['a', 'b', 'c']

数组的 map filter 方法也可用于 Set

var set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x*2));
// Set {2, 4, 6}

var set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x%2) ==0 ));
// Set {2, 4}

Set 很容易实现并集(Union)、交集(Intersect)和差集(Difference)

var a = new Set([1,2,3]);
var b = new Set([2,3,4]);

var union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

var intersect = new Set([...a].filter(x => b.has(x) ));
// Set {2, 3}

var diff = new Set([...a].filter(x => !b.has(x) ));
// Set {1}

WeakSet

与 Set 区别

var ws = new WeakSet();
ws.add(1); // Uncaught TypeError: Invalid value used in weak set
ws.add(Symbol()); // Uncaught TypeError: Invalid value used in weak set
var a = [[1,2], [3,4]];
var ws = new WeakSet(a);

上面代码中 a 数组的成员成为 WeakSet 的成员,而不是 a 数组本身。因此数组的成员只能是对象。

WeakSet 没有 size forEach 属性,没有办法遍历它的成员。

WeakSet不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet的一个用处,是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。

Map

基本用法

对象(Object) 本质上是键值对的集合(Hash结构), 但是只能用字符串当键(字符串 - 值)。

Map 数据结构中各种类型的值(包括对象)都可以当作键(值 - 值)。

var m  = new Map();
var o = {a: 'abcd'};

m.set(o, 'content');
m.get(o); // 'content'

m.has(o); // true
m.delete(o); // true
m.has(o); // false

Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

var  m = new Map([
    ['name1', 'a'],
    ['name2', 'b']    
]);

m.size // 2
m.has('name1') // true

true 和 字符串 'true' 在 Map 表示两个值

var  m = new Map([
    [true, 'a'],
    ['true', 'b']    
]);

m.get(true)  // 'a'
m.get('true')  // 'b'

重复赋值会覆盖

var m = new Map();
m
 .set('a', 'a')
 .set('a', 'b')

m.get('a') // 'b'

读取一个未知的键,则返回undefined new Map().get('abc')

只有对用一个对象的引用 Map 才会视为同一个键

var m = new Map();

m.set(['a'], 123);

m.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

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

所以说,Map 的键是和内存地址绑定的,内存地址不同,就视为两个键。这样就解决了同名属性的问题。

在 Map 中 NaN 视为同一个键 0-0 也是同一个键,因为 0 === -0

属性 方法

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

let map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

[...map.values()]
// ["no", "yes"]

[...map.keys()]
// ["F", "T"]

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// F no
//T yes

使用扩展运算符(...)Map结构可转为数组结构

var map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

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

[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]

结合数组的map方法、filter方法,可以实现Map的遍历和过滤(Map本身没有map和filter方法)

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

var map1 = new Map(
  [...map0].filter(([k, v]) => k < 3)
);
// 产生Map结构 {1 => 'a', 2 => 'b'}

var map2 = new Map(
  [...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 产生Map结构 {2 => '_a', 4 => '_b', 6 => '_c'}

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

  1. Map转为数组

    使用扩展运算符

    var m = new Map().set(true, 7).set({foo: 3}, ['abc']);
    [...m]
    //  [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
  2. 数组转为Map

    数组直接放入Map的构造函数

    var m = new Map([ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]);
    m
    // Map {true => 7, Object {foo: 3} => ["abc"]}
  3. Map转为对象

    所有的键为字符串的Map可转换为数组

    function strMapToObj(map) {
     let obj = Object.create(null);
     for(let [k,v] of map){
       obj[k] = v;
     }
    
     return obj;
    }
    var m = new Map().set('a', 'a1a2a3').set('true', true);
    strMapToObj(m);
    // Object {a: "a1a2a3", true: true}
  4. 对象转为Map

    function objToStrMap(obj){
     let m = new Map();
     for(let [k,v] of obj){
       m.set(k,v)
     }
     return m
    }
  5. Map转为JSON

    Map的键名都是字符串,可以直接转为JSON

    function mapToJson(map){
     return JSON.stringify(strMapToObj(map));
    }
    var m = new Map().set('yes', true).set('no', false);
    strMapToJson(m)
    // '{"yes":true,"no":false}'

    Map的键名如果有非字符串,可选择转为数组JSON

     function mapToArrayJson(m){
       return JSON.stringify([...m])
     }
     var mm = new Map().set(true, 7).set({foo: 3}, ['abc']);
     mapToArrayJson(mm)
     // "[[true,7],[{"foo":3},["abc"]]]"
  6. JSON转为Map

    正常情况下所有键名都是字符串

    function jsonToStrMap(json){
     return objToStrMap(json);
    }
    jsonToStrMap('{"yes":true,"no":false}')
    // Map {'yes' => true, 'no' => false}
    

    有一种特殊情况 整个 JSON 是一个数组,每个数组成员本身又是一个有两个成员的数组,这时课一一对应的转为Map

    function 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

WeakMap 没有遍历操作,无法清空,只有四个方法可用:get()、set()、has()、delete()

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++;
  myWeakmap.set(myElement, logoData);
}, false);

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

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

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()

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