iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

JavaScript 従来の関数とアロー関数の違い

こんにちは!
株式会社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

通常のメソッドは、thisobjを参照するため、合計値が正しく出力されています。

対して、アロー関数は独自の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

参考資料

developer.mozilla.org

qiita.com

typescriptbook.jp