基于迭代器实现一个循环方法

1. 前言

为什么会有这篇文章呢?因为我在 xtt-utils 这个 js 函数库中实现了链式调用后,我想在链式调用中实现一个循环方法,所以我就想到了用迭代器来实现一个循环方法。

为什么使用迭代器呢?因为 js 中可迭代对象均实现了 可迭代协议, 这些对象包括 Array, Map, Set, String, 甚至 TypedArray, arguments, NodeList 等等。几乎可以说除了 Object 以外的所有可能需要循环的对象都实现了迭代器协议。

那么直接面向迭代器编程,就可以实现一个通用的循环方法,而不需要针对不同的结构(Object 除外)实现不同的循环方法。

2. 实现

要循环一个可迭代对象很简单,只需要使用 for...of 循环即可

js 复制代码
const fori = (target, callback) => {
	for (const item of target) {
		callback(item);
	}
};
// fori([3,2,1], (v) => console.log(v))
// 3 -> 2 -> 1
// fori("abc", (v) => console.log(v))
// a -> b -> c

然后对 Object 进行处理,因为 Object 没有实现迭代器协议,所以简单使用 Object.entries() 转换为可迭代对象

js 复制代码
const fori = (target, callback) => {
	// Symbol.iterator 是迭代器协议接口
	if (target[Symbol.iterator]) {
		for (const item of target) {
			callback(item);
		}
	} else if (target instanceof Object) {
		for (const item of Object.entries(target)) {
			callback(item);
		}
	}
};
// fori({a: 1, b: 2}, (v) => console.log(v))
// ["a", 1] -> ["b", 2]

这样就可以实现一个通用的循环方法,然后对该方法进行一些标准化处理,让参数和 callback 的参数和原生的大概保持一致

js 复制代码
const fori = (target, callback, thisArg) => {
	let i = 0;
	if (target[Symbol.iterator]) {
		for (const item of target) {
			callback.call(thisArg, item, i, target);
			i++;
		}
	} else if (target instanceof Object) {
		for (const item of Object.entries(target)) {
			callback.call(thisArg, item, i, target);
			i++;
		}
	}
};
// fori([3,2,1], (v, i) => console.log(v, i))
// 3 0 -> 2 1 -> 1 2

大功告成! 这样就实现了一个通用的循环方法。但是还需要解决另一种使用场景下的问题,那就是 Promise, 在下面的代码块中,我们循环处理一些 Promise 对象,发现 callback 获得的参数是 pending 状态的 Promise,我们需要对 Promise 对象进行进一步的处理。

js 复制代码
const prom = (value, time) => {
	return new Promise((res, rej) => {
		setTimeout(() => {
			res(value);
		}, time);
	});
};
fori([prom(3, 300), prom(2, 700), prom(1, 100)], async (v) => console.log(await v));
// 1 -> 3 -> 2

那么有没有办法在循环内部处理 Promise,使 callback 中的实参就是处理后的结果呢?答案是肯定的,并且非常简单,那就是使用 for await ... of, 其使用和 for ...of 一样。

同时可迭代协议中也有一个 异步可迭代协议, 该协议中的 next() 方法返回的是一个 Promise 对象,所以我们同样需要使用 for await ... of 来处理异步迭代器。

js 复制代码
const fori = (target, callback, thisArg, asyncIterator) => {
	let i = 0;
	if (asyncIterator || target[Symbol.asyncIterator]) {
		// 因为 for await ... of 也有 await 关键字,只能在 async 函数中使用,所以使用一个立即执行的 async 函数来包裹
		(async () => {
			for await (const item of target) {
				callback.call(thisArg, item, i, target);
				i++;
			}
		})();
	} else if (target[Symbol.iterator]) {
		for (const item of target) {
			callback.call(thisArg, item, i, target);
			i++;
		}
	} else if (target instanceof Object) {
		for (const item of Object.entries(target)) {
			callback.call(thisArg, item, i, target);
			i++;
		}
	}
};

const prom = (value, time) => {
	return new Promise((res, rej) => {
		setTimeout(() => {
			res(value);
		}, time);
	});
};
fori([prom(3, 300), prom(2, 700), prom(1, 100)], (v) => console.log(v), this, true);
// 3 -> 2 -> 1
fori([3, 2, 1], (v) => console.log(v));
// 3 -> 2 -> 1
fori("abc", (v) => console.log(v));
// a -> b -> c
fori({ a: 1, b: 2 }, (v) => console.log(v));
// ["a", 1] -> ["b", 2]
fori(new Set([3, 2, 2, 1]), (v) => console.log(v));
// 3 -> 2 -> 1
fori(new Map(Object.entries({ a: 1, b: 2 })), (v) => console.log(v));
// ["a", 1] -> ["b", 2]

这样就实现了一个可以解决包括 Promise 在内的通用循环方法,那么还有最后一个需求,那就是函数的返回值!仅在 callback 进行处理但是不收集处理结果是不够的,因为需要在链式调用中使用,所以需要将处理结果返回。

那么怎么处理返回结果呢?我们可以使用一个数组来收集处理结果,然后返回该数组,但是这种简单的处理方式只能处理非 Promise 的情况,因为 Promise 是异步的,这个数组的值将直接为空。所以在 asyncIterator 时,需要返回一个 Promise 对象,然后在 Promise 对象中返回处理结果。

js 复制代码
const fori = (target, callback, thisArg, asyncIterator) => {
	let i = 0;
	if (asyncIterator || target[Symbol.asyncIterator]) {
		return new Promise(async (res, rej) => {
			const resList = [];
			for await (const item of target) {
				resList.push(
					new Promise((res) => {
						res(callback.call(thisArg, item, i, target));
					})
				);
				i++;
			}
			Promise.all(resList)
				.then((v) => {
					res(v);
				})
				.catch((e) => {
					rej(e);
				});
		});
	} else if (target[Symbol.iterator]) {
		const resList = [];
		for (const item of target) {
			const res = callback.call(thisArg, item, i, target);
			i++;
			resList.push(res);
		}
		return resList;
	} else if (target instanceof Object) {
		const resList = [];
		for (const item of Object.entries(target)) {
			const res = callback.call(thisArg, item, i, target);
			i++;
			resList.push(res);
		}
		return resList;
	}
};
const prom = (value, time) => {
	return new Promise((res, rej) => {
		setTimeout(() => {
			res(value);
		}, time);
	});
};
await fori([prom(3, 300), prom(2, 700), prom(1, 100)], (v) => v, this, true);
// [3, 2, 1]
fori([3, 2, 1], (v) => v + 1);
// [4, 3, 2]
fori({ a: 1, b: 2 }, ([k, v]) => v);
// [1, 2]

至此就实现了基于迭代器的 js 循环方法,可以解决日常开发中大部分的循环问题。函数开源在 xtt-utils 中, 欢迎阅读,也欢迎提出意见,也求一个 star

相关推荐
Vennn5 分钟前
Android自动化:使用 Web 方式实现某音未读消息检查与采集
前端·javascript·vue.js
Smilezyl7 分钟前
为了搞懂 AI Agent,我用 6000 行 JS 代码手搓了一个零依赖的 Coding Agent
前端·javascript·github
掰头战士9 分钟前
搞定JavaScript类型判断,一文就够了
javascript
周凡1231 小时前
AI 时代的 Web JavaScript 逆向分析实践与思考
前端·javascript·人工智能
zhoumeina991 小时前
分段创建产品,tab 页切换又要保留缓存
前端·javascript
The Sheep 20231 小时前
EFcore 查询数据
java·javascript
怕浪猫1 小时前
Electron 开发实战(七):网络通信与 API 集成全解
前端·javascript·electron
w_t_y_y2 小时前
vue父子组件通信(一)父子调用和通信(2)VUE3
前端·javascript·vue.js
ZC跨境爬虫2 小时前
跟着 MDN 学CSS day_42:等分轨道、层叠放置与混合布局
前端·javascript·css·ui·html