一、for in和for of的区别
相信大家面试的时候都经常会被问到for in 和for of的区别,要答上他们表面上的区别很好回答
- 时间点不同:for in 在js出现之初就有,for of出现在ES6之后
- 遍历的内容不同:for in用于遍历对象的可枚举属性(包括原型链上的可枚举属性),for of用于遍历可迭代对象的值
再举个例子,遍历一个数组:
// for in
const arr = ['a','b','c','d']
for(const index in arr) {
console.log(index)
}
// 打印结果:'0' '1' '2' '3',可以发现打印的是数组的下标,数组是特殊的对象,下标是数组对象身上的可枚举属性,打印的就是这个可枚举属性
// for of
for(const item of arr) {
console.log(item)
}
// 打印结果:'a' 'b' 'c' 'd',for of打印的就是数组里的每一项元素的值
一句话总结就是:for of遍历键值对的键,for in 遍历键值对的值。
大部分人估计都能答到上面提到的东西,但当面试官问到这个问题时,我们如何才能够回答得让面试官眼前一亮呢,这时候就需要跟面试官延申了
延申大概就是从一只蝴蝶煽动翅膀开始,一步一步跟他讲到如何在德克萨斯引起了龙卷风,就是得跟面试官吹,你的能耐。
上文我们提到了可枚举属性,可迭代对象,这两个就是突破点,一点一点慢慢跟面试官说,说到他不愿意听了,打断我们为止
二、可枚举属性
js里的属性分为数据属性与访问器属性,这里又可以吹访问器属性与数据属性分别是啥(暂不展开讲了),每一个js对象身上的属性,都有一个属性描述符用于描述属性的特性,访问器属性与数据属性都有四个特性,有两个特性相同,有两个特性互斥,我们平时用的最多的是数据属性,以数据属性为例,通过Object.getOwnPropertyDescriptor(obj, property),可以拿到属性描述符,此方法顾名思义,获取对象自身属性的属性描述符,自身也就是说不能获取到原型链上的属性,第一个参数是对象,第二个参数是属性字符串。例子:
// 还是以数组为例,获取数组的属性'0'
console.log(Object.getOwnPropertyDescriptor(['a'],'0'))
/**
* {
* configurable: true,
* enumerable: true,
* value: "a",
* writable: true
* }
*/
/* 可以看到返回了一个对象,这个对象就是属性描述符,属性描述符的属性一般被称为特性,可以看到,第二个特性enumerable就代表属性是否可枚举
可枚举的属性就可以通过for in与Object.keys遍历,数组身上有一个length属性,但上面我们用for in 循环的时候并没有打印length,说明
length属性是一个不可枚举属性,我们来看一下length属性的属性描述符:
*/
console.log(Object.getOwnPropertyDescriptor(['a'],'length'))
/**
* {
* configurable: false,
* enumerable: false,
* value: 1,
* writable: true
* }
*/
// 可以看到数组身上的length属性的属性描述符里,enumerable为false,它是一个不可枚举属性,属性描述符还有其他的特性,此处就不展开描述了,感兴趣可以自行查阅
至此我们知道了for in遍历对象的属性是根据此属性是否可枚举来遍历的,那for of来遍历对象的值是根据什么来遍历的呢,这就需要知道可迭代对象的概念了
三、可迭代对象
for of用于遍历可迭代对象,那什么是可迭代对象呢,很简单,实现了[Symbol.iterator]方法的对象,就被称为可迭代对象,Symbol是ES6以后新出的基本数据类型,可以创建唯一值,一般用作对象属性,由此特性,在Symbol上出现了一些公共符号,例如[Symbol.iterator],公共符号意味着大家都可以用此属性,此属性的作用也众所周知,大概是这么个意思,其它的公共符号还有[Symbol.hasInstance],[Symbol.toPrimitive]等等等等,它允许我们改变原生对象的行为,例如隐式转换的时候我们需要它转换成啥,调用Object.prototype.toString.call(obj)的时候我们希望它打印啥等等,它们的属性值有的是方法需要我们去实现,有的是基本数据类型。此处就不展开了,再说回[Symbol.iterator],这个属性的值是一个方法,需要我们去实现,具体怎么实现呢,看下面的例子
// 我们定义一个对象,给这个对象一个range属性,表示范围,我们希望最后迭代的时候从这个范围的开始迭代到这个范围的最后,每次迭代,值就加2
const obj = {
range: [10,100],
[Symbol.iterator]() {
let start = this.range[0]
let end = this.range[1]
return {
next() {
let val = start
start += 2
let done = val > end ? true : false
return {
value: val,
done,
}
},
[Symbol.iterator](){
return this
},
}
}
}
上面的obj对象就是一个实现了[Symbol.iterator]的可迭代对象,看起来比较复杂,但其实理清了它都做了些啥,其实也就不是特别复杂了
- [Symbol.iterator]方法返回了一个对象,这个对象被称作迭代器对象
- 迭代器对象里面有一个next方法,这个next方法又返回了一个对象,这个对象叫做迭代器结果对象
- 迭代器结果对象里面有两个属性,value和done,value就是每一次迭代返回的值,done为true时迭代停止
- 在for of 进行遍历obj时,[Symbol.iterator]方法会执行一次,之后每一次迭代都执行一次迭代器对象的next方法,返回迭代器结果对象里的value值,直到done为true,停止迭代
可以看到我们自己实现的可迭代对象与之前for of的描述好像有点差异,不再是遍历键值对的值,而是我们想怎么遍历就怎么遍历,这也应了之前我们所说的公共符合可以扩展原生对象的各种能力,我们可以给我们的每一个对象自定义一个怎么迭代的方式,让它按照我们定义的方式来进行遍历。
**注意:**上面可以看到,我们给迭代器对象自身也加了一个[Symbol.iterator]方法,返回自身,这代表我们希望迭代器对象本身也是可迭代的(忘了为什么了,有兴趣自己去查哈哈),同时实现迭代器还用到了闭包,this指向等内容,全是知识点哈哈。
for of 与 …扩展运算符都会去对象自身或者原型链上找[Symbol.iterator]方法,找到了就调用,进行上面的步骤进行遍历,找不到就报错,所以很多人说的for of跟…扩展运算符不能用于普通对象是错的,能不能用关键在于此对象是否是可迭代对象,并注意区分可迭代对象与可枚举属性。
console.log(Array.prototype)
// Symbol(Symbol.iterator): ƒ values()
当我们打印数组的原型时,可以看到数组实现了[Symbol.iterator]方法,这也是之所以数组可以用for of与…扩展运算符的原因
大家有没有发现可迭代对象实现[Symbol.iterator]方法好麻烦,怎么这么复杂,于是,就有了生成器函数
四、生成器函数
生成器函数可以直接返回一个迭代器对象,语法如下:
function* generator() {
yield 1
yield 2
yield 3
}
// generator执行,返回迭代器对象
const iterator = generator()
// 模拟每次迭代
let iteratorResult = iterator.next()
while(!iteratorResult.done) {
console.log(iteratorResult.value)
iteratorResult = iterator.next()
}
/* 依次打印1 2 3,yield就像return一样,在每次调用迭代器对象的next方法时会将yield之后的值返回到迭代器结果对象里的value属性上,在
yield 3 之后再次调用就返回undefined并将迭代器结果对象的done值设为true,此次迭代完成
*/
/* yield xxx 不仅可以返回xxx给迭代器结果对象的value值,同时,(yield xxx)作为一个整体,会在生成器函数里得到值,得到的值时什么呢,是
迭代器对象调用next时传入的值,需要注意的是,调用生成器函数时,只返回迭代器对象,不会执行生成器函数里的一行代码,只有调用迭代器对象的next方法时,生成器函数里面的代码才开始执行,生成器函数会在每次遇到yield时暂停,然后等待调用next,next()传入的值,需要通过(yield xxx)接收,但第一次调用next时,并没有遇到暂停的yield,所以第一次传入的值会丢失,如下面的例子
*/
function* smallNumbers(){
console.log("next()第一次被调用;参数被丢弃")
let y1 = yield 1;
console.log("next()第二次被调用;参数是:", y1)
let y2 = yield 2;
console.log("next()第三次被调用;参数是:", y2)
let y3 = yield 3;
console.log("next()第四次被调用;参数是:", y3)
}
let g = smallNumbers();
console.log("创建了生成器,代码未运行")
let n1 = g.next("a");
console.log("生成器回送", n1.value)
let n2 = g.next("b");
console.log("生成器回送", n2.value)
let n3 = g.next("c");
console.log("生成器回送", n3.value)
let n4 = g.next("d");
console.log("生成器回送", n4.value)
/*
创建了生成器,代码未运行
next()第一次被调用;参数被丢弃
生成器回送 1
next()第二次被调用;参数是: b
生成器回送 2
next()第三次被调用;参数是: c
生成器回送 3
next()第四次被调用;参数是: d
生成器回送 undefined
*/
以上基本上就是生成器函数的基本内容了,不知道大家有没有发现,生成器函数的一个非常牛逼的特点,就是只有在迭代器对象调用next时才会执行代码,遇到yield就暂停,生成器函数可以暂停代码的执行,利用此特性我们可以与回调函数一起,实现很多效果,例如冒泡排序在页面上的展示,鼠标点一下冒泡排序动一下,再比如基于生成器实现异步的代码实现,暂停代码执行,向后端发请求,等到请求拿到结果了,再调用next让代码继续执行,这时你们会不会联想到什么,对,async await,async await就是生成器函数的语法糖,配合promise,用生成器函数加yield也可以实现async await同样的效果,示例如下:
// 模拟一个异步请求
function getData(str){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(str)
},1000)
})
}
// 然后写一个生成器函数,yield模拟await
function* pretendAsync(){
const res1 = yield getData('111')
const res2 = yield getData('222')
console.log(res1)
console.log(res2)
}
// 最后利用生成器的特性写一个运行这个生成器,将promise里的数据返回给res
function run(asyncFn){
//运行生成器函数,获取迭代器对象
const iterator = asyncFn()
//递归调用exec函数,将请求得到的数据通过迭代器的next方法传参给res
function exec(res){
//第一次.next,参数会丢失,同时res是undefined,但之后每次next的res都是返回的数据
let result = iterator.next(res)
//递归出口
if(result.done){
return
}
//result.value是getData得到的promise,通过这个promise.then(exec),递归调用exec
result.value.then(exec)
}
exec()
}
run(pretendAsync)
非常巧妙,至此,面试官已经对你刮目相看了