JavaやKotlin、Swiftなど、多くのオブジェクト指向言語では、this はクラス内メソッドなどで利用され、そのインスタンス自身を指します。
JavaScriptでは挙動が異なります。
本記事では、JavaScriptにおける this の仕様をまとめます。
りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ. 言語・環境編】を参考にしています。
めちゃくちゃ良書で、JavaScript・TypeScript、Reactについて深堀りされているのでおすすめの本です!
this に関するおすすめルール
りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ. 言語・環境編】では、下の2つを守ることが推奨されています。
-
this
はClass構文内でしか使わない - Class構文内では、メソッドを含めたあらゆる関数の定義をアロー関数式で行う
その理由を、this やClass構文の内部的な意味合い、アロー関数式の挙動を踏まえて解説します。
this は暗黙の関数引数
JavaScriptでは、this は暗黙的に関数の引数として受け取った、実行時のコンテキストであるオブジェクトそのもの、と言えます。
「???」
という感じだと思うので以下説明します。
例えば、Javaでは以下のようにthisを使うと思います。
class Person {
String name;
Person(String name) {
this.name = name;
}
void greeting() {
println("Hi", this.name);
}
}
上記ケースでは、 greeting
はクラス内メソッドであり、その中で利用されている this は、 greeting
関数の引数として実行インスタンスが暗黙的に渡されているとも解釈できます。
ですが、わざわざそんな回りくどい説明をする必要はなく、単に実行インスタンス自身を指したものが this である、と説明した方が圧倒的に分かりやすいです。
では、JavaScriptでは何故単純に実行インスタンスを指していると言わず、実行時コンテキストなのか?
JavaScriptでは、this はクラス内メソッド以外の普通の関数でも参照でき、呼び出す状況によって参照先が変わるからです。
例えば、以下のケース。
function myFunc() {
console.log(this)
}
myFunc() // -> Window {window: Window {...}, self: Window ...}
Class内メソッドではなく、剥き出しの関数を呼んでいるだけですが、その内部で this を呼び出せています。
console.log
の出力は Window
になっており、これはブラウザ上のJavaScriptでは最上位のオブジェクトを意味しています。(ブラウザ上JavaScriptで扱うすべてのデータはこの Window
オブジェクトに属する)
つまり、実行側の実行コンテキストがデフォルトでは、最上位オブジェクトでは Window
になっているわけです。
(Node.js環境だと Object [global]
になります)
ちなみに、実行コンテキストは意図的に呼び手が改変して明示的に渡すこともできます。
function myFunc() {
console.log(this)
}
myFunc.call({foo: "FOO"}) // -> {foo: "FOO"}
call
関数は、 Function
オブジェクトのプロトタイプメソッドで、通常暗黙的に渡される this 引数を明示的に渡すことができます。
new と this の関係
this を説明する上で、 new
は深く関係しています。
クラスのインスタンスを作成するものとしてオブジェクト指向プログラミングではお馴染みのnew
ですが、JavaScriptでは少し異なる動きをします。
JavaScriptでは、 new
を実行すると、そのプロトタイプオブジェクトを作り、それをコンストラクタの引数に this として暗黙的に与えます。
ひとつずつ解説します。
まず、プロトタイプオブジェクトとは、FunctionやArrayオブジェクトが持つプロパティです。
オブジェクトごとに設定されているデフォルトオブジェクトみたいなもの。
console.log(Array.prototype) // -> []
console.log(Object.prototype) // -> {}
console.log((function () {}).prototype) // -> {}
そして、 new
をすることで、対象のプロトタイプオブジェクトを作った上で、そのコンストラクタ関数内での this は作られたプロトタイプオブジェクトを指し示すことになります。
具体的には以下のようなこと。
function Animal(name) {
// this -> Animal Function の prototype (= {}) が暗黙的に引数として与えられる。
this.name = name;
};
const tiger = new Animal('Tiger');
console.log(tiger); // -> Animal{ name: 'Tiger'}
上記のように Animal
に対して new することで、関数内で this としてプロトタイプオブジェクト( {}
)が与えられています。
つまり上記の例では、与えられた this ( {}
)に対して、 name
というプロパティを追加している、ということになります。
ご存じの通り、 new
はClassからインスタンスを作るために使わるのが一般的かと思います。
ですが、JavaScriptでは上記の通り、 new
は関数を呼び出すのに使います。
もちろん、 new SomeClass()
というように、Classに対して new
を使うのがJavaScriptでも普通ですよね?
これは、Classが特別なわけではなく、JavaScriptでの Class の実体はただの関数表現のシンタックスシュガー、だからです。
class AnimalClass {
constructor(name) {
this.name = name
}
}
function AnimalFunc(name) {
this.name = name
}
// 同じ
const lionClass = new AnimalClass('Lion')
const lionFunc = new AnimalFunc('Lion')
console.log(lionClass.name) // -> Lion
console.log(lionFunc.name) // -> Lion
上記は、Classを class
を使って表現した AnimalClass
と、 function
を使って表現した AnimalFunc
を比べたものです。
見た目は違いますが、挙動は同じように動いています。
.
で連結されたメソッド内で実行された時の this
const someObj = {
name: "Some Object",
run: function() {
console.log(this)
}
}
someObj.run() // -> {name: "Some Object", run: ƒ run()}
上記のケースでは、 run
に暗黙的に渡されている引数 this は、 .
で連結されている someObj
自体を指しています。
これは、Javaなどの他のオブジェクト指向言語でのインスタンス参照としての this と似ていて理解しやすいですね。
strict mode と this の関係
冒頭に書いた通り、単に関数の中で this を参照するなど、と、グローバル変数として機能して、最上位オブジェクトである Window
(ブラウザ環境) or Object [global]
(Node.js環境)が参照されます。
これを利用すると、簡単にグローバル変数の汚染ができてしまいます。
function myFunc() {
this.AAA = "Dummy AAA..."
console.log(this)
}
myFunc()
// (出力)
// <ref *1> Object [global] {
// (略)
// AAA: 'Dummy AAA...' <--- グローバル変数に好きなプロパティを追加できてしまった!!
// }
これを防ぐために、 strict mode というものがES2015から実装されています。
function myFunc() {
'use strict' // <--- strict mode を指定
this.AAA = "Dummy AAA..."
console.log(this)
}
myFunc() // -> TypeError: Cannot set properties of undefined (setting 'AAA')
これを利用すると、上記の通り this が undefined になり、グローバル変数としての this にアクセスすることを防いでくれます。
正確には、実行コンテキストが存在しない場合、 this が undefined になります。
これはグローバル変数としての this へのアクセスを防ぐだけなので、 new
での暗黙的 this の注入は参照できます。
new myFunc() // -> myFunc { AAA: 'Dummy AAA...' }
Class は単なる関数のシンタックスシュガーですが、Class には自動的に strict mode になり、更に new しないと呼び出せない、という特徴があります。
よって、Class の方はいちいち strict mode にする必要がない点で安全で便利と言えます。
クラスメソッド内関数での this に起きる不都合
以下のようなClassを使ったコードを実行するとエラーになります。
class MyClass {
constructor(param) {
this.param = param;
}
run() {
function _run() {
console.log(`this.param: ${this.param}`)
}
_run()
}
}
const myClass = new MyClass("HELLO")
myClass.run()
// -> TypeError: Cannot read properties of undefined (reading 'param')
エラーの理由は、
-
run()
実行時にコンテキストが何もないので、_run()
には暗黙的に最上位オブジェクトが this として入ろうとする。 - Class内では自動的に
strict mode
になっている。 -
_run
関数内で this にアクセスは出来なくなりエラー。
上記のコードは、Javaなどの一般的なオブジェクト指向言語からすると、 _run()
内の this もクラスのインスタンス(上記でいうと myClass
)を参照することを期待しそうで、感覚的にエラーが理解しにくいと感じます。
これを回避するにはいくつかの方法がありますが、最も他のオブジェクト指向言語の感覚と似た形で解決する方法は、アロー関数を使うようにすることです。
アロー関数での this の扱いの特徴
アロー関数には、通常の関数と違い、以下のような特徴があります。
- 暗黙的引数としての this を持たない
- this アクセスするときは関数の外のスコープの this を参照する
先ほどの例をアロー関数で書き直します。
class MyClass {
constructor(param) {
this.param = param;
}
run() {
const _run = () => {
console.log(`this.param: ${this.param}`)
}
_run()
}
}
const myClass = new MyClass("HELLO")
myClass.run()
// -> this.param: HELLO
上記のように、アロー関数によって一般的なオブジェクト指向言語の感覚と一致した理解しやすい動きになりました。
細かく説明すると、 _run()
はアロー関数なので、暗黙的引数の this を持ちません。その代わりにその外側である、 run()
のスコープの this を参照します。
run()
の this は、「.
で連結されたメソッド内で実行された時の this」の扱いとなり、 myClass.run()
という呼び方からも、 this と myClass
は等しくなってくれます。
まとめ
以上を踏まえて、りあクト! TypeScriptで始めるつらくないReact開発 第3.1版【Ⅰ. 言語・環境編】では、最初に挙げた下の2つを守ることが推奨されているわけです。
🙇♂️
-
this
はClass構文内でしか使わない - Class構文内では、メソッドを含めたあらゆる関数の定義をアロー関数式で行う
これまで、上記のようなルールを聞いたことがある人は多いかもしれませんが、ここまでの流れを読めばその理由を本当に理解することができるかと思います。