スキップしてメイン コンテンツに移動

Object.prototype.hasOwnProperty のポリフィルを書いた

「JavaScriptのレガシー挙動を定めたAnnex Bをひたすら読む記事」へのリンクを追記。(2024/05/09)

Object.setPrototypeOf(O, proto) full/complete polyfill」へのリンクと「stackoverflow / IE8 getPrototypeOf method」の引用を追記。dojoではスーパークラスを辿れる仕組みを用意しています。 (2024/05/02)

ES5 以下用の俺々クラスでは __proto__ を補ってチェーンを辿れる書き方をした方が良いのか?または、instance.constructor.prototype.constructor は Super Class を参照し、instance.constructor はコンストラクタ(Sub Class) を参照するようにしておくとか。コンストラクタの上書きに失敗した環境があったので(確か Android Opera 12)この Mix かな? (参考)

参考リンクを追加。テストを微修正。(2024/04/29)

__proto__ が使えない環境でもテストが通るようにしましたが、__proto__ がないとキツイですね…。古い Safari に __proto__ が居たら良いのですが…変更点は完成版のハイライト部分です。

prototype から prototype.constructor.prototype を取得すると自身が返るのはそうなのですが、Object.prototype へ遡る方法はないのでしょうか?(2024/04/28, 2024/04/29)

Object.prototype.hasOwnProperty はあまり使うことは無いのですが、ユーザーエージェント判定のために手に入り得るブラウザの全ての、プロパティを調べまくりたいと思い立って、polyfill を書きました。Safari だけ3(または2)からのサポートなのですよね。

案の定、遅そうな感じです。hasOwnProperty 自体がこういった特殊な用途寄りのメソッドで、速い、省メモリなコードを書かなくてはいけないウェブアプリケーションで使っていたら、ナニか設計がマズいのかもしれません。

コーディング過程

組込みの際に不要な機能を端折れるように、完成版に至る過程を記録しておきます。

はじめの一歩

まずは instance のプロパティ値と、その __proto__ のプロパティ値を比較します。不一致の場合、自前のプロパティを持つと判断します。多くのケースでこれで足りるのかもしれませんが、心許ないですね。

function hasOwnProperty(instance, property){
  var __proto__ = getPrototypeOf(instance);

  return instance[property] !== __proto__[property];
};

function getPrototypeOf(instance){
  return instance.__proto__ !== void 0 ? instance.__proto__ : instance.constructor.prototype;
};

NaN に対処する

続いて同値なのに val !== val が成立する NaN に対処するために isNaN 関数を用意します。組込み関数ではなく新たに定義しているのは、Closure Compiler による最適化のためです。

どちらかが NaN でない場合、自前のプロパティを持つと判断できます。

function hasOwnProperty(instance, property){
  function isNaN(val){
    return val !== val;
  };

  var __proto__     = getPrototypeOf(instance),
      instanceValue = instance[property],
      defaultValue  = __proto__[property];

  return isNaN(instanceValue) !== isNaN(defaultValue) || instanceValue !== defaultValue;
};

同値の場合は?

ここからは少しややこしくなってきます。instance.own = 1, instance.__proto__.own = 1 の場合に対処します。

  1. まずは __proto__ がプロパティを持つか?調べます
  2. __proto__ が持つ場合
    1. hasOwnProp 関数内で __proto__ の値を上書きして、instance のプロパティ値の変化を観察します
    2. 続いて、先ほど上書きした __proto__ の値を復帰する処理です。値が変化したりプロパティが居なくなっていたら、値を戻します。
  3. __proto__ が持たない場合、instance は持つか調べます。ちなみにここに入るのは instanceValuedefaultValueundefined の場合しかありません。普段はあまり意識しませんが undefined 値のプロパティを持つ場合と持たない場合があるのはややこしいですね。

hasProp という関数を定義したのは、in operator をサポートするのが IE5.5 以降なので、対応ブラウザによっては差し替える必要があるためです。

function hasOwnProperty(instance, property){
  var __proto__     = getPrototypeOf(obj),
      instanceValue = instance[property],
      defaultValue  = __proto__[property];

  return isNaN(instanceValue) !== isNaN(defaultValue) ||
         instanceValue !== defaultValue ||
         (hasProp(__proto__, property) // 1.
           ? hasOwnProp(__proto__, defaultValue, instance, instanceValue, property) // 2.
           : hasProp(instance, property) // 3.
         );
};

function hasProp(obj, property){
  return property in obj;
};
function hasOwnProp(__proto__, defaultValue, instance, instanceValue, property){
  var result;

  __proto__[property] = !defaultValue;
  result = instanceValue === instance[property]; // 2.-1.
  delete __proto__[property];
  if (__proto__[property] !== defaultValue || !hasProp(__proto__, property)) {
    __proto__[property] = defaultValue; // 2.-2.
  };
  return result;
};

NaN で同値の場合は?

続いて instance.own = NaN, instance.__proto__.own = NaN の場合に対処します。

  1. 2 に進むのは両者が NaN の場合です。この時点では instance.hasOwnProperty('own') === true なのかは不明です。
  2. hasOwnPropIfNaN 関数内で __proto__ の値を上書きして、instance のプロパティ値の変化を観察します。
  3. 続いて、先ほど同様に、上書きした __proto__ の値を復帰する処理です。
function hasOwnProperty(instance, property){
  var __proto__     = getPrototypeOf(obj),
      instanceValue = instance[property],  // NaN
      defaultValue  = __proto__[property]; // NaN

  return isNaN(instanceValue) !== isNaN(defaultValue)
         ? true
         : isNaN(instanceValue) // 1.
           ? hasOwnPropIfNaN(__proto__, instance, property) // 2.
           : instanceValue !== defaultValue
             ? true
             : hasProp(__proto__, property)
               ? hasOwnProp(__proto__, defaultValue, instance, instanceValue, property)
               : hasProp(instance, property);
};

function hasOwnPropIfNaN(__proto__, instance, property){
  var result;

  __proto__[property] = true;
  result = isNaN(instance[property]);
  delete __proto__[property];
  if (!isNaN(__proto__[property])) {
    __proto__[property] = NaN; // 3.
  };
  return result;
};

プロトタイプチェーンの深いところに同値が居る場合に手当てする

さて、__proto__ を書き換えて復帰するコードを2つ追加しましたが、ここまでのコードは不完全です。instance.__proto__.hasOwnProperty('own') === true, instance.__proto__.__proto__.hasOwnProperty('own') === true の場合に復帰を失念します。実用上は問題なさそうな気もしますが副作用がまだあるというわけです。

そこで __proto__ にプロパティを復帰すべきなのか、再帰呼び出しで判断します。

ちなみに当初は setPrototypeOf(__proto__, null) (の polyfill)でプロトタイプチェーンを切って判定する実装でしたが、JavaScript エンジンの最適化とメチャクチャ相性が悪そうなのでこちらに替えました。

function hasOwnProperty(instance, property){
  var __proto__, instanceValue, defaultValue;

  if(instance === Object.prototype){
    return hasProp(instance, property);
  };
  __proto__ = getPrototypeOf(instance);
  // ...
};

function hasOwnPropIfNaN(__proto__, instance, property){
  var result, protoHasOwnProp = hasOwnProperty(__proto__, property);

  __proto__[property] = true;
  result = isNaN(instance[property]);
  if (protoHasOwnProp) {
    __proto__[property] = NaN;
  } else {
    delete __proto__[property];
  };
  return result;
};
function hasOwnProp(__proto__, defaultValue, instance, instanceValue, property){
  var result, protoHasOwnProp = hasOwnProperty(__proto__, property);

  __proto__[property] = !defaultValue;
  result = instanceValue === instance[property];
  if (protoHasOwnProp) {
    __proto__[property] = defaultValue;
  } else {
    delete __proto__[property];
  }
  return result;
};

完成版コードとテスト

完成版の polyfill とテストコードです。Object.prototype.own = undefined; といった意地悪な状況にもパスしています。

__proto__ のない環境では、チェーンを遡れなくなったタイミングで、Object.prototype をチェックして再帰呼び出しを抜ける処理にしました。(2024/04/28)

/**
 * Object.prototype.hasOwnProperty polyfill
 *   Author : itozyun
 *   URL    : https://outcloud.blogspot.com/2024/04/hasOwnProperty-polyfill.html
 */
function hasOwnProperty(instance, property){
  /* if(instance.hasOwnProperty){
      return instance.hasOwnProperty(property);
  }; */
  function hasOwnPropIfNaN(__proto__, instance, property){
    var result, protoHasOwnProp = hasOwnProperty(__proto__, property);

    __proto__[property] = true;
    result = isNaN(instance[property]);
    if (protoHasOwnProp) {
      __proto__[property] = NaN;
    } else {
      delete __proto__[property];
    };
    return result;
  };
  function hasOwnProp(__proto__, defaultValue, instance, instanceValue, property){
    var result, protoHasOwnProp = hasOwnProperty(__proto__, property);

    __proto__[property] = !defaultValue;
    result = instanceValue === instance[property];
    if (protoHasOwnProp) {
      __proto__[property] = defaultValue;
    } else {
      delete __proto__[property];
    }
    return result;
  };
  function isNaN(value){
    return value !== value;
  };
  function getPrototypeOf(instance){
    if(instance.__proto__ !== void 0){
      return instance.__proto__;
    };
    var oPrototype = Object.prototype,
        __proto__;

    return instance === oPrototype
           ? null
           : instance === (__proto__ = instance.constructor.prototype)
             ? oPrototype
             : __proto__;
  };
  function hasProp(obj, property){
    return property in obj;
  };

  var __proto__ = getPrototypeOf(instance), instanceValue, defaultValue;

  if(!__proto__){
    return hasProp(instance, property);
  };
  defaultValue  = __proto__[property];
  instanceValue = instance[property];

  return isNaN(instanceValue) !== isNaN(defaultValue)
         ? true
         : isNaN(instanceValue)
           ? hasOwnPropIfNaN(__proto__, instance, property)
           : instanceValue !== defaultValue
             ? true
             : hasProp(__proto__, property)
               ? hasOwnProp(__proto__, defaultValue, instance, instanceValue, property)
               : hasProp(instance, property);
};

function test(obj, property){
  return hasOwnProperty(obj, property) === obj.hasOwnProperty(property) ? 'OK' : 'FAIL';
};

var a = {};
a.own = 1;
console.log(1, test(a, 'own'));

function b(){};
b.prototype.own = 1;
b.prototype.say = console.log;

c = new b;
console.log(2, test(c, 'own'));

c.own = 1;
console.log(3, test(c, 'own'));

delete b.prototype.own;
console.log(4, test(c, 'own'));

b.prototype.own = undefined;
var c = new b;
console.log(5, test(c, 'own'));

c.own = undefined;
console.log(6, test(c, 'own'));
console.log(7, b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

delete b.prototype.own;
console.log(8, test(c, 'own'));
console.log(9, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

delete c.own;
console.log(10, test(c, 'own'));
console.log(11, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

Object.prototype.own = undefined;
console.log(12, test(c, 'own'));
console.log(13, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

delete Object.prototype.own;
console.log(14, test(c, 'own'));
console.log(13, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

c.own = undefined;
console.log(14, test(c, 'own'));
console.log(15, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

b.prototype.own = NaN;
var c = new b;
console.log(16, test(c, 'own'));

c.own = NaN;
console.log(17, test(c, 'own'));
console.log(18, b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

delete b.prototype.own;
console.log(19, test(c, 'own'));
console.log(20, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

delete c.own;
console.log(21, test(c, 'own'));
console.log(22, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

Object.prototype.own = NaN;
console.log(23, test(c, 'own'));
console.log(24, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

delete Object.prototype.own;
console.log(25, test(c, 'own'));
console.log(26, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

c.own = NaN;
console.log(27, test(c, 'own'));
console.log(28, !b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

b.prototype.own = undefined;
var c = new b;
console.log(29, test(c, 'own'));
console.log(30, b.prototype.hasOwnProperty('own') ? 'OK' : 'FAIL');

c.say('complete');

hasProp の代替

in operator をサポートしない環境で、構文エラーを起こさせず代替するためのコードです。なお、new Function で動的に関数を作るより、(可能なら)ブラウザバージョン毎に JavaScript をコンパイルする方が望ましい。

var hasProp =
    getEngineVersionOf('Trident') < 5.5 || getEngineVersionOf('TridentMobile') < 6
    ? function(obj, property){
        var key;

        if (obj[property] !== void 0) {
          return true;
        };
        for (key in obj) {
          if (property === key) {
            return true;
          };
        };
        return false;
      }
    : new Function('o,p', 'return p in o');
  1. Javascript HasOwnProperty Polyfill
  2. Object.setPrototypeOf(O, proto) full/complete polyfill with boolean flag for partial implementation.
  3. メモ書き的な / Object.prototype.hasOwnProperty() は変数に入れた方が速い?
  4. [レガシーJavaScript] ES3環境のブラウザでObject.create(null)相当のオブジェクトを作る
  5. 枕を欹てて聴く / Function.prototype.bind は何がいいのか
  6. 404 Blog Not Found / javascript - Function.prototype.bind を無理矢理捕縛してみた
  7. DQNEO日記 / JavaScriptで、メソッドをコールバックとして渡す方法(コールバック関数でthisをbindさせる方法)
  8. JavaScriptのレガシー挙動を定めたAnnex Bをひたすら読む記事 / __proto__ プロパティ

dojo でスーパークラスを辿る

Classes created with Dojo.declared store metadata with their superclasses so you don't need to use getPrototypeOf.

I think you can get the first superclass with

MyClass.prototype.constructor._meta.bases[1]

and its prototype with

MyClass.prototype.constructor._meta.bases[1].prototype

(bases[0] seems to be the class itself)


Although why are you even needing to get the prototype? Its very likely you will end up reimplementing some feature that is already provided by dojo.declare