如果不使用 for 循环,你会如何重写它?
const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;
let result;
// Find the first number
for (let i = 0; i < a.length; i++) {
const r = expensiveFunction(a[i]);
if (r > 100) {
result = r;
break;
}
}
console.log(result);
我天真的做法:
const result = a.map(expensiveFunction).find(x => x > 100);
console.log(result);
但是这会在所有元素上运行
expensiveFunction
,我想避免这种情况。在上述情况下,我们应该避免跑步expensiveFunction(4)
。
有些语言有
find_map
(例如,Rust),我在 lodash 或下划线中都没有找到它。
内置
map
是贪婪的,所以你必须编写自己的惰性版本:
const a = [2, 5, 78, 4];
const expensiveFunction = n => {
console.log('expensiveFunction for', n);
return 2 * n
};
function *map(a, fn) {
for(let x of a)
yield fn(x);
}
function find(a, fn) {
for(let x of a)
if (fn(x))
return x;
}
r = find(map(a, expensiveFunction), x => x > 100)
console.log('result', r)
与库存
map
不同,这个 map
是一个生成器,可以根据需要返回(产生)结果,而不是立即处理整个数组。此示例中的 find
和 map
是“协程”,它们会玩某种乒乓球游戏,其中 find
询问结果,而 map
在询问时提供结果。一旦 find
对它所得到的感到满意,它就会退出,map
也会退出,因为没有人再询问它的结果了。
您还可以将
map
、find
和朋友添加到 IteratorPrototype
,使它们可用于所有迭代器并能够使用点表示法:
const IteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()));
Object.defineProperties(IteratorPrototype, {
map: {
value: function* (fn) {
for (let x of this) {
yield fn(x);
}
},
enumerable: false
},
find: {
value: function (fn) {
for (let x of this) {
if (fn(x))
return x;
}
},
enumerable: false
},
});
//
const a = [2, 5, 78, 4];
const expensiveFunction = n => {
console.log('expensiveFunction', n);
return 2 * n
};
let r = a.values().map(expensiveFunction).find(x => x > 100);
console.log(r)
这是一个基于此技术的小型库:https://github.com/gebrkn/armita
find-map.ts
function* map<T, U>(a: T[], fn: (x: T) => U) {
for (let x of a) yield fn(x);
}
function find<T>(a: Generator<T, void, unknown>, fn: (x: T) => boolean) {
for (let x of a) if (fn(x)) return x;
}
export function mapFind<T, U>(
collection: T[],
mapper: (item: T) => U,
finder: (item: U) => boolean
): U | undefined {
const mapperGenerator = map(collection, mapper);
return find(mapperGenerator, finder);
}
map-find.spec.ts
注意:这些测试使用 Bun,但应该与 Jest 相差不远。
import { describe, expect, it, mock } from "bun:test";
import { mapFind } from "./map-find";
describe("findMap", () => {
const collection = [2, 5, 78, 4];
const expensiveFunction = mock((n: number) => {
console.log("expensiveFunction for", n);
return 2 * n + ""; // Wanting to test with the change of types
});
const condition = (x: string) => x.length > 2;
const result = mapFind(collection, expensiveFunction, condition);
it("only calls the expensive function 3 times", () => {
expect(expensiveFunction).toHaveBeenCalledTimes(3);
});
it("returns the expected result", () => {
expect(result).toEqual("156");
});
});
您可以使用
.reduce
,唯一的缺点是一旦找到值就无法停止,但您不必为每个值运行expensiveFunction
。
这是一个例子:
const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;
const result = a.reduce((acc, cur) => {
if (!acc) {
const r = expensiveFunction(cur);
if (r > 100) {
acc = r;
}
}
return acc;
}, null);
console.log(result);
类似这样的事情
const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;
let findMap = arr => {
let found = arr.find(a => expensiveFunction(a) > 100)
return found !== undefined ? expensiveFunction(found) : found
}
console.log(findMap(a));
警告:- 只是出于好奇,但是 hacky 或者你可以称之为滥用
find
const a = [2, 5, 78, 4];
const expensiveFunction = n => 2 * n;
let findMap = arr => {
let returnValue;
let found = arr.find(a => {
returnValue = expensiveFunction(a)
return returnValue > 100
})
return returnValue
}
console.log(findMap(a));
这是 Titus
.reduce
答案的简洁实用版本:
const arr = [2, 5, 78, 100]
const result = arr.reduce((a,v) => (a > 100 && a) || expensiveFunction(v), null)
console.log(result)
它迭代整个数组,但一旦满足条件就停止执行昂贵的函数。
这是我个人使用的,以防对任何人有帮助:
const result = arr.reduce((a,v) => a || expensiveFunction(v), null)
为什么不使用更智能的查找功能?
let desiredValue;
const result = a.find( x =>{
desiredValue = expensiveFunction(x);
return desiredValue > 100;
});
console.log(desiredValue);
找到第一个结果后,它会立即退出昂贵的循环。
如果您愿意接受数组中第一个匹配元素被修改,您可以这样做:
a[a.findIndex((value, index) => {
value = expensiveFunction(value);
return (value > 100 && (a[index] = value))
})] //Returns 156
否则,您将需要使用占位符变量来完成这项工作 - 很可能使 for 循环成为最干净的选项。
映射本质上是通过一个函数运行每个值,并将其结果作为新数组中该索引处的值返回。考虑到这一点,只需将
expensiveFunction
移动到 find()
方法中,如下所示:
const result = a.map(expensiveFunction).find(x => x > 100);
const result = a.find(x => {
const expensiveResult = expensiveFunction(x);
return expensiveResult > 100;
});
现在,它只会在迭代数组时检查每个元素的
expensiveFunction
结果,并在找到值后停止。
** 注意,由于您实际上并未映射数组,因此
find()
方法的结果将是该索引处的原始值,而不是 expensiveResult
的值。
如果您希望返回
expensiveResult
的值,并且不需要在找到的元素上再次运行 expensiveFunction
,您可以将此逻辑移至轻量且可重用的抽象中,如下所示:
// Function definition
function findMap(arr, fn, condition) {
for (const elem of arr) {
const result = fn(elem);
if (condition(result)) {
return result;
}
}
}
// Usage
const result = findMap(a, expensiveFunction, x => x > 100);
…现在又是同样的抽象,但这次没有按照原始问题的要求进行循环:
// Function definition
function findMap(arr, fn, condition) {
let result;
arr.find(x => {
const fnResult = fn(x);
const conditionMet = condition(fnResult);
if (conditionMet) {
result = fnResult;
}
return conditionMet;
})
return result;
}
// Usage (same as above)
const result = findMap(a, expensiveFunction, x => x > 100);
我遵循的方法是将调用“expenseFunction”函数的次数减少到尽可能少的次数。为此,我使用了“分治算法”。您将数组分为两半,并对分割元素调用昂贵的函数来决定继续进行哪一半。递归地执行此步骤,直到找到 100 以上的最小元素。特别是对于非常大的数组,此方法会将昂贵的函数调用减少到明显更小的数量。因此,“expenseFunCaller”函数将经济地调用您的“expenseFunction”。数组也应该先排序。
const a = [2, 5,78, 80].sort((a,b) => a-b);
const expensiveFunction = n => 2 * n;
const expensiveFunCaller= ([...arr]) =>{
if(arr.length<2){
let r = expensiveFunction(arr[0]);
if(r>100) return r;
return;
}
else if(arr.length === 2){
let r = expensiveFunction(arr[0]);
if(r>100) return r;
r = expensiveFunction(arr[1]);
if(r>100) return r;
return;
}
let idx = Math.floor(arr.length / 2);
let r = expensiveFunction(arr[idx]);
return (r<100)?expensiveFunCaller(arr.slice(idx+1, arr.length)):expensiveFunCaller(arr.slice(0, idx+1));
}
console.log(expensiveFunCaller(a));
for
循环有一些有趣的属性,用它们的实用性抵消了丑陋的代码。
本问题中描述的 findMap
功能可以通过如下简单函数来实现:
function mapFind(array, mapFn, findFn) {
for (const value of array) {
const mapResult = mapFn(value);
if (findFn(mapResult)) {
return mapResult;
}
}
}
const result = mapFind([ 2, 5, 78, 4, 100 ], n => 2 * n, r => r > 100);
console.log(`Result: ${result}`);
TS版本:
function mapFind<T, U>(array: T[], mapFn: (value: T) => U, findFn: (value: U) => unknown): U | undefined {
for (const value of array) {
const mapResult = mapFn(value);
if (findFn(mapResult)) {
return mapResult;
}
}
}
const result = mapFind([ 2, 5, 78, 4, 100 ], n => 2 * n, r => r > 100);
console.log(`Result: ${result}`);
您可以对可迭代对象执行多个操作,而仅迭代一次值。
下面是在我的 iter-ops 库的帮助下执行此操作的示例:
import {pipe, map, skipWhile} from 'iter-ops';
const a = [2, 5, 78, 4];
const res = pipe(
a,
map(m => 2 * m), // your expensive function
skipWhile(r => r <= 100)
);
console.log('result:', res.first);
在上面,它将执行与您的
for-loop
示例中相同数量的步骤。
您可以通过使用三元运算符来简化条件和使用filter()来删除数组的布尔(null)值来通过最短的方法。
const a = [2, 5, 78, 100];
const result = a.map((n)=> 2*n > 100 ? 2*n : null ).filter(Boolean)[0];
console.log(result);