アロー関数とは
ES2015の新構文の一つ「アロー関数」とは、無名関数の省略記法です。無名関数ではない、いわゆる「関数宣言」には使うことができません。また、後述しますが無名関数とアロー関数とは完全に等価というわけではないため、取り扱いにはいくつかの注意が必要です。
//従来の関数式 var fn = function (x) {/* 関数本体 */}; //上記の関数式の無名関数部分(右辺)をアロー関数に置き換えたものが以下です。 var fn = (x) => {/* 関数本体 */}; //次の様な「関数宣言」をアロー関数に置き換えることは出来ません。 function fn(x) {/* 関数本体 */}
基本構文
アロー関数のいくつかの基本的な書き方の例を紹介します。
/** * 引数部分の構文パターン */ //従来の関数式 var fn = function (x) {console.log(x);}; //上記をアロー関数に置き換えたもの var fn = (x) => {console.log(x);}; //関数の引数が複数ある場合はカンマで区切ります var fn = (x, y, z) => {console.log(x, y, z);}; //関数の引数が1つのみならば引数部分の丸括弧を省略できます var fn = x => {console.log(x);}; //引数が無い場合は丸括弧は省略できません var fn = () => {console.log('hoge');}; /** * 関数本体部分の構文パターン */ //従来の関数式 var fn = function (x, y) {return x + y;}; //上記をアロー関数に置き換えたもの var fn = (x, y) => {return x + y;}; //関数が1つのreturn文のみの場合は波括弧とreturnキーワードを省略できます var fn = (x, y) => x + y; //波括弧とreturnキーワードを省略する場合、 //関数がオブジェクトリテラルを返すならば、丸括弧で囲む必要があります。 var fn = (x, y) => ({result: x + y}); var fn = (x, y) => {result: x + y}; //これだとエラーになってしまう
通常の無名関数との違い
アロー関数は無名関数の省略記法ではあるのですが、通常の無名関数と完全に等価というわけではないことに注意しなければなりません。
thisの参照が関数定義時に決まる(※重要)
従来の無名関数とアロー関数とでは関数内部から参照するthisの扱いが異なります。
無名関数は関数の呼び出し方によって関数内のthisの値が変化しますが、アロー関数内部のthisは関数定義時に決定します(レキシカルなthis)。このことは特に重要で注意が必要な点でもあるので次節でもう少し掘り下げます。
argumentsオブジェクトを持たない
アロー関数内部ではargumentsオブジェクトにアクセスすることができません。
var fn1 = function (param) {console.log(arguments[0]);}; fn1('hoge'); // hoge var fn2 = (param) => {console.log(arguments[0])}; fn2('fuga'); // Error: arguments is not defined
コンストラクタとして振る舞うことはできない
先ほども書きましたが、アロー関数ではthisの値が関数定義時に決まるため、必然的にコンストラクタとしては使用することができません。
次の例は従来の無名関数でのコンストラクタ定義例と、アロー関数でのコンストラクタ定義例ですが、アロー関数の方はコンストラクタとして呼び出した時点でエラーが発生してしまいます。
var Person = function (name, age) { this.name = name; this.age = age; }; var taro = new Person('Taro', 16); console.log(taro.age); //16
var Person = (name, age) => { this.name = name; this.age = age; }; //コンストラクタとして呼び出した時点でエラーが発生 var taro = new Person('Taro', 16); //Error: not a constructor
thisの扱いに注意
アロー関数を使うにあたって、特に注意が必要なのは「this」キーワードの存在です。この部分については通常の無名関数とは大きく扱い方が異なっています。
通常の無名関数の場合、関数内の「this」の参照先は、「関数がどのようにして呼ばれたか」によって変化します。
(参考:JavaScriptの「this」は「4種類」?? – Qiita)
しかし、アロー関数では関数定義時のコンテキスト(スコープ)のthisを常に参照するようになります。言い換えると、アロー関数はそれが定義された場所によって関数内部のthisの値が固定されるということです。
次の例では、obj というオブジェクトを作成し、show というメソッドを定義しています。
show メソッドの中では、funcA(通常の関数式)とfuncB(アロー関数)の2つの関数をそれぞれ呼び出しています。
どちらの関数もやっていることは同じ(console.log(this) でthisの内容を出力している)だけですが、出力される内容が異なります。
'use strict'; var obj = { show: function () { console.log(this); //Object var funcA = function () { console.log(this); //undefined }; funcA(); var funcB = () => { console.log(this); //Object }; funcB(); } }; obj.show();
funcAでは「undefined」、funcBは「Object」がそれぞれ出力されます。
通常の関数式(funcA)では関数内のthisの値は関数の呼び出し方によって決まります。ここでは「関数呼び出しパターン」が使われているため、funcA内部でのthisの値は「undefined」となります。(※strictモードではない場合はグローバルオブジェクトになります)
これに対し、アロー関数(funcB)内のthisの値は、呼び出し方に関係なく関数が定義された場所によって決まります。これは、関数が定義されている場所のthisと等価ということであり、アロー関数の外側のthis(上記コードの5行目のthis)と同じであるということです。(上記コードの5行目と14行目のthisは「obj」オブジェクトを指しています)
callやapplyを介して呼ばれる場合
JavaScriptの関数をcallやapplyを介して呼びだすと、その関数内のthisの値を任意の値に置き換えることができます。
'use strict'; var fn = function () { console.log('my name is ' + this.name); }, hoge = { name: 'hoge' }, fuga = { name: 'fuga' }; fn.call(hoge); //my name is hoge fn.apply(fuga); //my name is fuga
次に、上記コードの冒頭の関数 fn をアロー関数に置き換えて実行した例を示します。
'use strict'; var fn = () => { console.log('my name is ' + this.name); }, hoge = { name: 'hoge' }, fuga = { name: 'fuga' }; fn.call(hoge); //my name is result fn.apply(fuga); //my name is result
上記のコードをChromeで実行すると、call, apply どちらで呼び出した場合も「my name is result」と出力されます。
これは、call, apply を介して関数を呼び出したにもかかわらず、関数内部のthisはグローバルオブジェクトを指しているためです。(※Chromeの場合、グローバルオブジェクトに既に name というプロパティが設定されており、その値が「result」になっています)
このことから、アロー関数内部の this は call や apply を介して呼び出したとしても置き換えられず、あくまでも関数定義時のスコープによって this の値が固定されるということが分かります。
jQueryでコールバック関数としてアロー関数を使う場合
jQueryを使っていてコールバック関数としてアロー関数を使いたい場合などは特に注意が必要です。
以下はそれぞれイベントハンドラとしての関数例とループ処理内でのコールバック関数の例です。
イベントハンドラのコード例
<button>ボタンです</button>
$('button').click(function () { console.dir(this); //button });
$('button').click(() => { console.dir(this); //Window });
ループ内のコールバック関数のコード例
<ul> <li>hoge</li> <li>piyo</li> <li>fuga</li> </ul>
$('li').each(function () { console.dir(this); //li, li, li });
$('li').each(() => { console.dir(this); //Window });
イベントハンドラ、コールバックいずれのケースにおいても、従来の無名関数ならばthisの値は「現在の要素」を参照しているのに対し、アロー関数ではthisの値は「Windowオブジェクト(グローバルオブジェクト)(※strictモードならばundefined)」を参照しています。これは先ほど説明した通り、アロー関数内のthisの値は、関数が定義されている場所のthisと等価であるためです。
このような問題があるため、jQueryのコールバック関数としてアロー関数を使う場合は、それぞれ以下の様にして現在の要素を仮引数として関数に渡す必要があります。(これが最適解かどうかはわかりませんが。。)
$('button').click(e => { console.dir(e.currentTarget); //button });
$('li').each((i, elm) => { console.dir(elm); //li, li, li });
上記はアロー関数を使う場合の例ですが、そもそもこのようなケースの場合、無理にアロー関数を使わず素直に従来の無名関数を使っても良いとは思います。
アロー関数で即時関数
アロー関数も通常の関数式同様、即時実行(即時関数)できるのですが、書き方に注意が必要です。
通常の関数式の場合
(function () { console.log('hoge'); //hoge }()); //または (function () { console.log('piyo'); //piyo })();
アロー関数の場合
(() => { console.log('hoge'); }()); //Error (() => { console.log('piyo'); //piyo })();
通常の関数式の場合、
(function () { /* 関数本体 */ }());
と書いても
(function () { /* 関数本体 */ })();
の様に書いてもどちらも即時実行されるのですが、アロー関数の場合は
(() => { /* 関数本体 */ })();
の様に記述しなければならないようです。
まとめ
アロー関数は関数をより簡潔に書くことが出来る新しい構文ではあるものの、thisの挙動など従来の関数とは扱いが異なる部分もあるため、単純に関数式の代替構文として位置づけてしまうのは危険だと言えそうです。
特に、jQueryのイベントハンドラやコールバック関数の様に、関数内のthisが重要な意味を持つようなケースでは注意が必要です。そのような場合は無理にアロー関数を使う必要も無いかもしれません。
こういった点を理解した上で正しくアロー関数を使うことができれば、より簡潔なコードを書くことが出来るようになると思います。以下は、アロー関数を使った方がスマートにコードが記述できるケースの一例です。
counter というオブジェクトに count というプロパティと、start というメソッドがあります。startメソッドを呼び出すと、1秒毎にcount プロパティをインクリメントさせます。
function Counter() { this.count = 0; this.start = function () { var self = this; setInterval(function () { self.count += 1; console.log(self.count); }, 1000); }; }; var counter = new Counter(); counter.start(); // 1, 2, 3, ...
上記のコードは問題なく期待した通りに動きますが、setInterval 関数にコールバックとして渡している無名関数をアロー関数に置き換えることでよりシンプルに記述できます。
function Counter() { this.count = 0; this.start = function () { setInterval(() => { this.count += 1; console.log(this.count); }, 1000); }; }; var counter = new Counter(); counter.start(); // 1, 2, 3, ...
前者のコードで setInterval 関数に渡された関数内では this はグローバルオブジェクトを指してしまいます。そのため、この関数内で count プロパティにアクセスするためには事前に別の変数(上記例ではself)にthisへの参照を渡しておく必要があります。(4行目の部分)。
これに対し、後者のコードの setInterval 関数ではアロー関数が渡されています。そのため関数内のthisの値はnewで新しく生成されるオブジェクト(counterオブジェクト)に固定されているので this.count を正しく参照することができます。
このように、従来ではthisの値を一旦別の変数に格納しておくようなケースではアロー関数を使った方がよりスマートにコードが記述できるようになると思います。
参考リンク
アロー関数 – JavaScript | MDN
アロー関数が実装された
arrow function の引数は括弧で囲っておいた方が良さそうな件
ArrowFunction 的即時関数 – hogehoge @teramako