for…of
及其应用
咱们晓得,ES6 中引入 for...of
循环,很多时候用以代替 for...in
和 forEach()
,并反对新的迭代协定。for...of
容许你遍历 Array(数组), String(字符串), Map(映射), Set(汇合),TypedArray(类型化数组)、arguments、NodeList对象、Generator等可迭代的数据结构等。for...of
语句在可迭代对象上创立一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
for...of
的语法:
for (variable of iterable) { // statement}// variable:每个迭代的属性值被调配给该变量。// iterable:一个具备可枚举属性并且能够迭代的对象。
罕用用法
{ // 迭代字符串 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
// 一般对象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()
用法及返回值
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,所以咱们失去以下遍历一般对象的办法
const obj = { foo: 'value1', bar: 'value2'}// 办法一:应用for of迭代Object.entries(obj)造成的二维数组,利用解构赋值失去valuefor(const [, value] of Object.entries(obj)){ console.log(value) // value1, value2}// 办法二:Map// 一般对象转Map// Map 能够承受一个数组作为参数。该数组的成员是一个个示意键值对的数组console.log(new Map(Object.entries(obj)))// 遍历一般对象生成的Mapfor(const [, value] of new Map(Object.entries(obj))){ console.log(value) // value1, value2}// 办法三:持续应用for infor(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
终止,在这些状况下,迭代器敞开。
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
迭代。
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 入门》——Iterator(遍历器)的概念:
简略来说,ES6 为了对立汇合类型数据结构的解决,减少了 iterator 接口,供 for...of
应用,简化了不同构造数据的解决。而 iterator 的遍历过程,则是相似 Generator 的形式,迭代时一直调用next办法,返回一个蕴含value(值)和done属性(标识是否遍历完结)的对象。
如何实现Symbol.iterator
办法,使一般对象可被 for of
迭代
根据上文的指引,咱们先看看数组的Symbol.iterator
接口:
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
接口:
// 一般对象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
接口后,咱们甚至还能够像上面这样把对象转换成数组:
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
接口,即可不影响内部迭代调用:
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
提出来进行独自封装,这样就能够对一类数据结构进行迭代操作了。当然,上面的代码只是最简略的示例,你能够在此基础上探索更多实用的技巧。
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 value2consoleEachData(obj2); // 1 2
一点补充
在写这篇文章时,有个问题给我带来了困扰:原生object对象默认没有部署Iterator接口,即object不是一个可迭代对象,那么,原生object对象的解构赋值是怎么样一种机制呢?
有一种说法是:ES6提供了Map数据结构,实际上原生object对象被解构时,会被当作Map进行解构。对于这点,大家有什么不同的观点吗?欢送评论区一起探讨。
参考资料
- MDN:for...of
- Understanding the JavaScript For...of Loop(【译文】)
- Iterator 和 for...of 循环
本文首发于集体博客,欢送斧正和star。