986 字
5 分鐘

JavaScript reduce 空陣列錯誤:初始值與 filter 後的資料契約

TypeError: Reduce of empty array with no initial value 出現在 filter().reduce() 時,先看 filter 後的陣列。API 原本有十筆資料,篩選後仍可能一筆不剩;缺少第二個參數的 reduce() 沒有可用的起始累加值,就會拋出錯誤。

加總可明確傳入 0,但平均值、最低價或第一筆符合條件的資料,需要先定義「沒有資料」代表什麼。不要把所有聚合都補成零。

為什麼一筆正常,零筆就失敗?#

依 MDN reduce 文件,省略初始值時,第一個存在的元素會成為 accumulator,callback 從後面的元素開始。單筆陣列甚至不會呼叫 callback;因此 callback 裡的欄位驗證也可能整段被跳過。

以下案例可獨立存成 reduce-basics.mjs 執行:

import assert from 'node:assert/strict';
const rows = [{ active: false, amount: 10 }];
const selected = rows.filter(row => row.active);
assert.equal(selected.length, 0);
assert.throws(() => selected.reduce((sum, row) => sum + row.amount), TypeError);
assert.equal(selected.reduce((sum, row) => sum + row.amount, 0), 0);
let calls = 0;
const single = [{ amount: 10 }];
const result = single.reduce((sum, row) => {
calls += 1;
return sum + row.amount;
});
assert.equal(calls, 0);
assert.equal(result, single[0]); // 結果是物件,並不是數字 10
assert.equal(single.reduce((sum, row) => sum + row.amount, 0), 10);

這個型別差異容易被多筆正常資料遮住。若預期結果永遠是數字,從數字初始值開始,並讓每次 callback 都回傳數字;不要讓第一個物件代替你的總和。

初始值要符合運算,無資料要符合產品#

加法的起始值可用 0、乘法可用 1,收集成陣列則用 []。這些選擇代表運算的起點,不等於所有業務問題的空值答案。例如「沒有訂單」的總額可以是零,但「沒有評分」的平均分數若顯示為零分,就改變了意思。

下面的平均值函式接受密集的有限數字陣列,空陣列回傳 null。它拒絕數字字串、NaN、Infinity 與陣列空洞,避免靜默把不完整資料當成有效樣本;若加總超出有限範圍,也明確失敗。

function meanOrNull(values) {
if (!Array.isArray(values)) throw new TypeError('Expected an array');
if (values.length === 0) return null;
for (let i = 0; i < values.length; i += 1) {
if (!Object.hasOwn(values, i) || !Number.isFinite(values[i])) {
throw new TypeError('Expected dense finite numbers');
}
}
const sum = values.reduce((total, value) => total + value, 0);
if (!Number.isFinite(sum)) throw new RangeError('Sum overflow');
return sum / values.length;
}

這裡採用一般 JavaScript Number,沒有提供金額精度或高精度統計保證。金融金額與大量浮點數累加,應另訂單位、捨入與精度策略。函式也沒有把壞資料自動排除;若產品允許略過資料,必須同時呈現被略過的筆數,不能讓平均值看似代表全部樣本。

接在函式後面執行的測試如下:

import assert from 'node:assert/strict';
assert.equal(meanOrNull([]), null);
assert.equal(meanOrNull([0]), 0);
assert.equal(meanOrNull([10, 20, 30]), 20);
assert.equal(meanOrNull([-10, 10]), 0);
for (const invalid of [['10'], [NaN], [Infinity], new Array(2), [undefined]]) {
assert.throws(() => meanOrNull(invalid), TypeError);
}
assert.throws(() => meanOrNull([Number.MAX_VALUE, Number.MAX_VALUE]), RangeError);
assert.throws(() => meanOrNull(null), TypeError);
assert.equal([0, 0].filter(Boolean).length, 0);
assert.equal([1, '2'].reduce((sum, value) => sum + value, 0), '12');

filter(Boolean) 會移除合法的零,不能拿來當成數字驗證。+ 遇到字串也可能變成串接;補了初始值之後,仍要檢查輸入型別。

length 大於零,也不保證有可累加的元素#

ECMAScript reduce 演算法會檢查索引是否存在。一般沒有數字索引原型屬性的環境下,new Array(3) 雖然 length 是 3,三個位置都是空洞;沒有初始值仍會失敗。這與 [undefined, undefined, undefined] 的三個實際元素不同。

import assert from 'node:assert/strict';
assert.throws(() => new Array(3).reduce((a, b) => a + b), TypeError);
assert.equal(new Array(3).reduce((a, b) => a + b, 0), 0);
assert.ok(Number.isNaN([undefined].reduce((a, b) => a + b, 0)));

上面所有 assertion 於 2026-10-21 由維護流程在 macOS、Node.js v24.13.0 執行,沒有連接真實 API,也沒有跑瀏覽器 UI。排錯時依序保留篩選前後筆數、輸入型別及空集合決策;若資料在 API 解析階段就不符合契約,先參考 fetch JSON 的格式與形狀檢查,不要等 reduce 拋錯才補救。

參考資料:

MDN:Array.prototype.reduce()

ECMAScript:Array.prototype.reduce

JavaScript reduce 空陣列錯誤:初始值與 filter 後的資料契約
https://laplusda.com/posts/javascript-reduce-empty-array-initial-value/
作者
Zero
發佈於
2026-10-21
許可協議
CC BY-NC-SA 4.0