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); }
}

この図でわかってもらえるだろうか。

http://alternateidea.com/assets/2007/7/18/execution-scope3.gif

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クラスにバインドしたからだ。図を使った説明が全然無かったので、次のような色つきの例を用意してみた。

http://alternateidea.com/assets/2007/6/29/binding-two.png

バインディングとイベント

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が十分じゃなかったからだ。より進んだ説明が必要なら、次のサイトもチェックしてみよう。