在对数组应用 Array.reduce 时改变数组的后果是什么

What are the consequences of mutating the array while applying Array.reduce to it

假设我有一个数组:

const ar = [1,2,3,4];

然后我对其应用 reduce 函数,并在该函数中删除如下元素:

ar.reduce((result, element, index, original)=>{
    original.pop();
}, []);

对于前两个元素,函数只会执行两次。这是可以理解的,因为我在之前的调用中删除了第 3 个和第 4 个元素。

但有趣的是,如果我执行该函数并删除当前元素:

ar.reduce((result, element, index, original)=>{
    original.splice(index, 1);
}, []);

函数仍然为前两个元素执行了两次。为什么不对 3rd4th 元素执行,因为它们保留在数组中?

是否在任何地方记录了此行为?也许在规范中?

在您的第二个示例中,它实际上是针对 1st3rd 元素执行的,而不是针对前两个元素执行的:

const ar = [1, 2, 3, 4];

ar.reduce((result, element, index, original)=>{
    console.log(element, index);
    original.splice(index, 1);
}, []);

console.log(ar);

1 2 3 4
^

这里,虽然reduce的element1index0,它调用splice,删除第一个元素,然后迭代到下一个索引:

2 3 4
  ^

这里reduce的element3index1。删除后,index 将等于 ar.length 并停止,留下

2 4

之所以reduceRight()仍然会访问所有元素是因为你向后迭代,在当前索引处拼接元素不影响之前的元素位置:

const ar = [1, 2, 3, 4];

ar.reduceRight((result, element, index, original)=>{
    console.log(element, index);
    original.splice(index, 1);
}, []);

console.log(ar);

以及演练:

element = 4, index = 3

1 2 3 4
      ^

element = 3, index = 2

1 2 3
    ^

element = 2, index = 1

1 2
  ^

element = 1, index = 0

1
^

回答你的问题,是ECMAScript documents this behavior for Array#reduce() as part of the specification:

The range of elements processed by reduce is set before the first call to callbackfn. Elements that are appended to the array after the call to reduce begins will not be visited by callbackfn. If existing elements of the array are changed, their value as passed to callbackfn will be the value at the time reduce visits them; elements that are deleted after the call to reduce begins and before being visited are not visited.

与上面完全相同的段落也适用于 reduceRight

下面是 Array#reduce() 的 polyfill,遵循规范中的步骤:

Object.defineProperty(Array.prototype, 'reduce', {
  configurable: true,
  writable: true,
  value: Array.prototype.reduce || function reduce(callbackfn) {
    "use strict";
    // 1.
    if (this === undefined || this === null) {
      throw new TypeError("Array.prototype.reduce called on null or undefined");
    }
    let O = Object(this);
    // 2.
    let len = ToLength(O.length);
    // 3.
    if (typeof callbackfn != 'function') {
      throw new TypeError(`${String(callbackfn)} is not a function`);
    }
    // 4.
    if (len == 0 && arguments.length < 2) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    // 5.
    let k = 0;

    let accumulator;
    // 6.
    if (arguments.length >= 2) {
      // a.
      accumulator = arguments[1];
    // 7.
    } else {
      // a.
      let kPresent = false;
      // b.
      while (!kPresent && k < len) {
        // i.
        let Pk = String(k);
        // ii.
        kPresent = Pk in O;
        // iii.
        if (kPresent) accumulator = O[Pk]; // 1.
        // iv.
        k++;
      }
      // c.
      if (!kPresent) throw new TypeError("Reduce of empty array with no initial value");
    }
    // 8.
    while (k < len) {
      // a.
      let Pk = String(k);
      // b.
      let kPresent = Pk in O;
      // c.
      if (kPresent) {
        // i.
        let kValue = O[Pk];
        // ii.
        accumulator = callbackfn(accumulator, kValue, k, O);
      }
      // d.
      k++;
    }
    // 9.
    return accumulator;
  }
});

function ToInteger(argument) {
  let number = Number(argument);

  if (isNaN(number)) return 0;

  switch (number) {
  case 0:
  case Infinity:
  case -Infinity:
    return number;
  }

  return parseInt(number);
}

function ToLength(argument) {
  let len = ToInteger(argument);

  if (len <= 0) return 0;
  if (len == Infinity) return Number.MAX_SAFE_INTEGER || Math.pow(2, 53) - 1;

  return len;
}