为什么数组可以用
for...of循环?为什么对象不行?今天我们来揭开JS里"可循环"的秘密------迭代器(Iterator)和可迭代对象(Iterable)。弄懂它们,你就能让自己的对象也支持for...of,甚至还能写出像Python生成器那样优雅的代码。
前言
你有没有好奇过,为什么数组可以用for...of遍历,而对象不行?为什么...扩展运算符可以展开数组,却不能直接展开对象?这背后其实是迭代器协议在起作用。
今天我们就来彻底搞懂这套机制,然后亲手造一个可以for...of遍历的对象。看完你会感叹:原来JS的循环还有这么多骚操作!
一、什么是可迭代对象?
如果一个对象实现了可迭代协议 ,它就是可迭代对象。可迭代协议要求对象有一个[Symbol.iterator]方法,这个方法返回一个迭代器。
简单来说:可迭代对象 = 有一个能返回迭代器的方法。
数组、字符串、Map、Set、arguments、NodeList等都是原生可迭代对象。所以你可以:
js
for (let item of [1,2,3]) { console.log(item); } // 数组
for (let char of 'hello') { console.log(char); } // 字符串
for (let [key,val] of new Map([[1,2]])) { } // Map
对象不是可迭代对象,所以for...of直接遍历对象会报错。
二、迭代器长什么样?
迭代器是一个对象,它有一个next()方法。每次调用next(),会返回一个对象:{ value: 任意值, done: boolean }。done表示是否遍历结束。
比如手动创建一个数组的迭代器:
js
const arr = ['a', 'b', 'c'];
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: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
你看,这个迭代器就像个"读取器",每次取一个值,直到取完。
三、自己实现一个可迭代对象
现在我们来造一个可以for...of遍历的对象。比如一个范围对象,能遍历从start到end的所有整数。
js
const range = {
start: 1,
end: 5,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (let num of range) {
console.log(num); // 1,2,3,4,5
}
就这么简单!只要对象有[Symbol.iterator]方法,并且返回一个带有next的对象,它就能被for...of遍历。
四、扩展运算符、解构赋值背后的迭代器
很多JS语法都依赖迭代器:
...扩展运算符:把可迭代对象展开成元素列表- 数组解构:
[a, b, ...rest] = iterable Array.from():把可迭代对象转成数组for...of循环Promise.all()、Promise.race()的参数也是可迭代对象
所以,只要你的对象是可迭代的,它就能享受这些语法糖。
js
const numbers = [...range]; // [1,2,3,4,5]
const [first, second, ...rest] = range; // first=1, second=2, rest=[3,4,5]
五、生成器函数:迭代器的快捷方式
还记得昨天的Generator吗?生成器函数返回的就是迭代器!所以我们可以用Generator来简化上面的代码:
js
const range = {
start: 1,
end: 5,
*[Symbol.iterator]() {
for (let i = this.start; i <= this.end; i++) {
yield i;
}
}
};
是不是简洁多了?*[Symbol.iterator]()就是Generator方法,每次yield一个值,for...of会自动调用next。
六、无限迭代器:永不停止的循环
迭代器可以无限进行下去,比如生成斐波那契数列:
js
const fibonacci = {
*[Symbol.iterator]() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
};
const fib = fibonacci[Symbol.iterator]();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
// 想取多少取多少
但注意:用for...of遍历无限迭代器会死循环,所以要手动控制。
七、提前终止迭代器:return方法
如果迭代器被提前终止(比如for...of中遇到break,或者解构只取前几个值),JS会调用迭代器的return方法(如果有的话)。这可以用来做清理工作。
js
const specialIterable = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
if (i < 3) return { value: i++, done: false };
return { done: true };
},
return() {
console.log('提前终止了');
return { done: true };
}
};
}
};
for (let x of specialIterable) {
console.log(x);
if (x === 1) break; // 触发return
}
// 输出:0,1, 然后打印"提前终止了"
八、实际应用:让对象可迭代
假设你有一个用户列表对象,你想让它支持for...of直接遍历用户:
js
const userList = {
users: [
{ name: '张三', age: 18 },
{ name: '李四', age: 20 },
{ name: '王五', age: 22 }
],
*[Symbol.iterator]() {
for (let user of this.users) {
yield user;
}
}
};
for (let user of userList) {
console.log(user.name); // 张三 李四 王五
}
这样,你的自定义对象就能像数组一样优雅地遍历了。
九、总结:迭代器无处不在
- 可迭代对象 :实现了
[Symbol.iterator]方法,返回一个迭代器。 - 迭代器 :实现了
next()方法,返回{ value, done }。 - 生成器函数:是迭代器最便捷的实现方式。
- 很多JS语法(
for...of、扩展运算符、解构)都依赖迭代器协议。
理解了这套机制,你就能:
- 让自定义对象支持
for...of - 创建无限序列
- 深入理解JS语法糖背后的原理
下次你写for...of时,脑子里可以浮现出迭代器一步步next的画面------这才是真正掌握了JS的底层。
明天我们将进入DOM操作与事件流,从JS的核心走向与页面的交互。如果你觉得今天的文章够"可迭代",点个赞让更多人看到。我们明天见!