可以迭代大部分数据类型的 for…of 为什么不能遍历普通对象?

您所在的位置:网站首页 使用for语句不可遍历的数据类型是 可以迭代大部分数据类型的 for…of 为什么不能遍历普通对象?

可以迭代大部分数据类型的 for…of 为什么不能遍历普通对象?

2024-07-09 19:38| 来源: 网络整理| 查看: 265

加入我们一起学习,天天进步

for…of 及其使用

我们知道,ES6 中引入 for...of 循环,很多时候用以替代 for...in 和 forEach() ,并支持新的迭代协议。for...of 允许你遍历 Array(数组), String(字符串), Map(映射), Set(集合),TypedArray(类型化数组)、arguments、NodeList对象、Generator等可迭代的数据结构等。for...of语句在可迭代对象上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

for...of的语法:代码语言:javascript复制for (variable of iterable) { // statement } // variable:每个迭代的属性值被分配给该变量。 // iterable:一个具有可枚举属性并且可以迭代的对象。常用用法代码语言:javascript复制{ // 迭代字符串 const iterable = 'ES6'; for (const value of iterable) { console.log(value); } // Output: // "E" // "S" // "6" } { // 迭代数组 const iterable = ['a', 'b']; for (const value of iterable) { console.log(value); } // Output: // a // b } { // 迭代Set(集合) const iterable = new Set([1, 2, 2, 1]); for (const value of iterable) { console.log(value); } // Output: // 1 // 2 } { // 迭代Map const iterable = new Map([["a", 1], ["b", 2], ["c", 3]]); for (const entry of iterable) { console.log(entry); } // Output: // ["a", 1] // ["b", 2] // ["c", 3] for (const [key, value] of iterable) { console.log(value); } // Output: // 1 // 2 // 3 } { // 迭代Arguments Object(参数对象) function args() { for (const arg of arguments) { console.log(arg); } } args('a', 'b'); // Output: // a // b } { // 迭代生成器 function* foo(){ yield 1; yield 2; yield 3; }; for (let o of foo()) { console.log(o); } // Output: // 1 // 2 // 3 }Uncaught TypeError: obj is not iterable代码语言:javascript复制// 普通对象 const obj = { foo: 'value1', bar: 'value2' } for(const item of obj){ console.log(item) } // Uncaught TypeError: obj is not iterable

可以看出,for of可以迭代大部分对象甚至字符串,却不能遍历普通对象。

如何用for...of迭代普通对象

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

Object.values()、Object.keys()、Object.entries()用法及返回值

代码语言:javascript复制const obj = { foo: 'value1', bar: 'value2' } // 打印由value组成的数组 console.log(Object.values(obj)) // ["value1", "value2"] // 打印由key组成的数组 console.log(Object.keys(obj)) // ["foo", "bar"] // 打印由[key, value]组成的二维数组 // copy(Object.entries(obj))可以把输出结果直接拷贝到剪贴板,然后黏贴 console.log(Object.entries(obj)) // [["foo","value1"],["bar","value2"]]

因为for...of可以迭代数组和Map,所以我们得到以下遍历普通对象的方法。

代码语言:javascript复制const obj = { foo: 'value1', bar: 'value2' } // 方法一:使用for of迭代Object.entries(obj)形成的二维数组,利用解构赋值得到value for(const [, value] of Object.entries(obj)){ console.log(value) // value1, value2 } // 方法二:Map // 普通对象转Map // Map 可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组 console.log(new Map(Object.entries(obj))) // 遍历普通对象生成的Map for(const [, value] of new Map(Object.entries(obj))){ console.log(value) // value1, value2 } // 方法三:继续使用for in for(const key in obj){ console.log(obj[key]) // value1, value2 } { // 方法四:将【类数组(array-like)对象】转换为数组 // 该对象需具有一个 length 属性,且其元素必须可以被索引。 const obj = { length: 3, // length是必须的,否则什么也不会打印 0: 'foo', 1: 'bar', 2: 'baz', a: 12 // 非数字属性是不会打印的 }; const array = Array.from(obj); // ["foo", "bar", "baz"] for (const value of array) { console.log(value); } // Output: foo bar baz } { // 方法五:给【类数组】部署数组的[Symbol.iterator]方法【对普通字符串属性对象无效】 const iterable = { 0: 'a', 1: 'b', 2: 'c', length: 3, [Symbol.iterator]: Array.prototype[Symbol.iterator] }; for (let item of iterable) { console.log(item); // 'a', 'b', 'c' } }注意事项

有别于不可终止遍历的forEach,for...of的循环可由break, throw, continue 或return终止,在这些情况下,迭代器关闭。

代码语言:javascript复制 const obj = { foo: 'value1', bar: 'value2', baz: 'value3' } for(const [, value] of Object.entries(obj)){ if (value === 'value2') break // 不会再执行下次迭代 console.log(value) // value1 }; [1,2].forEach(item => { if(item == 1) break // Uncaught SyntaxError: Illegal break statement console.log(item) }); [1,2].forEach(item => { if(item == 1) continue // Uncaught SyntaxError: Illegal continue statement: no surrounding iteration statement console.log(item) }); [1,2].forEach(item => { if(item == 1) return // 仍然会继续执行下一次循环,打印2 console.log(item) // 2 })

For…of 与 For…in对比

for...in 不仅枚举数组声明,它还从构造函数的原型中查找继承的非枚举属性;

for...of 不考虑构造函数原型上的不可枚举属性(或者说for...of语句遍历可迭代对象定义要迭代的数据。);

for...of 更多用于特定的集合(如数组等对象),但不是所有对象都可被for...of迭代。

代码语言:javascript复制 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' }普通对象为何不能被 for of 迭代

前面我们有提到一个词叫“可迭代”数据结构,当用for of迭代普通对象时,也会报一个“not iterable”的错误。

实际上,任何具有 Symbol.iterator 属性的元素都是可迭代的。我们可以简单查看几个可被for of迭代的对象,看看和普通对象有何不同:

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

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

Iterator(遍历器)

关于Iterator(遍历器),可以参照阮一峰老师写的《ECMAScript 6 入门教程—异步遍历器》教程。

简单来说,ES6 为了统一集合类型数据结构的处理,增加了 iterator 接口,供 for...of 使用,简化了不同结构数据的处理。

而 iterator 的遍历过程,则是类似 Generator 的方式,迭代时不断调用next方法,返回一个包含value(值)和done属性(标识是否遍历结束)的对象。

如何实现Symbol.iterator方法,使普通对象可被 for of 迭代

依据上文的指引,我们先看看数组的Symbol.iterator接口:

代码语言:javascript复制const arr = [1,2,3]; const iterator = arr[Symbol.iterator](); console.log(iterator.next()); // {value: 1, done: false} console.log(iterator.next()); // {value: 2, done: false} console.log(iterator.next()); // {value: 3, done: false} console.log(iterator.next()); // {value: undefined, done: true}

我们可以尝试给普通对象实现一个Symbol.iterator接口:

代码语言:javascript复制// 普通对象 const obj = { foo: 'value1', bar: 'value2', [Symbol.iterator]() { // 这里Object.keys不会获取到Symbol.iterator属性,原因见下文 const keys = Object.keys(obj); let index = 0; return { next: () => { if (index < keys.length) { // 迭代结果 未结束 return { value: this[keys[index++]], done: false }; } else { // 迭代结果 结束 return { value: undefined, done: true }; } } }; } } for (const value of obj) { console.log(value); // value1 value2 };

上面给obj实现了Symbol.iterator接口后,我们甚至还可以像下面这样把对象转换成数组:

代码语言:javascript复制console.log([...obj]); // ["value1", "value2"] console.log([...{}]); // console.log is not iterable (cannot read property Symbol(Symbol.iterator))

我们给obj对象实现了一个Symbol.iterator接口,在此,有一点需要说明的是,不用担心[Symbol.iterator]属性会被Object.keys()获取到导致遍历结果出错,因为Symbol.iterator这样的Symbol属性,需要通过Object.getOwnPropertySymbols(obj)才能获取,Object.getOwnPropertySymbols() 方法返回一个给定对象自身的所有 Symbol 属性的数组。

有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法:

扩展运算符...:这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组(毫不意外的,代码[...{}]会报错,而[...'123']会输出数组['1','2','3'])。

数组和可迭代对象的解构赋值(解构是ES6提供的语法糖,其实内在是针对可迭代对象的Iterator接口,通过遍历器按顺序获取对应的值进行赋值。而普通对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。);

yield*:_yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口;

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用;

字符串是一个类似数组的对象,也原生具有Iterator接口,所以也可被for of迭代。

迭代器模式

迭代器模式提供了一种方法顺序访问一个聚合对象中的各个元素,而又无需暴露该对象的内部实现,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。

迭代器模式为遍历不同的集合结构提供了一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。

不难发现,Symbol.iterator实现的就是一种迭代器模式。集合对象内部实现了Symbol.iterator接口,供外部调用,而我们无需过多的关注集合对象内部的结构,需要处理集合对象内部的数据时,我们通过for of调用Symbol.iterator接口即可。

比如针对前文普通对象的Symbol.iterator接口实现一节的代码,如果我们对obj里面的数据结构进行了如下调整,那么,我们只需对应的修改供外部迭代使用的Symbol.iterator接口,即可不影响外部迭代调用:

代码语言:javascript复制const obj = { // 数据结构调整 data: ['value1', 'value2'], [Symbol.iterator]() { let index = 0; return { next: () => { if (index < this.data.length) { // 迭代结果 未结束 return { value: this.data[index++], done: false }; } else { // 迭代结果 结束 return { value: undefined, done: true }; } } }; } } // 外部调用 for (const value of obj) { console.log(value); // value1 value2 }

实际使用时,我们可以把上面的Symbol.iterator提出来进行单独封装,这样就可以对一类数据结构进行迭代操作了。

当然,下面的代码只是最简单的示例,你可以在此基础上探究更多实用的技巧。

代码语言:javascript复制const obj1 = { data: ['value1', 'value2'] } const obj2 = { data: [1, 2] } // 遍历方法 consoleEachData = (obj) => { obj[Symbol.iterator] = () => { let index = 0; return { next: () => { if (index < obj.data.length) { return { value: obj.data[index++], done: false }; } else { return { value: undefined, done: true }; } } }; } for (const value of obj) { console.log(value); } } consoleEachData(obj1); // value1 value2 consoleEachData(obj2); // 1 2一点补充

在写这篇文章时,有个问题给我带来了困扰:原生object对象默认没有部署Iterator接口,即object不是一个可迭代对象。

对象的扩展运算符...等同于使用Object.assign()方法,这个比较好理解。那么,原生object对象的解构赋值又是怎样一种机制呢?

代码语言:javascript复制let aClone = { ...a }; // 等同于 let aClone = Object.assign({}, a);

有一种说法是:ES6提供了Map数据结构,实际上原生object对象被解构时,会被当作Map进行解构。关于这点,大家有什么不同的观点吗?欢迎评论区一起探讨。

同时,ECMAScript后面又引入了异步迭代器for await...of 语句,该语句创建一个循环,该循环遍历异步可迭代对象以及同步可迭代对象,详情可查看MDN:for-await...of。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3