如何提前终止reduce()方法?

159

我如何打破reduce()方法的迭代?

for循环:

for (var i = Things.length - 1; i >= 0; i--) {
  if(Things[i] <= 0){
    break;
  }
};

reduce()

Things.reduce(function(memo, current){
  if(current <= 0){
    //break ???
    //return; <-- this will return undefined to memo, which is not what I want
  }
}, 0)

1
以上代码中的current是什么?我看不出它们如何执行相同的操作。无论如何,还有一些可能会提前跳出循环的方法,如someeveryfind - elclanrs
someevery返回布尔值,而find返回单个记录,我想要的是运行操作以生成备忘录。current是当前值。参考 - Julio Marins
我的意思是第一段代码中的 current 是什么? - elclanrs
3
回答是你无法在 reduce 中提前退出,你需要使用内置函数来提前退出或者创建自己的帮助函数,或者使用lodash或其他库。请问您能否提供一个完整的示例说明您想要做什么? - elclanrs
@Cristy,当然可以,但是数组元素可以以任何顺序添加并且可以移动。此外,数组只是对象,因此它们的属性之所以有顺序的概念,是因为它们的索引值,例如for..in可能会以任何顺序返回值(IE曾经按照添加顺序返回)。;-) - RobG
显示剩余6条评论
17个回答

157
你可以通过改变reduce函数的第四个参数"array"来在任何迭代中中断.reduce()调用。不需要自定义reduce函数。请参阅文档以获取完整的.reduce()参数列表。 < p > < em > Array.prototype.reduce((acc, curr, i, array))

第4个参数是正在迭代的数组

const array = ['apple', '-pen', '-pineapple', '-pen'];
const x = array
.reduce((acc, curr, i, arr) => {
    if(i === 2) arr.splice(1);  // eject early
    return acc += curr;
  }, '');
console.log('x: ', x);  // x:  apple-pen-pineapple

为什么要使用这个方法?

我能想到的唯一使用这个方法而不是其他许多解决方案的原因是,如果您想保持算法的函数式编程方法,并且希望以最声明性的方式实现它。如果您的整个目标是将数组字面上缩小到另一个非假值原始类型(字符串、数字、布尔值、符号),那么我认为这确实是最好的方法。

为什么不使用?

有一整套理由可以反对改变函数参数,因为这是一种不好的做法。


更新

一些评论家指出,在.reduce()逻辑中为了提前退出,原始数组被改变。

因此,我稍微修改了答案,添加了.slice(0)在调用后续的.reduce()步骤之前,得到原始数组的副本。 注意:完成相同任务的类似操作有slice()(不够明确)和扩展运算符[...array]性能略有下降)。请记住,所有这些都会将额外的常数因子线性时间添加到总运行时间... + O(n)中。

该副本可以保留原始数组,防止迭代过程中发生的突变导致退出。

const array = ['apple', '-pen', '-pineapple', '-pen'];
const x = array
    .slice(0)                         // create copy of "array" for iterating
    .reduce((acc, curr, i, arr) => {
       if (i === 2) arr.splice(1);    // eject early by mutating iterated copy
       return (acc += curr);
    }, '');

console.log("x: ", x, "\noriginal Arr: ", array);
// x:  apple-pen-pineapple
// original Arr:  ['apple', '-pen', '-pineapple', '-pen']


6
这是非常糟糕的建议,因为 splice 会对 array 进行可见的修改操作。根据函数式编程范式,你应该使用 reduce 写成 continuation passing style 或者利用惰性求值带有右结合性质的 reduce。或者,作为简单的替代方案,可以使用纯递归。 - user6445533
1
@KoushikChatterjee 我的陈述对于我的隐含意义是正确的。但对于你的明确意思来说则不正确。你应该提出修改陈述的建议,以包含你的观点,我会进行编辑,因为这将改善整个答案。 - Tobiah Rex
1
@TheNickyYo 因此,这部分标题为“为什么不?”。 - Tobiah Rex
2
我更喜欢使用展开运算符来避免任何不必要的变异,[...array].reduce()。 - eballeste
1
没错 @eballeste,这个简单的步骤让这个解决方案变得非常棒。 - gdibble
显示剩余7条评论

45

不要使用reduce。只需使用普通迭代器(如for等)在数组上进行迭代,并在满足条件时跳出循环。


3
是的,如果需要打破循环,使用函数式编程可能不是最佳实践的原因可以通过解释来改进这个答案。人们会认为OP完全了解基本的迭代器,也许他们只是想避免污染范围,谁知道呢。 - Phil Tune
1
我认为这个答案有价值,应该保留。虽然原帖作者可能知道他想要使用 reduce,但其他人在寻找解决方案时可能会发现它很有帮助(正如赞数所示)。 - Shaido

22

只要您不关心返回值,就可以使用像someevery这样的函数。当回调函数返回false时,every会中断,而当它返回true时,some会中断:

things.every(function(v, i, o) {
  // do stuff 
  if (timeToBreak) {
    return false;
  } else {
    return true;
  }
}, thisArg);

编辑

有些评论说"这不能像reduce那样做",这是真的,但它可以。以下是使用every以类似于reduce的方式进行操作,并且当达到中断条件时立即返回的示例。

// Soruce data
let data = [0,1,2,3,4,5,6,7,8];

// Multiple values up to 5 by 6, 
// create a new array and stop processing once 
// 5 is reached

let result = [];

data.every(a => a < 5? result.push(a*6) : false);

console.log(result);

这是因为push方法的返回值为添加新元素后result数组的长度,该长度总是大于等于1(因此为true),否则返回false,循环停止。


50
但如果他试图进行reduce操作,那么根据定义,他确实关心返回值。 - user663031
1
@torazaburo——当然,但我没有看到它在 OP 中被使用,而且还有其他获取结果的方法。;-) - RobG
const isKnownZone = KNOWN_ZONES.some((v) => curZone.substr(v.length) === v) 我可以使用reduce,但那不是很高效。我考虑的方式是some和every都是布尔函数...一些元素为true,每个元素都为true,在集合中。 - Ray Foss
使用 every 无法满足使用 reduce 的目的。 - fedeghe
有没有一种方法可以使用异步reducer函数来实现这个? - Abdel P.

10

当然,使用内置的reduce函数是没有办法提前退出循环的。

但是你可以编写自己的reduce函数,使用特殊的标记来识别何时应该停止循环。

var EXIT_REDUCE = {};

function reduce(a, f, result) {
  for (let i = 0; i < a.length; i++) {
    let val = f(result, a[i], i, a);
    if (val === EXIT_REDUCE) break;
    result = val;
  }
  return result;
}

这样使用它,当您达到99时对数组求和但退出:

reduce([1, 2, 99, 3], (a, b) => b === 99 ? EXIT_REDUCE : a + b, 0);

> 3

1
您可以使用“惰性求值”或“CPS”(https://dev59.com/w5Pfa4cB1Zd3GeqPCFkt)来实现所需的行为: - user5536315
这个答案的第一句话是不正确的。你可以使用break语句,详见下面的答案。 - Tobiah Rex

6

Array.every可以为退出高阶迭代提供一个非常自然的机制。

const product = function(array) {
    let accumulator = 1;
    array.every( factor => {
        accumulator *= factor;
        return !!factor;
    });
    return accumulator;
}
console.log(product([2,2,2,0,2,2]));
// 0


1
但是如果不使用突变,你如何做到这一点? - cphoover

1
你可以通过抛出异常来破坏任何代码和迭代器中的每个构建:
function breakReduceException(value) {
    this.value = value
}

try {
    Things.reduce(function(memo, current) {
        ...
        if (current <= 0) throw new breakReduceException(memo)
        ...
    }, 0)
} catch (e) {
    if (e instanceof breakReduceException) var memo = e.value
    else throw e
}

7
这可能是所有答案中执行效率最低的。try/catch会打断现有的执行上下文并回退到“慢路径”执行方式。可以告别V8在幕后进行的任何优化。 - Evan Plaice
8
不够极端。这么改怎么样:如果(current <= 0){window.top.close()} - user56reinstatemonica8

1
您可以使用try...catch语句来退出循环。

try {
  Things.reduce(function(memo, current){
    if(current <= 0){
      throw 'exit loop'
      //break ???
      //return; <-- this will return undefined to memo, which is not what I want
    }
  }, 0)
} catch {
  // handle logic
}


0
由于 promise 具有 resolvereject 回调参数,因此我创建了带有 break 回调参数的 reduce 工作函数。它接受与本机 reduce 方法相同的所有参数,除了第一个参数是要处理的数组(避免猴子补丁)。第三个 [2] initialValue 参数是可选的。请参见下面的片段以查看 function reducer。

var list = ["w","o","r","l","d"," ","p","i","e","r","o","g","i"];

var result = reducer(list,(total,current,index,arr,stop)=>{
  if(current === " ") stop(); //when called, the loop breaks
  return total + current;
},'hello ');

console.log(result); //hello world

function reducer(arr, callback, initial) {
  var hasInitial = arguments.length >= 3;
  var total = hasInitial ? initial : arr[0];
  var breakNow = false;
  for (var i = hasInitial ? 0 : 1; i < arr.length; i++) {
    var currentValue = arr[i];
    var currentIndex = i;
    var newTotal = callback(total, currentValue, currentIndex, arr, () => breakNow = true);
    if (breakNow) break;
    total = newTotal;
  }
  return total;
}

这里是已修改为数组 methodreducer 脚本:

Array.prototype.reducer = function(callback,initial){
  var hasInitial = arguments.length >= 2;
  var total = hasInitial ? initial : this[0];
  var breakNow = false;
  for (var i = hasInitial ? 0 : 1; i < this.length; i++) {
    var currentValue = this[i];
    var currentIndex = i;
    var newTotal = callback(total, currentValue, currentIndex, this, () => breakNow = true);
    if (breakNow) break;
    total = newTotal;
  }
  return total;
};

var list = ["w","o","r","l","d"," ","p","i","e","r","o","g","i"];

var result = list.reducer((total,current,index,arr,stop)=>{
  if(current === " ") stop(); //when called, the loop breaks
  return total + current;
},'hello ');


console.log(result);

0

如果您不需要返回一个数组,也许可以使用some()?

改用some,它会在需要时自动中断。将this累加器发送给它。您的测试和累加函数不能是箭头函数,因为它们的this在箭头函数创建时就已经确定了。

const array = ['a', 'b', 'c', 'd', 'e'];
var accum = {accum: ''};
function testerAndAccumulator(curr, i, arr){
    this.tot += arr[i];
    return curr==='c';
};
accum.tot = "";
array.some(testerAndAccumulator, accum);

var result = accum.tot;

在我看来,这是更好的解决方案,相对于已经接受的答案,只要您不需要返回一个数组(例如在数组操作链中),因为您没有改变原始数组,也不需要复制它,对于大数组来说这可能是不好的。

0

reduce 方法内部无法使用 break。根据您想要实现的目标,您可以更改最终结果(这也是您可能想要这样做的原因之一)

const result = [1, 1, 1].reduce((a, b) => a + b, 0); // returns 3

console.log(result);

const result = [1, 1, 1].reduce((a, b, c, d) => {
  if (c === 1 && b < 3) {
    return a + b + 1;
  } 
  return a + b;
}, 0); // now returns 4

console.log(result);

请注意:您不能直接重新分配数组参数。

const result = [1, 1, 1].reduce( (a, b, c, d) => {
  if (c === 0) {
    d = [1, 1, 2];
  } 
  return a + b;
}, 0); // still returns 3

console.log(result);

然而(如下所指出的),您可以通过更改数组的内容来影响结果:

const result = [1, 1, 1].reduce( (a, b, c, d) => {
  if (c === 0) {
    d[2] = 100;
  } 
  return a + b;
}, 0); // now returns 102

console.log(result);


1
关于“您不能直接改变参数值,以影响后续计算”的说法是不正确的。ECMA-262规定:如果更改数组的现有元素,则它们作为传递给callbackfn的值将是reduce访问它们时的值。您的示例无法工作,因为您正在为d分配一个新值,而不是修改原始数组。请用d[2] = 6替换d = [1, 1, 2],看看会发生什么。;-) - RobG

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接