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
- 是一种key和value相等的特殊对象
- set对象中的值是唯一的
- 具有迭代器(
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原型上的,不妨去浏览器看看都有哪些
常用的方法其实上面已经都介绍了,里面的Symbol(Symbol.iterator)
就是迭代器属性
Map
- 可以用任意数据类型作key的一种对象
- 也具有迭代器属性
普通对象的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原型的方法
基本上都带大家认识了一遍,各位小伙伴也可以自己试试。
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的存在意义是什么?我们不妨来验证下是否果真如此
啊?浏览器的GC没有清理它!这是什么情况?
那我手动去清理它
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中需要我们手动扫走他,浏览器扫没扫走你也无法控制
阮一峰老师讲弱引用的话术是下面这样的
话语比较官方,可能比较难以理解,总之,你要清楚的是浏览器清理机制你也无法控制,弱引用的对象如果后面没有强引用,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]