読者です 読者をやめる 読者になる 読者になる

LIFULL Creators Blog

「株式会社LIFULL(ライフル)」の社員によるブログです。

__proto__が変わりゆく件について

@nazomikan こんにちは、@nazomikanです

この記事は(JavaScript - Client Side - Advent Calendar 2013)の15日目の記事です。

@teramako 妙な古臭さを感じるのだが…。 http://d.hatena.ne.jp/teramako/20131216/p1

と的確な斧をいただいたので記事を修正しています

これまで標準化されていなかった__proto__がES6で標準化されようとしています。

参考: draft_proto_spec_rev2.pdf

(大変古かったので差し替えました。)

参考: Object.prototype.__proto__

※ まだドラフトで確定ではないのでご注意ください。

Object.prototype.__proto__

Enumerable: false

Configurable: true

内部変数のUnderscoreProtoEnabledの真偽によってこのプロパティの操作を追跡するかしないかを決定します。

UnderscoreProtoEnabledはObject.prototpyeをもっていればデフォルトでtrueになります。

重要なのは__proto__Object.prototype上にいると明言されるようになったこと

と、__proto__UnderscoreProtoEnabledの挙動が明言されたこと。

(Rev20 Oct 28 2013の時点でUnderscoreProtoEnabledの内部プロパティに関する記述が消えてました)

元となるのはこの話

__proto__標準化の背景

我らが唯一神のDouglasのGood Partsにこんなコードがあった。

that.on = function (type, ....) {
   ....
   var registry = {};
   ....
   if (registry.hasOwnProperty(type)) {
       registry[type].push(handler);
   } else {
       registry[type] = [handler];
   }
   ....
};

なんの変哲もないコードのように見えるがこのコードは本当の意味でセーフティではないのだ。

いちゃもんをつけるような話になりかねないのだが、例えばなんらかのモンキーパッチ(Object.prototypeへの変更を加えたりとか)によってregistryhasOwnPropertyを持っていない可能性もある。

これはhasOwnPropertyuncurryThisで初期時に束縛していれば解決できる。

var uncurryThis = Function.prototype.bind.bind(Function.prototype.call)
  , hasOwn = uncurryThis(Object.prototype.hasOwnProperty)
  ;

//...

that.on = function (type, ....) {
   ....
   var registry = {};
   ....
   if (hasOwn(registry, type)) {
       registry[type].push(handler);
   } else {
       registry[type] = [handler];
   }
   ....
};
...

はじめて見る人は面食らうかもしれないがちょっと前に(ここ)http://wiki.ecmascript.org/doku.php?id=conventions:safe_meta_programming で流行った安全なコードだ。

付録的に末尾に書いといたので興味があれば見てほしい。

とりあえず、uncurryThisを用いたことによって、たとえregistryがモンキーパッチやObject.create(null)でhasOwnPropertyをもっていなかったとしても安全に動作するように変更できた。

これでこのコードは安全になったように見える。

しかし問題はまだあった。

ここからが本題で、typeという変数が関数の外から渡されてきていることに注目したい。

このtypehasOwnPropertyによってのみバリデーションをされている。

そう、ここが論点なのだ。

話の流れ的に分かるかもしれないがtype__proto__という文字列だったときに問題が起きる。

これまで__proto__は特に標準化もしていなかったためにそれぞれのブラウザ間で以下のような場合に評価結果が異なってくるのだ。

[{}.hasOwnProperty('__proto__'), {}.__proto__.hasOwnProperty('__proto__')]
false,false       // Chrome
true,true         // WebKit Nightly
false,true        // FF Nightly
false,false       // Opera 12 alpha
throws TypeError  // IE10 Preview 2

(2011/12/26の時の話です)

IEが__proto__をサポートしてないがためにexceptionを発生させるが、そんなことは問題ではない。

ここで驚くのはWebKit Nightlyの挙動でしょう。

{}.hasOwnProperty('__proto__')の挙動がこのブラウザでだけtrueを指し示している。

最初のコードをもう一度見直してみよう

that.on = function (type, ....) {
   ....
   var registry = {};
   ....
   if (registry.hasOwnProperty(type)) { //....(1)
       registry[type].push(handler); // ....(2)
   } else {
       registry[type] = [handler];
   }
   ....
};

先ほどの実行結果を元に考えるとtypeの値が__proto__という文字列だった場合、(1)は

if (registry.hasOwnProperty('__proto__')) {

となり、Webkit Nightlyでのみtrueを返すことになる。

これによって(1)を通過し、(2)を実行する時に

registry['__proto__'].push(handler);

となり、__proto__はobjectなのでObject.prototype.pushなんてもってないよとばかりにエラーを発生させるのだ。

とまぁ、こんなブラウザ間での挙動が引き起こした穴を埋めるため標準化が始まりました。

今後(といっても確定ではないが)、__proto__Object.prototype上にいると定義されるわけなので

[{}.hasOwnProperty('__proto__'), {}.__proto__.hasOwnProperty('__proto__')]

の実行結果は[false, true]に統一されていくのでしょう。

setPrototypeOf

ここまで__proto__標準化の流れについて色々書きましたがES6からはObject.setPrototypeOfが定義されつつあるので__proto__系の操作はそちらを通して行われるほうが今後としてはよいようです。

@teramakoさんまさかりありがとうございました(^-^)/

おまけ(uncurryThis)

以外と難しいので説明

obj.func1

func1においてthisの指す値はobjになる。 そう、つまりこの形においてthisとは.より左の部分を指す

つまり

var uncurryThis = Function.prototype.bind.bind(Function.prototype.call)

Function.prototype.bindthisFunction.prototype.callに変更している(= .より前を差し替えている)という風に言えるので

Function.prototype.call.bind

と等価だと言える。

さらにそれをふまえた上で

var uncurryThis = Function.prototype.bind.bind(Function.prototype.call),
    hasOwn = uncurryThis(Object.prototype.hasOwnProperty);

if (hasOwn(registry, type))

を見てみると

var uncurryThis = Function.prototype.bind.bind(Function.prototype.call),
    // === Function.prototype.call.bind
    hasOwn = uncurryThis(Object.prototype.hasOwnProperty);
    // === Function.prototype.call.bind(Object.prototype.hasOwnProperty)
    // === Object.prototype.hasOwnProperty.call

if (hasOwn(registry, type))
// === if (Object.prototype.hasOwnProperty.call(registry, this))

と等価であることがわかる。

こんな感じでcallのthisを束縛してモンキーパッチに対抗するみたいな方法のことです。


株式会社ネクストでは、一緒に世の中をもっと便利に楽しくしたい仲間を募集中です。 主体的に楽しみながら仕事できる方からのエントリーお待ちしています! http://recruit.next-group.jp/