JavaScriptのスコープとバインディングを理解する
http://alternateidea.com/blog/articles/2007/7/18/javascript-scope-and-binding
JavaScriptのスコープとバインディングを理解する
バインディングのキモは、それが実行スコープ −関数xはオブジェクトyのスコープで実行される、とか− をコントロールする手段にすぎないってことだ。最初はなんのことだか分かりにくいけど、いくつかのninja referencesを使えば全部説明できるから誰でも理解できるだろう。
What's my name fool
バインディングの基本的なところを理解するために、次の例を試してみよう。
var Car = function() { this.name = 'car'; } var Truck = function() { this.name = 'truck'; } var func = function() { alert(this.name); } var c = new Car(); var t = new Truck(); func.apply(c); func.apply(t);
これはFirebugで簡単に実行できて、carとtruckそれぞれのアラートが二つあがってくるだろう。なんでこれがうまく動くか理解するには、まずいくつかの基本的なことを理解しておく必要がある。
JavaScriptでは関数はオブジェクトで、applyを使うとあるオブジェクトのメソッドを別のオブジェクトに適用(apply)できる。これが実行スコープを制御するということの基本的な意味だ。
funcはどのオブジェクトにも属して無いじゃないか、と思うかもしれないけどそうじゃない。実はwindowオブジェクトに属してるんだ。(直接目の前のコードに記述されているという意味で)明示的なオブジェクトの中にスコープされていない関数は全て次のようにみなされると思うといい:
// 次のような書き方はしないように。これは単なる説明用の例なので。 function window() { this.func = function() { alert(this.name); } }
この図でわかってもらえるだろうか。
Rubyなら
いままでRubyでこういうやり方をしたことは無いんだけど、大体こんな感じでできるだろう。
class Car attr_accessor :name def initialize @name = 'car' end end class Truck attr_accessor :name def initialize @name = 'truck' end end def func puts @name end c = Car.new t = Truck.new eval "func", c.send(:binding)
Prototypeとそのヘンテコなブロック
開発者がPrototypeを使うときに主な混乱の原因になるのがバインディング、特にブロックとイベントの中にあるそれだ。そしてこの二つがPrototypeとPrototypeを使って書かれたJavaScriptの中でバインディングが使われる主な場所になってる。次の例を見てみよう。
var Ninja = Class.create(); Ninja.prototype = { initialize: function(abilities) { this.abilities = [ 'Kick you in the face', 'Rip out your spleen' ]; this.abilities.each(function(ability) { this.executeAbility(ability) }); }, executeAbility: function(ability) { console.log(ability); } } // このメソッドはNinjaクラスにあるメソッドと同じ名前であることに注意。 function executeAbility(ability) { console.log("I was called from the window object:" + ability); } new Ninja();
Firebugでこの興味深いJavascriptを実行すると、windoオブジェクトからコールされてコンソールに2行追加されるはずだ。なぜこうなるのかと言うと、eachに渡される無名関数の内部にあるthisはwindowオブジェクトで、僕らが定義したクラスじゃ無いからだ。もちろんこれは期待する動作ではないのでbindを使ってスコープを制御することにする。次の例のようにコードにbindを追加して、もう一度実行しよう。
this.abilities.each(function(ability) { this.executeAbility(ability) }.bind(this));
上のコードを実行すれば僕らのクラスのexecuteAbilityが実行されるだろう。これはexecuteAbilityの実行をNinjaクラスにバインドしたからだ。図を使った説明が全然無かったので、次のような色つきの例を用意してみた。
バインディングとイベント
PAJ(Plain Ass JavaScript)では、コールバック内部のthisはイベントが発生したエレメントになっているので、とても簡単に次のように書ける。
var ninja = document.getElementById('ninja'); ninja.addEventListener('click', function () { alert(this.tagName); }, false);
だけど、Prototypeではthisはコールバックの中でもwindowを指すので、メソッドが属するオブジェクトに束縛するためにbindを使う必要がある。
var Ninja = Class.create(); Ninja.prototype = { ... addObservers: function() { $('item').observe('click', this.kickSomeone.bindAsEventListener(this)); }, kickSomeone: function(event) { // Works because `this` is the Ninja instance // Without binding it would be the window this.someOtherMove(); } ... }
bindAsEventListenerはeventオブジェクトが第一引数として渡してくれることが、bindとbindAsEventListenerの唯一の違いだ。
おまけの小技: Early Binding
ときどき追加の引数をコールバック関数に渡す必要があることもあるだろう。また、バインド時の引数の値が必要なこともあるだろう。僕が何をいいたいか分かりにくければ、次の例を試してみよう。
var phrase = "This is SPAARRTTAAAA!"; $('somelink').observe('click', sayIt); function sayIt(event) { console.log(phrase); } phrase = "Red sauce on PAASTAAAA!"
somelinkがクリックされたとき、phraseの値は"This is SPAARRTTAAAA!"だと思うだろう。
だけど違うんだ。イベントリスナに登録された時点でphraseの値が"This is SPAARRTTAAAA!"だったとしても、'''イベントが実行される前に'''値は変更されてしまう。なので実際にコンソールに表示されるのは"Red sauce on PAASTAAAA!"だ。Not quiet the dramatic effect we wanted.これに対応するために、バインディングが作成される時にコールバックにphraseの値を渡すことができる。
var phrase = "This is SPAARRTTAAAA!"; $('somelink').observe('click', sayIt.bindAsEventListener(this, phrase)); function sayIt(event, phrase) { console.log(phrase); } phrase = "Red sauce on PAASTAAAA!";
これで、実行時(execution time)ではなく評価時(runtime)に値をバインドしたので、コードの後半でphraseの値が変更されたにもかかわらず、ちゃんと"This is SPAARRTTAAAA!"を受け取ることができる。
まとめ
これまで見てきたように、バインディングはそれほど難しいわけじゃなく、単に理解できるように説明するのが難しいだけだ。うまく説明できてたらいいんだけど、うまく行ってなかったとしてもそれはninja referencesが十分じゃなかったからだ。より進んだ説明が必要なら、次のサイトもチェックしてみよう。
ランダムな色
var color = "#" + Math.floor(Math.random() * 0xffffff).toString(16);