GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing developer.mozilla.org — Array.prototype.reduce() - JavaScript | MDN このページはコミュニティーの尽力で英語から翻訳されました。MDN Web Docs コミュニティーについてもっと知り、仲間になるにはこちらから。
Array.prototype.reduce()
|
accumulator |
currentValue |
index |
返値 | |
|---|---|---|---|---|
| 最初の呼び出し | 15 |
16 |
1 |
31 |
| 2 番目の呼び出し | 31 |
17 |
2 |
48 |
| 3 番目の呼び出し | 48 |
18 |
3 |
66 |
| 4 番目の呼び出し | 66 |
19 |
4 |
85 |
array の要素は処理中に変化しません。常に [15, 16, 17, 18, 19] です。 reduce() の返値は、コールバック呼び出しの最後の返値である (85) となるでしょう。
ここでは、同じアルゴリズムで同じ配列を減らしますが、 reduce() の 2 番目の引数として 10 という initialValue を渡します。
[15, 16, 17, 18, 19].reduce(
(accumulator, currentValue) => accumulator + currentValue,
10,
);
コールバックは 5 回呼び出され、それぞれの呼び出しにおける引数と返値は次のようになります。
accumulator |
currentValue |
index |
返値 | |
|---|---|---|---|---|
| 最初の呼び出し | 10 |
15 |
0 |
25 |
| 2 番目の呼び出し | 25 |
16 |
1 |
41 |
| 3 番目の呼び出し | 41 |
17 |
2 |
58 |
| 4 番目の呼び出し | 58 |
18 |
3 |
76 |
| 5 番目の呼び出し | 76 |
19 |
4 |
95 |
この場合の reduce() の返値は 95 となります。
オブジェクトの配列に含まれた値の合計値を出すには、すべての項目を関数内で取得できるようにするために initialValue を指定する必要があります。
const objects = [{ x: 1 }, { x: 2 }, { x: 3 }];
const sum = objects.reduce(
(accumulator, currentValue) => accumulator + currentValue.x,
0,
);
console.log(sum); // logs 6
pipe 関数は一連の関数を受け取り、新しい関数を返します。新しい関数が引数で呼び出されると、一連の関数が順番に呼び出され、それぞれが前回関数の返値を受け取ります。
const pipe =
(...functions) =>
(initialValue) =>
functions.reduce((acc, fn) => fn(acc), initialValue);
// 合成に使用する素材
const double = (x) => 2 * x;
const triple = (x) => 3 * x;
const quadruple = (x) => 4 * x;
// 特定の値の乗算のための合成関数
const multiply6 = pipe(double, triple);
const multiply9 = pipe(triple, triple);
const multiply16 = pipe(quadruple, quadruple);
const multiply24 = pipe(double, triple, quadruple);
// 使用方法
multiply6(6); // 36
multiply9(9); // 81
multiply16(16); // 256
multiply24(10); // 240
プロミスシーケンスは、非同期で行われることを除けば、基本的に前回の節で示された関数のパイプ接続です。
// パイプと比較すると、 fn(acc) は acc.then(fn) に変更され、
// initialValue はプロミスであることが保証されます。
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce((acc, fn) => acc.then(fn), Promise.resolve(initialValue));
// 合成に使用する構成要素
const p1 = async (a) => a * 5;
const p2 = async (a) => a * 2;
// 構成された関数は、最終的にすべてプロミスで包まれるため、
// プロミス以外を返すこともできます。
const f3 = (a) => a * 3;
const p4 = async (a) => a * 4;
asyncPipe(p1, p2, f3, p4)(10).then(console.log); // 1200
asyncPipe は async/await を使用して実装することもでき、 pipe との類似性をよりよく示しています。
const asyncPipe =
(...functions) =>
(initialValue) =>
functions.reduce(async (acc, fn) => fn(await acc), initialValue);
reduce() は疎配列の欠落している要素をスキップしますが、値が undefined の場合はスキップしません。
console.log([1, 2, , 4].reduce((a, b) => a + b)); // 7
console.log([1, 2, undefined, 4].reduce((a, b) => a + b)); // NaN
reduce() メソッドは this の length プロパティを読み込み、次にキーが length より小さい非負の整数である各プロパティにアクセスします。
const arrayLike = {
length: 3,
0: 2,
1: 3,
2: 4,
3: 99, // length が 3 であるため reduce() からは無視される
};
console.log(Array.prototype.reduce.call(arrayLike, (x, y) => x + y));
// 9
reduce() のような多目的の高次関数は強力ですが、特に経験の浅い JavaScript の開発者にとっては理解しにくい場合があります。他の配列メソッドを使用した方がコードが明快になる場合、開発者は reduce() を使用する他の利点と読み取り可能性のトレードオフを比較検討する必要があります。
reduce() は常に for...of ループと同等ですが、上位スコープの変数を変更する代わりに、各反復処理で新しい値を返すことに注意してください。
const val = array.reduce((acc, cur) => update(acc, cur), initialValue);
// これは、次のものと同等です
let val = initialValue;
for (const cur of array) {
val = update(val, cur);
}
前述のように、人々が reduce() を使用したいと思う理由は、データを不変とする関数型プログラミングの手法を模倣するためです。したがって、アキュムレーターの不変性を支持する開発者は、次のように反復処理ごとにアキュムレーター全体をコピーする傾向があります。
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
const currCount = Object.hasOwn(allNames, name) ? allNames[name] : 0;
return {
...allNames,
[name]: currCount + 1,
};
}, {});
このコードは非効率的です。というのも、各イテレーターは allNames オブジェクト全体をコピーする必要があり、これは固有の名前がいくつあるかによってサイズが大きくなってしまうからです。このコードは最悪の場合、 N が names の長さだとすると O(N^2) のパフォーマンスになります。
各反復処理で allNames オブジェクトを変更したほうがよりよいでしょう。しかし、 allNames がいずれにせよ変更されるのであれば、 reduce() を for ループに変換した方がより明確です。
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.reduce((allNames, name) => {
const currCount = allNames[name] ?? 0;
allNames[name] = currCount + 1;
// allNames を返さないと、次の反復処理で undefined を受け取る
return allNames;
}, Object.create(null));
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = Object.create(null);
for (const name of names) {
const currCount = countedNames[name] ?? 0;
countedNames[name] = currCount + 1;
}
したがって、アキュムレーターが配列やオブジェクトで、反復処理ごとに配列やオブジェクトをコピーしている場合、誤ってコードに 2 次的な複雑さを導入してしまい、大きなデータですぐにパフォーマンスが低下してしまう可能性があります。これは実際のコードでも現れています。例えば、 Making Tanstack Table 1000x faster with a 1 line change を参照してください。
reduce() の受け入れられる用途のいくつかは上で指定されたものです(特に、配列の合計、プロミスの順序付け、関数のパイプ処理)。他にも reduce() よりも優れた代替手段が存在する場合があります。
配列の平坦化。代わりに flat() を使用してください。
const flattened = array.reduce((acc, cur) => acc.concat(cur), []);
const flattened = array.flat();
プロパティによるオブジェクトのグループ化。代わりに Object.groupBy() を使用してください。
const groups = array.reduce((acc, obj) => {
const key = obj.name;
const curGroup = acc[key] ?? [];
return { ...acc, [key]: [...curGroup, obj] };
}, {});
const groups = Object.groupBy(array, (obj) => obj.name);
オブジェクトの配列に格納されている配列の連結。代わりに flatMap() を使用してください。
const friends = [
{ name: "Anna", books: ["Bible", "Harry Potter"] },
{ name: "Bob", books: ["War and peace", "Romeo and Juliet"] },
{ name: "Alice", books: ["The Lord of the Rings", "The Shining"] },
];
const allBooks = friends.reduce((acc, cur) => [...acc, ...cur.books], []);
const allBooks = friends.flatMap((person) => person.books);
配列内の重複項目の除去。代わりに Set と Array.from() を使用してください。
const uniqArray = array.reduce(
(acc, cur) => (acc.includes(cur) ? acc : [...acc, cur]),
[],
);
const uniqArray = Array.from(new Set(array));
配列の要素の削除や追加。代わりに flatMap() を使用してください。
// 数値の配列を受け取り、完全二乗を平方根に分割します。
const roots = array.reduce((acc, cur) => {
if (cur < 0) return acc;
const root = Math.sqrt(cur);
if (Number.isInteger(root)) return [...acc, root, root];
return [...acc, cur];
}, []);
const roots = array.flatMap((val) => {
if (val < 0) return [];
const root = Math.sqrt(val);
if (Number.isInteger(root)) return [root, root];
return [val];
});
配列から要素を取り除くだけなら filter() も使用できます。
要素の検索、または要素が条件を満たすかどうかのテスト。代わりに find() と findIndex() または some() と every() を使用してください。これらのメソッドには、配列全体を反復処理することなく、結果が確定したらすぐに返すという好ましいこともあります。
const allEven = array.reduce((acc, cur) => acc && cur % 2 === 0, true);
const allEven = array.every((val) => val % 2 === 0);
reduce() が最良の選択である場合は、ドキュメント化と変数名の意味づけをすることで、可読性の欠点を軽減する手助けになります。
| 仕様書 |
|---|
| ECMAScript® 2027 Language Specification # sec-array.prototype.reduce |