従来の関数とアロー関数の違い
JavaScriptの関数は、関数宣言、関数式、アロー関数の3通りの方法で作れます。
アロー関数は後発
JavaScriptの歴史を紐解くと、元々は関数宣言と関数式しかありませんでした。この2つの機能上の違いはほぼありません。この2つはまとめて「従来の関数」と呼びます。アロー関数は、従来の関数の問題点を解決するために、あとで導入されたものです。
構文の簡潔さ
従来の関数は構文の長さが問題でした。JavaScriptではよくコールバック関数を書きます。コールバック関数とは、関数の引数として渡される関数を言います。従来の関数は、関数を書くたびにfunction
キーワードを書く必要があります。処理が1行だけでも、複数行要するコーディングスタイルもあります。書くのも読むのもわずらわしいコードになりがちです。一方で、アロー関数は短くシンプルな記述になります。
js
// 従来の関数(関数式)[1, 2, 3].map (function (n ) {returnn + 1;});// アロー関数[1, 2, 3].map ((n ) =>n + 1);
js
// 従来の関数(関数式)[1, 2, 3].map (function (n ) {returnn + 1;});// アロー関数[1, 2, 3].map ((n ) =>n + 1);
引き算で再設計されたアロー関数
アロー関数が後発だと聞くと、従来の関数に機能が追加されたものと思われるかもしれません。実は逆です。引き算のアプローチでアロー関数は再設計されました。従来の関数が持つ機能から、関数としては余計な機能を削ったり、複雑な仕様を単純化したりしたものです。そのため、シンプルに「関数らしさ」がより際立つものになっています。どのような機能が間引かれたか見ていきましょう。
コンストラクタ
関数の機能の本質は、入力から計算結果を返すことです。JavaScriptの従来の関数にはこの本質以外に、オブジェクトを生成するコンストラクタの役割も担います。関数をコンストラクタとして扱うにはnew
演算子を用います。
js
functionCat (name ) {this.name =name ;}// Catオブジェクトを生成するconstcat = newCat ("ミケ");console .log (cat );
js
functionCat (name ) {this.name =name ;}// Catオブジェクトを生成するconstcat = newCat ("ミケ");console .log (cat );
関数にコンストラクタ機能がついているのは、一見すると便利そうです。しかし、関数にnew
演算子をつけるべきかどうかを、使い手が判断する必要がでてしまいます。それを判断するには、関数の処理内容を読んでみるまで分かりません。コンストラクタとして実行すべき関数を、普通の関数として呼び出ししてまうとバグの原因になりえます。
アロー関数はコンストラクタになれません。もしもJavaScriptでnew
演算子を使うと実行エラーになります。誤用の心配がありません。
js
constCat = (name ) => {};constcat = newCat ("ミケ");
js
constCat = (name ) => {};constcat = newCat ("ミケ");
TypeScriptでは、従来の関数でもコンストラクタとして使えないようになっています。もし、関数を誤ってnew
したとしても、コンパイルエラーで警告されるので安心です。
ts
functionCat (name : string) {/* ... */}const'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.7009'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.cat = newCat ("ミケ");
ts
functionCat (name : string) {/* ... */}const'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.7009'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.cat = newCat ("ミケ");
まとめると、JavaScriptではコンストラクタになれるかどうかは意識する必要がありますが、TypeScriptではコンパイルエラーで気づけるので、JavaScriptほど注意を払う必要はないということになります。
thisの指すもの
従来の関数では、変数this
の指すものが実行時の文脈で決まるという仕様があります。言い換えると、同じ関数であっても、関数の呼び出し方や呼び出される環境によってthis
が別のものを参照するようになります。次のthis
をコンソールに表示する従来の関数を例に見てみましょう。
js
functionshowThis () {console .log (this);}
js
functionshowThis () {console .log (this);}
このshowThis
関数を普通に実行した場合、this
が指すのはグローバルオブジェクトです。グローバルオブジェクトとはブラウザではWindow
オブジェクトです。Window
オブジェクトはページのサイズやURL、表示するHTML(DOM)などを操作するAPIを提供するオブジェクトです。
js
showThis ();
js
showThis ();
JavaScriptにはstrictモードがあります。これは危険な処理ができないよう制約する実行モードです。strictモードを有効にするには"use strict"
をコードの冒頭に書きます。strictモードでshowThis
を実行すると、this
の値はundefined
になります。
js
"use strict";showThis ();
js
"use strict";showThis ();
ちなみにTypeScriptでは、コンパイラオプションalwaysStrict
を有効にすると、コンパイル後のJavaScriptがstrictモードになります。
また、JavaScriptにはスクリプトモードとモジュールモードがあります。モジュールモードのJavaScriptでは、export
やimport
の構文が使えます。このモードでは自動的にstrictモードになります。そのため、モジュールモードでshowThis
を実行すると、this
の値はundefined
になります。
js
export {};showThis ();
js
export {};showThis ();
関数はオブジェクトのメソッドとして呼び出すこともできます。showThis
関数をメソッド呼び出しした場合、this
が指す値はメソッドが紐づくオブジェクトになります。
js
constfoo = {name : "Foo" };// 関数をオブジェクトのメンバーにするfoo .showThis =showThis ;// メソッドとして呼び出すfoo .showThis ();
js
constfoo = {name : "Foo" };// 関数をオブジェクトのメンバーにするfoo .showThis =showThis ;// メソッドとして呼び出すfoo .showThis ();
従来の関数はコンストラクタとして呼び出せることを説明しましたが、コンストラクタとして呼び出した場合、this
は生成中のオブジェクトを指します。
js
functionshowThis () {this.name = "Foo";console .log (this);}newshowThis ();
js
functionshowThis () {this.name = "Foo";console .log (this);}newshowThis ();
上で例示してきたとおり、従来の関数は実行の文脈でthis
の内容が動的に決まります。そのため、従来の関数は呼び出し方に注意を払う必要があります。使い方を誤るとバグに繋がる危険性があります。
文脈 | thisの値 |
---|---|
通常の呼び出しshowThis() | グローバルオブジェクト(Window ) |
通常の呼び出し + strictモードshowThis() | undefined |
メソッド呼び出しobj.showThis() | メソッドが属するオブジェクト(obj ) |
コンストラクタ呼び出しnew showThis() | 生成中のオブジェクト |
アロー関数のthis
はレキシカルスコープで静的です。つまり、定義したときにthis
が指すものが決定し、関数の呼び出し方(文脈)に左右されません。this
の値は明瞭です。
たとえば、次のtimer
オブジェクトは1秒後にメッセージを表示するstart
メソッドを持ちます。start
メソッドで1秒後にtimer
のmessage
フィールドの値を出力する処理を予約しています。
start
関数のthis
はtimer
を指します(❶)。1秒経つと、this.message
を出力しようとします。従来の関数は、this
がグローバルオブジェクトのWindow
を指すため、undefined
が出力されます(❷)。一方のアロー関数は、this
がレキシカルスコープのthis
を指します(❸)。このthis
はtimer
です。よって、message
フィールドの値"時間です!"
が正常に出力されます。
js
constoneSecond = 1000;consttimer = {message : "時間です!",start : function () {console .log (this); // ❶// 従来の関数setTimeout (function () {console .log (this.message ); // ❷},oneSecond );// アロー関数setTimeout (() => {console .log (this.message ); // ❸},oneSecond );},};timer .start ();
js
constoneSecond = 1000;consttimer = {message : "時間です!",start : function () {console .log (this); // ❶// 従来の関数setTimeout (function () {console .log (this.message ); // ❷},oneSecond );// アロー関数setTimeout (() => {console .log (this.message ); // ❸},oneSecond );},};timer .start ();
call
、apply
、bind
の振る舞い
JavaScriptの関数はオブジェクトで、call
、apply
、bind
の3つのメソッドが生えています。このメソッドは関数を呼び出すものですが、従来の関数では、第一引数にthis
が何を指すかを指定できます。
js
functionshowThis () {console .log (this);}constobj = {name : "foo" };showThis .bind (obj )(); // objをthisにバインドして、関数呼び出し
js
functionshowThis () {console .log (this);}constobj = {name : "foo" };showThis .bind (obj )(); // objをthisにバインドして、関数呼び出し
アロー関数にも、call
、apply
、bind
が生えていますが、第一引数に値を渡してもthis
は上書きされません。
js
constshowThis = () => {console .log (this);};constobj = {name : "foo" };showThis .bind (obj )();
js
constshowThis = () => {console .log (this);};constobj = {name : "foo" };showThis .bind (obj )();
arguments変数の有無
従来の関数では、arguments
という特殊な変数が自動的に定義されます。この値は引数の配列です。
js
functionfoo () {console .log (arguments );}foo (1, 2, 3);
js
functionfoo () {console .log (arguments );}foo (1, 2, 3);
arguments
は可変長引数を実現するには便利ですが、関数を実装する多くの場合、利用することのない余計な変数という見方もできます。アロー関数にはarguments
がありません。アロー関数で可変長引数を実現したい場合は、残余引数...
を用います。
js
constfoo = (...args ) => {console .log (args );};foo (1, 2, 3);
js
constfoo = (...args ) => {console .log (args );};foo (1, 2, 3);
ジェネレーター
JavaScriptにはジェネレーターという複数の値を生成できる特殊な関数があります。ジェネレーターは、function
キーワードにアスタリスクをつけ、yield
文で生成する値を記述します。
js
function*generateNumbers () {yield 1;yield 2;yield 3;}
js
function*generateNumbers () {yield 1;yield 2;yield 3;}
ジェネレーターの値はfor-ofなどの反復処理で取り出せます。
js
for (constvalue ofgenerateNumbers ()) {console .log (value ); // 1、2、3の順で出力される}
js
for (constvalue ofgenerateNumbers ()) {console .log (value ); // 1、2、3の順で出力される}
ジェネレーターを定義できるのは従来の関数だけです。アロー関数はそもそもジェネレーター構文をサポートしていないため、ジェネレーターを定義することはできません。
安全性が強化されたアロー関数
アロー関数は、従来の関数にあった危険な仕様が改善されています。
引数名の重複
JavaScriptの従来の関数は、引数名の重複が許されます。引数が重複した場合、最後の引数に渡された値が採用されます。
js
functionfoo (a ,a ,a ) {console .log (a );}foo (1, 2, 3);
js
functionfoo (a ,a ,a ) {console .log (a );}foo (1, 2, 3);
この仕様はバグを引き起こしやすいものですが、従来の関数でもstrictモードにすることで、引数名の重複を構文エラーにできます。
js
"use strict";functionfoo (a ,a ) {}// ^構文エラー
js
"use strict";functionfoo (a ,a ) {}// ^構文エラー
アロー関数が導入される際には、こうした危険な仕様が最初から省かれました。アロー関数で引数名が重複した場合、strictモードのオンオフにかかわらず常に構文エラーになります。
js
constfoo = (a ,a ) => {};// ^構文エラー
js
constfoo = (a ,a ) => {};// ^構文エラー
TypeScriptでは、従来の関数でも引数名の重複はコンパイルエラーになります。
ts
functionDuplicate identifier 'a'.foo (: number, a : number) {} a
Duplicate identifier 'a'.2300
2300Duplicate identifier 'a'.
Duplicate identifier 'a'.
ts
functionDuplicate identifier 'a'.foo (: number, a : number) {} a
Duplicate identifier 'a'.2300
2300Duplicate identifier 'a'.
Duplicate identifier 'a'.
そのため、TypeScriptにおいては、従来の関数とアロー関数の間に、そもそも安全面での差はありません。
関数名の重複
JavaScriptでは変数宣言するときにconst
とlet
、var
のいずれかで行います。var
はJavaScriptの初期から存在する宣言方法ですが、const
とlet
は2015年に追加されたものです。大きな違いは、const
は宣言時にのみ値が代入できる宣言方法で、let
は宣言後でも値を変更できる宣言方法です。
const
とlet
は、var
の問題点を解決するために導入されました。var
の問題点のひとつが何度も同じ変数名で変数宣言できる点でした。たとえば、value
という変数がすでに宣言されていたとしても、もう一度var value
で変数宣言しなおすと、特にエラーになることはなく実行できてしまいます。
js
varvalue = 1;varvalue = 2;console .log (value );
js
varvalue = 1;varvalue = 2;console .log (value );
この仕様は、意図しない変数の上書きに気づきにくく、不具合の要因になることがしばしばあります。const
やlet
は変数名が重複している場合は、エラーになります。つまり、var
よりも安全なコーディングが行えます。
js
letvalue = 1;letvalue = 2; // 構文エラー
js
letvalue = 1;letvalue = 2; // 構文エラー
関数宣言で作った関数はvar
に相当します。そのため、重複した関数名で関数が作れてしまいます。
js
functionfoo () {console .log ("1つ目の関数");}functionfoo () {console .log ("2つ目の関数");}foo ();
js
functionfoo () {console .log ("1つ目の関数");}functionfoo () {console .log ("2つ目の関数");}foo ();
アロー関数は、変数宣言と同じ構文で作るため、var
を避けてlet
またはconst
を使うコーディングをしている限り、関数名の重複が起こりえません。
js
constfoo = () => {};constfoo = () => {};// ^^^構文エラー
js
constfoo = () => {};constfoo = () => {};// ^^^構文エラー
もちろん、アロー関数でもvar
を用いて関数を作った場合は、関数名が重複できてしまいます。しかし、最近のJavaScriptのベストプラクティスでは、var
を使わないことが推奨されています。そのため、関数宣言と比べて、アロー関数のほうがずっと関数名重複のミスを低減できる状況が多いです。
TypeScriptでは、関数宣言でも重複した関数名がコンパイルエラーになります。
ts
functionDuplicate function implementation.2393Duplicate function implementation.() {} foo functionDuplicate function implementation.2393Duplicate function implementation.() {} foo
ts
functionDuplicate function implementation.2393Duplicate function implementation.() {} foo functionDuplicate function implementation.2393Duplicate function implementation.() {} foo
したがって、関数名の重複問題に関しては、TypeScriptでは安全性の差がありません。
巻き上げと関数定義と呼び出しの順序
関数宣言とアロー関数では巻き上げ(hoisting)が起こるか否かの違いがあります。巻き上げとは、変数が宣言される前のコードで、その変数を参照できる仕様です。
巻き上げとは、変数スコープの途中で宣言された変数が、変数スコープの冒頭に変数宣言を自動的に持ってくる仕様です。巻き上げられた変数はundefined
で初期化された状態になります。次の例では、value
の変数宣言よりも先に、変数value
を参照していますが、これはエラーにならずundefined
が出力されます。
js
console .log (value );varvalue = 1;
js
console .log (value );varvalue = 1;
これは変数value
に巻き上げが起こり、console.log(value)
よりも手前でvalue
の変数宣言がなされるためです。上のコードは、実質的に次のコードと同じ意味になります。
js
varvalue ;console .log (value );value = 1;
js
varvalue ;console .log (value );value = 1;
関数宣言でも類似の巻き上げが起こります。var
の巻き上げと異なる点は、関数はundefined
で初期化されるのではなく、関数の実装も合わせて巻き上げられる点です。そのため、関数宣言よりも手前で関数呼び出しが行えます。
js
foo ();functionfoo () {console .log ("実行しました");}
js
foo ();functionfoo () {console .log ("実行しました");}
コードに書かれた順序が、関数呼び出し、関数宣言の順になるだけで、関数の巻き上げには問題点はありません。
学びをシェアする
JavaScriptのアロー関数の特徴
・構文が短い
・thisがレキシカルスコープ
・コンストラクタになれない
・ジェネレータになれない
・引数の重複が起こらない
・関数の宣言重複が起きにくい
・巻き上げが起きにくい
『サバイバルTypeScript』より
従来の関数とアロー関数の使い分け
上では、従来の関数(関数宣言と関数式)とアロー関数の機能上の違いを見てきました。違いを踏まえた上で、この2つはどちらを使ったほうがよいのでしょうか。もしどちらも使う場合、どのような基準で使い分けたらよいのでしょうか。
従来の関数を使うべきか、アロー関数を使うべきかは、意見が分かれるところです。アロー関数は従来の関数の問題点を解決した新しい機能であるため、できるだけアロー関数を使うべきという考えの人もいます。一方で、関数宣言とアロー関数は適度に使い分けるべきという意見の人もいます。アロー関数よりも関数宣言を積極的に使うべきと考える人もいるでしょう。どこでアロー関数を使い、どこで従来の関数を使うか。こうした基準は議論が尽きないところです。どのような判断基準が正しいと断言できるものではありません。
それでも、従来の関数とアロー関数の使い分け方は、個人やチームといったひとつのソースコードを共有する範囲では、一貫した決まりで使い分けることが重要です。ここからは、自分なりの使い分けを考えられるようになるために、判断材料の手がかりを示したいと思います。ここで提示することが普遍的に正しいとは限りません。読んだ上で自分なりの使い分け方を考えてみてください。
特に理由がない場合、アロー関数を使うほうが無難です。なぜかと言うと、アロー関数は関数としての最低限の機能をもったシンプルな関数だからです。上で見たように、従来の関数にはコンストラクタやthis
の動的な解釈などさまざまな機能があり、それらの機能を使わない場合は余計な機能になります。機能が多い分、コーディング時に考慮しないといけないことが増えます。アロー関数はミニマムな機能に抑えられているので、細かいことを気にせず書ける利点があります。
アロー関数が特に相性がいいところはコールバック関数です。たとえば、配列オブジェクトのArray
には、各要素に対して処理をかけるメソッドがいくつかあります。これらのメソッドは引数に関数を渡す必要があります。次の例は、数値の配列に対してfilter
メソッドを用い、偶数だけを抽出するコードです。このコードでは関数式をコールバック関数に渡しています。
js
constnums = [1, 2, 3, 4];consteven =nums .filter (function (n ) {returnn % 2 === 0;});console .log (even );
js
constnums = [1, 2, 3, 4];consteven =nums .filter (function (n ) {returnn % 2 === 0;});console .log (even );
これをアロー関数に置き換えると、次のようにシンプルな記述になります。
js
constnums = [1, 2, 3, 4];consteven =nums .filter ((n ) =>n % 2 === 0);console .log (even );
js
constnums = [1, 2, 3, 4];consteven =nums .filter ((n ) =>n % 2 === 0);console .log (even );
こうしたコールバック関数ではアロー関数を積極的に使うことで、コードの記述量が減ったり、コードが意図する処理が目立つといったメリットが出てきます。
従来の関数も出番がないわけではありません。HTMLのボタンがクリックされたときに何らかの処理をしたい場合、addEventListener
メソッドを使います。任意の処理をコールバック関数としてこのメソッドに渡すことで、好きな処理が行なえます。
js
button .addEventListener ("click",コールバック関数 );
js
button .addEventListener ("click",コールバック関数 );
処理の中でクリックされたボタンを参照する場合、渡す関数が従来の関数なら変数this
でボタンを参照できます。下の例では、クリックした「保存」ボタンの表示を「保存中…」に変えるコードです。this.innerText
でボタン表示を変更しています。このようなthis
の使い方をしたい場合はアロー関数では書くことができません。
html
<button id="save">保存</button><script>const button = document.getElementById("save");button.addEventListener("click", function () {this.innerText = "保存中…";});</script>
html
<button id="save">保存</button><script>const button = document.getElementById("save");button.addEventListener("click", function () {this.innerText = "保存中…";});</script>
上の場合でも、button
を参照すればアロー関数も使えます。なので、従来の関数でなければならない決定打ではありません。
html
<button id="save">保存</button><script>const button = document.getElementById("save");button.addEventListener("click", () => {button.innerText = "保存中…";// ^^^buttonを参照});</script>
html
<button id="save">保存</button><script>const button = document.getElementById("save");button.addEventListener("click", () => {button.innerText = "保存中…";// ^^^buttonを参照});</script>
オブジェクトのメソッドとして関数を作る場合は、従来の関数を選ぶ理由になります。this
でオブジェクトを参照できるからです。たとえば、次の例fullName1
メソッドのように、メソッドでオブジェクトのプロパティを用いる場合、this
で参照するのが便利です。
js
consttaroYamada = {firstName : "Taro",lastName : "Yamada",// 従来の関数fullName1 : function () {return this.firstName + " " + this.lastName ;},// アロー関数fullName2 : () => {return this.firstName + " " + this.lastName ;},};console .log (taroYamada .fullName1 ());console .log (taroYamada .fullName2 ());
js
consttaroYamada = {firstName : "Taro",lastName : "Yamada",// 従来の関数fullName1 : function () {return this.firstName + " " + this.lastName ;},// アロー関数fullName2 : () => {return this.firstName + " " + this.lastName ;},};console .log (taroYamada .fullName1 ());console .log (taroYamada .fullName2 ());
アロー関数を用いたfullName2
はthis
がオブジェクトを指さないため、期待どおりの動作になりません。もし、アロー関数を使う場合は、this
ではなくtaroYamada.firstName
のようにオブジェクトの変数名を参照する必要があります。
js
consttaroYamada = {firstName : "Taro",lastName : "Yamada",fullName : () => {returntaroYamada .firstName + " " +taroYamada .lastName ;},};console .log (taroYamada .fullName ());
js
consttaroYamada = {firstName : "Taro",lastName : "Yamada",fullName : () => {returntaroYamada .firstName + " " +taroYamada .lastName ;},};console .log (taroYamada .fullName ());
従来の関数には巻き上げがあるおかげで、理解しやすいコードになる場合もあります。たとえば、プログラムを処理過程ごとに関数でグルーピングし、プログラムの冒頭で関数呼び出しを羅列することで、そのプログラムの処理の概要が読み始めのところでわかりやすくなることがあります。
js
// プログラムの概要step1 ();step2 ();step3 ();// 各処理の詳細functionstep1 () {/* 処理の詳細 */}functionstep2 () {/* 処理の詳細 */}functionstep3 () {/* 処理の詳細 */}
js
// プログラムの概要step1 ();step2 ();step3 ();// 各処理の詳細functionstep1 () {/* 処理の詳細 */}functionstep2 () {/* 処理の詳細 */}functionstep3 () {/* 処理の詳細 */}
アロー関数は、const
やlet
、var
で作る必要があるため、関数の巻き上げが起こりません。そのため、上のサンプルコードのように先に処理の概要を示すようなパターンはそのまま書くことができません。
js
step1 ();step2 ();step3 ();conststep1 = () => {};conststep2 = () => {};conststep3 = () => {};
js
step1 ();step2 ();step3 ();conststep1 = () => {};conststep2 = () => {};conststep3 = () => {};
もし上の書き方と近い表現をアロー関数で行う場合、処理の概要を書いた関数を定義し、その関数をプログラムの最後で呼び出す書き方になります。
js
constmain = () => {step1 ();step2 ();step3 ();};conststep1 = () => {};conststep2 = () => {};conststep3 = () => {};main ();
js
constmain = () => {step1 ();step2 ();step3 ();};conststep1 = () => {};conststep2 = () => {};conststep3 = () => {};main ();
関数が関数であることを目立たせたい場合に、関数宣言を使うという選択もあります。アロー関数は、変数宣言と同じ書き方で書くので、それが値なのか関数なのかがひと目では分かりにくいと感じる人も中にはいます。変数宣言の間に、アロー関数と関数宣言がある次の例を見比べてください。どちらがぱっと見て関数であると分かりやすいでしょうか。
js
// 変数宣言の間にあるアロー関数conststr = "foo";constobj = {value :str };constfunc = (n ) =>n + 1;constnums = [1, 2, 3];// 変数宣言の間にある関数宣言conststr = "foo";constobj = {value :str };functionfunc (n ) {returnn + 1;}constnums = [1, 2, 3];
js
// 変数宣言の間にあるアロー関数conststr = "foo";constobj = {value :str };constfunc = (n ) =>n + 1;constnums = [1, 2, 3];// 変数宣言の間にある関数宣言conststr = "foo";constobj = {value :str };functionfunc (n ) {returnn + 1;}constnums = [1, 2, 3];