Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别? #6

Open
machangzhi opened this issue Jan 24, 2019 · 34 comments
Open
Labels

Comments

@machangzhi
Copy link

machangzhi commented Jan 24, 2019

http://es6.ruanyifeng.com/#docs/set-map
看完这个就可以了

@zero578
Copy link

zero578 commented Jan 25, 2019

Set
1.成员不能重复
2.只有健值,没有健名,有点类似数组。
3. 可以遍历,方法有add, delete,has
weakSet

  1. 成员都是对象
  2. 成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
  3. 不能遍历,方法有add, delete,has
    Map
  4. 本质上是健值对的集合,类似集合
  5. 可以遍历,方法很多,可以干跟各种数据格式转换
    weakMap
    1.直接受对象作为健名(null除外),不接受其他类型的值作为健名
  6. 健名所指向的对象,不计入垃圾回收机制
  7. 不能遍历,方法同get,set,has,delete

@yygmind yygmind changed the title Set、Map、WeakSet 和 WeakMap 第四题:Set、Map、WeakSet 和 WeakMap Feb 12, 2019
@sisterAn
Copy link
Collaborator

sisterAn commented Feb 16, 2019

Set 和 Map 主要的应用场景在于 数据重组数据储存

Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

1. 集合(Set)

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

Set 本身是一种构造函数,用来生成 Set 数据结构。

new Set([iterable])

举个例子:

const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))

for (let i of s) {
    console.log(i)	// 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]	// [1, 2, 3]

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是**NaN等于自身,而精确相等运算符认为NaN不等于自身。**

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

let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1])	// [5, "5"]
  • Set 实例属性

    • constructor: 构造函数

    • size:元素数量

      let set = new Set([1, 2, 3, 2, 1])
      
      console.log(set.length)	// undefined
      console.log(set.size)	// 3
  • Set 实例方法

    • 操作方法
      • add(value):新增,相当于 array里的push

      • delete(value):存在即删除集合中value

      • has(value):判断集合中是否存在 value

      • clear():清空集合


        let set = new Set()
        set.add(1).add(2).add(1)
        
        set.has(1)	// true
        set.has(3)	// false
        set.delete(1)	
        set.has(1)	// false

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

        const items = new Set([1, 2, 3, 2])
        const array = Array.from(items)
        console.log(array)	// [1, 2, 3]
        // 或
        const arr = [...items]
        console.log(arr)	// [1, 2, 3]
    • 遍历方法(遍历顺序为插入顺序)
      • keys():返回一个包含集合中所有键的迭代器

      • values():返回一个包含集合中所有值得迭代器

      • entries():返回一个包含Set对象中所有元素得键值对迭代器

      • forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值

        let set = new Set([1, 2, 3])
        console.log(set.keys())	// SetIterator {1, 2, 3}
        console.log(set.values())	// SetIterator {1, 2, 3}
        console.log(set.entries())	// SetIterator {1, 2, 3}
        
        for (let item of set.keys()) {
          console.log(item);
        }	// 1	2	 3
        for (let item of set.entries()) {
          console.log(item);
        }	// [1, 1]	[2, 2]	[3, 3]
        
        set.forEach((value, key) => {
            console.log(key + ' : ' + value)
        })	// 1 : 1	2 : 2	3 : 3
        console.log([...set])	// [1, 2, 3]

        Set 可默认遍历,默认迭代器生成函数是 values() 方法

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

        所以, Set可以使用 map、filter 方法

        let set = new Set([1, 2, 3])
        set = new Set([...set].map(item => item * 2))
        console.log([...set])	// [2, 4, 6]
        
        set = new Set([...set].filter(item => (item >= 4)))
        console.log([...set])	//[4, 6]

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

        let set1 = new Set([1, 2, 3])
        let set2 = new Set([4, 3, 2])
        
        let intersect = new Set([...set1].filter(value => set2.has(value)))
        let union = new Set([...set1, ...set2])
        let difference = new Set([...set1].filter(value => !set2.has(value)))
        
        console.log(intersect)	// Set {2, 3}
        console.log(union)		// Set {1, 2, 3, 4}
        console.log(difference)	// Set {1}

2. WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中

WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
  • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素

属性:

  • constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数

    const arr = [[1, 2], [3, 4]]
    const weakset = new WeakSet(arr)
    console.log(weakset)

方法:

  • add(value):在WeakSet 对象中添加一个元素value
  • has(value):判断 WeakSet 对象中是否包含value
  • delete(value):删除元素 value
  • clear():清空所有元素,注意该方法已废弃
var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window)	// true
ws.has(foo)	// false

ws.delete(window)	// true
ws.has(window)	// false

3. 字典(Map)

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值
  • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
const m = new Map()
const o = {p: 'haha'}
m.set(o, 'content')
m.get(o)	// content

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

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作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

如果读取一个未知的键,则返回undefined

new Map().get('asfddfsasadf')
// undefined

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

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

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

Map 的属性及方法

属性:

  • constructor:构造函数

  • size:返回字典中所包含的元素个数

    const map = new Map([
      ['name', 'An'],
      ['des', 'JS']
    ]);
    
    map.size // 2

操作方法:

  • set(key, value):向字典中添加新元素
  • get(key):通过键查找特定的数值并返回
  • has(key):判断字典中是否存在键key
  • delete(key):通过键 key 从字典中移除对应的数据
  • clear():将这个字典中的所有元素删除

遍历方法

  • Keys():将字典中包含的所有键名以迭代器形式返回
  • values():将字典中包含的所有数值以迭代器形式返回
  • entries():返回所有成员的迭代器
  • forEach():遍历字典的所有成员
const map = new Map([
            ['name', 'An'],
            ['des', 'JS']
        ]);
console.log(map.entries())	// MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}

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

map[Symbol.iterator] === map.entries
// true

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

对于 forEach ,看一个例子

const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};

let map = new Map([
    ['name', 'An'],
    ['des', 'JS']
])
map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);
// Key: name, Value: An
// Key: des, Value: JS

在这个例子中, forEach 方法的回调函数的 this,就指向 reporter

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

  1. Map 转 Array

    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log([...map])	// [[1, 1], [2, 2], [3, 3]]
  2. Array 转 Map

    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log(map)	// Map {1 => 1, 2 => 2, 3 => 3}
  3. Map 转 Object

    因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。

    function mapToObj(map) {
        let obj = Object.create(null)
        for (let [key, value] of map) {
            obj[key] = value
        }
        return obj
    }
    const map = new Map().set('name', 'An').set('des', 'JS')
    mapToObj(map)  // {name: "An", des: "JS"}
  4. Object 转 Map

    function objToMap(obj) {
        let map = new Map()
        for (let key of Object.keys(obj)) {
            map.set(key, obj[key])
        }
        return map
    }
    
    objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
  5. Map 转 JSON

    function mapToJson(map) {
        return JSON.stringify([...map])
    }
    
    let map = new Map().set('name', 'An').set('des', 'JS')
    mapToJson(map)	// [["name","An"],["des","JS"]]
  6. JSON 转 Map

    function jsonToStrMap(jsonStr) {
      return objToMap(JSON.parse(jsonStr));
    }
    
    jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}

4. WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

属性:

  • constructor:构造函数

方法:

  • has(key):判断是否有 key 关联对象
  • get(key):返回key关联对象(没有则则返回 undefined)
  • set(key):设置一组key关联对象
  • delete(key):移除 key 的关联对象
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);

5. 总结

  • Set
    • 成员唯一、无序且不重复
    • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    • 可以遍历,方法有:add、delete、has
  • WeakSet
    • 成员都是对象
    • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
    • 不能遍历,方法有add、delete、has
  • Map
    • 本质上是键值对的集合,类似集合
    • 可以遍历,方法很多可以跟各种数据格式转换
  • WeakMap
    • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
    • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    • 不能遍历,方法有get、set、has、delete

6. 扩展:Object与Set、Map

  1. Object 与 Set

    // Object
    const properties1 = {
        'width': 1,
        'height': 1
    }
    console.log(properties1['width']? true: false) // true
    
    // Set
    const properties2 = new Set()
    properties2.add('width')
    properties2.add('height')
    console.log(properties2.has('width')) // true
  2. Object 与 Map

JS 中的对象(Object),本质上是键值对的集合(hash 结构)

const data = {};
const element = document.getElementsByClassName('App');

data[element] = 'metadata';
console.log(data['[object HTMLCollection]']) // "metadata"

但当以一个DOM节点作为对象 data 的键,对象会被自动转化为字符串[Object HTMLCollection],所以说,Object 结构提供了 字符串-值 对应,Map则提供了 值-值 的对应

本文始发于我的博客:Set、WeakSet、Map及WeakMap

@peakDragonCheung
Copy link

4 Object 转化为 Map , 其中一行代码 map.set(key, obj[k]) 这里是不是写错了,不是 obj[k],而是obj[key]。

@peakDragonCheung
Copy link

  1. 扩展:Object与Set、Map,1. Object 与 Set ,console.log(properties['width']? true: false),少了一个1

@sisterAn
Copy link
Collaborator

sisterAn commented Feb 18, 2019

ok,已改正,谢谢 @peakDragonCheung

@kscript
Copy link

kscript commented Feb 19, 2019

WeakMap 里有一段写成了 WeakSet。

@BeADre
Copy link

BeADre commented Feb 19, 2019

Set 实例方法下面遍历方法中使用map、filter那个地方 打印写错了 应该是[2,3,4]和[4]吧

@wenbintian
Copy link

在 “与其他数据结构的相互转换 的 4.Object 转 Map” 参数名称写错了, 应该是 “obj” 而不是 "map"

@ghost
Copy link

ghost commented Feb 22, 2019

我觉得看这篇技术文章也可以

@zhoufanglu
Copy link

new Set([1,2,3]).length 值应该是undefind 而不是0

@noctiomg
Copy link

为了学习 ES6 里面 Set、Map、WeakSet 和 WeakMap 的知识,懒癌晚期的我第一选择立刻翻阅阮一峰老师的 ES6入门 ,挑选里面的局部内容进行摘抄和理解。

Set

基本用法

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

看到这句我们可以基本明白,Set 是 “无重复的值的数组 (Array) ”。

Set 实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。
    Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
  • add(value):添加某个值,返回 Set 结构本身。
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

然后我们会发现,Set 居然没有 push 、shift 这类的方法吗?不是说是无重复值的数组吗?
原来,Set 的本质还是一个对象,它并不是数组。我们看一下 Set 的构造函数

语法

new Set([iterable]);

参数

iterable如果传递一个可迭代对象,它的所有元素将不重复地被添加到新的 SetSet**为空。

返回值

一个新的Set对象。

*可迭代对象

这里提到了可迭代对象,很多时候我们只记得可迭代对象一般是能够被 for( ... in ... ){} 进行遍历的对象/值。比较常见的可迭代对象:字符串、数组、对象。按照这种说法,那 Object 岂不是也可以被 Set 作为参数……

image.png

个屁!并不能。凭啥?我们来看看在 MDN 中的真正的定义:

The for...of statement creates a loop iterating over iterable objects, including: built-in String, Array, Array-like objects (e.g., arguments or NodeList), TypedArray, Map, Set, and user-defined iterables.

内置字符串对象,数组,类数组……这些才是在 JS 语言中真正的可迭代对象(说起来字符串本身也是一种类数组哦)。所以刚刚的测试报错,我们就用 DevTools 所理解的伪数组(有 length 属性、且有 splice 方法的对象)来骚操作一下 :

image.png

为啥有问题?好好看报错!cannot read property Symbol(Symbol.iterator)
也就是说,如果你想创建一个可迭代对象,你需要让这个对象(类)拥有一个私有标识:Symbol.iterator 。确切地来说,Set 的构造器要求对象具有的这一私有的标识,本质上要求应该是一个“具有 next 方法、且每次 next 方法会返回一个具有 done 和 value 两个属性的对象”的方法,done 的值为布尔值、为 false 则可以继续执行 next 取下一个值。多说无益,show u my code :

var foo = {
	0 : 'zero',
	1 : 'one',
	2 : 'two',
	3 : 'three',
	length : 4
};
foo[Symbol.iterator] = function(){
	let i = 0;
    let l = this.length;
	let that = this;
	console.log('someone is using the iterator')
    return {
      next() {
        if (i < l) {
          console.log('now:'+that[i]+',progress:'+i+'/'+l)
          return { done: false, value: that[i++] };
        }
        return { done: true };
      }
    };
}
new Set(foo);

输出结果如图:

image.png

关于迭代器的一些知识搜索来源于 David Tang 博客中的 《Iterables and Iterators in JavaScript》 ,原文干货很多,建议 Mark 。
按照这种思路,我们甚至可以写一些坑爹东西忽悠 Set 构造器:
image.png

就此打住,我们把重心转移回 Set 上。
刚刚我们看到 Set 可以理解为无序的、无重复子元素的数组,所以 Set 理所应当也具有一些和数组相似的方法:

Set 结构的实例有四个遍历方法,可以用于遍历成员。

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

理解到这里,用 Set 给一些存了基本类型数据的数组去重,就很好理解了。

Map

阮一峰老师在文中有一个特别好的总结,我们摘录下:

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

const data = {};
const element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"

上面代码原意是将一个 DOM 节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值 - 值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

粗暴理解下,Map 是一个可以用 “任何值” 作为 **键名 **的 对象 。更严谨地说,不是“任何值”,而是“任何指针”。可以用阮一峰老师的例子说明:

同理,同样的值的两个实例,在 Map 结构中被视为两个键。

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

上面代码中,变量k1k2的值是一样的,但是它们在 Map 结构中被视为两个键。
由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

最好玩的是,Map 和 Set 的构造器所传参数是一样的——无参数、或者可迭代对象。
只要基于数组理解 Set , 基于对象理解 Map ,其实他俩在意义和特性上是很好理解的,具体的一些方法和属性可以参考阮一峰老师 ES6入门 上的 这一章节 来具体学习。

WeakSet

顾名思义,WeakSet 是“弱 Set”——弱引用版本的 Set。光是知道这句话是不行的,很多同学在刚接触这个定义的时候会有这种猜想:

// 以下代码的输出结果为猜想值
var ws = new WeakSet();
var a = {foo:'bar'};
ws.add(a);
console.log(ws); 
/* 应输出:
WeakSet {{…}}
	__proto__: WeakSet
  [[Entries]]: Array(1)
  	0: value: {foo: "bar"}
    length: 1
*/ 
delete a;
console.log(ws); 
/* 应输出:
WeakSet {}
	__proto__: WeakSet
	[[Entries]]: Array(0)
		length: 0
*/

然后果不其然,我们会被 pia pia 打脸:

image.png
 
“说好的弱引用呢?”
甚至我们掏出 MDN 会发现一个特别神奇的事情:WeakSet 几乎不兼容各种主流浏览器,只有 Chrome 被标注支持了,甚至 Chrome 也要强调:只有开启实验性 JavaScript 才支持。所以 WeakSet 到底是何许码也?既然明码标价是弱引用,那怎么样才能触发它的这个特性,回收后让 WeakSet 中的相关内容消失?

既然已经走到了这里,我们就一口气把 JavaScript 浏览器端和 WeakSet 相关的内存管理、弱引用等知识都搞清楚。首先我们了解下 JavaScript 里有关变量回收的一些规则(参考文章):

在Javascript是可以使用delete来手动删除变量,通过这样的方法让GC来回收内存,但在JS中并不是所有的对象都可以被删除的, kangex在他的博文中对此作了详细说明:Understanding delete
在JS中通过 var\function 声明因含有DontDelete,而不可被删除;
但是对象的属性、数组成员却是可以删除的;
因此如果我们要回收某个对象可以使用Object来封装一下。

所以我们再修改一下上方的代码。

var test = {
	name : 'test',
	content : {
		name : 'content',
		will : 'be clean'
	}
};
var ws = new WeakSet();
ws.add(test.content);
console.log('清理前',ws);
delete test.content;
console.log('清理后',ws)

但是结果却依然不行,如图:

image.png

原来,JavaScript 语言中,内存的回收并不是在执行 delete 操作符断开引用后即时触发的,而是根据运行环境的不同、在不同的运行环境下根据不同浏览器的回收机制而异的。比如在 Chrome 中,我们可以在控制台里点击 CollectGarbage 按钮来进行内存回收:

image.png

在点击此按钮后,我们再打印上方的 ws 变量:

image.png

关于在不同浏览器环境下手动进行内存回收的具体异同,可参考:如何手动触发 JavaScript 垃圾回收行为?
每次都必须使用 delete 一个一个删除属性吗?并不,delete 的意义是“断开引用”,同样的,我们也可以用这种方式来进行清理:

var test = {
	name : 'test',
	content : {
		name : 'content',
		will : 'be clean'
	}
};
var ws = new WeakSet();
ws.add(test.content);
console.log('清理前',ws); // 清理前 WeakSet {{…}}
test.content = null;
console.log('清理后',ws); // 清理后 WeakSet {{…}}

// -- 进行手动回收 --

console.log(ws); // WeakSet {}

这样我们就彻底搞清楚了:JavaScript 会在执行内存回收时,清除掉 被引用次数为0 的那部分内存;而 WeakSet 是只能储存对象的(或者说只能储存内存指针而非静态值)、并且它对对象的引用将不计入对象的引用次数,当清除对象属性、对应的内存被清理之后,WeakSet 中记录的内存地址上不再有内容,它将自动断开与这条引用的关联 —— 也正因如此,它所储存的内容会受到开发者对其他对象操作的被动影响,所以 WeakSet 在设计上就设计成了没有“长度”、“遍历”概念的特殊弱引用 Set 型。

这样的弱引用,用途上可以开一些脑洞,比如阮一峰老师的例子:

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,也不会出现内存泄漏。——阮一峰

相比 WeakMap,它的应用能力不是特别强,或许这也是它目前没有被广泛支持的原因吧。

WeakMap

理解了迭代器、弱引用、内存回收,对 WeakMap 我们就可以很简单地去理解了:
WeakMap 是一个只能以 对象 作为键名的 Map,同时 WeakMap 上 每个键名对应的引用也是弱引用的。
也就是我们刚刚 WeakSet 的值的那种实验,在 WeakMap 的键名上是依然存在的。比如:

var a = {b:{c:'42'}};
var wm = new WeakMap();
wm.set(a.b,'love & peace');
// WeakMap {{…} => "love & peace"}
delete a.b;
// 手动执行 CollectGarbage
console.log(wm);
// WeakMap {}

懂得很多道理,却依然过不好这一……呸!既然知道定义了就应该知道怎么用!我们先以阮一峰老师的例子 A 来看:

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

把 DOM 节点用作它的键名是一个常见场景,对应的可以做各种各样的骚操作。再看阮一峰老师的例子 B :

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('DONE'));
c.dec()
c.dec()
// DONE

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

在这两个例子的基础上,我的理解是:WeakMap 非常擅长去配合 非常态的实例、节点、属性 一同使用,在那些内容被销毁时跟着一起被回收。很多时候我们不得不用一些变量来给这些东西做各种各样的辅助,比如 计数器、状态标识、临时值储存……在这种情况下,我们学习了 WeakMap ,就可以用 WeakMap 来做这个辅助的集中管理。

顺带一提, WeakMap 的浏览器支持性完爆 WeakSet ……

image.png

感悟

虽然最初只是想大概知道下这几个 ES6 新出的小老弟是干啥用的,不过顺便就把所有的相关知识都梳理了下。我们已经可以看到这些 ES6 的福利正在逐渐普及,和我一样是万年切图仔的同学们也要适当充实下自己在基础方面的知识,不要只知其然不知其所以然啦~

@yygmind yygmind changed the title 第四题:Set、Map、WeakSet 和 WeakMap 第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别? Apr 26, 2019
@qiufeihong2018
Copy link

@sisterAn 差集有问题啊

@pansy199211
Copy link

Map: key可为任何类型的键值对
Set: 取值不重复的集合
Weak-:弱引用,不计入垃圾回收

@513107329
Copy link

WeakSet和Set都是构造函数,可以使用new命令创建相应的数据结构,并且值都是唯一的;
WeakSet 与 Set 的区别:
1、WeakSet 的成员只能是对象,而不能是其他类型的值,而 Set 对象的成员可以是任意类型的值;
2、WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了);
3、由于以上第二点的原因,es6规定WeakSet不能够被遍历,无法使用keys,values等遍历方法,并且不存在size属性。

Map和WeakMap都是构造函数,用域生成键值对的集合;
Map和WeakMap的区别:
1、WeakMap只接受对象作为键名,不接受其他类型的值作为键名,而Map可以接受任何类型的值作为键名;
2、WeakMap的键名所引用的对象都是弱引用,所以这个对象的其他引用都被清除,垃圾回收机制就会释放这个对象所占用的内存;
3、由于第二点的原因,WeakMap不存在遍历操作,没有size的属性,因为WeakMap的键名是不可预测的,可能在不知道的时间,作为键名的对象就被垃圾回收机制清除;
4、不支持clear方法。

@weinaisha
Copy link

  • set 类似数组,可运用实现数组去重,去重类似严格等于符号「===」两个空对象/数组由于内存地址不同严格不等于,虽然NaN===NaN //false 但是在set结构中认为两个NaN是相同的
var arr = [2,'2',2,[],[],{},{},NaN,NaN,3]
var _arr = [...new Set(arr)]
console.log(_arr)   //[2,'2',[],[],{},{},NaN,3]
  • Map 多运用于数据存储,和普通Object键值对不同的是,Map的键可以是任何类型而Object只能是字符串

  • set和WeakSet
    set方法:add,has,delete,clear(可遍历) WeakSet: add,has,delete(不可遍历)
    接受的成员不同,WeakSet只接受成员是对象

new WeakSet([[2]])     //WeakSet {Array(1)}
new WeakSet([{q:1}])   //WeakSet {{…}}
new WeakSet([()=>{}])  //WeakSet {ƒ}
new WeakSet([1])       //TypeError: Invalid value used in weak set
new Set([1])           //Set(1) {1}
  • Map 和 WeakMap
    Map方法:set,get,has,delete,clear,可遍历:keys(),values(),entries(),forEach()
    WeakMap方法:set,get,has,delete,不可遍历
    WeakMap的键只能是对象(null除外)
new Map([['foo',1]])        //Map(1) {"foo" => 1}
new WeakMap([['foo',1]])    //TypeError:Invalid value used as weak map key
new WeakMap([[{foo:1},1]])  //WeakMap {{…} => 1}
  • WeakSet 和WeapMap的弱引用
let arr = new Array(5*1024)
let map = new WeakMap();
map.set([arr, 'foo']);  //这一步new Array(5*1024)被引用了两次
let arr = null          //释放外部引用,WeakMap内部引用回自动被垃圾回收清除

@NuoHui
Copy link

NuoHui commented Jul 13, 2019

英文原文关于Set.Map

Set与WeakSet区别:

1. WeakSet只能存放对象
2. WeakSet不支持遍历, 没有size熟悉
3. WeakSet存放的对象不会计入到对象的引用技术, 因此不会影响GC的回收
4. WeakSet存在的对象如果在外界消失了, 那么在WeakSet里面也会不存在

Map与WeakMap区别:

1. WeakMap只能接受对象作为键名字(null除外)
2. WeakMap键名指向对象不会计入对象的引用数

弱引用最大的特点就是: 我们有时候需要对对象添加一些数据, 但是又不希望把该引用计入到引用计数影响了GC。
以前非弱引用对象需要我们手动清除引用(xx = null)然后被GC回收, 现在弱引用不需要我们这么做, 只要弱引用指向的对象不再被其他对象引用, 那么弱引用对象就会自动消失

@xiaoxixi6633
Copy link

xiaoxixi6633 commented Jul 25, 2019

1.Set其实就是类数组对象 , 不是标准的数组,没有下标所以不能使用for 循环,但是能使用forEach for of 循环
2.没有length属性,有size属性
3.方法:add delete has clear
4.主要用去数组去重 方法如下:

let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]	// [1, 2, 3]

也可以结合Array from 方法实现数组去重

var arr = [1,2,3,3];
var set2 = new Set(arr);
var arr2 = Array.from(set2);
console.log(arr2);// [1,2,3]
 

@heightzhang
Copy link

set 和 WeakSet的区别

  1. WeakSet 的成员只能是对象,而不能是其他类型的值
  2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

map与WeakMap的区别

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  2. WeakMap是弱引用,不计入垃圾回收机制。

总结:

  1. 弱引用的只是键名,而不是键值。键值依然是正常引用。
  2. 弱引用的应用是它的键名所指向的对象将来可能会消失,有助于防止内存泄漏。比如:在网页的 DOM 元素上添加数据、当该 DOM 元素被清除,其所对应的WeakSet/WeakMap记录就会自动被移除。

@Kisnnnnn
Copy link

  • Set
const s = new Set([1,3,3,4,4,2])
// {1,3,4,2}

内部成员唯一性
键可以为 对象、其他类型
可以Map/forEach/filter
可以[...Set]循环

  • WeakSet(弱应用)
    键只能为对象
    垃圾回收机制会回收对象
    没有entries,size,keys,values

  • Map

const m = new Map([
    ["name","Kisn"],
    ["age","25"]
])

内部成员-键存在唯一性
键可以为 对象、其他类型
可以Map/forEach/filter
可以[...Map]循环

  • WeakMap
    内部成员只能是对象
    内部的键值对会随着引用键的对象消失而被垃圾回收机制回收
    因为随时会被回收,所以不存在长度size,从而不存在entries,values,keys

WeakMap和WeakSet 都不会引用值的对象消失,而被回收,因为值是被存储进去的。

@MissNanLan
Copy link

Set结构和Map结构都用过,
WeakSet听都没有听过,这次算学习了
WeakSet与Set结构有两个区别

Set与WeakSet

1、Set对象有size、add、delete、clear、has
2 、WeakSet的成员只能是对象( Iterable 接口的对象,都可以作为 WeakSet 的参数)
image

image

3、 垃圾回收机制,如果其他对象不再引用该对象,那么垃圾回收机制会回收改对象的所占用的内存。
WeakSet 没有size属性,没有办法遍历它的成员,它只有add、has、delete三个方法。

Map与MapSet

1、 Map数据结构域不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构
Map的方法有size、set、get、has、delete、clear
2、 WeakMap与Map对象的区别.WeakMap只接受对象作为键名(null除外)
3、WeakMap一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性,只有
set、get、has、delete这四个方法

@aeolusheath
Copy link

Map 的key 可以为引用类型,也可以为基础数据类型,可以理解为对javascript普通对象的扩展。js普通对象key只能为字符串。
WeakMap的key只能为对象【引用类型】;键名指向的值若没有被引用,垃圾回收机制会自动回收该键值对所占用的内存。 没有.size属性 也没法儿forEach去遍历。 只有4个方法 get()、set()、has()、delete() 。

Set 为非重复元素的集合,Set 集合里面的每一项不能重复,引用类型则是地址不能重复,基础类型则是值不能重复。
WeakSet 的元素只能是对象【引用类型】;元素若没有被其他对象引用,垃圾回收机制会自动回收该对象所占用的内存。 没有.size属性 也没法儿forEach去遍历。只有3个方法add()、delete()、has()。

@yft
Copy link

yft commented Sep 5, 2019

所以, Set可以使用 map、filter 方法

应该不能说 Set 可以使用 map filter 方法吧,只是 Set 和数组互转很方便,结合数据的 map filter 方法,可以实现很方便的交集、并集、差集。

@XiaoDHuang
Copy link

关于weakMap与weakSet弱引用该如何理解

这是周爱民老师在极客时间 《javascript核心原理》专栏给出给我的答案, 分享给大家。

弱引用是向weakSet/weakMap中添加一个目标对象的引用,但添加是目标对象的引用计数不增加。比较来说:

var x = {}; // <-右边的对象字面量的引用计数加1
var y = x; // <- 再加1
weakSet.add(x); // <-不加1
weakSet.add(y); // <-也不加1
delete x; // 减1
delete y; // 再减1
...

到这里,由于对象的引用计数为0了,所以weakSet中的那个被add()进去的x、y就自动被回收了。——weakSet/weakMap具备这种机制。

所以weakSet/weakMap没有size这个属性,它不安全。——你刚读了它的值,它自己自动回收了一下,就又变掉了。

如果感兴趣大家可以通过链接去订阅(只想表示很硬核)http://gk.link/a/10fj0

@guestccc
Copy link

guestccc commented Feb 27, 2020

@sisterAn 差集错了,应该是

let difference = [...new Set([...set1, ...set2])].filter(value => !new Set([...set1].filter(value => set2.has(value))).has(value))

@wulichenyang
Copy link

Set
1.成员不能重复
2.只有健值,没有健名,有点类似数组。
3. 可以遍历,方法有add, delete,has
weakSet

  1. 成员都是对象
  2. 成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
  3. 不能遍历,方法有add, delete,has
    Map
  4. 本质上是健值对的集合,类似集合
  5. 可以遍历,方法很多,可以干跟各种数据格式转换
    weakMap
    1.直接受对象作为健名(null除外),不接受其他类型的值作为健名
  6. 健名所指向的对象,不计入垃圾回收机制
  7. 不能遍历,方法同get,set,has,delete

老哥你很喜欢健身吗?

@lanshanmao
Copy link

Set
1.成员不能重复
2.只有健值,没有健名,有点类似数组。
3. 可以遍历,方法有add, delete,has
weakSet

  1. 成员都是对象
  2. 成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
  3. 不能遍历,方法有add, delete,has
    Map
  4. 本质上是健值对的集合,类似集合
  5. 可以遍历,方法很多,可以干跟各种数据格式转换
    weakMap
    1.直接受对象作为健名(null除外),不接受其他类型的值作为健名
  6. 健名所指向的对象,不计入垃圾回收机制
  7. 不能遍历,方法同get,set,has,delete

Set不是没有键名,是键名和值相同,const set = new Set() set.add(444) set.keys()可获得键名的Iterator对象,set.forEach可遍历

@soraly
Copy link

soraly commented Jun 11, 2020

Set: 1. 成员唯一、无序且不重复 2. [value, value],键值与键名是一致的(或者说只有键值,没有键名)
3. 可以遍历,方法有:add、delete、has
WeakSet: 只能插入对象,否则会报错。弱引用,可以被垃圾回收机制回收。所以适合用来保存DOM节点,不容易造成内存泄漏。不可遍历。方法同Set
Map: 本质上是键值对的集合,类似集合,key不局限于字符串,方法有get、set、has、delete

@RoeyXie
Copy link

RoeyXie commented Jul 28, 2020

🧡 Map类似于对象,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

// Set 实例的方法
// 操作方法:
Map.prototype.add(value) // 添加某个值,返回 Map 结构本身。
Map.prototype.delete(value) // 删除某个值,返回一个布尔值,表示删除是否成功。
Map.prototype.has(value) // 返回一个布尔值,表示该值是否为Map的成员。
Map.prototype.clear() // 清除所有成员,没有返回值。
// 遍历方法
Map.prototype.keys() // 返回键名的遍历器
Map.prototype.values() // 返回键值的遍历器
Map.prototype.entries() // 返回键值对的遍历器
Map.prototype.forEach() // 使用回调函数遍历每个成员

🧡 Set类似于数组,与数组的区别是它的成员值都是唯一的,是不重复的。

// Set 实例的方法
// 操作方法:
Set.prototype.add(value) // 添加某个值,返回 Set 结构本身。
Set.prototype.delete(value) // 删除某个值,返回一个布尔值,表示删除是否成功。
Set.prototype.has(value) // 返回一个布尔值,表示该值是否为Set的成员。
Set.prototype.clear() // 清除所有成员,没有返回值。
// 遍历方法
Set.prototype.keys() // 返回键名的遍历器
Set.prototype.values() // 返回键值的遍历器
Set.prototype.entries() // 返回键值对的遍历器
Set.prototype.forEach() // 使用回调函数遍历每个成员

🧡 WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

  • 这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为0,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。

  • 由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。这些特点同样适用于WeakMap 结构。

// WeakSet 结构有以下三个方法。
WeakSet.prototype.add(value) // 向 WeakSet 实例添加一个新成员。
WeakSet.prototype.delete(value) // 清除 WeakSet 实例的指定成员。
WeakSet.prototype.has(value) // 返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

🧡 WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有两点:首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制

@moruifei
Copy link

Set
1.成员不能重复
2.只有健值,没有健名,有点类似数组。
3. 可以遍历,方法有add, delete,has
weakSet

  1. 成员都是对象
  2. 成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
  3. 不能遍历,方法有add, delete,has
    Map
  4. 本质上是健值对的集合,类似集合
  5. 可以遍历,方法很多,可以干跟各种数据格式转换
    weakMap
    1.直接受对象作为健名(null除外),不接受其他类型的值作为健名
  6. 健名所指向的对象,不计入垃圾回收机制
  7. 不能遍历,方法同get,set,has,delete

Set对象并非只有键值没有键名 只是键值键名一样而已。

@Cosen95
Copy link

Cosen95 commented Dec 4, 2020

Set 和 Map 主要的应用场景在于 数据重组数据储存

Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

1. 集合(Set)

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

Set 本身是一种构造函数,用来生成 Set 数据结构。

new Set([iterable])

举个例子:

const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))

for (let i of s) {
    console.log(i)	// 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]	// [1, 2, 3]

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是**NaN等于自身,而精确相等运算符认为NaN不等于自身。**

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

let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1])	// [5, "5"]
  • Set 实例属性

    • constructor: 构造函数
    • size:元素数量
      let set = new Set([1, 2, 3, 2, 1])
      
      console.log(set.length)	// undefined
      console.log(set.size)	// 3
  • Set 实例方法

    • 操作方法

      • add(value):新增,相当于 array里的push

      • delete(value):存在即删除集合中value

      • has(value):判断集合中是否存在 value

      • clear():清空集合

        let set = new Set()
        set.add(1).add(2).add(1)
        
        set.has(1)	// true
        set.has(3)	// false
        set.delete(1)	
        set.has(1)	// false

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

        const items = new Set([1, 2, 3, 2])
        const array = Array.from(items)
        console.log(array)	// [1, 2, 3]
        // 或
        const arr = [...items]
        console.log(arr)	// [1, 2, 3]
    • 遍历方法(遍历顺序为插入顺序)

      • keys():返回一个包含集合中所有键的迭代器

      • values():返回一个包含集合中所有值得迭代器

      • entries():返回一个包含Set对象中所有元素得键值对迭代器

      • forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值

        let set = new Set([1, 2, 3])
        console.log(set.keys())	// SetIterator {1, 2, 3}
        console.log(set.values())	// SetIterator {1, 2, 3}
        console.log(set.entries())	// SetIterator {1, 2, 3}
        
        for (let item of set.keys()) {
          console.log(item);
        }	// 1	2	 3
        for (let item of set.entries()) {
          console.log(item);
        }	// [1, 1]	[2, 2]	[3, 3]
        
        set.forEach((value, key) => {
            console.log(key + ' : ' + value)
        })	// 1 : 1	2 : 2	3 : 3
        console.log([...set])	// [1, 2, 3]

        Set 可默认遍历,默认迭代器生成函数是 values() 方法

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

        所以, Set可以使用 map、filter 方法

        let set = new Set([1, 2, 3])
        set = new Set([...set].map(item => item * 2))
        console.log([...set])	// [2, 4, 6]
        
        set = new Set([...set].filter(item => (item >= 4)))
        console.log([...set])	//[4, 6]

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

        let set1 = new Set([1, 2, 3])
        let set2 = new Set([4, 3, 2])
        
        let intersect = new Set([...set1].filter(value => set2.has(value)))
        let union = new Set([...set1, ...set2])
        let difference = new Set([...set1].filter(value => !set2.has(value)))
        
        console.log(intersect)	// Set {2, 3}
        console.log(union)		// Set {1, 2, 3, 4}
        console.log(difference)	// Set {1}

2. WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中

WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
  • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素

属性:

  • constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数
    const arr = [[1, 2], [3, 4]]
    const weakset = new WeakSet(arr)
    console.log(weakset)

方法:

  • add(value):在WeakSet 对象中添加一个元素value
  • has(value):判断 WeakSet 对象中是否包含value
  • delete(value):删除元素 value
  • clear():清空所有元素,注意该方法已废弃
var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window)	// true
ws.has(foo)	// false

ws.delete(window)	// true
ws.has(window)	// false

3. 字典(Map)

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值
  • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
const m = new Map()
const o = {p: 'haha'}
m.set(o, 'content')
m.get(o)	// content

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

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作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

如果读取一个未知的键,则返回undefined

new Map().get('asfddfsasadf')
// undefined

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

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

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

Map 的属性及方法

属性:

  • constructor:构造函数
  • size:返回字典中所包含的元素个数
    const map = new Map([
      ['name', 'An'],
      ['des', 'JS']
    ]);
    
    map.size // 2

操作方法:

  • set(key, value):向字典中添加新元素
  • get(key):通过键查找特定的数值并返回
  • has(key):判断字典中是否存在键key
  • delete(key):通过键 key 从字典中移除对应的数据
  • clear():将这个字典中的所有元素删除

遍历方法

  • Keys():将字典中包含的所有键名以迭代器形式返回
  • values():将字典中包含的所有数值以迭代器形式返回
  • entries():返回所有成员的迭代器
  • forEach():遍历字典的所有成员
const map = new Map([
            ['name', 'An'],
            ['des', 'JS']
        ]);
console.log(map.entries())	// MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}

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

map[Symbol.iterator] === map.entries
// true

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

对于 forEach ,看一个例子

const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};

let map = new Map([
    ['name', 'An'],
    ['des', 'JS']
])
map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);
// Key: name, Value: An
// Key: des, Value: JS

在这个例子中, forEach 方法的回调函数的 this,就指向 reporter

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

  1. Map 转 Array
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log([...map])	// [[1, 1], [2, 2], [3, 3]]
  2. Array 转 Map
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log(map)	// Map {1 => 1, 2 => 2, 3 => 3}
  3. Map 转 Object
    因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。
    function mapToObj(map) {
        let obj = Object.create(null)
        for (let [key, value] of map) {
            obj[key] = value
        }
        return obj
    }
    const map = new Map().set('name', 'An').set('des', 'JS')
    mapToObj(map)  // {name: "An", des: "JS"}
  4. Object 转 Map
    function objToMap(obj) {
        let map = new Map()
        for (let key of Object.keys(obj)) {
            map.set(key, obj[key])
        }
        return map
    }
    
    objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
  5. Map 转 JSON
    function mapToJson(map) {
        return JSON.stringify([...map])
    }
    
    let map = new Map().set('name', 'An').set('des', 'JS')
    mapToJson(map)	// [["name","An"],["des","JS"]]
  6. JSON 转 Map
    function jsonToStrMap(jsonStr) {
      return objToMap(JSON.parse(jsonStr));
    }
    
    jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}

4. WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

属性:

  • constructor:构造函数

方法:

  • has(key):判断是否有 key 关联对象
  • get(key):返回key关联对象(没有则则返回 undefined)
  • set(key):设置一组key关联对象
  • delete(key):移除 key 的关联对象
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);

5. 总结

  • Set

    • 成员唯一、无序且不重复
    • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    • 可以遍历,方法有:add、delete、has
  • WeakSet

    • 成员都是对象
    • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
    • 不能遍历,方法有add、delete、has
  • Map

    • 本质上是键值对的集合,类似集合
    • 可以遍历,方法很多可以跟各种数据格式转换
  • WeakMap

    • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
    • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    • 不能遍历,方法有get、set、has、delete

6. 扩展:Object与Set、Map

  1. Object 与 Set
    // Object
    const properties1 = {
        'width': 1,
        'height': 1
    }
    console.log(properties1['width']? true: false) // true
    
    // Set
    const properties2 = new Set()
    properties2.add('width')
    properties2.add('height')
    console.log(properties2.has('width')) // true
  2. Object 与 Map

JS 中的对象(Object),本质上是键值对的集合(hash 结构)

const data = {};
const element = document.getElementsByClassName('App');

data[element] = 'metadata';
console.log(data['[object HTMLCollection]']) // "metadata"

但当以一个DOM节点作为对象 data 的键,对象会被自动转化为字符串[Object HTMLCollection],所以说,Object 结构提供了 字符串-值 对应,Map则提供了 值-值 的对应

本文始发于我的博客:Set、WeakSet、Map及WeakMap

整理的很不错,学习了~

@zhipairen
Copy link

WeakSet与Set不同点:
1、类Set的结构,但WeakSet的健值只能为对象,不允许存储基本类型值
2、WeakSet为弱映射,只有在可访问的情况下,才能存储在set中,若set中的值被垃圾回收,则自动清除
3、WeakSet 不可迭代,不能调用 size、keys() 等方法;注: 垃圾回收下,不能确定WeakSet下有效的键值,故不能获取所有值

WeakMap与Map不同点:
1、类Map的集合,但 WeakMap的键名必须为对象
2、WeakMap为弱集合,在垃圾回收时,可被标记回收;
3、WeakMap不支持迭代及不能调用keys()、values()、entires()方法

@sy19971224
Copy link

set确定能使用map、filter方法吗?我自己试是报错的哇,数组的方式应该不能给set用吧

@alanhe421
Copy link

set确定能使用map、filter方法吗?我自己试是报错的哇,数组的方式应该不能给set用吧

用call就行了,因为set是可迭代的。

@90neoCulture
Copy link

90neoCulture commented May 11, 2021

Set 和 Map 主要的应用场景在于 数据重组数据储存

Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

1. 集合(Set)

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

Set 本身是一种构造函数,用来生成 Set 数据结构。

new Set([iterable])

举个例子:

const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))

for (let i of s) {
    console.log(i)	// 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]	// [1, 2, 3]

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是**NaN等于自身,而精确相等运算符认为NaN不等于自身。**

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

let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1])	// [5, "5"]
  • Set 实例属性

    • constructor: 构造函数
    • size:元素数量
      let set = new Set([1, 2, 3, 2, 1])
      
      console.log(set.length)	// undefined
      console.log(set.size)	// 3
  • Set 实例方法

    • 操作方法

      • add(value):新增,相当于 array里的push

      • delete(value):存在即删除集合中value

      • has(value):判断集合中是否存在 value

      • clear():清空集合

        let set = new Set()
        set.add(1).add(2).add(1)
        
        set.has(1)	// true
        set.has(3)	// false
        set.delete(1)	
        set.has(1)	// false

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

        const items = new Set([1, 2, 3, 2])
        const array = Array.from(items)
        console.log(array)	// [1, 2, 3]
        // 或
        const arr = [...items]
        console.log(arr)	// [1, 2, 3]
    • 遍历方法(遍历顺序为插入顺序)

      • keys():返回一个包含集合中所有键的迭代器

      • values():返回一个包含集合中所有值得迭代器

      • entries():返回一个包含Set对象中所有元素得键值对迭代器

      • forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值

        let set = new Set([1, 2, 3])
        console.log(set.keys())	// SetIterator {1, 2, 3}
        console.log(set.values())	// SetIterator {1, 2, 3}
        console.log(set.entries())	// SetIterator {1, 2, 3}
        
        for (let item of set.keys()) {
          console.log(item);
        }	// 1	2	 3
        for (let item of set.entries()) {
          console.log(item);
        }	// [1, 1]	[2, 2]	[3, 3]
        
        set.forEach((value, key) => {
            console.log(key + ' : ' + value)
        })	// 1 : 1	2 : 2	3 : 3
        console.log([...set])	// [1, 2, 3]

        Set 可默认遍历,默认迭代器生成函数是 values() 方法

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

        所以, Set可以使用 map、filter 方法

        let set = new Set([1, 2, 3])
        set = new Set([...set].map(item => item * 2))
        console.log([...set])	// [2, 4, 6]
        
        set = new Set([...set].filter(item => (item >= 4)))
        console.log([...set])	//[4, 6]

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

        let set1 = new Set([1, 2, 3])
        let set2 = new Set([4, 3, 2])
        
        let intersect = new Set([...set1].filter(value => set2.has(value)))
        let union = new Set([...set1, ...set2])
        let difference = new Set([...set1].filter(value => !set2.has(value)))
        
        console.log(intersect)	// Set {2, 3}
        console.log(union)		// Set {1, 2, 3, 4}
        console.log(difference)	// Set {1}

2. WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中

WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以
  • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素

属性:

  • constructor:构造函数,任何一个具有 Iterable 接口的对象,都可以作参数
    const arr = [[1, 2], [3, 4]]
    const weakset = new WeakSet(arr)
    console.log(weakset)

方法:

  • add(value):在WeakSet 对象中添加一个元素value
  • has(value):判断 WeakSet 对象中是否包含value
  • delete(value):删除元素 value
  • clear():清空所有元素,注意该方法已废弃
var ws = new WeakSet()
var obj = {}
var foo = {}

ws.add(window)
ws.add(obj)

ws.has(window)	// true
ws.has(foo)	// false

ws.delete(window)	// true
ws.has(window)	// false

3. 字典(Map)

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值
  • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
const m = new Map()
const o = {p: 'haha'}
m.set(o, 'content')
m.get(o)	// content

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

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作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

如果读取一个未知的键,则返回undefined

new Map().get('asfddfsasadf')
// undefined

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

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

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

let map = new Map();

map.set(-0, 123);
map.get(+0) // 123

map.set(true, 1);
map.set('true', 2);
map.get(true) // 1

map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3

map.set(NaN, 123);
map.get(NaN) // 123

Map 的属性及方法

属性:

  • constructor:构造函数
  • size:返回字典中所包含的元素个数
    const map = new Map([
      ['name', 'An'],
      ['des', 'JS']
    ]);
    
    map.size // 2

操作方法:

  • set(key, value):向字典中添加新元素
  • get(key):通过键查找特定的数值并返回
  • has(key):判断字典中是否存在键key
  • delete(key):通过键 key 从字典中移除对应的数据
  • clear():将这个字典中的所有元素删除

遍历方法

  • Keys():将字典中包含的所有键名以迭代器形式返回
  • values():将字典中包含的所有数值以迭代器形式返回
  • entries():返回所有成员的迭代器
  • forEach():遍历字典的所有成员
const map = new Map([
            ['name', 'An'],
            ['des', 'JS']
        ]);
console.log(map.entries())	// MapIterator {"name" => "An", "des" => "JS"}
console.log(map.keys()) // MapIterator {"name", "des"}

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

map[Symbol.iterator] === map.entries
// true

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

对于 forEach ,看一个例子

const reporter = {
  report: function(key, value) {
    console.log("Key: %s, Value: %s", key, value);
  }
};

let map = new Map([
    ['name', 'An'],
    ['des', 'JS']
])
map.forEach(function(value, key, map) {
  this.report(key, value);
}, reporter);
// Key: name, Value: An
// Key: des, Value: JS

在这个例子中, forEach 方法的回调函数的 this,就指向 reporter

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

  1. Map 转 Array
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log([...map])	// [[1, 1], [2, 2], [3, 3]]
  2. Array 转 Map
    const map = new Map([[1, 1], [2, 2], [3, 3]])
    console.log(map)	// Map {1 => 1, 2 => 2, 3 => 3}
  3. Map 转 Object
    因为 Object 的键名都为字符串,而Map 的键名为对象,所以转换的时候会把非字符串键名转换为字符串键名。
    function mapToObj(map) {
        let obj = Object.create(null)
        for (let [key, value] of map) {
            obj[key] = value
        }
        return obj
    }
    const map = new Map().set('name', 'An').set('des', 'JS')
    mapToObj(map)  // {name: "An", des: "JS"}
  4. Object 转 Map
    function objToMap(obj) {
        let map = new Map()
        for (let key of Object.keys(obj)) {
            map.set(key, obj[key])
        }
        return map
    }
    
    objToMap({'name': 'An', 'des': 'JS'}) // Map {"name" => "An", "des" => "JS"}
  5. Map 转 JSON
    function mapToJson(map) {
        return JSON.stringify([...map])
    }
    
    let map = new Map().set('name', 'An').set('des', 'JS')
    mapToJson(map)	// [["name","An"],["des","JS"]]
  6. JSON 转 Map
    function jsonToStrMap(jsonStr) {
      return objToMap(JSON.parse(jsonStr));
    }
    
    jsonToStrMap('{"name": "An", "des": "JS"}') // Map {"name" => "An", "des" => "JS"}

4. WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的。

属性:

  • constructor:构造函数

方法:

  • has(key):判断是否有 key 关联对象
  • get(key):返回key关联对象(没有则则返回 undefined)
  • set(key):设置一组key关联对象
  • delete(key):移除 key 的关联对象
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);

5. 总结

  • Set

    • 成员唯一、无序且不重复
    • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
    • 可以遍历,方法有:add、delete、has
  • WeakSet

    • 成员都是对象
    • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
    • 不能遍历,方法有add、delete、has
  • Map

    • 本质上是键值对的集合,类似集合
    • 可以遍历,方法很多可以跟各种数据格式转换
  • WeakMap

    • 只接受对象作为键名(null除外),不接受其他类型的值作为键名
    • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
    • 不能遍历,方法有get、set、has、delete

6. 扩展:Object与Set、Map

  1. Object 与 Set
    // Object
    const properties1 = {
        'width': 1,
        'height': 1
    }
    console.log(properties1['width']? true: false) // true
    
    // Set
    const properties2 = new Set()
    properties2.add('width')
    properties2.add('height')
    console.log(properties2.has('width')) // true
  2. Object 与 Map

JS 中的对象(Object),本质上是键值对的集合(hash 结构)

const data = {};
const element = document.getElementsByClassName('App');

data[element] = 'metadata';
console.log(data['[object HTMLCollection]']) // "metadata"

但当以一个DOM节点作为对象 data 的键,对象会被自动转化为字符串[Object HTMLCollection],所以说,Object 结构提供了 字符串-值 对应,Map则提供了 值-值 的对应

本文始发于我的博客:Set、WeakSet、Map及WeakMap

map转obj那块,需要加上判断key的类型,因为map的key可以是任意类型,而obj不可以

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests