likes
comments
collection
share

从for in 和for of的区别吹到async await

作者站长头像
站长
· 阅读数 18

一、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)

非常巧妙,至此,面试官已经对你刮目相看了

转载自:https://juejin.cn/post/7241838768016605244
评论
请登录