hoisting エッセンシャル - 駆け足で始めるjavascriptの変数その1 -

駆け足シリーズ変数編、まず始めに変数に関連してhoistingだけをピックアップ。

hoistingとは前方参照のための仕組みです。

javascriptの定義はすべてコードの実行前にそのすべてが解決されます。つまりどこに定義があってもまるで前方宣言されたかのように扱われます。それによって関数の前方参照が実現されます。しかし、これは関数だけではなく定義すべてが同じタイミングで解決されるので変数についても同様なことなのです。

しかし、変数においてはextentの期間外では束縛されていないのでたとえ変数が存在していても未定義となります。前方参照、extentより前、つまり変数が存在する以前(過去)より参照してもjavascriptのundefinedになるわけです。

そういうわけでextent外の変数を参照すればundefinedになります。むしろ、extent外なので当然のことですね。これは前方参照したからこそかならずundefinedになります。このundefinedには意味がありません。しかし、必ずundefinedになります。つまり未定義であることが保証されています。そして、実際に値が束縛されるコードが実行されれば正しい値が必ず束縛されます。この狭間に攻撃コードを挿入することはjavascriptでは困難です。となればエラーの主な原因はヒューマンエラーです。

しかし、無条件にすべての変数を関数の先頭で定義してしまってはstrict modeで検出されるはずの二重定義エラーなどをマスクしてしまうのです。strict modeには色々問題が多いので手放しで喜んで使えるものではありませんが、それでも有用である場合があります。その有用性を自ら失わせるコーディングは良くありません。

それにhoistingというのはただの前方参照、hoistingといういびつな別名に惑わされるべきでもありません。

ピンクのユニコーン・パターン

今回はjavascript特有のデザインパターン。それも間違ったgood partsの話。

1. 変数は関数の先頭で定義する。(過去に定義された変数)


今すぐやめましょう。おかしな刷り込みに騙されてはいけません。

変数の宣言を実際に使う位置より遠くで宣言すれば実際に使う時までに間違った代入などを行われる可能性があります。そのため、変数は使う付近で宣言します。これはどんな言語にも共通するパターンです。javascriptも例外ではありません。これが覆される特別な事情など一切ありません。

よくあるのがhoisting問題に対する予防策として先頭で定義しろという高説がありますが、すでに説明したとおりhoisting問題というのは仕様を理解していない故のただの混同です。本質的にはvarの付け忘れであるにもかかわらず、実行コンテキストと変数の関係を理解していないのでそれを発見できないでいるのです。決してhoistingが関与しているわけではないことはすでに明白です。

そして、varを付け忘れた場合、すべての変数を先頭で定義してしまうと本来、未来に現れるはずの変数が過去にすでに存在するので参照できてしまいます。更に、本来使われる以前から存在しているので実際に使われる部分以外で不正に使われている可能性があります。

この不正な変更は先頭で定義時に必ず初期値を束縛したとしても検出出来ません。なぜなら不正に使った後で本来使う時までに初期値に戻せば区別がつかないからです。この様な場合、どんな副作用が起こるか不明です。不正な謎の入力は事前に検知することも事後に調べることも非常に困難でしょう。

それに先頭で定義することにより少なくともundefinedになることが有意だとする声もありますが全く無意味です。本来ならその変数はundefinedであることを期待していないはずです。さらにhoistingされてもundefinedになります。javascriptは人の手によってundefinedが束縛されたのかは区別がつきません。なのでhoistingによるundefinedも先頭で定義された最低保証されたundefinedも我々が欲している結果ではありません。

即ち、先頭で定義されたundefinedも前方参照によるundefinedも本質ではないということです。さらに、変数を使う直近で定義しておけば不正に使うことは出来ません。外から関数のCallにアクセスする手段がないので定義前に不正に定義されることもありません。

なのでヒューマンエラーによる使う前に同じ名前の変数が定義されてしまったと言うことだけが問題になります。これは二重定義なのでstrict modeで防ぐことが出来ます。しかし、変数をすべて先頭で定義してしまってはそれは叶いません。strict modeの効果をマスクしてしまっているのです。

これらのことにより変数を関数の先頭で定義する事はよくありません。むしろ言語の本来の機能を失わせているに過ぎません。さらにhoisting問題のようなおかしな誤解を植え付ける根幹になり得ます。

にも関わらずこの様なパターンが多用されているのはそれだけjavascriptが理解されていないという証明になります。

ついでに言うと、varの付け忘れはstrict modeで対処しますが前方参照によるundefinedはletで対処します。しかし、letがつかえるのはecmascriptでは6からです。javascriptでは何年も前に使えるものが未だ使えない時代遅れの仕様です。さらに処理系では実装が遅れています。

この事実を正しく認識することにも意味があります。それにより議論が活発化するはずだからです。また、この時代遅れの原因はecma-262の政治的な理由です。letはjavascript2.0の機能なので本来なら97年頃にはすでに使えるようになっていたはずです。正しく認識することによりこの様な惰性を許さない世の中も必要なものの一つです。

2.javascriptは数値をオブジェクトとして扱えない。(すべてはオブジェクトである)


これが間違いだということはすぐに気づきます。しかし、問題はここからです。

(new Number(10)).toString()//()有無問わず

この様なことはしてはいけません。javascriptの型変換の適応範囲は広いので(10).toString()で十分です。なぜ()を付けなければならないのかはパーサーの制限ですね。.が小数点ではないということを伝えたいだけです。ならばそれ以上の余計なことをする必要はありません、可読性を落とすだけです。

そして以下のようなわかりづらい書き方もやめましょう。
10 .toString()
/*
こっちは最後のドットの次に改行できると
言う利点があるがおそらく理解されない。
*/
10..toString()
これらはシンタックス的に正しいですがわかりづらいです。

3. (function(){}())。(必要なものはどれ?)


はっきり言ってシンタックスを理解しているのか疑わしい書き方です。JSLintなどが強制しますがただの洗脳です。思考停止せずに疑問を持ちましょう。

では問題です。

var x = (function(x){ return x;}(1));

このコードにはオブジェクトは幾つありますか?
答えは4つありますが今回重要なのは1つだけです。さて、その重要な一つのオブジェクトとはなにか解るでしょうか?

それは、関数オブジェクトです。ついでに言うとこのコードを疑うあなたの心です。

では関数オブジェクトはどこからどこまでですか?

もちろんfunction(x){ return x;}の範囲です。ここを構文解析するとこれが関数オブジェクトだとパーサは伝えます。しかし、構文解析の制限で連続して()が来ると関数宣言だと誤認します。ecmascriptだと関数宣言は名前を持たなければいけないので名前がないエラーになります。javascriptだと名前はなくてもいいが関数定義(javascriptに関数宣言と関数式の区別はない)の直後に()が来てはいけないのでシンタックスエラーです。

よってパーサにどこからどこまでが関数であるかを知らせる必要があります。これは先程の2.と同じ構文解析の制限由来のものです。即ち、先ほどと同じように以下のようにすれば良いことがわかります。

var x = (function(x){ return x;})(1);

このコードでは括弧が付いているのでパーサは括弧が閉じるまでが一塊だと正しく認識できます。つまり、(function(x){ return x;})までが関数だと理解します。これは括弧を閉じるまで関数のシンタックスの解析が完了できないことに由来します。

この事は(function(x){ return x;})が最小の括弧の範囲であることを示しています。そして、その中にあるのは期待した通りの関数オブジェクトです。これが必要最小限です。}の後ろまで最初の)を持ってくる理由はありません。コードは簡潔に必要最小限であることを保つことが重要です。必要最小限というのは1.の変数は使う直前で定義するという事とも一貫しています。さらに2.の構文解析の問題の対処法とも一貫性があります。
これらの一貫性はソースコードの完全性を保つために必要不可欠です。場所ごとにコーディングスタイルが異なっていては読みづらく完全性も失われます。

それと気づいていない人のために念を押しますが(function(x){ return x;}(1))と(function(x){ return x;})(1)では意味が違います。前者の括弧は関数の戻り値を括っていますが後者は関数オブジェクトを括っています。我々が必要としているのは後者です。そして、これが必要最小限です。前者のスタイルでは括弧の中が我々が必要とするオブジェクトであるとパーサに知らせるという完全性が保てないのです。先に言ったようにその他コードとの統一も出来ない。このふたつの観点から完全性が失われているということが重要です。

もっと簡単に言いましょう。カッコの中身が関数なのとカッコの中身が関数から返った値ではパーサにどれが関数かを知らせるのに必要なものはどっちですか?

当然答えは関数です。ソースには何もトークン列だけが含まれているわけではありません。コーディングした人物の思考が含まれていて、これが最も重要でありソースの本質です。この本質を見誤るようなコーディングルールやスタイルを使ってはいけません。

たとえば1.のことは関数型プログラミングにも通用します。正しく関数型プログラミングを活用できていれば思考の流れはそのまま関数になります。正しく関数型プログラミングを活用できていれば変数を過去に定義するというような、思考と乖離したコードは自然と書かなくなるはずです。そして、正しく書かれたjavascriptは関数型プログラミングを多用するので必然的にこれらは守られるはずです。

4.stringとnumberの変換にparseIntを使うなかれ。


論外です。javascriptのstringとnumberは相互に変換可能です。これを理解していればわざわざ字句解析させるなんて無駄なことはしません。さらに言うとparseIntは不正なフォーマットを渡しても字句解析します。Number関数によって明示的型変換すればNaNになるのでエラーチェックが可能です。ついでに言うとparseIntを使うときは必ず基数を明示しましょう。parseIntはN進数変換に使うものです。


まとめに入りましょう。間違ったデザインパターンの刷り込みに疑問を持ち、間違ったpartsをgood partsだと思い込まないようにjavascriptへの理解を深めましょう。間違ったgood partsはただの幻想です。

貴方が欲しいのはピンクのユニコーンですか? ユニコーンに洗脳されてはいませんか?

その間違ったhoisting問題をぶち壊す!

以下のコードの問題を挙げて下さい。
var name = "global";

(function (){
    console.log(name);
    var name = 20;
    console.log(name);
})();

console.log(name);
hoisting問題だと答えた人……失格です。シベリア送り25ルーブルを命じます。
thisがないと答えた人、甘い! このままでは実行コンテキストが曖昧です。thisとは何ですか。正しいコードに直して下さい。
var name = "global";

(function (){
    console.log(this.name)
    var name = 20
    console.log(name);
}).call(this)

console.log(name)
つまり、この場合の実行コンテキストはトップレベルスコープですね。callにthisを渡しており、関数にも囲まれていない(ネストしていない)ことから推測できます。

hoisting問題だと答えた人もthisがないと答えた人も前者のコードを書いた人物がトップレベルのnameを関数式の中の一つ目のconsole.logで参照しようとしているという意思を推察することは出来たかと思います。

しかし、これがhoisting問題だと答えた人は実行コンテキストと言う存在を理解していません。

さらにこのコードはhoistingとは関係ありません。なぜならjavascriptは動的extentなのでコンテキストを抜けると変数の寿命が尽きるのでhoistingされたとしても一つ目のnameは20を参照することはありません。

このコードで重要なのはコードがどこで実行され変数はどこに作られるかということです。javascriptにおける実行コンテキストと変数の関係について知る必要があります。

vajascriptでは実行コンテキストに入るとまず、スコープチェーンが形成され、定義(宣言)の解決が行なわれます。関数はオブジェクトなのでインスタンスが作られることによって想像に容易でしょうが、では定義された変数はどのように解決されるでしょうか?

それには変数の実体化という処理が行われます。まずそのコンテキストに存在する変数定義(宣言)が実体化され変数オブジェクトというオブジェクトのプロパティとして作られます。

しかし、この時点ではまだ変数の束縛は行なわれていません。それが例え宣言と束縛が同時に行なわれているコードであってもです。

そして、すべての定義の解決が行なわれたあとになって、今度はコードが実行されます。つまり、文や式が実行されるので変数への束縛はこのタイミングで行われます。



このとき既に変数の定義は実体化されているので後方で定義されたはずの変数であっても既に存在しています。これがいわゆるhoistingです。この仕組みは前方参照を行うためにあります。つまり関数を前方参照するためのものです。変数の定義のはたまたま関数定義と同じタイミングで解決されているのです。

では最初のコードのconsole.log(name)は何をしているのでしょうか?

関数における変数オブジェクトとはCallオブジェクトです。そして、関数のコンテキストに入った時点でvar name = 20は解決されnameは既に存在しています。しかし、束縛は実行時に行なわれるのでまだnameはundefinedです。

なのでconsole.log(name)のnameはundefinedを参照していることになります。この次にvar name = 20の式が実行されることになるのでこれが実行されて初めてnameは20を参照します。そこで2回目のconsole.log(name)では20を出力できるのです。hoistingされているのでこれは正しい挙動です。

しかし、関数内のnameのvarを付け忘れるとどうなるでしょう?
以下のコードです。
var name = "global";

(function (){
    console.log(name);
    name = 20;
    console.log(name);
})();

console.log(name);
まず、関数内でnameは定義されていないので一つ目のconsole.log(name)はスコープチェーンを辿ってトップレベルスコープに定義されたnameを参照します([[Get]]が呼ばれます)。次のname = 20では同じくスコープチェーンを辿ってトップレベルスコープに定義されたnameが発見され、そして、破壊的代入されます([[Set]]/[[Put]]が呼ばれます)。

これは関数内の変数としてnameが定義されていないためCallオブジェクトにnameが存在していないからです。Callオブジェクトにnameが存在していれば前方参照(hoisting)が出来るためトップレベルのnameが汚染されることはありません。

つまりhoistingが問題になっているわけではありません。むしろ状況の悪化に歯止めをかけています。

本当に問題なのはvarを付け忘れていること、そして、nameのコンテキストが曖昧なことがよりこの問題の真の悪を隠している事です。

nameのコンテキストが曖昧とはどういうことでしょうか?

では一回目のnameが属しているのはどこでしょうか?

それは、トップレベルスコープです。なので修飾するとthis.nameとなるのです。修飾されていないから曖昧です。実はstrict modeでは修飾されていない変数はdeleteで参照した時には警告が出ます。javascriptは動的スコープなので修飾されていなければどこの変数を削除しようとしているのか曖昧だからです。

なら修飾されていない変数全部警告したら便利かと思うかもしれませんが、そうはいきません。
function f(){
 var a=10;
 // (1)
}
と言うコードのaを修飾するとどうなるでしょう?

正解はarguments.caller.aです。しかし、arguments.callerはjavascript1.4以降は明示的に未定義です。よって結果的にundefinedが返ります。さらにarguments.callerによってCallオブジェクトを返す仕様はjavascriptの標準化前に正式に廃止されています。

さらに最近のecmascript準拠の実装ではargumentsにcallerプロパティ自体を持っていません。結果、undefinedが返ります。(例えばseamonkeyはcallerプロパティを持っていないのでundefined、rhinoはCallオブジェクトを返す仕様が正式に廃止されているのでjs1.3として実行してもnull、1.4以降なら明示的に未定義なので結果的にundefined)

この事によりすべての変数を修飾するということは不可能です。実はすべての変数を修飾不可能ということはhoistingが害がないことを表します。修飾されていないということは現在のコンテキストに変数が定義されていなければスコープチェーンを辿って検査されます。[[Get]]だろうと[[Set]]だろうと同じです。

もし定義されているのならばhoistingによってどのタイミングでも変数を発見できるのでスコープチェーンを辿ることはなく、そもそも変数を汚染したり悪影響を与えることは出来ないからです。それにスコープチェーンをたどるという挙動は外側の変数は内側の変数から見えるということを実現するのに不可欠です。なので修飾するしないに係わらずコードを書いた人物が間違っていなければ書いたとおりに正しく動作できます。

もしこれがhoistingがなければ前方参照した変数はスコープチェーンを辿ってしまい正しい変数を取得することができなくなってしまいます。

ただし、変数を前方参照する意味はありません。なぜならたとえ変数が存在していようがまだ束縛されていないのでundefinedを返すからです。この戻り値はほとんどの場合において役に立ちません。

例えばこのコード。
console.log(this.hasOwnProperty("name"))// --> true
var name = "global";
console.log(this.hasOwnProperty("name"))// --> true
このコードは現在のコンテキストにnameが定義されているか見ていますがこれだけでは何の意味も成しません。

しかし、こうすれば意味を成します。
var debug;

if(this.hasOwnProperty("debug")){

}
束縛された内容に係わらず定義済みというごく限られた場合です。

ではhoisting問題とは何なのか?

hoisting問題とはvarの付け忘れによる意図しない変数束縛と修飾されていない変数の曖昧性によるヒューマンエラーをhoistingによる意図しない挙動と混同しているに過ぎません。

なぜそれを混同するのかというと、実行コンテキストの振る舞いを理解していないからです。例えばvarを付けないとトップレベルコンテキストに変数を作るといった誤解など。

すなわち、ただの理解不足による勘違い、思い込みに過ぎません。

varを付けようが付けまいが変数が変数オブジェクトのプロパティであることは変わりません。varを付け忘れてhoistingされず、前方参照されなかった結果スコープチェンを辿っているのです。

varの有る無しはそのプロパティがDontDelete属性になるかどうかの違いしかありません。varを付けるとDontDelete属性が付きます。varを付けないとDontDelete属性は付きません。

というわけで、以下のコードを見てみましょう。変数は曖昧性をなくすため修飾しています。
this.a=10;
delete this.a;// -> true
var a=10;
delete this.a;// -> false
結果はコメントのとおりです。ちなみにnashornではstrict mode時にdelete演算子で変数を修飾しなかった場合の警告文が間違っています。

SyntaxError: cannot delete "a" in strict modeと言われますが嘘です。varを付けなければdelete出来ます。正確には変数が修飾されていないという趣旨の文を出すべきです。そもそもSyntaxErrorですらありません。セマンティックスの問題です。

Object.createを使ってはいけないの続き

Object.createを使ってはいけないのビューが増えているので逆にObject.createを使うケースを紹介してみる。

Object.createは[Prototype]を変更するので普段はこれが余計なわけだ。
だから、Object.definePropertiesを使わなければならない。

例えばこういう事だ──。
var a = Object.create([], {
    add:{
        value:Array.prototype.push,
        configurable: true, enumerable: true, writable: true
    }
});

a.add(1,2,3)
console.log(a.length)
console.log(a)

var b = Object.defineProperties([], {
     add:{
        value:Array.prototype.push,
        configurable: true, enumerable: true, writable: true
    }
});

b.add(1,2,3)
console.log(b.length)
console.log(b)
aの間違いに気付けるだろうか?
aはArrayではなくArray-likeだ。aの様に定義するのではなくbの様に定義するのが正しい。

この様にObject.createの使い方はややこしい。
そして、普段aの様に定義したくなる事はない。bの様に定義したつもりで間違って使っているのがほとんどだろう。
しかし、意図的にaの様に定義したくなる時が稀によくある。

それが、以下のケースだ──。

function O(){
}
O.prototype={
    a:1
};

function O2(){
 O.apply(this, arguments);
}
// これではダメ(1)
//O2.prototype = O.prototype;

// これでは実引数を渡さなかった時、エラーを投げるコンストラクタに対応できない(2)
//O2.prototype = new O;

// なので__proto__を使う
//var temp = {};
//temp.__proto__ = O.prototype;
//O2.prototype = temp;

// しかし、__proto__は非標準なので今までは、
// 確実に(2)のケースに対応する手段がなかった。
// es5からはObject.createがあるのでそれを使う。
O2.prototype = Object.create(O.prototype);
O2.prototype.b=2;

// こうでもいい
//O2.prototype = Object.create(O.prototype, {
// b:{
//  value: 2,
//  configurable: true, enumerable: true, writable: true
// }
//});

console.log("O.a "+(new O).a)
console.log("O.b "+(new O).b)
console.log("O2.a "+(new O2).a)
console.log("O2.b "+(new O2).b)

console.log(new O2 instanceof O2)
console.log(new O2 instanceof O)

console.log(Object.getPrototypeOf(O2.prototype) == O.prototype)
var o2=new O2
console.log(Object.getPrototypeOf(o2))

このケースではprototypeチェーンを用いてO2の振る舞いをO.prototypeに委譲したい。
しかし、コメントに書いてある通り(1)では不適切となる。
なぜなら、O2.prototype = O.prototypeとするとO2.prototype.bはつまるところO.prototypeのbとして定義されてしまうからだ。

これを正しく機能させるには(2)のようにする。
ちなみにこれはjavascriptの移譲における常識だが間違っているコードが多い。しかし、サイ本を始めとして正しく書かれている教本もまた多い。

だがしかし、(2)の方法ではコンストラクタへの実引数が存在しない時にエラーを投げるなど、特別な処理をしているコンストラクタで正しく扱えない。



そこで、__proto__を使うのだが__proto__は標準ではない。
最終的に出てくるのがObject.createだ。これで(2)のケースにも対応できて標準でもある。万事解決となる。

つまり、Object.createのほとんど唯一の使い道は既存のオブジェクトがプロトタイプ・オブジェクトとなる必要がある場合だ。
Object.createでは[[Prototype]]に第一引数をsetするのでそれが果たせる。そして、Object.createが返す新たなインスタンスをprototypeチェーンに追加してやればいい。
ここでも、プロトタイプ・オブジェクトとprototypeチェーンの違いを完璧に理解しておく必要がある。でなければ、最初に示したaの様な間違いを犯すことになる。

これがObject.createのほとんど唯一使い道ではあるが、正しく移譲されたコードを書いていると多用することに気づくだろう。
ES WikiのProxyのサンプルのようにProxyを使う場合はとても良く使う。

jvascriptで書いたゲームエンジンが吹っ飛んだのでCnvas2DベースからCanvas3Dベースへ変更して作り直し。

と言う訳で今回は初めてjavascriptのコアから離れた話。

とりあえずMMD動かして見た。




Roberts




Roberts+テカテカ




ヘアライン処理





ヘアライン(パラメタ違い)





Robertsいじってキツめに輪郭出して更にベタ塗り風(ライトの反射がなかったり)





水彩画風





水彩画風カスタム


これらは全部MMD.jsのシェーダーに合わせてある、というかMMD.jsを元に作ってある。水彩画風はパラメタ違いもあるけど水彩画風カスタムは色々いじってある。

で、なんでシェーダーの話したかというと、オリジナルのMMD.jsはjavascriptのタイマ使ってるので標準のシェーダーでも30fpsくらいしか出ないけどrequestAnimationFrameはもっと出るって話をしたかったわけで……。

 ただこっちは 60fpsでぶん回そうとするからスキップとかは自分で実装しなきゃいけないから面倒だよねって話。

ただそれだけなのでシェーダーの実装はかなり適当。ヘアラインとかいい加減だし(だけどトゥーンシェーダーだとなかなか良い感じに見える)