LoginSignup
2166
1702

More than 1 year has passed since last update.

JavaScript: 通常の関数とアロー関数の違いは「書き方だけ」ではない。異なる性質が10個ほどある。

Last updated at Posted at 2020-03-22

アロー関数は、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

したがって、通常関数はclassextendsできますが、アロー関数はできません:

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

アロー関数はletconstの仕様上、同じ関数名で定義を上書きすることができません:

let arrow = () => {}
let arrow = () => {}
//=> SyntaxError: Identifier 'arrow' has already been declared

もちろん、アロー関数でもvarで宣言すると、上書き可能です:

var arrow = () => {}
var arrow = () => {}

違い10: super, new.targetの有無

アロー関数 - JavaScript | MDNによれば、アロー関数には束縛されたsupernew.targetが無いようですが、これを示すサンプルコードが思いつかなかったので割愛します。

まとめ

本稿では、JavaScriptの2種類の関数、通常関数とアロー関数の性質の違いについて説明しました。通常関数とアロー関数では、様々な性質の違いあり、単純に書き方の違いではないことが分かりました。


最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:Twitter@suin

2166
1702
17

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2166
1702