- 1. コンストラクタとしての使用
- 2. thisのバインディング
- 3. superのバインディング
- 4. メソッドとしての使用
- 5. argumentsの有無
- 6. prototypeプロパティの有無
- 7. call、apply、bind の使用
- 8. ジェネレータとしての使用
- まとめ
こんにちは!
株式会社iimonでエンジニアをしている遠藤です。
最近ふと、アロー関数と従来のfunction式を用いた関数(以降「従来の関数」と呼びます)の違いをあまり意識せずに、何とな〜くでアロー関数を使ってしまっているな〜と思うことがありました。
そこで、今回は従来の関数とアロー関数の構文以外の主な違いについてまとめてみました。
1. コンストラクタとしての使用
従来の関数は、関数の機能の本質である"入力を受け取って計算結果を返す"役割の他に、"オブジェクトを生成するコンストラクタ"の役割を担うことができます。
ES6からjsにもクラスが追加されましたが、ES5までは、コンストラクタ関数とnew
演算子を用いて、オブジェクトを生成していました。
以下の記事が参考になります! https://tech.iimon.co.jp/entry/2023/11/28/131533
ES6で追加されたアロー関数は、コンストラクターとして使用することができません。
const Foo = () => {}; const foo = new Foo(); // エラーが出る // VM248:2 Uncaught TypeError: Foo is not a constructor at <anonymous>:2:13
そのため、関数やコンストラクターが new
演算子を使用して呼び出されたかどうかを検出するnew.target
にアクセスすることもできません。
const foo = () => { if (new.target) return }; // エラーが出る // Uncaught SyntaxError: new.target expression is not allowed here
2. thisのバインディング
従来の関数式では、this
の値は関数がどのように呼ばれたかによって決定されます。
function func() { console.log(this) } /** 通常の呼び出し */ func() // 出力結果: Window { /* … */ } (グローバルオブジェクト) /** strictモード */ "use strict"; func() // 出力結果: undefind /** オブジェクトのメソッドとして呼び出し */ const foo = { name: "hoge" } foo.func = func foo.func() // 出力結果: {name: 'hoge', func: ƒ}
そのため、従来の関数はthis
の値が推測しにくい場合があります。
例えば、以下のようにコールバック関数として使用する場合です。
function Bomb() { // Bomb()コンストラクターは自身をthisと定義する。(Bomb{}) this.message = "爆発!!!"; setTimeout(function() { // この関数はグローバルオブジェクトをthisとして定義する(Window { /* … */ }) console.log(this.message) }, 1000); } const p = new Bomb(); // 出力結果: undefind
setTimeout
のコールバック関数に従来の関数を用いる場合、this
はグローバルオブジェクトを指します。
適切なthisを渡すためには、this
を別の変数に割り当てる、もしくは束縛関数を使って明示的にthis
を束縛する必要があります。
- 別の変数に割り当てる
function Bomb() { const self = this; self.message = "爆発!!!"; setTimeout(function() { // 変数selfによって、bombオブジェクトを参照する console.log(self.message) }, 1000); } const p = new Bomb(); // 出力結果: "爆発!!!"
bind()
の使用
function Bomb() { this.message = "爆発!!!"; setTimeout((function() { // bind()によってthisはbombオブジェクトを参照するようになる console.log(this.message) }).bind(this), 1000); } const p = new Bomb(); // 出力結果: 爆発!!!
対して、アロー関数は自身のthis
を持たず、定義されたスコープのthisを継承します(レキシカルスコープ)。
そのため、特にコールバック関数やイベントハンドラなどでthis
の値を推測しやすくなります。
先ほどの例をアロー関数に書き換えると以下のようになります。
function Bomb() { // Bomb()コンストラクターは自身をthisと定義する。(Bomb{}) this.message = "爆発!!!"; setTimeout(() => { // thisはbombオブジェクトを参照する console.log(this.message) }, 1000); } const p = new Bomb(); // 出力結果: 爆発!!!
アロー関数内のthis
はbombオブジェクトを参照しています。
3. superのバインディング
アロー関数ではthis
と同様super
は定義されたスコープに依存します。(thisと似たような説明になるので省略します)
4. メソッドとしての使用
前述の通り、アロー関数は自分自身でthis
を持たないので、メソッドとしては使用できません。
例を挙げて見ていきます。
- 通常のメソッド
const obj = { price: 100, quantity: 5, sum(){ console.log("1.thisの値:", this); console.log("2.合計:", this.price * this.quantity); } } obj.sum() // 出力結果: // 1.thisの値: {price: 100, quantity: 5, sum: ƒ} // 2.合計: 500
- アロー関数
const obj = { price: 100, quantity: 5, sum: () => { console.log("1.thisの値:", this); console.log("2.合計:", this.price * this.quantity); } } obj.sum() // 出力結果: // 1.thisの値: Window { /* … */ } (グローバルオブジェクト) // 2.合計: NaN
通常のメソッドは、this
がobj
を参照するため、合計値が正しく出力されています。
対して、アロー関数は独自のthis
を持たないため、this
はグローバルオブジェクトを参照しています。そのため、計算結果が取得できていません。
クラスの場合は、クラスの本体がthis
コンテキストを持っています。
そのため、アロー関数を使ってメソッドを定義しようとした場合、アロー関数はクラスのthis
コンテキストを閉じ、アロー関数内のthis
はインスタンスを参照します。
けれども、これは関数自身のバインディングでなく、クロージャであるため、this
の値が実行コンテキストによって変わることはありません。
また、これはメソッドではなく、クラスのインスタンスにバインドされたプロパティになるので、各インスタンスに個別に存在することとなります。
(自分はこの辺曖昧になってました。。。)
通常のメソッドでは、実行のコンテキストによってthisが変わるため、変数に代入した場合はthisが失われ、undefind
になります。
また、メソッドはプロトタイプチェーンの一部となり、全てのインスタンスが同じメソッドを共有します。
class MyClass { hoge = "hoge"; normalMethod(){ console.log(this) }; arrowMethod = () => { console.log(this); }; } const obj = new MyClass(); obj.normalMethod(); // 出力結果: MyClass {hoge: 'hoge', arrowMethod: ƒ} obj.arrowMethod(); // 出力結果: MyClass {hoge: 'hoge', arrowMethod: ƒ} const normalMethod = obj.normalMethod const arrowMethod = obj.arrowMethod normalMethod(); // 出力結果:undefind arrowMethod(); // 出力結果: MyClass {hoge: 'hoge', arrowMethod: ƒ} console.log(obj.hasOwnProperty('normalMethod')); // 出力: false console.log(obj.__proto__.hasOwnProperty('normalMethod')); // 出力: true console.log(obj.hasOwnProperty('arrowMethod')); // 出力: true console.log(obj.__proto__.hasOwnProperty('arrowMethod')); // 出力: false
前述の通り、
ので、メモリ効率は通常のメソッドとして定義した時の方が良さそうです。
そのため、クラス内では2. thisのバインディング
で述べたイベントハンドラやコールバック関数でthis
を明示的にバインドする必要がある場合などはアロー関数を使い、特に理由がなければ通常のメソッドとして定義するのが良さそうだなと個人的には思いました。
5. argumentsの有無
アロー関数は自身のargumentsオブジェクトを持ちません。
const arguments = "foo" function hoge () { console.log(arguments) } const foo = () => { console.log(arguments) } hoge() // 出力: Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ] foo() // 出力: foo
引数のリストを取得したいという場合は、代わりに残余引数を使います。
const hoge = (...args) => { console.log(args) } hoge() // 出力: []
6. prototypeプロパティの有無
前述した通り、アロー関数はコンストラクタとして使用できず、また独自のthis
を持たないためprototype
プロパティを持ちません。
const hofe = function (){} const foo = () => {}; console.log("prototype" in hoge) // 出力結果: true console.log("prototype" in foo); // 出力結果: false
7. call、apply、bind の使用
アロー関数は、アロー関数が定義されているスコープに基づいてthis
の値を定義しており、関数の呼び出し方によってこの値が変わることはないため、これらは無効となります。
8. ジェネレータとしての使用
アロー関数ではそもそも構文がないので、ジェネレータ関数を定義できません。
ジェネレータ関数についての詳細は省きます。
function* 宣言 - JavaScript | MDN
まとめ
従来の関数とアロー関数の違いについてまとめてみました。
具体的にどう使い分けるかは所属するチームの方針によるところもあるかもしれませんが、こういった性質の違いをちゃんと理解した上で使い分けられるようになりたいなと思いました。
記載内容に表現や認識の誤りがあればご指摘いただけますと幸いです!
また、弊社ではエンジニアを募集しております。
ぜひカジュアル面談でお話ししましょう!
ご興味ありましたら、ご応募ください! Wantedly / Green
参考資料