作为一名有五年以上前端开发经验的工程师,我深知在日常工作中遍历对象是多么常见的操作。但每次看到同事还在用for-in循环,然后不得不加上hasOwnProperty检查时,我就忍不住想:是时候把迭代器的魔法传授给大家了!
在JavaScript中,数组、字符串、Map、Set等数据结构原生支持for-of循环,这种语法简洁明了,不会遍历原型链上的属性。但普通对象却不支持这种语法,这给我们的开发带来了不少困扰:
for-in循环时,必须配合hasOwnProperty检查,代码显得冗长for-in循环的遍历顺序不保证与属性定义顺序一致javascript复制// 传统的对象遍历方式
const user = { name: '张三', age: 30 };
for (const key in user) {
if (user.hasOwnProperty(key)) {
console.log(key, user[key]);
}
}
要让对象支持for-of循环,我们需要了解两个核心概念:
[Symbol.iterator]方法,该方法返回一个迭代器对象next()方法,该方法返回{value: any, done: boolean}形式的对象javascript复制// 最简单的可迭代对象实现
const iterableObject = {
[Symbol.iterator]() {
let step = 0;
return {
next() {
step++;
if (step === 1) return { value: '第一步', done: false };
if (step === 2) return { value: '第二步', done: false };
return { value: undefined, done: true };
}
};
}
};
for (const step of iterableObject) {
console.log(step); // 输出"第一步", "第二步"
}
手动实现迭代器虽然直观,但代码量较大。ES6引入的生成器函数可以大大简化这一过程:
javascript复制const user = { name: '李四', age: 25, job: '工程师' };
user[Symbol.iterator] = function* () {
// 使用Reflect.ownKeys获取所有自有属性(包括Symbol属性)
const keys = Reflect.ownKeys(this);
for (const key of keys) {
yield [key, this[key]]; // 返回键值对数组
}
};
// 现在可以用for-of遍历对象了
for (const [key, value] of user) {
console.log(key, value);
}
提示:生成器函数(function*)会自动返回一个迭代器对象,每次调用next()时,函数会执行到下一个yield语句暂停。
当使用for-of循环时,JavaScript引擎会执行以下步骤:
[Symbol.iterator]()方法获取迭代器next()方法next()返回一个包含value和done属性的对象done为true时停止迭代javascript复制// 手动模拟for-of循环的工作过程
const iterator = user[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const [key, value] = result.value;
console.log(key, value);
result = iterator.next();
}
完整的迭代器协议还包含可选的return()和throw()方法。return()方法会在迭代提前终止时被调用(如使用break或return):
javascript复制const breakableIterable = {
[Symbol.iterator]() {
return {
next() {
return { value: Math.random(), done: false };
},
return() {
console.log('迭代被提前终止');
return { done: true };
}
};
}
};
for (const num of breakableIterable) {
console.log(num);
if (num > 0.9) break; // 会触发return()方法
}
除了for-of循环,可迭代对象还可以用于:
[...obj]const [first, second] = objArray.from():Array.from(obj)Promise.all()等接受可迭代对象的方法javascript复制// 将可迭代对象转为数组
const pairs = [...user]; // [[name, '李四'], [age, 25], [job, '工程师']]
// 解构赋值
const [firstProp] = user;
console.log(firstProp); // ['name', '李四']
在实际项目中,我们经常需要处理配置对象,并希望按照特定顺序处理其中的属性:
javascript复制const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retryTimes: 3,
logging: true
};
// 按字母顺序遍历配置项
config[Symbol.iterator] = function* () {
const keys = Object.keys(this).sort();
for (const key of keys) {
yield `${key}: ${this[key]}`;
}
};
for (const line of config) {
console.log(line);
// apiUrl: https://api.example.com
// logging: true
// retryTimes: 3
// timeout: 5000
}
对于嵌套较深的状态树(如Vuex/Pinia store),我们可以实现深度优先遍历:
javascript复制const state = {
user: {
info: { name: '王五', id: 123 },
permissions: ['read', 'write']
},
settings: {
theme: 'dark',
notifications: true
}
};
function* deepTraverse(obj, path = []) {
for (const [key, value] of Object.entries(obj)) {
const currentPath = [...path, key];
if (typeof value === 'object' && value !== null) {
yield* deepTraverse(value, currentPath);
} else {
yield { path: currentPath.join('.'), value };
}
}
}
state[Symbol.iterator] = function() {
return deepTraverse(this);
};
for (const {path, value} of state) {
console.log(`${path} = ${JSON.stringify(value)}`);
// user.info.name = "王五"
// user.info.id = 123
// user.permissions.0 = "read"
// user.permissions.1 = "write"
// settings.theme = "dark"
// settings.notifications = true
}
ES2018引入了异步迭代器,可以用于处理异步数据流:
javascript复制const asyncData = {
fetchUser: () => Promise.resolve({ name: '赵六' }),
fetchPosts: () => Promise.resolve([1, 2, 3]),
fetchComments: () => Promise.resolve(['a', 'b'])
};
asyncData[Symbol.asyncIterator] = async function* () {
const keys = Object.keys(this);
for (const key of keys) {
if (typeof this[key] === 'function') {
const result = await this[key]();
yield { key, result };
}
}
};
(async () => {
for await (const {key, result} of asyncData) {
console.log(`${key}:`, result);
}
})();
next(),复杂的计算会影响性能javascript复制// 性能优化的迭代器示例
const largeData = {
// 假设有大量数据
data1: computeExpensiveValue1(),
data2: computeExpensiveValue2(),
// ...
[Symbol.iterator]() {
const keys = Object.keys(this).filter(k => k.startsWith('data'));
let index = 0;
return {
next: () => {
if (index >= keys.length) return { done: true };
const key = keys[index++];
// 惰性求值 - 只在访问时才计算
const value = typeof this[key] === 'function'
? this[key]()
: this[key];
return { value, done: false };
}
};
}
};
javascript复制/**
* 可迭代用户对象
* 迭代顺序:先基本属性,后特殊属性
* 不包含Symbol属性和原型链属性
*/
class IterableUser {
constructor(data) {
Object.assign(this, data);
}
*[Symbol.iterator]() {
const basicProps = ['name', 'age', 'email'];
const otherProps = Object.keys(this)
.filter(k => !basicProps.includes(k) && typeof k !== 'symbol');
for (const prop of [...basicProps, ...otherProps]) {
yield [prop, this[prop]];
}
}
}
当遇到TypeError: obj is not iterable错误时,可以按照以下步骤排查:
[Symbol.iterator]方法next()方法next()方法返回正确的{value, done}格式javascript复制function debugIterable(obj) {
// 1. 检查是否是函数
if (typeof obj[Symbol.iterator] !== 'function') {
console.error('对象没有实现[Symbol.iterator]方法');
return false;
}
// 2. 获取迭代器
const iterator = obj[Symbol.iterator]();
// 3. 检查next方法
if (typeof iterator.next !== 'function') {
console.error('迭代器缺少next()方法');
return false;
}
// 4. 测试调用
const result = iterator.next();
if (typeof result !== 'object' || !('done' in result)) {
console.error('next()没有返回{done, value}格式的对象');
return false;
}
return true;
}
在现有项目中引入可迭代对象时,需要注意:
[Symbol.iterator]不会影响现有的for-in循环或Object.keys()等操作Object.prototype上添加迭代器,这会影响到所有对象javascript复制// 安全地为特定类型的对象添加迭代器
function makeIterable(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
// 避免重复添加
if (typeof obj[Symbol.iterator] === 'function') return obj;
// 创建新对象而不是修改原对象
const iterableObj = Object.create(obj);
iterableObj[Symbol.iterator] = function* () {
for (const key of Object.keys(this)) {
yield [key, this[key]];
}
};
return iterableObj;
}
迭代器非常适合表示无限序列,因为它们是惰性求值的:
javascript复制// 无限斐波那契数列
const fibonacci = {
[Symbol.iterator]() {
let [prev, curr] = [0, 1];
return {
next() {
[prev, curr] = [curr, prev + curr];
return { value: curr, done: false };
}
};
}
};
// 使用take工具函数获取有限序列
function* take(iterable, n) {
let count = 0;
for (const item of iterable) {
if (count++ >= n) return;
yield item;
}
}
console.log([...take(fibonacci, 10)]); // [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
我们可以创建工具函数来组合多个迭代器:
javascript复制// 合并多个可迭代对象
function* zip(...iterables) {
const iterators = iterables.map(it => it[Symbol.iterator]());
while (true) {
const results = iterators.map(it => it.next());
if (results.some(r => r.done)) return;
yield results.map(r => r.value);
}
}
const names = ['张三', '李四', '王五'];
const scores = [90, 85, 95];
for (const [name, score] of zip(names, scores)) {
console.log(`${name}: ${score}`);
}
迭代器可以用于实现状态历史记录:
javascript复制class StateHistory {
constructor(initialState) {
this.states = [initialState];
this.index = 0;
}
push(state) {
this.states.length = this.index + 1; // 移除未来的状态
this.states.push(JSON.parse(JSON.stringify(state)));
this.index++;
}
canUndo() { return this.index > 0; }
canRedo() { return this.index < this.states.length - 1; }
undo() {
if (!this.canUndo()) return null;
return this.states[--this.index];
}
redo() {
if (!this.canRedo()) return null;
return this.states[++this.index];
}
*[Symbol.iterator]() {
for (let i = 0; i < this.states.length; i++) {
yield {
state: this.states[i],
isCurrent: i === this.index
};
}
}
}
const history = new StateHistory({ counter: 0 });
history.push({ counter: 1 });
history.push({ counter: 2 });
history.undo();
for (const {state, isCurrent} of history) {
console.log(state, isCurrent ? '(当前)' : '');
}