Object.createを使ってはいけない

題のとおりです。本当にこれだけなんで他に話すことが無いですが説明しないとわからないと思うので説明しようと思います。「知ってるよ!」って人は恐らく今回は新しく得るものはないでしょう。

Object.create()はprototype.jsが元で追加されましたが、アレとは挙動が違います。クラスベースを模倣するために使っているかと思われますが、確かにObject.create()はクラスベースを模倣します。しかし、不完全です。むしろダメダメです。それがObject.create()を使ってはいけない理由です。

なぜダメか、簡単なことです。prototypeプロパティとconstructorプロパティを変更しないからです。
前に書いたと思いますがクラスベースを模倣するには__proto__に代入して同時にprototypeプロパティとconstructorプロパティを──と色々しなければ正しく働きません。そうせずに作られたインスタンスというのは暗黙に意図しない挙動、つまりバグを含んでいます。またそれを直すにはjavascriptを熟知していなければ不可能です。
また、それらを説明し正しいコードを書くのにそれこそサイ本一つ書き上げられます。そこまでしてただクラスベースを模倣するだけの事しかやってくれません。

よってコストが非常に高い割に得るものがなく非常にナンセンスです。なのでクラスベースをどう模倣するかはこのくらいにして本題に戻ります。

Object.create()は暗黙にこれと同じ事をします。なのでナンセンスで潜在的なバグをもれなく伴います。例えばprototypeプロパティに正しく代入してないので以下のコードが失敗します。

// prototype is NOT prototype object!
var prototype = {"f" : function(){}};
var child = Object.create(new Object, prototype);
var other = Object.create(new Object, prototype);
child == other;// -> false
child instanceof Object;// -> true
child instanceof prototype;// -> false

最後がなぜfalseか分かりますか?

第一のヒントは実はこの最後のコードはTypeErrorが投げられます。
第二のヒントはObjectに対して[[HasInstance]]は使えません。
最後のヒントは以下のコードです。

function anonymous(){}
anonymous.prototype = prototype;
child instanceof anonymous;// -> false

そしてObject.createがやっているのは以下のコードです。

// シグネチャが(anonymous, prototype)であるとして
child = new Object;
child.__proto__ = anonymous;
for(let p in prototype){
    child[p] = prototype[p];
}

さてプロトタイプの移譲がObject.create()にはないことが分かるでしょうか。
Object.create()は既存のオブジェクトではない匿名のオブジェクトの新しいインスタンスを作る関数です。クラスベース風に言うと匿名クラスの新しいインスタンスです。
なのでinstanceof演算子はすべてのオブジェクトのルートであるObjectオブジェクトにしか反応しません。

Object.create()と一致する伝統的なコードは以下のコードです。

//実際にはprototypeが持つプロパティのコンテキストがchildに移ったかのように扱う
child = new (function(){});
child.__proto__ = anonymous;
for(let p in prototype){
    child[p] = prototype[p];
}

または

child = new (function(){
    this.f = prototype.f;
    // その他のインスタンスプロパティ
});
child.__proto__ = anonymous;

要するにコード中に出てくるanonymousが伝統的なコードだとコンストラクタ関数、Object.create()ではインスタンスの違いです。

Object.create()では既存のオブジェクトの新しいインスタンスを作る場合、第一引数には直接の既存のインスタンス(GlobalにあるObjectなどは"Object"という名前のプロパティに束縛された関数のインスタンスであることに注意)を渡しますが作られるのはあくまでも匿名オブジェクトのインスタンスです。
そのインスタンスは既存のオブジェクトの直接のインスタンスではなく匿名オブジェクトの直接のインスタンスです。

(仕様的にはObject.create()もnew式も匿名のnew Objectを呼び出したかのように振る舞いますがこれはルートのsuperコンストラクタを呼び出しているに過ぎません。
重要なのはObject.create()はその後__proto__に代入するだけです。new式ではその後固有のオブジェクトに必要な初期化が行なわれます)

ただし第一引数は__proto__に代入されるのでinstanceof演算子はtrueを返します。__proto__に代入されたのでプロトタイプオブジェクトが持つプロパティも持ち合わせます。

しかし、直接のインスタンスではないのでその事によりそれらのプロパティは正しく働きません。繰り返しになりますがあくまでも匿名オブジェクトのインスタンスだからです。
第二引数は新たに作られたインスタンスのインスタンスプロパティになります。

さて、ついて来られたでしょうか。短く言うと[[HasInstance]]は同じだが[[Class]]が違うと置き換えられます。そして、Object.create()の第一引数に関数インスタンス(関数プロトタイプオブジェクトのインスタンス)を渡すとObject.create()の挙動は怪しくなります。var o = Object.create(Foo)などの事です。本当にやりたいのはvar o = Object.create(new Foo)の方です。(なにか言いたいことがあるでしょうがそれは出来ません)

既存のオブジェクトの新しいインスタンスを作るために既存のインスタンスを渡すということは結局のところ何をするかというとObject.create(new Foo)としているのです。

function Foo(){}
var a=new Foo;
var ins = Object.create(a)



function Foo(){}
var ins = Object.create(new Foo)

は同じですね。Object.createでは既にインスタンスが存在していることが前提で、既存のインスタンスはどうやって作るかというとnew 式です。つまり結局のところ伝統的なコードを書かなければいけません。

FooはFunctionのインスタンス、new FooはObjectのインスタンスということが分からなければ、全てがオブジェクトであるという事とプロトタイプオブジェクト、オブジェクト、インスタンスの関係を思い出して下さい。

でももう一つ重要なことも思い出して下さい。prototypeを移譲しないので結局は正しく働きません。わかりやすいのはArrayでしょうか。あれは困りますね。
そこでtriangle operatorですが、しかし、最終的に仕様から除外されこれで恐らく二度と復活することはないでしょう。

このように、Object.create()は使ってはいけませんと説明するだけでこんなに長く難しくなりました。Object.create()が__proto__へ代入するという知識を暗黙に必須にしていたので知らなかった人は話の内容が全くわからなかった思います。それが潜在的なバグに繋がるのです。

さて、ここでObject.create()が何をしているか説明しました。
では、あなたはObject.create()が使われているコードをすべてチェックし、すべての潜在的なバグを検証し(問題を起こさず潜むケースと明るみになるケースがある)それらすべてを修正することは出来るでしょうか。

Object.definePropertiesだけの仕様を検討するのも良いかと思います。

何度も言うように__proto__への代入によるsubtypingは不完全なので諦めましょう。ただそれだけでナンセンスで潜在的なバグから防衛できます。

個人的には魔法派なので__proto__もtriangle operatorもぜひ欲しいところです。

javascriptは代入モデルではなく束縛モデルである

javascriptの変数は代入モデルではなくて束縛モデルです。ただし、破壊的代入もシャドウイングも出来ます。const宣言はそれら2つを無効にします。

まずは束縛モデルを知らない人向けに説明していきます。新たなオブジェクトを利用するにはそれに名前をつけて識別できるようにすることが必要です。これを束縛といいます。「変数にオブジェクトを束縛する」と言います。一度束縛された変数は変更できません。

しかし、中には束縛を変更できる束縛モデルも存在します。それが破壊的代入と隠蔽(shadowing)です。隠蔽とカプセル化を混同している人がいるので注意しましょう。それらの違いについてはここでは説明しません。

破壊的代入と隠蔽が出来る代入モデルでは一度束縛された変数に代入することが出来ます。または、再宣言することによって隠蔽できます。この2つによってすでに束縛されている変数を変更できます。

変数の宣言と束縛は別物で、それらは別々に行えます。例えばver宣言や関数の仮引数です。

Argumentsオブジェクトは実引数の参照で仮引数ではないので注意しましょう。


流れとして──

  • 1)変数は宣言することが出来ます
  • 2)宣言されていて未だ束縛されていない変数は束縛することが出来ます
これ以降、束縛されたオブジェクトはプログラムから自由に利用することが可能になります。 それに加えてjavascriptでは──
  • 3)束縛されている変数は破壊的代入と隠蔽が行えます
  • 4)宣言されていない変数を参照した場合、Globalオブジェクトのプロパティとみなします

これらの追加の仕組みによって代入モデルと同じように変数が扱えます。これはプログラムが状態を持つということです。しかし、プログラムが状態を持つとそのことによりバグが発生します。そのために3),4)を出来なくするのがconst宣言です。

ここから先は束縛モデルを知っている人にも関係ある話です。
const宣言された変数もその変数に未だ束縛されていないなら束縛することが可能です。しかし、一度束縛されてしまうと処理系に変更不可能とマークされ3),4)は出来なくなるのです。今はまだ詳しく説明しませんが宣言されただけで束縛されていないのであれば不定なので実際に評価されるまでそれが何であるかは解りません。

しかし、代入モデルにおいてconst定数に初期化を伴わないというのは不自然かもしれません。ですが、const宣言は定数ではなく変数です。定数とは即値(literal)を言います。なので宣言時に束縛されていなければ、その後束縛することが出来ます。これらの違いは代入モデルと束縛モデルの考え方の違いにあります。

代入モデルでは変数には代入できるので変数の中身を結果的に変更出来ます。(副作用を持つということです)しかし、束縛モデルでは変数には代入出来ない、イコール変数には束縛するものと捉え、未だ束縛していなければ束縛できるのです。(副作用を持たない)

つまり、代入モデル脳で考えるならconst宣言は変数を宣言しているだけなので初期化とは別の処理と捉えることができます。C言語における宣言部と実行部を想像するとわかりやすいと思います。

ではconst宣言は定数を作るものではないとすると何をするのかというと、最初に戻るわけです。
思い出してみましょう。javascriptでは破壊的代入と隠蔽を行えます。よって定数ではなく変数の破壊的代入と隠蔽を禁止しているのです。

そのためjavascriptのconstはimmutableでもありません。immutableにしたい場合はオブジェクトのプロパティ属性を[[DontDelete]]で[[ReadOnly]]にします。

    これはes5のconfigurableとwritableをfalseに設定することに相当し、属性の意味が逆なので真偽値が逆になっていることに注意して下さい。(ただしconfigurableでは属性の変更そのものも禁止にします)

これにはObject.freeze()を使います。[[ReadOnly]]が付いているのが肝です。これがなければ削除できない変数です。実装ではconstは[[DontDelete]]を付けて、さらに破壊的代入と隠蔽を禁止するマークを付けます。ここでも分かるようにconstは定数ではなく変数です。

このようにjavascriptは破壊的代入と隠蔽を備えた束縛モデルで、constはこの2つを禁止します。

しかし、es6のconstは宣言時に初期化を伴わなければいけなくなりました。これは仕様の本質ではなく実装の都合による変更です。(宣言時に初期化されていなければV8で最適化できないんです)
ここら辺の暗黙のアーリーバインディングを許す糞仕様周りはes6が勧告されてからのお話ということでそれはまた別のお話にて……。

undefinedってなんだろう

初っ端から答えを書きますがカウンタブルな未定義です。未定義とは三値理論の未定義のことです。

しかし、三値理論の未定義と少し違うのはundefinedは処理系が識別し評価された結果に対応するユニークな値ということです。なので、カウンタブルなのです。

実際には処理系にとって非カウンタブルな未定義が存在します。それには主に3つあります。

  • プロパティのlookupでそれが存在しなかった
  • プロパティがdeleteされて存在しなくなった
  • 未だ束縛されていない変数(const含む)
この3つは値が必要になった為、これらの操作が発生した時に露わになります。プロパティが必要でルックアップしたが見つからなかった、すでにdeleteされていたので在ったのか無かったのか区別がつかない、宣言はされているけど束縛されていない。

これらの状況は値という結果を必要としているにも関わらず、値が存在しないのでこのままではチューリングマシンが停止しません。これら3つの操作は低レベルにみるとすべてスロットの操作です。スロットを操作した結果、チューリングマシンが停止して結果を得ることが出来ますが、スロットが存在しなければどうすることも出来ません。

そこでスロットがなかったことを表すオラクルを導入することにします。
まず、スロットが存在しなければそれを特別に識別し、特例として直ちにチューリングマシンを停止します。このとき、このチューリングマシンはオラクルが発生したことを示す値を返しますが、それはjavascriptのコードではありません。

なので次に問題になるのはそのオラクルがjavascriptの値として評価できないということです。そこでオラクルに対応するカウンタブルな値を用意します。こうすることにより、値が必要になった時に評価されそのオラクルはjavascript処理系によって値として扱えるようになります。

これがundefinedの正体です。

undefinedが束縛された変数とこのオラクルな未定義は別物でプログラムコードからこの2つを区別することは出来ません。さらにプログラムコードからからはオラクルな未定義を識別することすら出来ません。


よって未定義の変数や存在しないプロパティにundefinedという値が束縛されているわけではありません。そして、時としてundefinedは特別扱いされます。typeofすると"undefined"と返ったり、スコープにundefinedを渡すとGlobalとみなされたり──。


しかし、処理系の挙動としてオラクルな未定義を垣間見ることは可能です。

rhinoのインタプリタやfirefoxのスクラッチパットの実行で──

var a;
a;

とすると何も返ってこないと思います。これはaが処理系にとってオラクルな未定義だからです。web consoleやスクラッチパットの表示で同じようにするとundefinedと返りますがこれはprintするためにaを値として必要としたためです。
オラクルな未定義を処理系にとってカウンタブルな未定義として評価すると対応するundefinedとして扱われたわけです。

あまり違いがないように思われますが、束縛されていない変数で変数を束縛するとオラクルな未定義を表すのでjavascriptの値として評価されない限りundefinedは返ってこないので、初期化し忘れた変数で別の変数を初期化すると思わぬ挙動をします。

Rhinoにtyped-array実装中

今、現在rhinoにtyped-arrayを実装中……なんだけどDataViewの実装が面倒。
VMの型はどうせ4バイトか8バイトしか扱ってないのに言語仕様にunsigned byteがないので浮動小数点数のビットパターンはリバースするときhogehogeしなきゃいけない。
さらにint未満は演算中にintに拡張されるからJITコンパイルされたあとじゃないと効率も悪い。
そもそもJavaVMはjavascriptみたいな動的言語、しかもレイトバインディングする言語を走らせるのには向いてない。
専用VMで走ってる実装より効率が悪いし、JavaVMは人間が静的に最適化したbyteコードよりJITが動的に吐いたマシン語のほうが圧倒的に早いので手書きで最適化するコストもあまり合わない。

VM自身もクラスとメソッドと分岐と無条件分岐くらいしか持ってない。invokedynamicが入ったけどアーリーバインディングじゃないと効果が薄い。そろそろVMそのものが古くなってきてるように思う。

ちなみにtyped-arrayはただのバイト列の入れ物なのでこれだけあっても意味が無い。
javascriptは伝統的にI/Oを持ってないので入れ物に入れられるものを取ってこれないのでこの際、FileAPIも実装しようかと考えているんだけど、本質的に関係ない依存仕様がやたら多い。
しかも、標準化団体がそれぞれ違う。ワーキンググループがみんな好き勝手に分裂し過ぎだと思う。

そういえばtyped-arrayについて

typed-arrayを型付き配列だと思ってそれを使えば早くなると思っている人が結構いますが、あれは配列ではなくポインタ代わりのメモリ内容のバッファなので早くなるわけではありません。
WebGLに使ったりメモリに書き込んだりするためのものでjavaでいうNIOです。VRAMの中見渡したり何かのbyte code渡したりエミュレータのためにCPUのマシン語渡したりCの配列渡したり構造体渡したりするものです。
実装は恐らくjsエンジンのGCに勝手に解放されたら困るのでエンジンの外でメモリ管理されているはずです(高速化のためにエンジンの中に置いているかも知れない)。どちらにせよメモリのマップなのでエンジンから扱うにはそれなりのオーバーヘッドを伴います。

typed-array以前は生のバイトの代わりにbase64で持ったりしたのでbase64からjsの配列への変換の分効率が悪かったのでCanvasなんかはtyped-array返したりします。
しかし、ここら辺はH/Wと実装にもろに依存(たとえばARMなら単精度でNEON使うかもしれないしそうでもないかもしれない)し、むしろtyped-arrayからjsの配列へ変換した方が早かったりします。

メモリを直接渡せればこういうボトルネックをなくせるために早いという事です。
つまり、生のバイトを扱う必用がある領域の話です。OpenGLにシステムメモリ渡したり画像をバイナリデータとしてネイティブに実装されたライブラリと通信したりする必要に迫られた時に使うものです。「ネイティブライブラリがCPUやGPUを叩くコードよりjavascriptコードのほうが遅いよね。ついでにエンディアンあったほうがもっと良いよね」という話です。
typed-arrayを使う理由(and 登場した経緯)にjavascriptエンジン内で完結する部分は関係ありません。限定的な場合を除き高速化のためのアプローチとしては間違っていて他の方法を考えたほうが賢明です。


jsの配列はハッシュだから遅いとかよく言われますが、そもそも歯抜けにならなければ最適化されハッシュにはならないので遅くなりません。
色々あるとは言え実装のソースコード読めば分かることですがサイ本にも間違って書かれているせいかここら辺の仕組みについては間違って広まっているようです。

実はすでに記事は執筆しているのですが、このブログの性格を差し引いてもニッチな内容な為まだ公開していません。
そのような記事がいま三本ほど溜まっているのでそのうち公開します。
undefinedの実装周りとかそれに関連してObjectのメソッドサーチ(lookupとdispatch)の実装云々とか更にそれと関連して歯抜けの配列の実装周りとか、全部関連連しているのでまとめて一度に公開しないと理解するのが難しいかと思われるのでいまはまだ全体として再考中です。

undefinedの話はjavascriptのバインディング、Objectの話は先のハッシュ云々、歯抜けの配列もハッシュ云々──と、相当深い部分に関わっているので説明するのが難しいんです。ちゃんとやるとそのテーマで本が書けるんじゃないでしょうか。

asm.jsとOdionMonkeyについて

今回は完全に雑記。

mozillaはアーリーバインディングしたがってて元々javascript2.0はただのバッファじゃない本物の型付き配列が出来たし変数の型指定も出来た。
これはプログラミング面でも速度面でも利点がある、Harmonyではアーリーバインディングとモジュール化は今後議論しないことが決定しているので言語(標準化)仕様にasm.jsのようなものを取り入れることは不可能です。

しかし、仕様の水面下、実装レベルでやっちゃおうというのが今回のこれ。

ただ、AOTコンパイラらしい。でも、AOTコンパイラといっても結局jsコードはネットからやってくるのでページを読み込んでソースをAOTコンパイラに食わせてから実行することは変わらない筈。
多分、アーリーバインディングとレイトバインディングじゃ実行速度がかなり違ってくるのでそこで差をつけようということなのか、
はたまた、Emscriptenを使うらしいので表現が変わる過程で発生するムダ(メモリの確保も静的にやるからGCもなくなるらしい)を極力減らしてバイトコードなりマシン語になるべく一対一で対応させるということなのかよく調べてないので詳しいことはわからない。

Rhinoコンパイラ見てて思ったけど変換先の表現がなんであれjavascriptのような言語を変換するとRhinoコンパイラみたいな遠まわしな遅いコードになると思う。そんなとき一対一の変換はJITじゃ出来ない部分もあって事前にやっちゃえよ!って流れ、だけど事前にやるには事前に得られる情報が必要だよねってことがasm.jsとOdionMonkeyなんだろう。


肝心なのはなんでmozillaはこんなことをしているかというとTC-39はアーリーバインディングをやりたがらないけど速度は標準化仕様弄り倒してでも上げようとしてる、でも結局レイトバインディングだから速度上げたいのに仕組みからして遅い、更に当たり前だけど事前に型を付けれない分余計遅い。この矛盾に別のアプローチで望んでるとも言えるし、本来の方向へ戻ろうとしていると捉えることも出来る。

どうせ今の最適化手法は複雑化が進んで限界が来るしV8のように最適化に失敗する事もあるはず、es5やHarmonyが失敗作だったてのは(今はそうでなくても)恐らく十年もすれば多くの人が気付いてることだろうし、netscapeの頃から常に未来を突き進み続けたmozillaだから最近の動きはいつものmozillaとして予測の範囲内。ただ、一人だけ突っ走っても周りがついて来れないので今後どうなるかは面白いところだと思う。

netscapeは無くなったけどmozillaが受け継いだゴジラ魂は未だ健在ということでwebの世界にはとってもいい事なんじゃないだろうか──。