一文解惑:ES6的for...of 不能遍历Object对象?

4,266 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

通过本篇文章,你可了解到以下知识点:

  1. for of的常用用法
  2. 和其他循环的区别
  3. 不能遍历Object对象?
  4. 如何遍历Object对象?
  5. 关乎for of不能遍历的Iterator介绍
  6. 如何给Object增加Symbol.iterator方法

如果你想了解更详细的解说,请认真往下阅读;如果你想快速查看结论,请看『总结』目录。

前言

随着前端的不断发展,光循环就出现了好多种方法,forforEachdo..whilefor...in等等,不过这些循环也都各有各的应用场景和优缺点。

ES6又为我们提供了新的循环方法for...of,它可以循环字符串数组及其他类数组对象,那作为最普遍存在的Object对象,按理,可以循环?

我们看一下下方的代码示例:

{
    // 迭代数组
  const iterable = ['a''b'];
  for (const value of iterable) {
    console.log(value);
  }
  // output: a b
}
{
    // 普通对象
    const obj = {
      a'A',
      b'B'
    }
    for(const item of obj){
      console.log(item)
    }
    // Uncaught TypeError: obj is not iterable
}

oh no,报错了:Uncaught TypeError: obj is not iterable。提示obj是不可迭代的,显然直接用for...of去遍历Object对象是不行的。

本篇文章就带你深入了解一下,可以遍历大部分数据结构的for...of为何不能遍历Object对象。

for of的概念和常用用法

es6阮一峰介绍

概念

ES6引入for...of循环,作为遍历所有数据结构的统一的方法,创建一个循环来迭代可迭代的数据结构,包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法

语法

  • variable:每个迭代的属性值被分配给该变量。
  • iterable:一个具有可枚举属性并且可以迭代的对象。
for (variable of iterable) {
    statement
}

常用用法

数组

数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器。

{
  // 迭代数组
  const iterable = ['a''b'];
  for (const value of iterable) {
    console.log(value);
  }
  // output: a b
}

对比for...in循环,for...in循环只能获得对象的键名,不能直接获取键值。而ES6提供的for...of允许遍历获得键值。

const iterable = ['a', 'b', 'c', 'd'];

for (let value in iterable) {  // value为键名索引
  console.log(value);
}
// output: 0 1 2 3

for (let value of iterable) { // value为键值,如需获取索引可以借助数组实例的`entries`方法和`keys`方法
  console.log(value);
}
 // output: a b c d

Set和Map结构

Set和Map结构介绍

Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。

const iterable = new Set(["a", "b", "b", "c"]);
for (let value of iterable) {
  console.log(value);
}
// output: a b c

const iterable = new Map();
es6.set("a", 1);
es6.set("b", 2);
es6.set("c", 3);
for (let [name, value] of iterable) {
  console.log(name + ": " + value);
}
// output:
// a: 1
// b: 2
// c: 3

类数组对象

1. 字符串

// 字符串
let str = "hello";

for (let s of str) {
  console.log(s); // h e l l o
}

2. DOM NodeList 对象

// DOM NodeList对象
let paras = document.querySelectorAll("p");

for (let p of paras) {
  p.classList.add("test");
}

3. arguments对象

// arguments对象
function printArgs() {
  for (let x of arguments) {
    console.log(x);
  }
}
printArgs('a', 'b');
// 'a'
// 'b'

对象(不能使用for...of遍历)

// 普通对象
const obj = {
  a'A',
  b'B'
}
for(const item of obj){
  console.log(item)
}
// Uncaught TypeError: obj is not iterable

我们发现,对于普通的对象,for...of结构不能直接使用,会报错,提示obj is not iterable,也就是说普通对象默认没有Iterator接口,必须部署了 Iterator 接口后才能使用。

和其他循环的区别

循环名称循环对象是否可中断循环是否有返回值
forfor循环体的length可以无返回值
forEach仅可循环数组、map、set等,不可循环字符串、普通对象不可以无返回值
do...while满足某种条件,则可一直循环,至少循环一次可以无返回值
while满足某种条件,则可一直循环可以无返回值
map组成新的数组成员,仅可循环数组,不可循环字符串、普通对象,set、map不可中断返回新数组,不影响原数组
filter过滤数组成员,仅可循环数组,不可循环字符串、普通对象,set、map不可中断返回新数组,不影响原数组
for...in可循环数组、对象,不可循环map、set。可遍历数字键名,还可遍历手动添加的其他键,甚至包括原型链上的键可以无返回值
for...of循环可迭代的对象,不可循环普通对象(统一数据结构遍历)可以无返回值
Array.prototype.newArr = () => {};
Array.prototype.anotherNewArr = () => {};
const array = ['foo''bar''baz'];
for (const value in array) { 
  console.log(value); // 0 1 2 newArr anotherNewArr
}
for (const value of array) {
  console.log(value); // 'foo', 'bar', 'baz'
}

不能遍历Object对象?

for...of介绍章节,我们提到了一个可迭代数据结构。那什么是可迭代数据结构呢?

实际上,任何具有 Symbol.iterator 属性的数据结构都是可迭代的。

当用for...of迭代普通对象时,会报obj is not iterable。也就是说普通对象是不可迭代的,没有Symbol.iterator属性。

我们查看几个可被for...of迭代的对象原型,看看和普通对象有何不同: Set数据结构

image.png

Array数组

image.png

Object对象

image.png

可以看到,这些可被for of迭代的对象,都实现了一个Symbol(Symbol.iterator)方法,而普通对象没有这个方法。

简单来说,for...of 语句创建一个循环来迭代可迭代的对象,可迭代的对象内部实现了Symbol.iterator方法,而普通对象没有实现这一方法,所以普通对象是不可迭代的。

关乎for of不能遍历的Iterator介绍

Iterator ES6阮一峰介绍

ES6 为了统一集合类型数据结构的处理,增加了 iterator 接口,供 for...of 使用,简化了不同结构数据的处理。而 iterator 的遍历过程,则是类似 Generator 的方式,迭代时不断调用next方法,返回一个包含value(值)和done属性(标识是否遍历结束)的对象。

如何遍历Object对象

通过前面的用法示例,我们知道,for...of可以迭代数组、Map等数据结构,顺着这个思路,我们可以结合对象的Object.values()Object.keys()Object.entries()方法以及解构赋值的知识来用for...of遍历普通对象。

1. Object扩展方法values()、keys()、entries()

Object.values()返回普通对象的键值组成的数组

Object.keys()返回普通对象的键名组成的数组

Object.entries()返回普通对象的键名键值组成的二维数组

const obj = {
  a'A',
  b'B'
}
// Object.values()
console.log(Object.values(obj)) // ["A", "B"]

// Object.keys()
console.log(Object.keys(obj)) // ["a", "b"]\

// Object.entries()
console.log(Object.entries(obj)) // [["a","A"],["b","B"]]

for (let [key, value] of Object.entries(obj)) {
    console.log(key, value);
}
// a A
// b B

2. Generator 函数重新包装对象

const obj = { a: 1, b: 2, c: 3 }

function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}

for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3

Object增加Symbol.iterator方法

给普通对象增加Symbol.iterator方法,不用担心[Symbol.iterator]属性会被Object.keys()获取到导致遍历结果出错,因为Symbol.iterator这样的Symbol属性,需要通过Object.getOwnPropertySymbols(obj)才能获取,Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。

// 普通对象
const obj = {
  a'A',
  b'B',
  [Symbol.iterator]() {
    // 这里Object.keys不会获取到Symbol.iterator属性
    const keys = Object.keys(obj);
    let index = 0;
    return {
      next() => {
        if (index < keys.length) {
          // 迭代结果 未结束
          return {
            valuethis[keys[index++]],
            donefalse
          };
        } else {
          // 迭代结果 结束
          return { valueundefineddonetrue };
        }
      }
    };
  }
}
// 可以直接使用for of遍历obj对象了
for (const value of obj) {
  console.log(value); // A B
};
// 也可以将对象转成数组
console.log([...obj]); // ['A', 'B']

默认调用Iterator接口的场合

  • 扩展运算符(...):ES6数组增加了扩展运算符,任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组(毫不意外的,代码[...{}]会报错,而[...'123']会输出数组['1','2','3'])。
  • 变量的结构赋值: 数组和可迭代对象的解构赋值(解构是ES6提供的语法糖,其实内在是针对可迭代对象Iterator接口,通过遍历器按顺序获取对应的值进行赋值。而普通对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量
  • yield*_yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口;
  • 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用;
  • 字符串:S6 为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历。

总结

for of不能遍历Object对象?

for...of不能遍历Object是因为for...of内部没有iterator接口,没有Symbol.iterator()这个属性,for...of只能遍历有iterator接口的数据结构。

for of如何遍历Object对象?

1. Object扩展方法

Object.values()返回普通对象的键值组成的数组

Object.keys()返回普通对象的键名组成的数组

Object.entries()返回普通对象的键名键值组成的二维数组

使用Object扩展方法处理后的数组,再进行for...of遍历。

2. Generator 函数重新包装对象

明细看具体章节

3. 给Object增加Symbol.iterator属性

明细看具体章节

image.png

本篇文章到这里就结束啦!知识有木有多一点点~~~

一起努力!一起进步!