🤙
Array.mapを文法的に説明できるようになろう
なんの記事?
JSのArray#map, Array#filter の説明
Intro
普段から何気なく使っているJSのArray#map, Array#filter ですが、
文法的に結構複雑だという印象です。
文法的に Array#map, Array#filter を理解するための一助になればと思います。
結論
Array#map, Array#filter は高階関数で、引数のコールバック関数を実行する。
???だと思うので、ひとつずつ説明できればと思います。
JS における関数
JSでは関数も変数と同様に代入することができます。
base
const getABC =()=> {
return "ABC";
};
const funcObject = getABC;
const abc = funcObject();
console.log(abc);
①getABC関数をfuncObject変数に代入する

②funcObjectに () を付けて、関数として実行する

③getABC関数の戻り値として、文字列ABCがabc変数に代入される

高階関数・コールバック関数
callbackサンプル
// 以下の出力はどうなる?
const getMsg = (type) => {
if (type === 0) {
return "Hi";
}
return "Bye";
};
const func = getMsg;
const result = func(1);
console.log(result);
// -> "Bye"
高階関数・コールバック関数とは?
JSは関数も変数のように扱える。
関数の引数に別の関数を指定する際に、
- 引数として使われる関数のことを「コールバック関数」という
- 関数を引数として受け取る関数のことを「高階関数(第一級関数)」という
- 以下の例における
append123(getABC);だと- コールバック関数 → getACB 関数
- 高階関数 → append123 関数
const append123= (func) => {
const result = func();
return result + "123";
};
const getABC = () => {
return "ABC";
};
const funcObject = append123(getABC);
console.log(funcObject);
高階関数・コールバック関数の使いどころ (私見)
「コールバック関数」を定義するとき=コードのデザインパターンとして、一番綺麗に書けそうだなってとき。
case 以下の2つの関数を共通化する
- 状況: if の条件部のみが異なる
-
i % 2 === 1とi % 2 === 0
-
- 処理の大枠は同じで「条件」の部分が異なる
- 引数で配列を受け取る
- 配列の長さ分のループ
- 「ある条件」に当てはまったらcount += 1
- ループ後にcountを返す
before
const countOddNum = (arr) => {
let count = 0;
for (let i = 0; i < arr.length; i++) {
if (i % 2 === 1) {
count += 1;
}
}
return count;
};
const countEvenNum = (arr) => {
let count = 0;
for (let i = 0; i < arr.length; i++) {
if (i % 2 === 0) {
count += 1;
}
}
return count;
};
リファクタリング例1
「条件」の部分を引数 remainder にする
- Good: 一番簡単な方法
- Bad:
- 何をしたい関数なのかわ借りにくくなる
- 引数が0,1以外のときはどうなる?
リファクタリング1
const countNum = (arr, remainder) => {
let count = 0;
for (let i = 0; i < arr.length; i++) {
if (i % 2 === remainder) {
count += 1;
}
}
return count;
};
リファクタリング例2
フラグ引数を定義し赤枠の部分を分岐で変える
- Good: 2つの関数の処理をまとめられている
- Bad:
- ネストが深いし無駄が多いし拡張性がない
- フラグ引数アンチ (私怨)
リファクタリング2
const countNum = (arr, isOdd) => {
let count = 0;
for (let i = 0; i < arr.length; i++) {
if (isOdd === true) {
if (i % 2 === 1) {
count += 1;
}
} else {
if (i % 2 === 0) {
count += 1;
}
}
}
return count;
};
リファクタリング例3
リファクタリング例2を改善し、ネストを浅くする
- Good: 2つの関数の処理をまとめられている
- Bad: フラグ引数で呼び出しが読みにくい
-
countNum(nums, true);という呼び出しをした際に、直感でtrueが何なのかわからない
-
const countNum = (arr, isOdd) => {
let count = 0;
for (let i = 0; i < arr.length; i++) {
if ((isOdd === true && i % 2 === 1)
|| (isOdd === false && i % 2 === 0)
) {
count += 1;
}
}
return count;
};
リファクタリング例4
コールバック関数を使う
- Good:
- 2つの関数の処理をまとめられている
- 命名から処理を推測しやすい
- Bad:
- 行数が増える
const isOddNum = (val) => {
return val % 2 === 1;
};
const isEvenNum = (val) => {
return val % 2 === 0;
};
const countNum = (arr, conditionFunc) => {
let count = 0;
for (let i = 0; i < arr.length; i++) {
if (conditionFunc(i) === true) {
count += 1;
}
}
return count;
};
const nums = [0, 1, 2, 3, 4, 5, 6];
const res0 = countNum(nums, isEvenNum);
const res1 = countNum(nums, isOddNum);
リファクタリング例5
例4を改善し、匿名関数を使用する
const countNum = (arr, conditionFunc) => {
let count = 0;
for (let i = 0; i < arr.length; i++) {
if (conditionFunc(i) === true) {
count += 1;
}
}
return count;
};
const nums = [0, 1, 2, 3, 4, 5, 6];
const res0 = countNum(nums, val => val % 2 === 0);
改めて使いどころ
「コールバック関数」を定義するとき=コードのデザインパターンとして、
一番綺麗に書けそうだなってとき。
今見た通り、コールバックを自前でどうしても実装しなければならない場合はない(はず)。
Array#map
Array#mapメソッドは高階関数で、引数のコールバック関数を実行する。
下記のサンプルでいうと
- array1は配列として宣言される
- mapに匿名関数
(x) => x * 2をコールバック関数として渡す - array1 の要素ごとにコールバック関数の処理が実行される
- mapの実行結果が非破壊的にmap1変数に代入される
const array1 = [1, 4, 9, 16];
// Pass a function to map
const map1 = array1.map((x) => x * 2);
console.log(map1);
// Expected output: Array [2, 8, 18, 32]
所感
記事タイトルをGeminiに考えてもらうのいいね!
Discussion