トップ «前の日記(2012-11-15) 最新 次の日記(2012-11-20)» 編集

Route 477



2012-11-17

[altjs] CoffeeScript、TypeScriptにおけるthisのスコープ問題への対処

JavaScriptでは「this」はダイナミックなスコープをもつ。このことはたまにバグの原因になる。例を挙げよう。

 var Animal = function(name){
   this.name = name;
   $("#animal").click(function(){
     $(this).text(this.name);
   }
 };

問題になるのは4行目の「$(this).text(this.name)」で、このfunctionが呼ばれる際にはthisはDOM要素を指しているため、「this.name」でAnimalの名前を取ることはできない。

解決策としては、以下のように_thisとかselfのような適当な変数を作って、外側のthisにアクセスできるようにしなければならない。

 var Animal = function(name){
   this.name = name;

   var _this = this;
   $("#animal").click(function(){
     $(this).text(_this.name);
   }
 };

jQueryやunderscore.js等にはjQuery.proxyや_.bindという関数があり、あるfunctionのthisを固定することができる。今回のように両方のthisを使い分けたいケースには利用できないが、 たいていのケースには役に立つだろう。

CoffeeScript

さて、CoffeeScriptに代表されるJSにコンパイルされる言語 (いわゆるaltjs) (Advent Calendarもあるよ) (いま参加者6人です) では、thisのスコープ問題に関して独自の構文をもって対応することが可能である。 CoffeeScriptとTypeScriptについてその挙動を調べてみたので、ここにメモしておく。

CoffeeScriptでは、通常の「->」の代わりに「=>」を使って関数式を記述した場合に、自動的に「_this」を定義するという機能がある。

しかし、以下のように「@」ではなく直接「this」と記述した場合も、_thisに置き換えられてしまうようだった。この仕様だと、今回のようなケースでは手で_thisを定義してやる必要があるため、注意が必要だ。

Animal = (name) ->
  @name = name

  $('#animal').click =>
    $(this).text(@name)

もう一点、functionがネストするケースについても調べてみた。

Animal = (name) ->
  @name = name

  some_func =>
    $('#animal').click =>
      $(this).text(@name)
  • some_funcとclickの両方が「=>」のとき:thisおよび@はanimalを指す。
  • some_funcが「=>」でclickが「->」のとき:Animalのところにvar _this=this;が入るが、thisおよび@は_thisに差し替えられないため、DOM要素を指す。
  • some_funcが「->」でclickが「=>」のとき:some_funcに渡すfunctionのところにvar _this=this;が入る。
  • some_funcとclickの両方が「->」のとき:thisおよび@はDOM要素を指す。

TypeScript

次に、Microsoftの開発したaltjs言語、TypeScriptについても見ていこう。 TypeScriptはJSとの互換性をかなり意識して設計されており、おそらくaltjsではトップクラスに綺麗なJSを出力する (まあ出力されたJSを手作業でメンテしたいかと考えると、そういうシチュエーションは避けたい気がするが、生成物の挙動が理解しやすいというメリットはあるかも)

話がそれたが、TypeScriptにおけるthisについて調査する。 最初は特に何もしてないのかと思ったが、「() => { alert(this) }」のような形の関数式を使った場合のみ、thisの差し替えが起こるようだ。これに関しては 仕様(PDF)の 4.9.2 Arrow Function Expressionsに書かかれている。

Playgroundで試すと、CoffeeScript同様「var _this = this;」を挿入する方式のようだ。

 $("#animal").click(() => { alert(this.name) });

のようにすると、thisが_thisに差し替えられる。 CoffeeScriptと同じく、2種類のthisを使い分けたい場合は手作業が必要。

ネストしたケースについても調べておく。

 some_func(() => {
   $("#animal").click(() => $(this).text(this.name));
 });
  • some_funcとclickの両方が「=>」のとき:thisおよび@はanimalを指す。
  • some_funcが「=>」でclickが「function」のとき:var _this=this;の挿入は行われない。thisおよび@はDOM要素を指す。
    • 挿入が行われないのは、some_funcに渡すコールバックで「this」を使っていないため。つまり差し替えを行うのは「=>」の直下のthisだけらしい。
  • some_funcが「function」でclickが「=>」のとき:thisおよび@はanimalを指す。
  • some_funcとclickの両方が「function」のとき:thisおよび@はDOM要素を指す。

ということで、内側だけ「=>」を使った場合の仕様がCoffeeScriptと異なる。

まとめ

CoffeeScriptの「=>」は「thisを一つ外側のthisにする」という機能だが、 TypeScriptの「=>」は「thisを現在のインスタンスにする」という機能である。

これから自分のaltjsを作ってみようと思っている人は、このあたりの仕様も考えてみると面白いだろう。

(JSXとHaxeは調べてないですが、何か機能があったら教えてください)

ちなみに

次世代のJavaScript仕様(EcmaScript6)になるべく議論が重ねられているES Harmonyにも、「=>」の導入が含まれているようだ。

まだ策定段階なのでなんとも言えないが、こっちの「Maximally minimal class」の中で「=>」を使ったときの仕様がCoffeeScriptのようになるのか、TypeScriptのようになるのかは気になるところである。(なんとなく、前者になりそうな気がするけど)

本日のツッコミ(全2件) [ツッコミを入れる]
mori_dev (2012-11-17 15:21)

> jQueryやunderscore.js等にはbindという関数があり、あるfunctionのthisを固定することができる。<br>jQuery だとそういうときに使うのは、jQuery.proxy() だと思います。

yhara (2012-11-20 11:02)

あ、jQuery.bindはイベントの関数なんですね…。ありがとうございます。