[EffectiveJavaScript輪読会6]標準クラスからの継承を避ける

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のシリーズでは、第4章の考察をお届けしました。

引き続き、第4章をお送りします。

目次と日程

第4章後半は「オブジェクトとプロトタイプ」です。クラスベースではなく、プロトタイプベースを採用した、JavaScriptのアイデンティティを追及する日々です。

  • 第1章 JavaScriptに慣れ親しむ
  • 第2章 変数のスコープ
  • 第3章 関数の扱い
  • 第4章 オブジェクトとプロトタイプ
  • 第5章 配列とディクショナリ
  • 第6章 ライブラリとAPI設計
  • 第7章 並行処理

LT大会は2021年11月13日です。がんばりましょう。

今日はさっそく1回目で、 「標準クラスからの継承を避ける」「プロトタイプを「実装の詳細」として扱おう」「やみくもなモンキーパッチを避ける」 の3本をまとめてお届けします。

テキスト第4章「オブジェクトとプロトタイプ」の項目40、41、42に対応しています。

今日のアジェンダ

  • 標準クラスからの継承を避ける
  • プロトタイプを「実装の詳細」として扱おう
  • やみくもなモンキーパッチを避ける

標準クラスからの継承を避ける

もし、あなたが、クラスを作成して、そのクラスに、組み込みオブジェクト(たとえばArrayなど)のメンバーを使えるようにしたいと思ったらどうしますか。

そんなときは、マイクラスのprototypeに、Arrayオブジェクトのプロトタイプを定義するでしょう。

項目33の復習ですが、Object.createメソッドは、既存のオブジェクトを新しく生成されるオブジェクトのプロトタイプとして使用して、新しいオブジェクトを生成します。

書籍では、Dirクラスのインスタンスdirに対し、Array.lengthが使えないかという試みを行っています。

function myFunction6_40_01() {

  function Dir(path, entries) {
    this.path = path;
    for (let i = 0; i < entries.length; i++) {
      this[i] = entries[i];
    }

  }

  Dir.prototype = Object.create(Array.prototype);

  const dir = new Dir('path', ['index', 'script', 'css']);
  console.log(dir.length); //0

}

これがうまく動作しないのは、Arrayオブジェクトと、lengthプロパティの関係性によります。

Arrayオブジェクトと、lengthプロパティは、オブジェクトがArrayオブジェクトであるときのみ正しく機能するようです。

オブジェクトの型を決める内部プロパティ

型と呼んでいいのか、わかりませんが、JavaScriptには、オブジェクトがどの型なのか定義をする場所があります。

それは、普段は表に見えない、仕様上の設計で、[[Class]]という内部プロパティに格納されています。

内部プロパティ[[Class]]は、Object.prototype.toString.call()でアクセス可能です。

先ほど作成した、Dirクラスは、Objectですので、Arrayオブジェクトのメンバーは正しく機能しないのでした。

function myFunction6_40_02() {

  console.log(Object.prototype.toString.call({})); //[object Object]
  console.log(Object.prototype.toString.call([])); //[object Array]
  console.log(Object.prototype.toString.call(0)); //[object Number]
  console.log(Object.prototype.toString.call('a')); //[object String]
  console.log(Object.prototype.toString.call(true)); //[object Boolean]
  console.log(Object.prototype.toString.call(/.*/)); //[object RegExp]
  console.log(Object.prototype.toString.call(function () { })); //[object Function]

  function Dir() { }
  console.log(Object.prototype.toString.call(new Dir())); //[object Object]

}

どうしても、マイクラスに組み込みArrayオブジェクトのメンバーを実装したいばあいは、「プロパティに配列のインデックスを持たせるといい」、と書籍には書いてました。

詳細は割愛しますが、Arrayオブジェクトのメンバーは、オブジェクトのプロパティにインデックスが振られた値があるかを判定しているようです。

このように、プロトタイプにて、メンバーを持たせることを、委譲と呼びます。

JavaScriptがプロトタイプベースのプログラミング言語という根幹に関わる仕様の話ですが、委譲については、またいつか学習してみたいと思います。

function myFunction6_40_03() {

  function Dir(path, entries) {
    this.path = path;
    this.entries = entries;
  }

  Dir.prototype.forEach = function (f, thisArg) {
    if (typeof thisArg === 'undefined') {
      thisArg = this;
    }
    this.entries.forEach(f, thisArg);
  };

  const dir = new Dir('path', ['index', 'script', 'css']);

  dir.forEach(element => console.log(element)); //index //script //css

}

プロトタイプを「実装の詳細」として扱おう

この項目では、以下の4点が語られていました。

  • オブジェクトはインターフェースである
  • プロトタイプが実装である
  • あなたが制御していないオブジェクトのプロトタイプの構造を調べるの避ける
  • あなたが制御していないオブジェクトの内部を実装するプロパティを調べるのは避ける

これは、JavaScriptは、prototypeのつながりで構成されていて、わたしたちが普段操作するオブジェクトは、実装であるprototypeを操作するインターフェースです、という意味です。

なので、実装であるprototypeを、触りなさんな、と言っています。

わたしたちが触らなくても、見知らぬライブラリが提供するコードは、あなたの知らないところで、上流のprototypeを書き換えている可能性もあります。

組み込みオブジェクトや、JavaScriptの実装部分は、先人たちが一生懸命、抽象化を行った努力の結晶です。

抽象を理解するのは結構ですが、理解したつもりでも、踏み入れなさんな、ということですね。

やみくもなモンキーパッチを避ける

組み込みオブジェクトや、上流のprototypeに手を加えることを、モンキーパッチと呼ぶそうです。

モンキーパッチは悪手であると、有名です。

下記は、かんたんな例です。

Arrayオブジェクトのメンバーであるsplitメソッドに、自分が思いついた仕様(配列を分割する位置を引数で渡す)を加えるコードです。

モンキーパッチ1は成功しているようです。

しかし、誰かがモンキーパッチ2として、書き換えている可能性もあります。

そうなると、splitメソッドは、意図した結果を返しません。

function myFunction6_42_01() {

  //モンキーパッチ1
  Array.prototype.split = function (i) {
    return [this.slice(0, i), this.slice(i)];
  }

  const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  console.log(arr.split(5)); //	[ [ 1, 2, 3, 4, 5 ], [ 6, 7, 8, 9, 10 ] ]
  console.log(arr.split(2)); //	[ [ 1, 2 ], [ 3, 4, 5, 6, 7, 8, 9, 10 ] ]

  //モンキーパッチ2
  Array.prototype.split = function () {
    const i = Math.floor(this.lenght / 2);
    return [this.slice(0, i), this.slice(i)];

  }

  //意図した結果を返さない
  console.log(arr.split(5)); //	[ [], [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] ]
  console.log(arr.split(2)); //	[ [], [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] ]

}

モンキーパッチとポリフォル

モンキーパッチが唯一有効な手段となるパターンが「ポリフォル」と呼ばれる、欠けている標準APIを補う手法だそうです。

しかし、GASでは、実行側のバージョンやランタイムを意識することは、ないかなと思い、この項目は飛ばします。

今回、プロトタイプを勉強しまして、@shotarosawada氏に大変お世話になりました。

プロトタイプについてのアウトプットは別のブログにしたいと思いますが、考察したコードを貼って、4章を締めくくりたいと思います。

function myFunction6_42_02() {

  //Arrayオブジェクトを生成する
  const arr = [];

  //全てのオブジェクトのprototypeチェーンの到着点はnull
  console.log(arr.__proto__.__proto__.__proto__); //null

  //生成されたオブジェクトの、継承元=Arrayオブジェクトの、prototype.toStringにアクセスしてみる
  console.log(arr.__proto__.toString); //	[Function: toString]
  console.log(arr.__proto__.toString()); //

  //それを、書き換える。
  arr.__proto__.toString = function () { return 'モンキーパッチです' };

  //継承元のprototypeを書き換える、モンキーパッチ(悪手)
  console.log(arr.toString); //[Function]
  console.log(arr.toString()); //'モンキーパッチです'

  //生成時に、継承元から複製してるか参照かは、引き続き不明ではあるが
  const arr2 = [];
  console.log(arr2.toString()); //'モンキーパッチです'

  //プロトタイプチェーンを辿ってモンキーパッチではなく、Arrayオブジェクトにダイレクトにモンキーパッチしてみると、
  Array.prototype.toString = function () { return '大元をモンキーパッチします' };

  //影響を及ぼすとうことは、複製ではなく、参照であろう
  console.log(arr.toString()); //大元をモンキーパッチします
  console.log(arr2.toString()); //大元をモンキーパッチします
  console.log([].toString()); //大元をモンキーパッチします

}

まとめ

以上で、4章後半を、3本まとめてお届けしました。

次回は、いよいよ5章に突入です。「軽量ディクショナリはObjectの直接インスタンスから構築しよう」「プロトタイプ汚染を予防するために、nullプロトタイプを使う」 を2本まとめてお届けします。

参考資料

Object.create() MDN

このシリーズの目次

タイトルとURLをコピーしました