ArrowFunctionのthis: ES.nextとCoffeeScriptとTypeScript

Firefox 22(Nightly) で ArrowFunction が実装されたが、扱いが難しい - hogehoge @teramako

id:teramakoさんの記事を読んで、ES.nextのArrow Functionって単純な省略記法じゃないんだーっとか思ったけど、よく考えたらCoffeeScriptだってfat arrowならthisをbindするわけで、むしろ自然だし便利ですよねと思い直しただけの記事です。

ES.next

var obj = {
  threshold: 3,
  getOverThreshold: function (items) {
    return items.filter(n => n > this.threshold);
  }
};

filterメソッドの中を普通のfunctionで定義しちゃうとthisがglobalオブジェクトになっちゃうんでselfとかbindとか使わないといけないのが、Arrow Functionならthisがobjになるよという話。

つーことはオブジェクトプロパティのfunctionは省略できないのかー!

var obj = {
  threshold: 3,
  // ここをArrow Funcitonにすると、
  getOverThreshold: (items) => {
    // getOverThresholdメソッド内のthisがglobal objectになっちゃう。
    return items.filter(n => n > this.threshold);
  }
};

と思ったけどES.nextではMethodDefinitionの省略記法が使えるようになる(実装ブラウザはまだない?)からそこは杞憂ということですね。

var obj = {
  threshold: 3,
  getOverThreshold(items) {
    return items.filter(n => n > this.threshold);
  }
};

クラスのメソッド定義も同じような感じでfunctionキーワード不要なので、要するに普通に書いてたらArrow Functionを使うべきか?とかほとんど考えないで良さそう。

CoffeeScript

CoffeeScriptには2種類のArrow Functionがあって使い分ける必要があります。

  • ->: functionの本当のシンタックスシュガー
  • =>: Fat Arrow。thisをbindするfunction定義 。ES.nextに近い動き*1
// Source
obj =
  threshold: 3,
  getOverThreshold: (items) -> // Thin Arrow
    items.filter (n) => n > this.threshold // Fat Arrow

// Compiled JavaScript
var obj;
obj = {
  threshold: 3,
  getOverThreshold: function(items) {
    var _this = this;
    return items.filter(function(n) {
      return n > _this.threshold;
    });
  }
};

両方fatにしちゃうとうまくいかない。

// Source
obj =
  threshold: 3,
  getOverThreshold: (items) =>
    items.filter (n) => n > this.threshold

// Compiled JavaScript
var obj,
  _this = this; // うわーん><
obj = {
  threshold: 3,
  getOverThreshold: function(items) {
    return items.filter(function(n) {
      return n > _this.threshold;
    });
  }
};

なので意識しないといけない。CoffeeScriptでがりがりロジック書いてる人は、この辺のtypoって結構あったりするのかな?

TypeScript

ES.nextを先取り実装してると評判のTypeScript。Arrow FunctionもES.nextに近いタイプで、thisをbindします。というか吐き出すコードは完全にCoffeeScriptと一緒。 (2013/03/25更新: 初出時ソースが微妙に間違ってましたスミマセン)

// Source
var obj = {
    threshold: 3,
    getOverThreshold: function(items: number[]): number[] {
        return items.filter(n => n > this.threshold);
    }
};

// Compiled JavaScript
var obj = {
    threshold: 3,
    getOverThreshold: function (items) {
        var _this = this;
        return items.filter(function (n) {
            return n > _this.threshold;
        });
    }
};

オブジェクトプロパティの定義もArrowにしてみても変化なし。

// Source
var obj = {
    threshold: 3,
    getOverThreshold: (items: number[]): number[] => {
        return items.filter(n => n > this.threshold);
    }
};

// Compiled JavaScript
var obj = {
    threshold: 3,
    getOverThreshold: function (items) {
        var _this = this;
        return items.filter(function (n) {
            return n > _this.threshold;
        });
    }
};

メソッド内でthis使ってみたら、頭にthisのbindが出てきました。が、メソッド内でthisの再bindをしているので挙動は変化なし。

// Source
var obj = {
    threshold: 3,
    getOverThreshold: (items: number[]): number[] => {
        console.log(this.threshold); // thisを参照する。
        return items.filter(n => n > this.threshold);
    }
};

// Compiled JavaScript
var _this = this; // なんか出てきた!
var obj = {
    threshold: 3,
    getOverThreshold: function (items) {
        var _this = this; // でもここで再定義。
        console.log(_this.threshold);
        return items.filter(function (n) {
            return n > _this.threshold;
        });
    }
};

この場合は動くけど、やっぱり怪しいから使わない方が良さげ。 TypeScriptはECMAScriptに追随するっぽいので、いずれメソッド定義の省略記法がそのうち使えるようになるんじゃないでしょうか(ES.nextに採用されれば)。

*1:厳密にはES.nextの方は「thisValueを環境が保持することができない」らしいです。 http://twitter.com/Constellation/status/314740828197224448