Vue インスタンスの methods にアロー関数を使ったら動作しない理由を this のスコープで説明します
🌼 はじめに
こんにちは、vue.js 初心者です。基本チュートリアルをやってるとき、methods
にアロー関数を使ったら正常に動作しないことを知ってそれを深堀りしたくなったので記事にします。
簡単なカウンターサンプルも作ってみましたので、皆さんも試してみてください。
2つのボタンが参照してる関数は中身は全く同じで、違うのは書き方だけです。
var app = new Vue({
el: '#app',
data: {
counter: 0
},
methods: {
addCounterFunction: function() {
this.counter++
},
addCounterArrowFunction: () => {
this.counter++
},
}
})
なぜアロー関数だけ動作しないのか、その理由はアロー関数はthis
のスコープが違うからのではないかと思いました。正確にどう違ってそれがどういう影響を与えてるかを今から検証します!
this
のスコープ
1. まずはthis
のスコープについて理解する必要があります。JS Primer にサンプルコードと一緒に詳しい説明がありますので、たくさん参考にさせていただきました。
内容が多いので、今回のテーマに直接関わる部分だけとってきて見ていきたいと思います。
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
になるのでthis
はobj
を示します。
こういう性質を利用して同じオブジェクトに所属する別のプロパティを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
内のthis
はmethods
になるはずです。なのになぜthis.counter
のような書き方でdata
配下の値にアクセスできるのでしょうか?
その答えは実際生成された Vue インスタンスにあります。確認のために一時的に関数内容を変更してみました。
methods: {
addCounterFunction: function() {
- this.counter++
+ console.log(this)
},
}
これでコンソール窓でthis
を確認してみましょう。
実際生成されたインスタンスを見ると定義のときとは多少違いがありますね。data
、methods
で書いた値たちが Vue インスタンス直下に入ってます。
クリックイベントが発火されるとき参照するオブジェクトはこちらのほうです。実行時にその値が決まるthis
の性質を利用して、インスタンス定義の時もthis.counter
のような書き方ができるのではないかと思います。
this
2-2. アロー関数のアロー関数も同じように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.counter
はundefined
となり、結果的にそのundefined
を+1してることになるのでそりゃ動かないでしょうというところです。
🌷 終わり
一応 vue.js の記事で書いたつもりですが、MDNのアロー関数ページにも「メソッドとして使用することはできません。」と書いてありますね。Vanilla Javascript の知識が足りないのがバレて恥ずかしい気持ちです(´∀`)
でも今回色々調べてthis
について少しは理解できました、、!かもしれません!(といってもこの記事で扱った内容は極一部ですが、、)
Discussion