如何提前中断 reduce() 方法?

How to early break reduce() method?

如何中断 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)

您可以使用 someevery 等函数,只要您不关心 return 值。 每个在回调return为假时中断,一些在return为真时中断:

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

编辑

有几条评论说“这和 reduce 做的不一样”,这是真的,但它可以。这是一个使用 every 的示例,其使用方式与 reduce 相似,一旦达到中断条件就立即 returns。

// 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 的 return 值是新元素被添加后 result 数组的长度推送,它将始终为 1 或更大(因此为真),否则它 return 为假并且循环停止。

当然没有办法让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

您不能从 reduce 方法内部中断。根据你想要完成的事情,你可以改变最终结果(这是你可能想要这样做的原因之一)

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);

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

不要使用reduce。只需使用普通迭代器(for 等)迭代数组,并在满足条件时中断。

我解决相同问题的另一个简单实现:

function reduce(array, reducer, first) {
  let result = first || array.shift()

  while (array.length > 0) {
    result = reducer(result, array.shift())
    if (result && result.reduced) {
      return result.reduced
    }
  }

  return result
}

您可以通过抛出异常来破坏每个代码 - 从而破坏迭代器中的每个构建:

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
}

更新

一些评论员提出了一个很好的观点,即原始数组正在发生变化,以便在 .reduce() 逻辑中尽早中断。

因此,我通过在调用后续 .reduce() 步骤之前添加 .slice(0) 稍微修改了答案 ,生成了原始数组。 注意:完成相同任务的类似操作是slice()(不太明确)和扩展运算符[...array])。请记住,所有这些都为总运行时间 + 1*(O(1)).

添加了一个额外的线性时间常数因子

副本用于保护原始数组免受导致从迭代中弹出的最终突变的影响。

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']


您可以通过改变 reduce 函数的第 4 个参数:“array”来中断 .reduce() 调用的任何迭代。不需要自定义 reduce 函数。有关 .reduce() 参数的完整列表,请参阅 Docs

Array.prototype.reduce((acc, curr, i, array))

第四个参数是数组被迭代。

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

为什么?:

我能想到的使用它而不是其他许多解决方案的一个也是唯一的原因是,如果您想为您的算法维护一种函数式编程方法,并且您想要尽可能多的声明性方法来实现它。如果您的整个目标是将数组从字面上减少为替代的非假基元(字符串、数字、布尔值、符号),那么我认为这实际上是最好的方法。

为什么不呢?

对于不改变函数参数,有一整套论据,因为这是一种不好的做法。

由于 promiseresolvereject 回调参数,我创建了带有 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;
}

这里是 reducer 作为数组 method 修改后的脚本:

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);

如果您想使用以下模式按顺序将 promise 与 reduce 链接起来:

return [1,2,3,4].reduce(function(promise,n,i,arr){
   return promise.then(function(){
       // this code is executed when the reduce loop is terminated,
       // so truncating arr here or in the call below does not works
       return somethingReturningAPromise(n);
   });
}, Promise.resolve());

但需要根据承诺内部或外部发生的事情来打破 事情变得有点复杂,因为 reduce 循环在第一个 promise 执行之前终止,使得在 promise 回调中截断数组变得无用,我最终得到了这个实现:

function reduce(array, promise, fn, i) {
  i=i||0;
  return promise
  .then(function(){
    return fn(promise,array[i]);
  })
  .then(function(result){
    if (!promise.break && ++i<array.length) {
      return reduce(array,promise,fn,i);
    } else {
      return result;
    }
  })
}

那么你可以这样做:

var promise=Promise.resolve();
reduce([1,2,3,4],promise,function(promise,val){
  return iter(promise, val);
}).catch(console.error);

function iter(promise, val) {
  return new Promise(function(resolve, reject){
    setTimeout(function(){
      if (promise.break) return reject('break');
      console.log(val);
      if (val==3) {promise.break=true;}
      resolve(val);
    }, 4000-1000*val);
  });
}

带中断的缩减功能版本可以实现为 'transform',例如。在下划线中。

我尝试使用配置标志来实现它以停止它,这样实现 reduce 就不必更改您当前使用的数据结构。

const transform = (arr, reduce, init, config = {}) => {
  const result = arr.reduce((acc, item, i, arr) => {
    if (acc.found) return acc

    acc.value = reduce(config, acc.value, item, i, arr)

    if (config.stop) {
      acc.found = true
    }

    return acc
  }, { value: init, found: false })

  return result.value
}

module.exports = transform

用法一,简单

const a = [0, 1, 1, 3, 1]

console.log(transform(a, (config, acc, v) => {
  if (v === 3) { config.stop = true }
  if (v === 1) return ++acc
  return acc
}, 0))

用法2,使用config作为内部变量

const pixes = Array(size).fill(0)
const pixProcessed = pixes.map((_, pixId) => {
  return transform(pics, (config, _, pic) => {
    if (pic[pixId] !== '2') config.stop = true 
    return pic[pixId]
  }, '0')
})

用法3,捕获配置作为外部变量

const thrusts2 = permute([9, 8, 7, 6, 5]).map(signals => {
  const datas = new Array(5).fill(_data())
  const ps = new Array(5).fill(0)

  let thrust = 0, config
  do {

    config = {}
    thrust = transform(signals, (_config, acc, signal, i) => {
      const res = intcode(
        datas[i], signal,
        { once: true, i: ps[i], prev: acc }
      )

      if (res) {
        [ps[i], acc] = res 
      } else {
        _config.stop = true
      }

      return acc
    }, thrust, config)

  } while (!config.stop)

  return thrust
}, 0)

我是这样解决的,例如在some方法中短路可以节省很多:

const someShort = (list, fn) => {
  let t;
  try {
    return list.reduce((acc, el) => {
      t = fn(el);
      console.log('found ?', el, t)
      if (t) {
        throw ''
      }
      return t
    }, false)
  } catch (e) {
    return t
  }
}

const someEven = someShort([1, 2, 3, 1, 5], el => el % 2 === 0)

console.log(someEven)

更新

更通用的答案可能如下所示

const escReduce = (arr, fn, init, exitFn) => {
    try {
      return arr.reduce((...args) => {
          if (exitFn && exitFn(...args)) {
              throw args[0]
          }
          return fn(...args)
        }, init)
    } catch(e){ return e }
}

escReduce(
  Array.from({length: 100}, (_, i) => i+1),
  (acc, e, i) => acc * e,
    1,
    acc => acc > 1E9 
); // 6227020800

给我们传递一个可选的 exitFn 来决定是否中断

假设你不需要return一个数组,也许你可以使用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;

在我看来,如果您不需要 return 数组(例如,在数组运算符链中),这是对已接受答案的更好解决方案,因为您不改变原始数组并且您不需要制作它的副本,这可能对大型阵列不利。

因此,要更早终止,要使用的成语是 arr.splice(0)。 这就提出了一个问题,为什么在这种情况下不能只使用 arr = [] 呢? 我试过了,reduce 忽略了分配,继续保持不变。 reduce 习语似乎响应拼接等形式,但不响应赋值运算符等形式??? - 完全不直观 - 并且必须作为函数式编程信条中的规则死记硬背...

const array = ['9', '91', '95', '96', '99'];
const x = array
.reduce((acc, curr, i, arr) => {
    if(i === 2) arr.splice(1);  // eject early
    return acc += curr;
  }, '');
console.log('x: ', x);  // x:  99195

问题是,在累加器内部不可能停止整个过程。因此,通过设计必须操纵外部范围内的某些东西,这总是会导致必要的突变。

正如许多其他人已经提到的,throwtry...catch 并不是真正可以称为 “解决方案”的方法。它更像是一种具有许多不良副作用的黑客攻击。

做到这一点的唯一方法没有任何突变是使用第二个比较函数,它决定是继续还是停止。为了仍然避免 for 循环,它必须用递归来解决。

代码:

function reduceCompare(arr, cb, cmp, init) {
    return (function _(acc, i) {
        return i < arr.length && cmp(acc, arr[i], i, arr) === true ? _(cb(acc, arr[i], i, arr), i + 1) : acc;
    })(typeof init !== 'undefined' ? init : arr[0], 0);
}

这可以像这样使用:

var arr = ['a', 'b', 'c', 'd'];

function join(acc, curr) {
    return acc + curr;
}

console.log(
    reduceCompare(
        arr,
        join,
        function(acc) { return acc.length < 1; },
        ''
    )
); // logs 'a'

console.log(
    reduceCompare(
        arr,
        join,
        function(acc, curr) { return curr !== 'c'; },
        ''
    )
); // logs 'ab'

console.log(
    reduceCompare(
        arr,
        join,
        function(acc, curr, i) { return i < 3; },
        ''
    )
); // logs 'abc'

我用它制作了一个 npm 库,还包含 TypeScript 和 ES6 版本。欢迎使用:

https://www.npmjs.com/package/array-reduce-compare

或 GitHub:

https://github.com/StefanJelner/array-reduce-compare

您可以使用 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
}

您可以编写自己的 reduce 方法。像这样调用它,所以它遵循相同的逻辑,您可以控制自己的转义/中断解决方案。它保留了功能风格并允许打破。

const reduce = (arr, fn, accum) => {
  const len = arr.length;
  let result = null;
  for(let i = 0; i < len; i=i+1) {
    result = fn(accum, arr[i], i)
    if (accum.break === true) {
      break;
    }
  }
  return result
}

const arr = ['a', 'b', 'c', 'shouldnotgethere']
const myResult = reduce(arr, (accum, cur, ind) => {
  accum.result = accum.result + cur;
  if(ind === 2) {
    accum.break = true
  }
  return accum
}, {result:'', break: false}).result

console.log({myResult})

或者创建您自己的归约递归方法:

const rcReduce = (arr, accum = '', ind = 0) => {
  const cur = arr.shift();
  accum += cur;
  const isBreak = ind > 1
  return arr.length && !isBreak ? rcReduce(arr, accum, ind + 1) : accum
}

const myResult = rcReduce(['a', 'b', 'c', 'shouldngethere'])
console.log({myResult})