likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 15

1. 前言

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

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

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

2. 实现

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

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() 转换为可迭代对象

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 的参数和原生的大概保持一致

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 对象进行进一步的处理。

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 来处理异步迭代器。

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 对象中返回处理结果。

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