Iterator|Set|Map|WeakSet

560 阅读11分钟

js引用数据类型:对象、数组、函数、日期、正则、set、map

在讲set-map之前我需要带大家认识下迭代器iterator这个属性

迭代器iterator

能够被for...of遍历的东西才具有迭代器属性

  •  这个属性字节面试被问过

for...of

for...of用例如下

var arr = [1, 2, 3]
var str = 'abc'
var obj = {
	a: 1,
	b: 2,
	c: 3
}

for(var item of arr){
	console.log(item)
} // 1 2 3
for(var item of str){
	console.log(item)
} // a b c
for(var item of obj){
	console.log(obj)
} // 报错:obj is not iterable

js中遍历方法太多了,但for...of这个遍历方法是专门供具有迭代器属性的数据结构来使用的。从上面用例我们就可以看出,数组、字符串都具有迭代器属性,但是普通对象是没有的,这里我对对象强调普通二字是因为Set和Map。

Set

  1. 是一种key和value相等的特殊对象
  2. set对象中的值是唯一的
  3. 具有迭代器(iterator)属性

set是一个类数组

上一次聊类数组还是介绍arguments这个东西,大家感兴趣可以去看看关于arguments这个函数属性的文章深入认识js四大常用数据类型判断,一次性带你搞明白(内含面试热点)! - 掘金 (juejin.cn)

arguments是函数中形参的统称

什么是类数组呢?

类数组本质上是一种对象,它与数组类似,具有数组的下标和长度属性,其他数组的方法它都没有,如pop、push

set如何创建呢?又如何查看呢?

let set = new Set([1,2,3,4])
console.log(set) // Set(4) {1, 2, 3, 4}

set是没有符号表达,它的表示方法就是上面这样,接受的参数必须是个数组,并且值唯一,如果我们重复放元素进去,最终得到的元素将会是去重后的

如果我们重复放值进去

let set = new Set([1,2,2,3,3,4])
console.log(set) // Set(4) {1, 2, 3, 4}

第一次见识到这个东西的时候我觉得很牛,完全可以用set方法对一个数组去重

去重

var arr = [1,1,1,2,2,'1']
function unique(){
	return Array.from(new Set(arr))
}
console.log(unique(arr)) // [ 1, 2, '1' ]

//可以用箭头函数对代码进行优化,这里是解构了类数组
var unique = (arr) => [...new Set(arr)]
console.log(unique(arr)) // [ 1, 2, '1' ]

想要用set对一个数组去重,最后输出还是一个数组,就必须进行一个转换,Array.from的作用就是专门用来将类数组转换成数组,当然你也可以直接用es6新增的解构进行输出,解构同样适用于类数组

下面开始介绍set的方法

has方法

has方法用于查看set中值是否存在

let set = new Set([1,2,3,4])
console.log(set.has(1)); // true

set并不能直接通过下标方法进行查看单个值,只能判断值有没有

add方法

add方法其实就像数组中的push,从尾部增加新值

let set = new Set([1,2,3,4])
set.add(5)
console.log(set) // Set(5) { 1, 2, 3, 4, 5 }

size属性

size就是数组的length,查看大小

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

delete方法

删除指定的元素

let set = new Set(['a','b','c'])
set.delete('a')
console.log(set) // Set(2) { 'b', 'c' }

clear方法

清空set中所有的值

let set = new Set([1,2,3,4])
set.clear()
console.log(set) // Set(0) {}

forEach方法

对set进行遍历

let set =  new Set(['a', 'b', 'c'])
set.forEach((item,index,set) =>{
    console.log(item,index,set);
})
// 下面是输出
a a Set(3) { 'a', 'b', 'c' }
b b Set(3) { 'a', 'b', 'c' }
c c Set(3) { 'a', 'b', 'c' }

从这里可以发现:set中的key和value是相同的!

既然set可以被自己的方法遍历,那能否被for...of遍历,看看他是否具有迭代器属性

let set =  new Set(['a', 'b', 'c'])
// for(var item of set.keys())
for(var item of set){
	console.log(item)
} // a b c

是有的,不过我们用set.keys()迭代会更多一点

这些方法都是用实例对象来调用的,因此我们可以推断出这些方法都是挂在Set原型上的,不妨去浏览器看看都有哪些

1.png

常用的方法其实上面已经都介绍了,里面的Symbol(Symbol.iterator)就是迭代器属性

Map

  1. 可以用任意数据类型作key的一种对象
  2. 也具有迭代器属性

普通对象的key是个字符串类型,我们不妨试试把一个数组挂到一个对象里面去当key

let obj = {
    a: 1,
    b: 2
}

var arr = [1,2]
// obj.arr = 3
obj[arr] = 3
console.log(obj); // { a: 1, b: 2, '1,2': 3 }

这里需要注意,当你想添加某个key去作对象的属性的时候,我们要用中括号,而非点,如果这里用obj.arr = 3,那么arr则被当成一个key

根据这个输出结果可以看出,这个数组中的元素成为了字符串。

所以map就解决了这个问题,map的意义,就是可以用任意数据类型做key,因此,你可以理解为进阶版的对象,弥补了普通对象的不足之处,这个东西出现的频率高于set。

map和set同样,都没有符号去表达,如何创建呢,如下

let map = new Map([['name','小黑子'],['age',18]])
console.log(map); // Map(2) { 'name' => '小黑子', 'age' => 18 }

括号里面是也是个数组的形式,每个元素作为一个键值对

下面开始介绍map的方法

get方法

获取键对应的值

let map = new Map([['name','小黑子'],['age',18]])
console.log(map.get('name')) // 小黑子

set方法

这里的set是map中的方法,不是上面讲的set引用类型,不要搞混淆了噢

let map = new Map([['name','小黑子'],['age',18]])
let obj = {a: 1}
map.set(obj,'哈哈')
console.log(map) // Map(3) { 'name' => '小黑子', 'age' => 18, { a: 1 } => '哈哈' }
console.log(map.get({a: 1})) // undefined
console.log(map.get(obj)) // 哈哈

这里解释下为什么倒数第二个不能打印,get查找方法是根据引用来查的,也就是地址,因此必须要用一个东西来装这个地址再来get。

这里突然想起一个小知识点,{} === {}

console.log({} === {}) // false

对于引用类型来说,每个对象都是不一样的,这里指的是地址噢,哪怕内容相同,上期文章说过,三个等于号===对于引用类型来讲,比较的是值,数据类型和地址

forEach方法

其实map和set的这个方法都是遍历,但是map有点不同在于参数顺序反了

let map = new Map([['name','小黑子'],['age',18]])
map.forEach((value,key,map)=>{
    console.log(value,key,map)
})
// 下面是输出结果
小黑子 name Map(2) { 'name' => '小黑子', 'age' => 18 }
18 age Map(2) { 'name' => '小黑子', 'age' => 18 }

map这里是先输出值,再输出键

map是否有迭代器呢

let map = new Map([['name','小黑子'],['age',18]])
// for(var item of map.entries())
for(var item of map){
	console.log(map)
}
// 输出结果
[ 'name', '小黑子' ]
[ 'age', 18 ]

也有,但是我们通常会用map.entries()

另外和set一样,也有has、delete、clear、size、keys等方法

我们去浏览器中看看map原型的方法

2.png

基本上都带大家认识了一遍,各位小伙伴也可以自己试试。

WeakSet

WeakSet和Set类似,你也完全可以理解为阉割之后的Set

  • WeakSet只能存储对象和Symbol类型
  • WeakSet没有迭代器属性,不可遍历
  • WeakSet只有add、delete、has方法
  • WeakSet是弱引用,不被垃圾回收机制所注意

下面就按照这个顺序讲一下:

WeakSet创建和Set一样,用实例对象

let ws = new WeakSet()

现在我们来验证下存放的类型是不是只能是对象和Symbol

ws.add(1) // TypeError: Invalid value used in weak set
ws.add(Symbol('hello')) // WeakSet {Symbol(hello)}
ws.add({}) // WeakSet {Symbol(hello),{}} 

没问题的,这里的原始类型我只验证了数字类型,原始7个中除了Symbol中,其余都是会报错的,引用类型都可以当作对象,你放函数,日期,数组都可以

我看到很多人的介绍文章都说只能是对象,不能是原始,咳咳,什么年代的文章,该提醒他们去更新下了

WeakSet是阉割版的Set,它没有迭代器属性,额……这个我咋解释,要不你们就接受他吧[doge]

因为它不被垃圾回收机制注意,随时被收走,不会被遍历,合理!

WeakSet是阉割版的Set,里面的方法只剩下这三个(add、delete、has)了,和上面的Set一样,我这里也不赘述了

弱引用和垃圾回收机制---重头戏来了!

弱引用:如果WeakSet中的对象在其他地方没有强引用,那么垃圾回收机制会自动清理掉该对象

垃圾回收机制真要讲这里是讲不完的,但是可以说的是,JavaScript中垃圾回收机制是自动进行的,不像C语言一样,需要free手动释放

讲到垃圾回收机制了,就要扯到内存,穿插一个小知识点(突然想到的)

const a = []
a.push(1)
console.log(a) // [1]

肯定有小伙伴以为这里会报错,const不能改变啊,但是这里其实就是因为一个内存问题,引用类型真正存在堆(heap)中,存在栈(stack)中的只是一个地址,所以a其实是个地址,a.push(1)就是沿着地址从栈跑到堆中找到这个数组,添加一个新值,最后输出a这个数组

好,回到正题,我们以前用普通对象的时候都是一个强引用,比如

let obj= {name: '小黑子'}
console.log(obj) // {name: '小黑子'}

这段代码对于浏览器来说,执行第一行代码的时候,垃圾回收机制(Garbage Collection),我们就形象地称他为清洁工(GC)吧,这个清洁工他会看下下面是否引用(强引用)了,下面需要打印这个对象就是一个强引用,如果没有强引用,执行完了let obj= {name: '小黑子'}这行代码,清洁工就会把它扫走

obj = null

这是众多垃圾回收机制中的一种,称为引用计数(这也是早期的GC方法,因为无法用在循环中,现在不再使用)。让一个对象等于null,就是给了这个对象一个垃圾的标签,等待清洁工去清理

let obj= {name: '小黑子'}
obj = null
console.log(obj) // null

而弱引用对于清洁工(GC)来说,GC是无视它的,一个对象只要是被WeakSet了,就会被弱引用,那么清洁工就会来清理它,这么说,那WeakSet的存在意义是什么?我们不妨来验证下是否果真如此

3.png

啊?浏览器的GC没有清理它!这是什么情况?

那我手动去清理它

4.png

GC还是没有去清理他,好吧,不卖关子了!其实这个bug曾经在StackOverflow里面广为流传,其原因是浏览器的GC是不受我们控制的,我们也无法得知GC何时执行

我们也可以在node中查看heapUsed

当然我们运行需要输入node --expose-gc myTest.js才能查看

global.gc()
console.log(process.memoryUsage()); // 初始heapUsed: 4612776 ≈ 4.4M
let obj= {name: '小黑子',age: new Array(5 * 1024 *1024)}
// 上面的Array这么写是为了让占用空间更大
let ws = new WeakSet()
ws.add(obj)
global.gc()
console.log(process.memoryUsage()); // 弱引入一个对象heapUsed: 46873120 ≈ 44.7M

obj = null
global.gc()
console.log(process.memoryUsage()); // 让obj被扫走heapUsed: 4929872 ≈ 4.7M

node中需要我们手动扫走他,浏览器扫没扫走你也无法控制

阮一峰老师讲弱引用的话术是下面这样的

5.png

话语比较官方,可能比较难以理解,总之,你要清楚的是浏览器清理机制你也无法控制,弱引用的对象如果后面没有强引用,GC会自动把他清走

应用场景

WeakSet其实就是可以解决一个内存泄漏问题,比如我们给一个按钮打上禁用,如果我们用了Set,这样是强引用,会浪费空间,WeakSet就可以回收掉这个浪费的空间,因为理论上来讲他就是空的,迟早会被清空掉

    <div id="wrap">
        <button id ="btn">确认</button>
    </div>

    <script>
        let wrap = document.getElementById('wrap');
        let btn = document.getElementById('btn');//节点对象
        //给btn打上标签
        //set是强引用,浪费空间,用new WeakSet()可以回收掉浪费的空间,理论上认为它是空
        const disabledElement = new WeakSet()
        disabledElement.add(btn)
        btn.addEventListener('click', () =>{
            wrap.removeChild(btn)
            console.log(disabledElement);
        })  
    </script>

WeakMap和WeakSet同理,是弱引用版本的Map,随时可能被浏览器GC清走

文章参考书籍:ECMAScript6入门---阮一峰

Set 和 Map 数据结构 - ECMAScript 6入门 (ruanyifeng.com)


如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge]