ES6のarrow functionとbabel
最近、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)
「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がんばってる。本当にすごい。