[EffectiveJavaScript輪読会6]プロトタイプ汚染を防御するためにhasOwnPropertyを使う

GAS

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

前回のおさらい

前回は、「順序を持つコレクションには、ディクショナリではなく配列を使おう」「Object.prototypeには、列挙されるプロパティを決して追加しない」を2本お届けしました。

[EffectiveJavaScript輪読会6]軽量ディクショナリはObjectの直接インスタンスから構築しよう
どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。前回のおさらい前回は、4章後半を、3本まとめてお...

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

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

今日のアジェンダ

  • hasOwnProperty()メソッド
  • callでレシーバに指定する
  • クラス化しよう
  • __proto__をkeyとして使う日

hasOwnProperty()メソッド

in演算子は、オブジェクトにプロパティが含まれているかbooleanを返します。

なので、このように、プロパティを書き換えることができます。

function myFunction6_45_01() {

  const dict = { alice: 34 };

  if ('alice' in dict) {
    dict.alice = 24;
  }

  console.log(dict); //{ alice: 24 }

}

そして、前回お伝えしたように、目には見えませんが、toStringやvalueOfというプロパティ(使われるときにはメソッド)は、プロトタイプチェーンに存在しています。

hasOwnProperty()メソッドは、自身のオブジェクトに定義されているプロパティのみルックアップします。

function myFunction6_45_02() {

  const dict = {};

  console.log('alice' in dict); //false
  console.log('toString' in dict); //true
  console.log('valueOf' in dict); //true

  console.log(dict.hasOwnProperty('alice')); //false
  console.log(dict.hasOwnProperty('toString')); //false

}

hasOwnProperty()メソッドを、条件式につかえば、プロパティを正しく(プロトタイプチェーンを意識せず)ルックアップしながら、処理をすすめられます。

function myFunction6_45_03() {

  const dict = {};

  if (dict.hasOwnProperty('alice')) {
    dict.alice;
  } else {
    undefined;
  }

  console.log('alice' in dict); //false

}

ただし、盲点があって、JavaScriptはとても自由なので、hasOwnPropertyという独自プロパティを定義できてしまいます。(そんなこと誰がするんだよ、というツッコミは心の奥にしまってください)

そうなると、先ほどのコードも台無しになってしまいます。

function myFunction6_45_04() {

  const dict = {};
  dict.hasOwnProperty = 10;
  console.log(dict.hasOwnProperty('alice')); //TypeError: dict.hasOwnProperty is not a function

}

callでレシーバに指定する

そこで考えられるのが、hasOwnProperty()メソッドを、callメソッドで結合される手法です。

項目20でもやりましたが、まず const hasOwn = Object.prototype.hasOwnProperty; として、hasOwnPropertyを由緒ある場所から調達してくることから始めます。

function myFunction6_45_05() {

  const dict = {};
  dict.alice = 24;

  // const hasOwn = Object.prototype.hasOwnProperty;

  //糖衣構文
  const hasOwn = {}.hasOwnProperty;

  hasOwn.call(dict, 'alice');

  console.log(dict.hasOwnProperty('alice')); //true

}

このようにレシーバを設定することで、hasOwnPropertyが上書きされても影響がでないように、策を講じられます。

function myFunction6_45_06() {

  const dict = {};
  dict.alice = 24;

  const hasOwn = {}.hasOwnProperty;
  console.log(hasOwn.call(dict, 'hasOwnProperty')); //false
  console.log(hasOwn.call(dict, 'alice')); //true

  dict.hasOwnProperty = 10;
  console.log(hasOwn.call(dict, 'hasOwnProperty')); //true
  console.log(hasOwn.call(dict, 'alice')); //true

  console.log(dict); //{ alice: 24, hasOwnProperty: 10 }
}

クラス化しよう

さきほどのcallメソッドを、プロジェクト全体に忍ばせるのは困難です。

なので、クラス化してしまいます。

function myFunction6_45_07() {

  function Dict(elements) {
    this.elements = element || {};
  }

  Dict.prototype.has = function (key) {
    return {}.hasOwnProperty.call(this.elements, key);
  };

  Dict.prototype.get = function (key) {
    return this.has(key) ? this.elements[key] : undefined;
  };

  Dict.prototype.set = function (key, val) {
    this.elements[key] = val;
  };

  Dict.prototype.remove = function (key) {
    delete this.elements[key];
  };

}

使い方はこのような感じです。

function myFunction6_45_08() {

  function Dict(elements) {
    this.elements = elements || {};
  }

  Dict.prototype.has = function (key) {
    return {}.hasOwnProperty.call(this.elements, key);
  };

  Dict.prototype.get = function (key) {
    return this.has(key) ? this.elements[key] : undefined;
  };

  Dict.prototype.set = function (key, val) {
    this.elements[key] = val;
  };

  Dict.prototype.remove = function (key) {
    delete this.elements[key];
  };

  const dict = new Dict({
    alice: 34,
    bob: 24,
    chris: 62
  });

  console.log(dict.has('alice')); //true
  console.log(dict.get('bob')); //24
  console.log(dict.has('valueOf')); //false

}

__proto__をkeyとして使う日

それでもやっぱり、__proto__をプロパティのキーとして使っちゃう人がいるかもしれません。

しかも、__proto__をin演算子が取り出すかどうかは、実行環境によって異なります。 (GAS、というよりChrome系のブラウザではこのような結果になりました)

function myFunction6_45_09() {

  const empty = Object.create(null);
  console.log('__proto__' in empty); //false

  const hasOwn = {}.hasOwnProperty;
  console.log(hasOwn.call(empty, '__proto__')); //false

}

なので、クラス化して、__proto__をキーとして使われたときの対策を、記述しておくしかなさそうです。

function myFunction6_45_10() {

  function Dict(elements) {
    this.elements = elements || {};
    this.hasSpecialProto = false;
    this.specialProto = undefined;
  }


  Dict.prototype.has = function (key) {
    if (key === '__proto__') {
      return this.hasSpecialProto;
    }

    return {}.hasOwnProperty.call(this.elements, key);
  };


  Dict.prototype.get = function (key) {
    if (key === '__proto__') {
      return this.specialProto;
    }

    return this.has(key) ? this.elements[key] : undefined;
  };


  Dict.prototype.set = function (key, val) {
    if (key === '__proto__') {
      this.hasSpecialProto = true;
      this.specialProto = undefined;
    } else {
      this.elements[key] = val;
    }
  };


  Dict.prototype.remove = function (key) {
    if (key === '__proto__') {
      this.hasSpecialProto = true;
      this.specialProto = undefined;
    } else {
      delete this.elements[key];
    }
  };

  const dict = new Dict();
  console.log(dict.has('__proto__')); //false
  
}

ほんとうに、このような対策をしていた方がいいのでしょうかね。

新版を期待したいです。

まとめ

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

次回は、「順序を持つコレクションには、ディクショナリではなく配列を使おう」「Object.prototypeには、列挙されるプロパティを決して追加しない」を2本」 をお届けします。

参考資料

このシリーズの目次

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