🤙

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変数に代入する
step1

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

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

高階関数・コールバック関数

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 === 1i % 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メソッドは高階関数で、引数のコールバック関数を実行する。

下記のサンプルでいうと

  1. array1は配列として宣言される
  2. mapに匿名関数 (x) => x * 2 をコールバック関数として渡す
  3. array1 の要素ごとにコールバック関数の処理が実行される
  4. 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