🏹

Vue インスタンスの methods にアロー関数を使ったら動作しない理由を this のスコープで説明します

2022/04/20に公開

🌼 はじめに

こんにちは、vue.js 初心者です。基本チュートリアルをやってるとき、methodsにアロー関数を使ったら正常に動作しないことを知ってそれを深堀りしたくなったので記事にします。

簡単なカウンターサンプルも作ってみましたので、皆さんも試してみてください。

2つのボタンが参照してる関数は中身は全く同じで、違うのは書き方だけです。

var app = new Vue({
  el: '#app',
  data: {
    counter: 0
  },
  methods: {
    addCounterFunction: function() {
      this.counter++
    },
     addCounterArrowFunction: () => {
      this.counter++
    },
  }
})

なぜアロー関数だけ動作しないのか、その理由はアロー関数はthisのスコープが違うからのではないかと思いました。正確にどう違ってそれがどういう影響を与えてるかを今から検証します!

1. thisのスコープ

まずはthisのスコープについて理解する必要があります。JS Primer にサンプルコードと一緒に詳しい説明がありますので、たくさん参考にさせていただきました。
https://jsprimer.net/basic/function-this/#function-this

内容が多いので、今回のテーマに直接関わる部分だけとってきて見ていきたいと思います。

1-1. アロー関数ではない関数

アロー関数ではないほうからいきましょう。

最初に知っておきたいことはthisの値が決まるタイミングです。

Arrow Function以外の関数(メソッドも含む)におけるthisは、実行時に決まる値となります。 言い方を変えるとthisは関数に渡される暗黙的な引数のようなもので、その渡される値は関数を実行するときに決まります。

「実行時に決まる値」がどういう意味かピンとこないかもしれませんので、簡単なサンプルコードを用意しました。

function sayHello() {
    console.log(`Hello, ${this.name}`)
}

const banana = {
    name: 'banana',
    greet: sayHello,
}
const apple = {
    name: 'apple',
    greet: sayHello,
}

banana.greet() // Hello, banana
apple.greet() // Hello, apple

もしsayHelloという関数が定義されたときthisが決まるのなら、どのオブジェクトで実行されても同じ値を出力するはずです。ですが、最後の実行結果でそれぞれ違う値を出力しています。なぜならメソッドが実行されたとき、そのメソッドが含まれてるオブジェクトを参照する仕様になってるからです。これが「実行時に決まる値」の意味です。

次は具体的にthisって何なのかですね。今回はメソッドについて扱うため、メソッドの場合を調べました。

関数におけるthisの基本的な参照先(暗黙的に関数に渡すthisの値)はベースオブジェクトとなります。 ベースオブジェクトとは「メソッドを呼ぶ際に、そのメソッドのドット演算子またはブラケット演算子のひとつ左にあるオブジェクト」のことを言います。

サンプルコードも一緒に見ましょう。

const obj = {
    method1: function() {
        return this;
    },
};

console.log(obj.method1()); // => obj

この場合method1のベースオブジェクトはドットのひとつ左にあるobjになるのでthisobjを示します。

こういう性質を利用して同じオブジェクトに所属する別のプロパティをthisで参照できます。

const person = {
    fullName: "Brendan Eich",
    sayName: function() {
        // `person.fullName`と書いているのと同じ
        return this.fullName;
    }
};
// `person.fullName`を出力する
console.log(person.sayName()); // => "Brendan Eich"

それでは何重にもネストしてるオブジェクトはどうでしょうか。

const obj1 = {
    obj2: {
        obj3: {
            method() {
                return this;
            }
        }
    }
};

console.log(obj1.obj2.obj3.method()); // => obj3

method内のthisのベースオブジェクトはobj3です。このときのベースオブジェクトはドットでつないだ一番左のobj1ではなく、メソッドから見てひとつ左のobj3となります。

1-2. アロー関数

次はアロー関数です。アロー関数は他の関数とは違って実行時ではなく定義時にその値が決まります。その理由は以下の説明に含まれてます。

Arrow Functionとそれ以外の関数で大きく違うことは、Arrow Functionはthisを暗黙的な引数として受けつけないということです。 そのため、Arrow Function内にはthisが定義されていません。このときのthisは外側のスコープ(関数)のthisを参照します。

つまり、Arrow Functionにおけるthisは「Arrow Function自身の外側のスコープに定義されたもっとも近い関数のthisの値」となります。

注目すべき部分はアロー関数にはthisがないということです。だから「自身の外側のスコープに定義されたもっとも近い関数」によってアロー関数のthisの値が決まります。これは実行時ではなく、定義時にすでに値が決まるということを意味します。

もし外側にも関数がない場合は、トップレベルのthisを参照します。(ブラウザならwindowオブジェクト、Node.jsならglobalオブジェクトなど)

1-1で見たサンプルコードを少し修正してみました。

const showThis = () => {
    // この関数の外側に関数は存在しないので
    // トップレベルの`this`と同じ値になる
    console.log(this) 
}

const banana = {
    name: 'banana',
    method: showThis,
}
const apple = {
    name: 'apple',
    method: showThis,
}

banana.method() // Window
apple.method() // Window

showThisを定義した時点で既にthisの値が決まるので、どのオブジェクトで使っても同じ結果を出します。今回はブラウザで実行したのでブラウザのグローバルオブジェクトであるWindowになりました。

ここで気になったことを(ブラウザで)実験してみました。まずはネストしてるオブジェクトの中にあるアロー関数でthisを使った場合です。

const obj = {
    obj2: {
        obj3: {
            method: () => console.log(this)
        }
    }
}
// 外側にも関数がないので Window になる
obj.obj2.obj3.method() // Window

いっぱいネストされても関数はなかったのでトップレベルるのthisであるWindowが返されました。

それでは外側に関数がある場合です。

const obj = {
    method(){
        const arrowFunction = () => console.log(this)
        arrowFunction()
    }
}
// 外側の関数であるmethodのthisと同じ値を参照、つまりobjになる
obj.method() // {method: ƒ}

アロー関数の外側にmethodという関数があるのでその関数のthisと同じ値になります。この例ではobjですね。

2. Vue インスタンス

2-1. インスタンスの定義と生成

それでは理解したthisの知識を活用して Vue インスタンスを分析します。

var app = new Vue({
  el: '#app',
  data: {
    counter: 0
  },
  methods: {
    addCounterFunction: function() {
      this.counter++
    },
  }
})

ここですこしおかしいと感じる部分があります。1-1で「メソッドにおいてthisの値はベースオブジェクトとなる」と学んだのでaddCounterFunction内のthismethodsになるはずです。なのになぜthis.counterのような書き方でdata配下の値にアクセスできるのでしょうか?

その答えは実際生成された Vue インスタンスにあります。確認のために一時的に関数内容を変更してみました。

  methods: {
    addCounterFunction: function() {
-      this.counter++
+      console.log(this)
    },
  }

これでコンソール窓でthisを確認してみましょう。

実際生成されたインスタンスを見ると定義のときとは多少違いがありますね。datamethodsで書いた値たちが Vue インスタンス直下に入ってます。

クリックイベントが発火されるとき参照するオブジェクトはこちらのほうです。実行時にその値が決まるthisの性質を利用して、インスタンス定義の時もthis.counterのような書き方ができるのではないかと思います。

2-2. アロー関数のthis

アロー関数も同じようにthisを確認してみましょう。

var app = new Vue({
  el: '#app',
  data: {
    counter: 0
  },
  methods: {
    addCounterFunction: function() {
      this.counter++
    },
     addCounterArrowFunction: () => {
-      this.counter++
+      console.log(this)
    },
  }
})

コンソール窓を見る前にもう答えが出たかもしれません。アロー関数の外側に関数がないですよね? つまりこのthisもブラウザトップレベルのthisであるWindowになるということです。

コンソール窓でもWindowが確認されました。

Windowにはcounterというプロパティがないので当然this.counterundefinedとなり、結果的にそのundefinedを+1してることになるのでそりゃ動かないでしょうというところです。

🌷 終わり

一応 vue.js の記事で書いたつもりですが、MDNのアロー関数ページにも「メソッドとして使用することはできません。」と書いてありますね。Vanilla Javascript の知識が足りないのがバレて恥ずかしい気持ちです(´∀`)

でも今回色々調べてthisについて少しは理解できました、、!かもしれません!(といってもこの記事で扱った内容は極一部ですが、、)

GitHubで編集を提案

Discussion