アロー関数は、function
キーワード使った関数のシュガーシンタックス、つまり、書き方が違うだけで機能は同じだという説明を耳にしたことはありませんでしょうか?
この説明は、正確ではなく実際は様々な違いがあります。本稿では、これら2つの関数の性質の違いについて解説していきます。
TypeScriptを交えた通常の関数とアロー関数の違いについての記事も書いてます。下記の記事もぜひご覧ください。
結論
本稿の結論を先に示すと、2つの関数の違いは次のとおりです。
詳しく知りたい方は、続きをお読み頂ければと思います。
2種類の関数
JavaScriptには大きく分けて2つの関数があります。ひとつは、function
キーワードを使って定義される関数です。
function () {}
もうひとつは、アロー関数(arrow function)です。名前のとおり矢印記号=>
を使って定義される関数です。
() => {}
本稿では、アロー関数とfunction
キーワードを使って定義される関数を区別するため、function
キーワードを使うほうの関数を「通常関数」と呼ぶことにします。英文で見かけるregular functionの翻訳になりますが、これは公式の用語ではなく、解説の便宜上のものとご理解頂ければと思います。単に「関数」というときは、通常関数とアロー関数どちらも指すこととします。
関数の歴史
歴史的に見ると、通常関数は古くからある言語機能であるのに対し、アロー関数は新しいものです。アロー関数はES2015(ES6)で導入されました。導入にあたっては、関数を短く書きたい、thisを束縛したくないという2つの理由があります。
通常関数とアロー関数の性質の違い
通常関数とアロー関数では、構文が違うというのは見て分かると思います。構文についての違いはここでは解説しません。
ここでは、文法以外の相違点をひとつひとつ見ていくことにします。
違い1: thisの指すもの
通常関数にはthis
があり、this
が何を指すのかは、通常関数を実行したタイミングで決まります。
function regular() {
return this
}
// グローバルスコープで実行した場合、thisはグローバルオブジェクトを指す
console.log(regular() === window) //=> true
// メソッドとして実行した場合、thisはオブジェクトを指す
const obj = { method: regular }
console.log(obj.method() === obj) //=> true
通常関数のthis
の振る舞いの詳細についての解説は他の投稿に譲ります:
アロー関数には、それ自体が保有するthis
はなく、関数の外のthis
を関数内で参照できるだけです。レキシカルスコープのthis
を参照します。つまり、アロー関数は定義したときに、this
が指すものがひとつに決まり、どうやって関数が実行されるかに左右されなくなります。
const arrow = () => {
return this
}
// 関数の外側のthis
const lexicalThis = this
// 関数定義したタイミングで、関数の外側のthisを参照する
console.log(arrow() === lexicalThis) //=> true
// メソッドとして実行しても、thisはメソッドが属するオブジェクトを指さない
const obj = { method: arrow }
console.log(obj.method() === obj) //=> false
console.log(obj.method() === lexicalThis) //=> true
// ちなみに上のarrowのthisの振る舞いを、通常関数で再現すると次のようになります:
function regular() {
return lexicalThis
}
以上の相違を踏まえると、同じ処理内容の関数でも、通常関数かアロー関数かによって、結果が異なってくる場合があるということが分かります:
this.name = 'bar'
const foo = {
name: 'foo',
regular: function () {
return this.name
},
arrow: () => {
return this.name
}
}
console.log(foo.regular()) //=> foo
console.log(foo.arrow()) //=> bar
違い2: newできるかどうか
通常関数はnew
することができますが、アロー関数はnew
することができません。つまり、アロー関数はコンストラクタになることができません。
function regular() {
}
const arrow = () => {}
new regular()
new arrow() //=> TypeError: arrow is not a constructor
したがって、通常関数はclass
でextends
できますが、アロー関数はできません:
function regular() {}
const arrow = () => {}
class Foo extends regular {}
class Bar extends arrow {} //=> TypeError: Class extends value () => {} is not a constructor or null
違い3: call
, apply
, bind
の振る舞い
通常関数は、call
, apply
, bind
メソッドの第一引数で、その関数のthis
を指すオブジェクトを指定することができます。アロー関数は、指定しても無視されます。
function regular() {
return this
}
const arrow = () => {
return this
}
const obj = {}
console.log(regular.bind(obj)() === obj) //=> true
console.log(arrow.bind(obj)() === obj) //=> false
console.log(regular.apply(obj) === obj) //=> true
console.log(arrow.apply(obj) === obj) //=> false
console.log(regular.call(obj) === obj) //=> true
console.log(arrow.call(obj) === obj) //=> false
違い4: prototype
プロパティの有無
通常関数にはprototype
プロパティがありますが、アロー関数にはありません。
function regular() {}
const arrow = () => {}
console.log(typeof regular.prototype) //=> object
console.log(typeof arrow.prototype) //=> undefined
ちなみに、どちらの関数も、内部プロパティ[[Prototype]]
は一緒です:
console.log(
Object.getPrototypeOf(regular)
=== Object.getPrototypeOf(arrow)
) //=> true
アロー関数のcall
メソッドなどは、この内部プロパティの[[Prototype]]
のものになります:
console.log(
Object.getOwnPropertyNames(
Object.getPrototypeOf(arrow)
)
)
//=> [
// 'length', 'name',
// 'arguments', 'caller',
// 'constructor', 'apply',
// 'bind', 'call',
// 'toString'
//]
違い5: arguments
の有無
通常関数は、arguments
で引数リストを参照できますが、アロー関数ではarguments
が定義されていないため、引数リストを参照できません。アロー関数のarguments
はレキシカルスコープ変数のarguments
を参照するだけです。
const arguments = 'hoge' // レキシカルスコープ変数
function regular() {
console.log(arguments)
}
const arrow = () => {
console.log(arguments)
}
regular(1, 2, 3) //=> [Arguments] { '0': 1, '1': 2, '2': 3 }
arrow(1, 2, 3) //=> hoge
ちなみに、アロー関数で引数リストを参照する場合は、可変長引数を定義する方法があります:
const arrow = (...arguments) => {
console.log(arguments)
}
arrow(1, 2, 3) //=> [ 1, 2, 3 ]
違い6: 引数名の重複
通常関数は、引数名の重複が許されますが、アロー関数は同じ引数名があるとエラーになります:
function regular(x, x) {}
const arrow = (x, x) => {} // SyntaxError: Duplicate parameter name not allowed in this context
通常関数でもStrict Modeの場合は、引数名の重複がエラーになります:
'use strict'
function regular(x, x) {} // SyntaxError: Duplicate parameter name not allowed in this context
違い7: 巻き上げ(hoisting)
通常関数は、var
相当の巻き上げ(hoisting)が起こります。なので、関数定義前に呼び出しのコードを書いても、関数が実行できます:
regular() // エラーにならない
function regular() {}
アロー関数は巻き上げが起こりません:
arrow() // ReferenceError: Cannot access 'arrow' before initialization
const arrow = () => {}
これは、アロー関数の性質というよりかは、const
の性質によるもので、通常関数の場合でも、const
などを使った定義では、巻き上げが起こりません:
regular() // ReferenceError: Cannot access 'regular' before initialization
const regular = function () {}
違い8: ジェネレータ関数
通常関数はジェネレータ関数を定義できますが、アロー関数はジェネレータ関数を定義する構文がそもそもありません。
function* regular() {
yield 1
}
違い9: 同じ関数名での再定義
通常関数は、同じ名前の関数を定義できます。最後の関数で上書きされます。
function regular() {
console.log(1)
}
function regular() {
console.log(2)
}
regular() //=> 2
これもvar
相当の振る舞いと理解できます:
var a = 1
var a = 2
console.log(a) //=> 2
アロー関数はlet
やconst
の仕様上、同じ関数名で定義を上書きすることができません:
let arrow = () => {}
let arrow = () => {}
//=> SyntaxError: Identifier 'arrow' has already been declared
もちろん、アロー関数でもvar
で宣言すると、上書き可能です:
var arrow = () => {}
var arrow = () => {}
違い10: super
, new.target
の有無
アロー関数 - JavaScript | MDNによれば、アロー関数には束縛されたsuper
やnew.target
が無いようですが、これを示すサンプルコードが思いつかなかったので割愛します。
まとめ
本稿では、JavaScriptの2種類の関数、通常関数とアロー関数の性質の違いについて説明しました。通常関数とアロー関数では、様々な性質の違いあり、単純に書き方の違いではないことが分かりました。
最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします→Twitter@suin