モダンJavaScriptプログラミングのテクニック

概要

社内の輪読会で『JavaScriptモダンプログラミング完全ガイド』を読んでいくことになった(推薦者は僕)。モダンなJavaScriptとは、おおむねES2015(ES6)以降を指すのであるが、ES2015で導入されたタグ付きテンプレートリテラルは特に好きな機能の一つだ。

Zennの記事でタグ付きテンプレートリテラルの解説を行ったのだが、その中で、Jestのtest.eachを模したサンプルのタグ付きテンプレートリテラルを作成した。
以下はその利用例。

each`
    name | age
    ${"Alice"} | ${20}
    ${"Bob"} | ${30}
`(({name, age}) => {
    console.log(`${name} is ${age} years old.`);
});

リテラルに記述したテーブル形式の、データ行の1行1行を入力として与え、後続の引数で指定した関数(アロー関数で記述している)を呼び出すような挙動を実現している。上記の例だとコンソールに以下のように出力される。

"Alice is 20 years old."
"Bob is 30 years old."

このタグ付きテンプレートリテラルの実体は以下のタグ関数だ。

function each(fragments, ...values) {
    const attrs = fragments[0].split('|').map(s => s.trim());
    const numOfAttrs = attrs.length;
    const array = [];
    let index = 0;
    while (index < values.length) {
        const row = values.slice(index, index + numOfAttrs);
        const obj = attrs.reduce((o, attr, i) => {
            return {...o, [attr]: row[i]};
        }, {});
        array.push(obj);
        index += numOfAttrs;
    }
    return (callback) => {
        array.forEach((e) => callback(e));
    };
}

タグ関数の書き方や、上記タグ関数の処理内容についてはZennの記事の方を参照してほしいのだが、このコードの細かい箇所にもモダンなJavaScriptのテクニックが散りばめられていて、古きよきJSしか知らない人にはとっつきにくい部分もあるかと思うので、本記事で補足したいと思う。

Array.prototype.map()

mapメソッドは、配列の各要素に対して関数を適用して変換をかけて、新しい配列を得るものだ。

const attrs = fragments[0].split('|').map(s => s.trim());

fragments[0]"name | age"という文字列が入っていた場合、split('|')により["name ", " age"]という配列が生成され、各要素をmapにより空白除去した結果、["name", "age"]という新しい配列が得られる。

アロー関数

mapメソッドの引数には変換関数を指定する。なので、先のサンプルコードは以下のように書いてもよい。

const attrs = fragments[0].split('|').map(function(s){ return s.trim();});

が、この記法は若干読みづらく、ノイズが多い。アロー関数という記法を用いると、もっと簡潔になる。元のサンプルでmapメソッドに与えていた以下の式がアロー関数である。

s => s.trim()

引数が1つなので省略しているが、括弧を省略せずに書くと以下となる。

(s) => s.trim()

ここでは詳しくは書かないが、this参照に関わる問題もあり、原則としてアロー関数を優先して使うのがよいと思う。

Array.prototype.reduce()

reduce関数は、配列の要素を、何らかの処理によって単一の出力値にまとめるものだ。関数型プログラミング的な用語を使うなら、畳み込む(fold)と言ってもいい。

前述のタグ関数での例はやや複雑なので、先に簡単な例を示す。

const sum =[1, 2, 3].reduce((acc, cur, idx) => acc + cur, 0);
console.log(sum);

これは、reduceメソッドを使って配列の要素の合計値を算出した例だ。
reduceメソッドの第一引数には、reducer関数と呼ばれる関数を指定する(アロー関数を使うとよい)。関数の引数accは各要素の反復の都度積み上げられる途中結果(アキュムレータ)。curは現在の反復で指し示す配列の要素、idxは配列のインデックス。
reduceメソッドの第二引数には、アキュムレータの初期値を指定する。指定しなかった場合は、配列の最初の要素が使用される。この例では指定してもしなくても結果は同じ。

アキュムレータの値の移り変わりを書くと、以下のような感じ。

0 (初期値) => 1 (0 + 1) => 3 (1 + 2) => 6 (3 + 3)

最後の値が、reduceメソッドの返す値となる。

という仕組みを踏まえて、eachタグ関数の実装を見る。

onst obj = attrs.reduce((o, attr, i) => {
    return {...o, [attr]: row[i]};
}, {});

何をやっているかというと、以下のテンプレートリテラルを解析した結果、パラメータ名の配列がattrsに入っている(["name", "age")。rowという別の配列には、例えば最初のデータ行の値が入っている(["Alice", 20])。これを元に、{name: "Alice", age: 20}のようなオブジェクトを作っている。

each`
    name | age
    ${"Alice"} | ${20}
    ${"Bob"} | ${30}
`

アキュムレータの初期値{}(空のオブジェクト)からの移り変わりを記す。

{} (初期値) => { name: "Alice" } => { name: "Alice", age: 20 }

オブジェクトを器として、1個1個プロパティを追加していっているのがわかる。
以下の文はちょっと説明が必要なので、次に述べる。

return {...o, [attr]: row[i]};

オブジェクトリテラルでのスプレッド構文

スプレッド構文とは、配列(要素)やオブジェクト(プロパティ)などの反復可能な要素を展開して利用するための構文である。ES2018で、オブジェクトリテラルでのスプレッド構文が使用可能となった。これを用いると、Object.assignを使うことなく、オブジェクトの(浅い)コピーやマージが可能となる。

const objX = { x: 1 };
const objY = { y: 2 };
const objXY = { ...objX, ...objY };
// { x: 1, y: 2 }
console.log(objXY);

なので、前述のeachタグ関数の以下の文は、oというオブジェクト(アキュムレータ)に、プロパティを1つ追加して新しいオブジェクトを生成していることになる。

return {...o, [attr]: row[i]};

ちなみにオブジェクトリテラルにおいてプロパティ名を単純な文字列ではなく、変数の値や関数呼び出し結果としたい場合、プロパティ名を角括弧[ ]で括ればよい。上記コードではそのテクニックを使っている。

高階関数

eachタグ関数の最後の部分。returnしているモノは何だろう?

return (callback) => {
    array.forEach((e) => callback(e));
};

正解は、アロー関数で記述された関数オブジェクトである。よって、each関数は「関数を返す関数」である。このように関数を引数で受け取ったり、結果として返却するような関数を高階関数と呼ぶ。
(注)以前からJavaScriptは関数を第一級オブジェクトとして扱っており、ES2015で導入されたわけではない。

分割(デストラクチャリング)

再掲となるが、以下のコード。

return (callback) => {
    array.forEach((e) => callback(e));
};

forEachメソッドを用いて、配列の各要素を取り出し、それを引数としてcallback関数を呼び出す(ような関数を返却している)。ここで配列の要素は、例えば{ name: "Alice", age: 20 }のようなオブジェクトである。

eachタグ関数を用いた、タグ付きテンプレートの利用例も再掲する。

each`
    name | age
    ${"Alice"} | ${20}
    ${"Bob"} | ${30}
`(({name, age}) => {
    console.log(`${name} is ${age} years old.`);
});

each関数は、「コールバック関数を引数で受け取り、それを複数回(データ行の数だけ)実行する関数」を返すのだった。そのコールバック関数(を表すアロー関数)の部分だけ抜き出す。

({name, age}) => {
    console.log(`${name} is ${age} years old.`);
}

eachタグ関数がコールバック関数を呼び出す際の引数はオブジェクトだったので、本来ここは以下のように書かれるはずだ。

(arg) => {
    console.log(`${arg.name} is ${arg.age} years old.`);
}

分割を使うことで、オブジェクトのプロパティをそれぞれ別の変数に受け取ることができるのだ。

まとめ

ES2015以降のモダンなJavaScriptプログラミング、ちゃんと勉強しよう!

タグ: