Babelを使うとアロー関数内のthisがundefinedになる

ややこしい問題に直面したので、メモしておく。

問題

次のように、jQueryでコールバック関数にアロー関数を用いたスクリプトを記述する。

$("item > title").each(() => {
  let title = $(this).text();
  console.log(title);
});

このES6の構文ををBabelでES5へトランスパイルすると...

$("item > title").each(function(idx) {
  var title = $(undefined).text();
  console.log(title);
});

アロー関数の内のthisundefinedになってしまう。

問題は、どうやらトランスパイル前後の、each関数でのthisの扱いの違いによるもののようだ。

以下、シンプルな例を考えてみる。

var arr = [1];

$(arr).each(() => {
  console.log(this); /* Window {} */
});

$(arr).each(function() {
  console.log(this); /* Number {} */
});

$(arr).each((idx, value) => {
  console.log(value); /* 1 */
});

アロー関数のthisはwindowオブジェクトを指し、無名関数では要素を指す。

これをBabelに通すと、自動的に"use strict"モードになり、スクリプトを実行した結果は次のように変わる。

/* "use strict" */
var arr = [1];

$(arr).each(() => {
  console.log(this); /* undefined */
})

$(arr).each(function() {
  console.log(this); /* 1 */
})

$(arr).each((idx, value) => {
  console.log(value); /* 1 */
});

"use strict"モードが適用され、非"use strict"モードのアロー関数ではwindowオブジェクトにbindされていたthisが、undefinedになっている。

each関数内でのthisは要素を指すが、アロー関数内のthisはそのコンテキストのthisに等しい。つまり、this === windowオブジェクトが成り立つ。加えて、"use strict"モードにおいては、このようなthisundefinedになるため、今回のような問題が発生する。

まとめ

まとめると、jQueryのコールバック関数で注意すべき点としては:

  • 無名関数内では、thisは要素を指す。
  • アロー関数では、thisはそのコンテキストのthisに等しい。

これに加えて、Babelを用いると、"use strict"モードが適用され、今回のように、windowオブジェクトを指していたアロー関数内のthisundefinedになる。

解決策

この結果を踏まえると、今回の解決策としては、

  1. そもそもアロー関数をやめる
  2. 要素の値である引数を使う
/* アロー関数をやめる */
$("item > title").each(function() {
  let title = $(this).text();
  console.log(title);
});

/* 要素の値である引数を使う */
/* 第一引数がインデックス、第二引数が要素の値 */
$("item > title").each((idx, value) => {
  let title = $(value).text();
  console.log(title);
});

多分あっているとは思うが、理解がふわふわしている。