在对数组应用 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);
}, []);
函数仍然为前两个元素执行了两次。为什么不对 3rd
和 4th
元素执行,因为它们保留在数组中?
是否在任何地方记录了此行为?也许在规范中?
在您的第二个示例中,它实际上是针对 1st 和 3rd 元素执行的,而不是针对前两个元素执行的:
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的element
是1
,index
是0
,它调用splice
,删除第一个元素,然后迭代到下一个索引:
2 3 4
^
这里reduce的element
是3
,index
是1
。删除后,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;
}
假设我有一个数组:
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);
}, []);
函数仍然为前两个元素执行了两次。为什么不对 3rd
和 4th
元素执行,因为它们保留在数组中?
是否在任何地方记录了此行为?也许在规范中?
在您的第二个示例中,它实际上是针对 1st 和 3rd 元素执行的,而不是针对前两个元素执行的:
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的element
是1
,index
是0
,它调用splice
,删除第一个元素,然后迭代到下一个索引:
2 3 4
^
这里reduce的element
是3
,index
是1
。删除后,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 tocallbackfn
. Elements that are appended to the array after the call toreduce
begins will not be visited bycallbackfn
. If existing elements of the array are changed, their value as passed tocallbackfn
will be the value at the time reduce visits them; elements that are deleted after the call toreduce
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;
}