こんにちは!
iimonのかねにわです。
クラスやメソッドを記述する際、thisを使ってプロパティ指定等をすることが多々ありますが、 具体的な挙動や意味をあまり理解できていませんでした。
過去に技術書でthisの項目を流し読みした際にも呼び出し方で参照先が変わる云々の記述があったのは覚えているのですが、 体系的な理解に至らないまま放置してしまっていたのでこの機会にしっかり整理してみようと思いました。
thisの指す内容は関数がどのように呼ばれたかで決まる
いきなり本題になりますが、thisの参照する内容は、そのthisが含まれる関数がどのようにして呼ばれたかで決定されます。
メソッドとして呼ばれた場合
例えば、以下のようなthisを使ってnameプロパティを参照するgreetメソッドを持つuserオブジェクトがあります。
const user = { name: "Hoge", greet: function () { console.log(`Hello, ${this.name}`) }, }; user.greet() > 'Hello, Hoge'
この時、thisによって参照されているのはgreet()の呼び出し元であるuserです。
つまり、this.nameすなわちuser.nameとなり、"Hoge"が取得できるという流れになります。
上記はthisを含む関数がオブジェクトの要素、つまりメソッドの場合です。
関数として呼ばれた場合
これが、関数単体として実行された場合は挙動がまた変化します。
以下のように、ただの関数として実行された場合、thisの参照先はWindowオブジェクトになります。
function greet(){ console.log(`Hello,${this}`) } greet() > 'Hello, [object Window]'
ちなみに、Strictモードになっている場合はWindowオブジェクトではなくundefinedとなります。
'use strict' function greet(){ console.log(`Hello,${this}`) } greet() > undefined
アロー関数として呼ばれた場合
これが、アロー関数における場合はまた特殊な挙動を示します。
結論から言って、アロー関数は内部にthisを持ちません。アロー関数内でthisが使用された場合、
thisは自身のアロー関数の外側のスコープにthisを探しに行きます。
以下の例をご覧ください。
const userOne = { name:'ユーザ1', greet: function() { const userTwo = { name:'ユーザ2', greet: () => { console.log(`Hello, ${this.name}`) } } userTwo.greet() } } userOne.greet() > 'Hello, ユーザ1' ←ユーザ2ではなく1になる
ここでは、userOneオブジェクトのgreetメソッドの中から、さらにuserTwoオブジェクトのgreetメソッドを読んでおり、
このメソッドはthisでnameプロパティを参照するアロー関数となっています。
これまでの流れだとthisはuserTwoのnameプロパティ(ユーザ2)を参照しそうですが、
結果はuserOneのnameプロパティ(ユーザ1)が参照されています。
これは、先に述べたようにアロー関数においては自身のthisを持たないため、外側のスコープのthisを参照しているためです。
上記の例だと、アロー関数を持つuserTwo.greet()を読んでいるuserOne.greet()に参照先が移り、
userOneのnameプロパティがthisによって参照されている状態です。
thisを特定の値で指定する
これまでの説明では、thisはその呼び出され方によって参照先が変動しましたが、これを任意の参照先にすることができます。
それがapply, bind, callメソッドを使う方法です。
applyによるthisの束縛
まずapplyメソッドから見ていきましょう。
fn.apply(obj, [args])として呼ぶことで、関数fnのthisの参照先を第1引数のobjとして呼び出すことができます。 fnへの引数は第2引数の配列の[args]です。
function greet(greet, punctuation) { return `${greet}, ${this.name}${punctuation}`; } const person = { name: 'Hoge' }; greet.apply(person, ['Hello', '!']); > 'Hello, Hoge!'
このように関数greetに対しapplyを使って呼び出すことで、greetの中のthis.nameはapplyの第1引数で指定された
personオブジェクトのname(='Hoge')を参照しています。
callメソッドによるthisの束縛
次にcallメソッドですが、こちらは先に見たapplyメソッドと引数の渡し方に違いがあるのみです。
callメソッドでは、呼び出し対象の関数に対し、引数を一つずつ渡します。
function greet(greet, punctuation) { return `${greet}, ${this.name}${punctuation}`; } const person = { name: 'Hoge' }; greet.apply(person, 'Hello', '!'); ←ここが配列ではなく一つずつ渡す > 'Hello, Hoge!'
bindによるthisの束縛
bindも同様に、thisを引数のオブジェクトに指定した関数を作成します。
bindの場合、対象の関数をその場で実行するのではなく、thisを束縛した新しい関数を作成します。
以下の例では、greetメソッドのthisを、personオブジェクトと紐づけた新しい関数オブジェクトboundGreetを作成しています。
function greet() { return `Hello, ${this.name}` } const person = { name: 'Hoge' }; const boundGreet = greet.bind(person); boundGreet() > 'Hello, Hoge'
以上がthisを明示的に指定する3つのメソッドです。
また、アロー関数の場合はthisを持たないため、上記3つのメソッドによる束縛はできません。
まとめ
ざっくりにはなってしまいましたが、以上がthisの挙動仕様になります。
この辺りの仕様が曖昧なままだとthisの取り扱いミスによるバグの混入にも繋がってきそうなので、良い復習になりました。
また、アロー関数時の挙動の違いは認識できていなかったのでその点も学びでした。
また余裕があれば、束縛メソッド等の使用によるメリットデメリットなどもう少し踏み込んだ内容も調べていければと思います。
ここまでお読みいただき、ありがとうございます。
弊社ではエンジニアを募集しております。
この記事を読んで少しでも興味を持ってくださった方は、ぜひカジュアル面談でお話ししましょう!
下記リンクよりご応募お待ちしております!
iimon採用サイト / Wantedly / Green
参考文献・記事
https://qiita.com/kenny_J_7/items/9fd18834c8bc0bb3c576
外村将大『独習JavaScript 新版』, 2023, 翔泳社
鈴木僚太『プロを目指す人のためのTypeScript入門』, 2024, 技術評論社