[EffectiveJavaScript輪読会6]軽量ディクショナリはObjectの直接インスタンスから構築しよう

GAS

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

前回のおさらい

前回は、4章後半を、3本まとめてお届けしました。

[EffectiveJavaScript輪読会6]標準クラスからの継承を避ける
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。前回のシリーズでは、第4章の考察をお届けしました。...

今回は、いよいよ5章に突入です。オブジェクトが終わって、配列の話になるのかな?と、おもいきや、まだまだオブジェクトのお話のようです。。。

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

テキスト第5章「配列とディクショナリ」の項目43、44に対応しています。

今日のアジェンダ

  • 軽量ディクショナリはObjectの直接インスタンスから構築しよう
  • プロトタイプ汚染を予防するために、nullプロトタイプを使う

軽量ディクショナリはObjectの直接インスタンスから構築しよう

JavaScriptのオブジェクトは、幅広い用途で使われています。

function myFunction6_43_00() {

  /* オブジェクトとは */

  /** キーと値がセットになったレコードである。 */
  const persons = {
    person1: { name: 'Tsujike', age: 40, hometown: 'Hokkaido' },
    person2: { name: 'Sawada', age: 32, hometown: 'Kumamoto' },
    person3: { name: 'Kohata', age: 28, hometown: 'Fukushima' }
  }

  /** メソッドを継承するオブジェクト指向のデータ抽象である */
  const Person = function (name) {
    this.name = name;
  }
  Person.prototype.getName = function () {
    return this.name;
  }

  const p1 = new Person('Yamaguchi');
  console.log(p1.getName(), typeof p1);

  /** 配列の要素である */
  const array = [
    { id: 001, name: 'Tsujike', age: 40 },
    { id: 002, name: 'Sawada', age: 32 },
    { id: 001, name: 'Kohata', age: 28 }
  ];

  /** ハッシュ(連想配列)である */
  const dictionary = { "a": 100, "b": 200, "c": 300 }

}

とくに、オブジェクトをデータ構造として、とらえたものが、JavaScript Object Notation(JSON) として、広く扱われているのは、周知の事実です。

前章では、構造化されたオブジェクトと継承を、プロトタイプとともに学習しました。

この章では、オブジェクトをコレクションとして扱う方法を、おなじくプロトタイプとともに、学んでいきます。

継承、プロトタイプチェーンのおさらい

for in文の考察をするまえに、継承とプロトタイプチェーンのおさらいをします。

リテラルで生成されたオブジェクト(以降、obj)は、Objectオブジェクトからprototypeを継承しています。

objは、obj自身にプロパティを定義できますし、プロトタイプチェーンを遡った場所にあるプロパティを書き換えることもできます。(モンキーパッチ=悪手)

個人的に5章は、プロパティをちゃんと理解してディクショナリとしてEffectiveに扱おう! という内容だと理解しました。

プロパティを理解するためのコードを書いてみたので、これをベースに進めたいと思います。

function myFunction6_43_00_02() {

  //オブジェクトの生成
  const obj = { name: 'Tom' };

  //Objectオブジェクトから、Object.prototypeを継承している
  console.log(obj.__proto__ === Object.prototype); //true

  //Object.prototypeには、valueOf()や、hasOwnProperty()など、6つのインスタンスメソッドがある。(非推奨は除く)
  //objはObject.prototypeから、インスタンスメソッド(プロパティ)を継承している
  console.log(obj.__proto__.valueOf); //[Function: toString]
  console.log(obj.__proto__.hasOwnProperty); //[Function: hasOwnProperty]

  //obj自身には、プロパティは存在していません
  // console.log(obj.prototype.valueOf); //TypeError: Cannot read property 'valueOf' of undefined
  // console.log(obj.prototype.hasOwnProperty); //TypeError: Cannot read property 'hasOwnProperty' of undefined

  //自身のプロパティに存在しない場合は、ひとつ上の階層のprototypeをルックアップするのが、プロトタイプチェーン
  console.log(obj.valueOf()); //{ name: 'Tom' }
  console.log(obj.hasOwnProperty('name')); //true

  //プロパティは、自身に定義できる
  obj.age = 40;

  //インスタンスメソッドと同じ名前のプロパティも定義できる
  obj.valueOf = function () { return '改造します' };

  //自身のプロパティに存在するので、プロトタイプチェーンは遡らない。
  console.log(obj.valueOf()); //'改造します'

  //for in文は、まず、自身のプロパティの中で、列挙可能なプロパティを取り出します。
  for (const key in obj) {
    console.log(key); //name age valueOf
  }

  //プロトタイプチェーン(Object.prototypeとイコール)に、独自のプロパティを定義する
  obj.__proto__.tsujike = function () { return '独自プロパティ1' };

  //objはプロトタイプチェーンを遡って、tsujikeを呼び出せる
  console.log(obj.tsujike()); //'独自プロパティ1'

  //for in文は、この独自プロパティも取り出してしまう(これを列挙させないよう定義するのがObject.defineProperty())
  for (const key in obj) {
    console.log(key); //name age valueOf tsujike
  }

  //もちろん、イコールなので、Object.prototypeに定義した、独自プロパティでも同じことが起きる
  Object.prototype.kenzo = function () { return '独自プロパティ2' };

  console.log(obj.kenzo()); //'独自プロパティ2'

  for (const key in obj) {
    console.log(key); //name age valueOf tsujike kenzo
  }

  //hasOwnProperty()は、あくまで、自身のプロパティをチェックしている
  console.log(obj.hasOwnProperty('valueOf')); //true
  console.log(obj.hasOwnProperty('kenzo')); //false

}

for in文

みなさんが普段お目にかかるfor in文は、このような連想配列の列挙だと思います。

function myFunction6_43_01() {

  const dict = { alice: 34, bob: 24, chris: 62 };
  const people = [];

  for (const name in dict) {
    people.push(`${name}:${dict[name]} `);
  }

  console.log(people); //	[ 'alice:34 ', 'bob:24 ', 'chris:62 ' ]

}

for in文は、プロトタイプチェーンを遡って、列挙可能なプロパティも取り出します。

以下のコードをでは、alice, bob, chrisに加えて、count, toStringの2つのプロパティが列挙可能になっていますので、カウントが5になります。

function myFunction6_43_02() {

  function NaiveDict() { }

  NaiveDict.prototype.count = function () {
    let i = 0;
    for (const name in this) {
      i++;
    }
    return i;
  };

  NaiveDict.prototype.toString = function () {
    return "[object NaiveDict]";
  };

  const dict = new NaiveDict();

  dict.alice = 34;
  dict.bob = 24;
  dict.chris = 62;

  console.log(dict.count()); //5

}

Array型であれ、String型であれ、プロパティを追加で定義できる(連想配列の辞書のような使い方ができる)のは、JavaScriptのすごい(自由な)ところです。

しかし、for in文が、プロトタイプチェーンを遡って、プロパティを取り出す影響は計り知れません。

function myFunction6_43_03() {

  const dict = new Array();

  dict.alice = 34;
  dict.bob = 24;
  dict.chris = 62;

  console.log(dict.bob); //24


  const dict2 = new String();

  dict2.alice = 34;
  dict2.bob = 24;
  dict2.chris = 62;

  console.log(dict2.bob); //24

  Array.prototype.first = function () {
    return this[0];
  }

  Array.prototype.last = function () {
    return this[this.length - 1];
  }

  const names = [];

  for (const name in dict) {
    names.push(name);
  }

  console.log(names); //	[ 'alice', 'bob', 'chris', 'first', 'last' ]
}

テキストでは、独自プロパティ(メソッド)を追加したりしなさんな、と言っています。

オブジェクトを生成して、ディクショナリ(連想配列の辞書のような使い方)としてなら、まだマシだということです。

function myFunction6_43_04() {

  const dict = {};

  dict.alice = 34;
  dict.bob = 24;
  dict.chris = 62;


  const names = [];

  for (const name in dict) {
    names.push(name);
  }

  console.log(names); //[ 'alice', 'bob', 'chris' ]
}

これで、すべてが解決したわけではありません。

まだまだプロトタイプは汚染可能です。それをどうやって防ぐのでしょうか。(そもそも組み込みオブジェクトのプロパティを使って、連想配列の辞書のような使い方をしなければいい、みたいなツッコミはなしです)

プロトタイプ汚染を予防するために、nullプロトタイプを使う

では、コンストラクタのprototypeプロパティを、nullかundefinedにすればいい、と考えた天才肌の方もいるでしょう。

しかし、インスタンスでは、そんなの関係ねー、と言わんばかりの結果です。

function myFunction6_44_01() {

  function C() { }
  C.prototype = null;

  const c = new C();
  console.log(Object.getPrototypeOf(c) === null); //false
  console.log(Object.getPrototypeOf(c) === Object.prototype); //true

}

そこで、ES5から実装された、Object.create()メソッドの登場です。

Object.create()メソッドは、組み込みオブジェクトのObjectオブジェクトの 静的メンバー (インスタンスを生成しなくても直接呼び出せるメンバー)です。

既存のオブジェクトを、新しく生成されるオブジェクトのプロトタイプとして使用して、新しいオブジェクトを生成します。

つまり、新しく生成されるオブジェクトのprototypeを強制的にnullにできるのです。(prototypeチェーンの終着駅はnullです)

おなじアプローチで、__proto__も可能ですが、非推奨ですし、Object.create(null)のほうがよいでしょう。

function myFunction6_44_02() {

  const x = Object.create(null);
  console.log(Object.getPrototypeOf(x) === null); //true

  const y = { __proto__: null };
  console.log(y instanceof Object); //false

}

テキストでは、「__proto__をディクショナリのキーに絶対使うな」 と忠告がありましたが、(誰がそんなことするんだよ)というツッコミは心の奥にしまっておきましょう。

まとめ

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

次回は、「プロトタイプ汚染を防御するためにhasOwnPropertyを使う」 をお届けします。

参考資料

このシリーズの目次

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