読者です 読者をやめる 読者になる 読者になる

pirosikick's diary

君のハートにunshift

ES6のarrow functionとbabel

JavaScript

最近、JSを書く時はBabelを使ってES6で書いている。 mochaでテストを書く場合も下記で簡単に導入できる。

$ npm i -D mocha babel
$ $(npm bin)/mocha --compilers js:babel/register

そんな感じでTestiumとmochaで下記のようにテストケースを書いて、テストを実行するとエラーが出た。

// test/home.js
'use strict';

import injectBrowser from 'testium/mocha';

describe('Some page', () => {
  before(injectBrowser());

  it('should display "Hello World"', () => {
    this.browser.navigateTo('/');
    this.browser.assert.httpStatus(200);
    this.browser.assert.elementHasText('h1', 'Hello World');
  });
});
$ mocha --compilers js:babel/register

  /home
    1) should display "Hello World"

  0 passing (2s)
  1 failing

  1) /home should displays "Hello World":
     TypeError: Cannot read property 'browser' of undefined
(stack traceは省略。。。)

ぬぬぬ。undefinedにbrowserなんてプロパティねえよ!このボケ!とのこと。

babelコマンドでテストがどう変換されているか確認してみる。

// babel test/home.jsの出力結果
"use strict";
var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };
var injectBrowser = _interopRequire(require("testium/mocha"));

describe("/home", function () {
  before(injectBrowser());

  it("should display \"Hello World\"", function () {
    undefined.browser.navigateTo("/");
    undefined.browser.assert.httpStatus(200);
    undefined.browser.assert.elementHasText("h1", "Hello World");
  });
});

arrow functionの中のthisがundefinedに変換されていた。 このように書くとうまくいく。

'use strict';

import injectBrowser from 'testium/mocha';

describe('/home', () => {
  before(injectBrowser());

  // ここはarrow functionを使わずにいつものFunction
  let browser;
  beforeEach(function () {
    browser = this.browser;
  });

  it('should display "Hello World"', () => {
    browser.navigateTo('/');
    browser.assert.httpStatus(200);
    browser.assert.elementHasText('h1', 'Hello World');
  });
});

arrow functionがただのFunctionの省略じゃないのは知っていたが、 なぜ、このようにarrow functionの中のthisはundefinedに変換されてしまうのか。

arrow functionはただの省略記法じゃない

ちょっと調べてみるとそれっぽいissueを発見。(他にもそれっぽいissueがいっぱいあったが、「これを見ろ」と下記のURLが貼られていたw)

github.com

「arrow functionはthisがbind」されるのが原因だ、とのこと。

https://github.com/nzakas/understandinges6/blob/master/manuscript/02-Functions.md#arrow-functions

var hoge = () => { ... }

// 普通のFunctionで書くとこんな感じ
var hoge = function () {
}.bind(this);

(他にもnewできない、argumentsが無いとか、普通のFunctionとちょっと違う)

「いやいや、でもdescribeの中で実行されるときに別のthisをbindされるかもしれないから、undefinedに変換してしまうのはやり過ぎなんじゃないのか」と思ったが、arrow functionはapply, callを使って後からthisを変更して実行することができない(普通のFunctionにbindを使った場合も同様)。故にBabelでは、定義時の状態でどれがthisになりうるか判断して、arrow function内のthisを書き換えてるみたい。

なので、さきほど失敗したテストケースの場合、describeに渡しているarrow functionにはglobalのthisがbindされるが、globalのthisはundefinedなので、それ以降のarrow functionのthisもundefinedに変換しているっぽい。

じゃあ、describeに渡しているarrow functionを普通のfuncitonに変えればいいのでは。

'use strict';
import injectBrowser from 'testium/mocha';

describe('/home', function () { // ここだけ普通のfunciton
  before(injectBrowser());

  it('should display "Hello World"', () => {  // ここはarrow function
    this.browser.navigateTo('/');
    this.browser.assert.httpStatus(200);
    this.browser.assert.elementHasText('h1', 'Hello World');
  });
})

変換してみる。↓

"use strict";
var _interopRequire = function (obj) { return obj && obj.__esModule ? obj["default"] : obj; };
var injectBrowser = _interopRequire(require("testium/mocha"));

describe("/home", function () {
  var _this = this;

  before(injectBrowser());

  it("should display \"Hello World\"", function () {
    _this.browser.navigateTo("/");
    _this.browser.assert.httpStatus(200);
    _this.browser.assert.elementHasText("h1", "Hello World");
  });
});

ちゃんとthisを_thisに代入してitのarrow functionではそれを参照するように書き換わっている!!いけそう!!!が、実行してみるとエラーがでる。

$ mocha --compilers js:babel/register

  /home
    1) should displays "Hello World"

  0 passing (1s)
  1 failing

  1) /home should display "Hello World":
     TypeError: Cannot call method 'navigateTo' of undefined
(stack traceは省略。。。)

before, beforeEach, itの中のthisは全て同じだと思っていたが、完全な思い込みだった。。。orz

'use strict';

describe('this', function () {
  var _this = this;

  before(function () {
    console.log('in before:', this===_this ? 'same' : 'not same');
  });

  it('...', function () {
    console.log('in it:', this===_this ? 'same' : 'not same');
  });
});

// 出力結果
in before: not same
in it: not same

なるほど。thisがなんなのかよくわかんない時にarrow functionは使うべきではないのかー。

まとめ

  • arrow functionはただの「普通のfunctionの省略記法」ではないので、気をつけなはれや!
  • babelがんばってる。本当にすごい。