我们知道,ES6 中引入 for...of
循环,很多时候用以替代 for...in
和 forEach()
,并支持新的迭代协议。
for…of
及其使用
ES6 中引入的 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)形成的二维数组,利用解构赋值得到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
终止,在这些情况下,迭代器关闭。
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
迭代。
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(遍历器)
以下摘自阮一峰大大的《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
接口,在此,不用担心[Symbol.iterator]
属性会被Object.keys()
获取到导致遍历结果出错,因为Symbol.iterator
这样的Symbol
属性,需要通过Object.getOwnPropertySymbols(obj)
才能获取,Object.getOwnPropertySymbols()
方法返回一个给定对象自身的所有 Symbol 属性的数组。
有一些场合会默认调用 Iterator 接口(即 Symbol.iterator 方法:
- 扩展运算符
...
:这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组(毫不意外的,代码[...{}]
会报错,而[...'123']
会输出数组['1','2','3']
)。 - 数组和可迭代对象的解构赋值(普通对象解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。);
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 value2
consoleEachData(obj2); // 1 2