for in 和 for of

245 阅读5分钟

一、for in(ES5)以任意顺序迭代对象的可枚举属性

1、什么叫可枚举属性

  1. 在属性对象中,enumerable值为true时,这个属性就是可枚举的
      const obj = { a: 'a', b: 'b' }

      Object.prototype.c = 'c'

      Object.defineProperty(obj, 'd', {
        value: 'd'
      })

通过Object.getOwnPropertyDescriptors(obj)可以获取到obj自身属性的描述对象

image.png

由此可见,a、b可以被for in遍历,而d不可以被fon in遍历

  1. 如果我们打印数组的属性描述对象:Object.getOwnPropertyDescriptors(['a', 'b'])

image.png

由此可见,数组也是可以被for in遍历的,数组中的length属性不会被for in遍历到。同理,字符串也可以被for in遍历

另外,symbol键不会被for..in遍历到。

通过实例方法propertyIsEnumerable也可以知道该属性是否可枚举:

      console.log(obj.propertyIsEnumerable('a')) // true
      console.log(obj.propertyIsEnumerable('c')) // false
      console.log(obj.propertyIsEnumerable('d')) // false

2、遍历的范围

for in会拿到原型上的数据

      const obj = {
        name: 'xx',
        age: 10
      }

      Object.prototype.aa = 'aa'

      for (const key in obj) {
        console.log(key, obj[key])
      }

若希望过滤掉原型上的属性:(为什么不用obj.hasOwnProperty(key)?

      for (const key in obj) {
        if (Object.hasOwn(obj, key)) {
          console.log(key, obj[key])
        }
      }

其实,我最希望你尽量不要使用for...in,因为它会查找原型,效率肯定比不过for...of:

      for (const key of Object.keys(obj)) {
        console.log(key, obj[key])
      }

3、得到的结果:for in遍历的是键名

      const obj = {
        name: 'xx',
        age: 10
      }

      for (const key in obj) {
        console.log(key)
      }
      /*
        name
        age
      */

二、for of(ES6)遍历可迭代数据

1、什么叫可迭代数据

  1. 在ES6中,具有Symbol.iterator属性,它是一个函数,返回一个对象,调用对象中的next方法能得到目标的每一项
      const arr = ['a', 'b']

      const iterator = arr[Symbol.iterator]()

      console.log(iterator.next()) // {value: 'a', done: false}
      console.log(iterator.next()) // {value: 'b', done: false}
      console.log(iterator.next()) // {value: undefined, done: true}

依次类推,String、Map、Set、TypedArray、arguments、NodeList都可以使用for of遍历

迭代器的实现: Iteartor迭代器规范

      var arr = ['a', 'b', 'c']
      arr[Symbol.iterator] = function () {
        var _this = this
        var index = 0
        return {
          next: function () {
            if (index === _this.length ) {
              return { value: undefined, done: true }
            }
            return { value: _this[index++], done: false }
          }
        }
      }
      for(const item of arr){
        console.log(item)
      }

遍历一个数组时,实际上内部走的是迭代器方法

2、遍历的范围

for of只能遍历自身的属性,不能遍历到原型上的属性

      const arr = ['a', 'b']

      Array.prototype.aa = 'aa'

      for (const item of arr) {
        console.log(item)
      }

如果在迭代器中加上Object.keys(this.__proto__).forEach(item=>this.push(this.__proto__[item]))可以模拟能拿到原型上的数据

3、得到的结果:for of遍历的是键值

      const arr = ['a', 'b', 'c']

      for (const item of arr) {
        console.log(item)
      }
      /*
        a
        b
        c
      */

for of也可以拿到下标,和entries()结合使用:

      const arr = ['a', 'b', 'c']

      for (const [index, item] of arr.entries()) {
        console.log(index, item)
      }
      /*
        0 'a'  ->  这里的0是数字
        1 'b'
        2 'c'
      */

三、for in和for of的使用场景

  • for in一般情况下用来遍历对象,也可以遍历数组,但是在遍历数组时会有些问题
  • for of一般情况下用来遍历数组,不可以遍历对象,因为对象没有迭代器
    • 使用[Symbol.iterator]检查某个数据类型有没有迭代器,数组和字符串可以使用for of遍历
      console.log({}[Symbol.iterator]) // undefined
      console.log(new Number(123)[Symbol.iterator]) // undefined
      console.log(new Boolean()[Symbol.iterator]) // undefined
      console.log([][Symbol.iterator]) // ƒ values() { [native code] }
      console.log(''[Symbol.iterator]) // ƒ [Symbol.iterator]() { [native code] }

四、为什么不推荐使用for in来遍历数组?

1、for in遍历数组时,得到的下标是字符串类型,容易疏忽掉

      const arr = ['a', 'b', 'c']
      for (const index in arr) {
        console.log(index, typeof index)
      }
      /*
        0 string
        1 string
        2 string
      */

2、for in会遍历到原型上的属性,在大多数场景下并不需要遍历原型上的属性,如果要过滤掉原型上属性还要做一层处理

      const arr = ['a', 'b', 'c']
      Array.prototype.aa = 'aa'
      for (const index in arr) {
        console.log(index, arr[index])
      }
      /*
        0 a
        1 b
        2 c
        aa aa
      */

从性能的角度说,for in的性能比较差

3、for in遍历的顺序

      var obj = {
        100: 100,
        50: 50,
        1: 1,
        0: 0,
        name: 'xx',
        '-0': '-0',
        '-100': '-100',
        3.14: 3.14
      }
      obj[10] = 10

      for (const key in obj) {
        console.log(key, obj[key])
      }

打印结果:

image.png

为什么?

因为for in遍历对象时,会首先找到对象中的非负整数属性,将这一部分的属性按照升序遍历,再找到其他的属性,按照创建时的顺序遍历出来

如果想按照创建时的顺序遍历出来,必须要避开属性名是非负整数

五、如果说非要使用for of来遍历对象呢?(仅做了解)

1、对象有length属性

因为obj内部没有实现迭代器,所以使用for of来遍历一个没有迭代器的对象会直接报错

      var obj = {
        0: 'a',
        1: 'b',
        2: 'c',
        length: 3
      }
      // Uncaught TypeError: obj is not iterable
      for (const key of obj) {
        console.log(key)
      }

如果给obj对象添加一个迭代器,或者给Object的原型上添加一个迭代器,那么就可以使用for of来遍历了

      obj[Symbol.iterator] = Array.prototype[Symbol.iterator]
      // Object.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]

仅仅是将数组的迭代器拿过来给对象使用,那么for...of遍历的对象需要有个length属性,此时的对象是一个类数组

2、对象没有length属性

而对一个对象实现迭代器,可以手写:

      const obj = {
        name: 'xx',
        0: 0,
        [Symbol()]: 'symbol属性值'
      }

      Object.prototype[Symbol.iterator] = function () {
        var _this = this
        var index = 0
        var keys = Reflect.ownKeys(this) // ES6:Reflect.ownKeys可以获取到symbol类型的键,Object.keys只能拿到非symbol类型的键
        return {
          next: function () {
            if (index > keys.length - 1) return { done: true, value: undefined }
            return { done: false, value: _this[keys[index++]] }
          }
        }
      }

      for (const item of obj) {
        console.log(item)
      }
      /*
        0
        xx
        symbol属性值
      */

六、for await of异步迭代器

for...of是同步打印,如:

      const asyncFn = (timeout) => {
        return new Promise((res) => {
          setTimeout(() => {
            res(timeout)
          }, timeout)
        })
      }

      const test = () => {
        const arr = [asyncFn(2000), asyncFn(1000), asyncFn(3000)]
        for (const item of arr) {
          item.then(console.log)
        }
      }
      test()
      /*
        每隔1秒,顺序打印1000 2000 3000
      */

有时,我们希望循环可以等待每个promise对象的状态变为resloved才进入下一步,此时就需要用到for await of循环:

      const test = async () => {
        const arr = [asyncFn(2000), asyncFn(1000), asyncFn(3000)]
        for await (const item of arr) {
          console.log(Date.now(), item)
        }
      }
      /*
        1670999130324 2000
        1670999130325 1000
        1670999131317 3000
      */

打印顺序和数组写入顺序一致,和Promise.all有点像