Chapter 08

2章.2 リビルドJavaScript:変数は箱ではない

Satoshi Takeda
Satoshi Takeda
2021.10.01に更新

経験則とプログラミング

ある程度特定のプログラミング言語を書けるようになると気付くのは難しいのですが、我々は空間的・視覚的・機械的に認知のショートカットを組み合わせてコーディングしているはずです。

特に人間は直感で手早く考える能力を使い経験則に基づいて判断するので、適切なメンタルモデルを持つということは重要なことでもあります。

実はこの本は Dan AbramovMaggie Appleton が配信している Just JavaScript にヒントを得てタイトルをつけています。JavaScript に関してメンタルモデルを捉え直すよい機会となり非常にわかりやすいコンテンツでした。

意図しない認知の変換

少しだけ Just JavaScript で触れていた内容を紹介しましょう。

const v = "Zalue";
v[0] = "V";
console.log(v);

何が出力されるか、いったん鼻先だけで考えてみてください。文字列へのインデックスアクセスで v[0] は元の値である Zalue の先頭文字を取得できる ZV に置き換えて…。残念ながらこれは Zalue と出力されます。

const a = [1, 2, 3]:
a[0] = 4;
console.log(a); // [4, 2, 3]

我々は配列の操作で上記のような出力がされるのを知っています。メンタルモデルを疑わずに文字列でも同じことが可能だと思ってしまうのは非常に危ういですね。ここで捉え直したいのは文字列がプリミティブ値であること、配列がプリミティブではないことです。プリミティブ値は読込専用でイミュータブルであると捉え直しましょう。

let pet = "Narwhal";
pet = "The Kraken";
console.log(pet);

ではこれは何を出力するでしょうか。先ほどの結果を踏まえるとブレそうですが The Kraken が出力されます。「プリミティブ値である文字列はイミュータブルじゃなかったのか」と思った人は、変数は箱で箱に入れた値である文字列を変更したととらえたのかもしれません。変更したのは変数の向き先であり値ではありません。値の割り当てを変えただけなのです。

こういった常日ごろは手癖や鼻先の判断を再構築できるよい機会になるので、興味があれば Just JavaScript を購読してみるとよいでしょう(昨年トライアルで購読していたときは無料だったんですが今は課金が必要なようです)。

React とプリミティブ値

前述のとおり React の抽象化は JSX を利用したコーディングで DOM 構造と結果が想起しやすく、メンタルモデル構築までのリーチが短いといった特徴がありました。このリーチの短さがトラップとなり、上記の認知の掛け違いも合わせてしまうと直感で誤ったステップのまま理解が進んでしまいます。

たとえば以下のコンポーネントの rerender を考えます。

+1 のカウントアップすると memo 化して影響を受けないはずの Hello コンポーネントが rerender されます。これはオブジェクトリテラルで Props を渡しているので rerender されているんですね。適切に rerender を抑止するにはプリミティブ値を Props として渡すべきでしょう(修正版)。

以下の結果がどうであったか、プリミティブ値とはなんであったかを踏まえればなぜ Hello コンポーネントが rerender されたかの理解が進みます。

console.log({name: "world"} === {name: "world"}); // 参照が違う
console.log("world" === "world"); // プリミティブで同値

Angular と参照等価

Angular には Pipe といったテンプレート内のテキスト補間内で利用可能な変換パイプオペレーターが作成可能です。下記では ISO date-time string を対象にフォーマットを指定して変換しています。

<p>current time: { today | date:'HH:mm' }</p>

この Pipe の機能ですが JavaScript ビルトインのプリミティブ値とオブジェクト参照等価を判断します。ビルトインの比較はパフォーマンスが優れているからですね。そのため変換対象となる値が参照等価である場合 rerender しません。以下の例では Add "Flying Hero" ! のボタンをいくら押しても、canFly: true な Hero の追加はされるものの画面に追加されることはありません。コンポーネントが抱える配列リストに追加されるものの、参照等価であるために rerender されないのです。

適切に rerender したい場合は新しい配列を作って返すことで画面に反映できますPipe のデコレータで pure: false を指定しても可能ですが、何にせよイミュータブルなデータとして渡すことを念頭に置くとプレーンなメンタルモデルで以降取り組めまそうですね。

この場合も配列に新しい要素を push した際には参照等価となるのだということを念頭におけばトラップにハマることはありません。React で { name: "world" } と記述するたびに新しい参照が生まれていたのも思い出しておきましょう。

const arr = [1, 2, 3];

const ref = arr;
ref.push(4);
console.log(arr === ref); // true

const copy = [...arr];
copy.push(4);
console.log(arr === copy); // false

ここで伝えたいのはライブラリの使い方ではありません。値がどういった性質を持つのか、メンタルモデルを再構築することでライブラリのパフォーマンス性能やドキュメントに書かれた内容を捉え直すということが重要なのです。