JavaScriptのコンストラクタ関数で使用されるthisについて、関数の定義方法やメソッドの呼び出し方法でthisの値が変わります。thisの挙動を理解するのが難しかったため、イメージを掴めるようにしました。そのため、ここで記載するthisの挙動は厳密なものではなく、あくまでイメージをつかむためのものなので、実際の挙動と異なる部分があるかと思いますがご了承ください。
thisの値の定義方法
まず重要なのが、アロー関数の場合とそれ以外の関数定義によってthisの値の定義方法が異なることです。アロー関数はES2015から導入された関数定義方法なので、アロー関数だけ挙動が異なると覚えておけば大丈夫です。
- 関数宣言文と関数リテラル:そのメソッドが実行された時点で所属しているオブジェクト
- アロー関数:アロー関数が定義された時点で所属しているオブジェクト
文章だとわかりづらいですが、スコープチェーンのような挙動です。スコープチェーンを理解すればthisの挙動が理解しやすくなります。
スコープチェーン
スコープチェーンとは、変数をブロックの一番内側から探索し、なければ一つ外側のブロックから探索、それを繰り返して最初に見つかった変数を使用することです。それにより、関数の外で変数が定義していても関数の中からその変数を参照することができます。
function outerFunc() {
let outerVal = "outer";
function innerFunc() {
let innerVal = "inner";
console.log(innerVal);
console.log(outerVal);
}
innerFunc();
}
outerFunc();
実行結果
inner
outer
outerFunc関数を実行すると、
- outerVal = "outer" を定義
- innerFunc関数を実行
- innerVal = "inner"を定義
- console.logでinnerValとouterValの値を出力
と処理されます。innerFunc関数ブロックの中にはouterVal変数は定義されていませんが、innerFunc関数の外側のouterFunc関数ブロックからouterVal変数を参照しています。
innerFunc関数の中でouterVal変数を変更すると、内側のブロックの変数が参照されます。
function outerFunc() {
let outerVal = "outer";
function innerFunc() {
let innerVal = "inner";
outerVal = "inner"; //innerFunc関数の中で outerValを書き換え
console.log(innerVal);
console.log(outerVal);
}
innerFunc();
}
outerFunc();
実行結果
inner
inner
逆に、ブロックの内側の変数は参照できません。
function outerFunc() {
let outerVal = "outer";
function innerFunc() {
let innerVal = "inner";
}
console.log(innerVal); // 外から関数スコープ内の変数を参照できない
}
outerFunc();
この挙動を理解しておけば、thisの挙動も理解しやすいです。
各関数のthisの挙動
スコープチェーンの挙動を念頭に、thisの挙動を確認してみます。
thisの基本的な考え方
上述のとおり、thisは関数の定義方法によって異なります。また、関数宣言文と関数リテラルで定義した場合については、「グローバルスコープから実行した」という条件があります。
関数の定義方法 | thisの定義タイミング |
---|---|
関数宣言文、関数リテラル | 関数をグローバルスコープから実行した時 |
アロー関数 | 関数を定義した時 |
それぞれの関数の宣言方法のthisの定義のイメージは下記の通り。
// 関数宣言文
function fn1() {
this; // 関数定義時はthisの値は定義されない
}
// 関数リテラル
const fn2 = function() {
this; // 関数定義時はthisの値は定義されない
}
// アロー関数
arrowFunc = () => {
this = window; // 関数定義時に暗黙的にthisが定義される
};
function outerFn() {
this; // 関数定義時はthisは定義されない
function innerFn() {
this; // 関数定義時はthisは定義されない
}
innerFn();
}
outerFn(); // 下記のように実行される
this = window; // 関数実行時にthisが設定される
innerFn(); //innerFnは下記のように実行される
this; // グローバルスコープ以外からでは値は定義されない。
// スコープチェーンでthis = windowとなる
このイメージを持っておくと、コールバック関数でのthisの挙動がわかりやすいです。
Strict Modeか否かで関数定義時のthisの値は異なりますが、今回は説明を省略します。
コールバック関数とthis
例えば、下記のようにコンストラクタ関数からオブジェクトを定義し、introductionメソッドを直接呼び出す場合とコールバック関数として呼び出す場合を比べます。
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
this.introduction = function () {
console.log(`Hello, I'm ${this.name}.`);
};
}
function fn(callback) {
callback();
}
let tanaka = new Person("tanaka", 30, "Tokyo");
tanaka.introduction();
fn(tanaka.introduction);
実行結果
(直接呼び出しの結果)
Hello, I'm tanaka.
(コールバック関数での結果)
Hello, I'm .
グローバルスコープから呼び出す場合はintroductionメソッドが実行された時に所属しているオブジェクトがthisの値になります。今回メソッドが所属しているオブジェクトはPersonコンストラクタで作成されたtanakaオブジェクトなのでthisの値は
this = tanaka
となります。コードの実行イメージは下記の通りです。
tanaka.introduction(); // 下記のように実行される
this = tanaka;
console.log(`Hello, I'm ${this.name}.`);
// tanaka.name = "tanaka" をテンプレートリテラルで代入
一方、コールバック関数としてintroductionメソッドを実行した場合、関数スコープ内からのメソッドの実行なので、thisの値は設定されません。実行イメージは下記のようになります。
fn(tanaka.introduction); // 以下のように実行される
this = window; // fn関数はwindowオブジェクト
tanaka.introduction(); // 下記のように実行される
this; // ブロックスコープからの実行のためthisが定義されない
// スコープチェーンでthis = windowとなる
console.log(`Hello, I'm ${this.name}.`); // window.nameは未定義
コールバック関数として呼び出した関数のthisはスコープチェーンにより、
this = window
となります。window.nameは定義されていないため、${this.name}には何も代入されず、
Hello, I'm .
と意図しない表示になります。このように、スコープチェーンのように考えると、thisの値を追いやすくなります。
また、thisの値を変えたくない場合、bindメソッドを使うとthisの値を固定できます。今回の場合、thisをtanakaから変更したくないため、
fn(tanaka.introduction.bind(tanaka))
とすることで、thisの値をtanakaに固定することができ、実行結果は、
Hello, I'm tanaka.
と、意図した動作となります。
他にもapplyメソッドとcallメソッドもthisを意図的に設定する方法ですが、今回は説明を省略します。
アロー関数の場合
アロー関数は、関数定義時にthisの値が決定します。上記の例のintroductionメソッドをアロー関数で定義します。
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
this.introduction = () => console.log(`Hello, I'm ${this.name}.`);
}
function fn(callback) {
callback();
}
let tanaka = new Person("tanaka", 30, "Tokyo");
fn(tanaka.introduction);
この実行結果は、
Hello, I'm tanaka.
となります。アロー関数の定義時に
this = tanaka
とthisが定義されるので、関数宣言文などのようにbindメソッドを使わなくてもthisの値を意図したものにすることができます。
thisについて勉強して感じたこと
関数の宣言方法はできるだけ統一したほうが良く、コンストラクタの中でのメソッドの定義は明確にルールを決めて記述するのがバグを抑えるポイントだと思いました。特に、アロー関数か否かでthisの定義が変わってしまうので、それを知らずに使うとthisの部分でエラーが出てしまうのが要注意ポイントだと思いました。