JavaScriptの基礎 - thisキーワード
概要
JavaScriptにおける this キーワードは、関数が「どのように呼び出されたか」によって参照先が動的に変化する特殊なキーワードである。
他の多くのオブジェクト指向言語とは異なり、this は「定義時」ではなく「呼び出し時」に決定される。(アロー関数を除く)
this は、関数に暗黙的に渡される引数と考えることができる。
グローバルコンテキストではウィンドウオブジェクトやグローバルオブジェクトを参照し、メソッド内ではそのメソッドが属するオブジェクトを参照し、コンストラクタ内では生成されたインスタンスを参照する。
this の参照先は、call、apply、bind の3つのメソッドを使用して明示的に指定することが可能である。
これらのメソッドを活用することで、関数の再利用性を高めたり、コールバック関数内での this の消失問題を解決することができる。
ES2015で導入されたアロー関数は自身の this を持たず、定義されたスコープの外側の this を参照するレキシカルthisの性質を持つ。
この性質により、コールバック内での this の消失問題を簡潔に解決できる。
現代のReact開発では、関数コンポーネントとHooksの普及により this を意識する場面は大幅に減少しているが、
レガシーコードの理解やクラスベースの設計を扱う時には this の動作を正しく理解しておくことが不可欠である。
thisの基本的な挙動
this の参照先は、関数が呼び出されるコンテキストによって決まる。
以下に、主要な呼び出しコンテキストと対応する this の参照先を示す。
グローバルコンテキスト
スクリプトの最上位レベル (いずれの関数にも属さない場所) で this を参照すると、グローバルオブジェクトを指す。
- Webブラウザ環境
thisは、windowオブジェクトを参照する。
- Node.js環境
thisは、globalThisオブジェクトを参照する。
// Webブラウザ環境
console.log(this === window); // true
// Node.js環境
console.log(this === globalThis); // true
関数内のthis
通常の関数を呼び出した場合、this の参照先は strictモードの有無によって異なる。
- Non-strictモード (デフォルト)
thisは、グローバルオブジェクトを参照する。
- Strictモード (
"use strict"または ESモジュール)thisは、undefinedになる。
// Non-strictモード
function showThis() {
console.log(this); // window (ブラウザ) または globalThis (Node.js)
}
showThis();
// Strictモード
"use strict";
function showThisStrict() {
console.log(this); // undefined
}
showThisStrict();
メソッド内のthis
オブジェクトのメソッドとして呼び出された場合、this はそのメソッドが属するオブジェクトを参照する。
const person = {
name: "太郎",
greet: function() {
console.log("こんにちは、" + this.name);
}
};
person.greet(); // "こんにちは、太郎" (thisはpersonを参照する)
メソッドを変数に代入して呼び出すと、this が消失する点に注意が必要である。
これは、メソッドを変数に代入した時点でオブジェクトとの関連が切れ、通常の関数呼び出しとして扱われるためである。
const person = {
name: "太郎",
greet: function() {
console.log("こんにちは、" + this.name);
}
};
person.greet(); // "こんにちは、太郎"
const greetFn = person.greet;
greetFn(); // "こんにちは、undefined" (thisが消失する)
コンストラクタ内のthis
new 演算子を使用して関数を呼び出すと、this は新しく生成されたインスタンスを参照する。
function Person(name, age) {
this.name = name; // 生成されるインスタンスのnameプロパティに代入される
this.age = age;
this.greet = function() {
console.log("こんにちは、" + this.name + "です");
};
}
const taro = new Person("太郎", 25);
const hanako = new Person("花子", 30);
taro.greet(); // "こんにちは、太郎です"
hanako.greet(); // "こんにちは、花子です"
bind / call / apply
call、apply、bind は、関数の this を明示的に指定するためのメソッドである。
これらを活用することで、this の消失問題を解決したり、異なるオブジェクトに対して同じ関数を再利用したりすることができる。
call
call メソッドは、this を束縛して関数を即座に実行する。
引数は個別に指定する。
基本構文を以下に示す。
関数名.call(thisArg, arg1, arg2, ...);
使用例を以下に示す。
function greet(greeting, punctuation) {
return greeting + "、" + this.name + punctuation;
}
const person = { name: "太郎" };
const result = greet.call(person, "こんにちは", "!");
console.log(result); // "こんにちは、太郎!"
apply
apply メソッドは、this を束縛して関数を即座に実行する。
call との違いは、引数を配列として指定する点である。
基本構文を以下に示す。
関数名.apply(thisArg, [argsArray]);
使用例を以下に示す。
function greet(greeting, punctuation) {
return greeting + "、" + this.name + punctuation;
}
const person = { name: "太郎" };
const args = ["こんにちは", "!"];
const result = greet.apply(person, args);
console.log(result); // "こんにちは、太郎!"
// 配列の要素を個別の引数として渡す用途にも使用される
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
const max = Math.max.apply(null, numbers);
console.log(max); // 9
bind
bind メソッドは、this を束縛した新しい関数を返す。
call や apply と異なり、即座に関数を実行しない。
基本構文を以下に示す。
const boundFn = 関数名.bind(thisArg, arg1, ...);
使用例を以下に示す。
function greet(greeting, punctuation) {
return greeting + "、" + this.name + punctuation;
}
const person = { name: "太郎" };
// thisを束縛した新しい関数を生成する
const boundGreet = greet.bind(person);
// 後から呼び出す
console.log(boundGreet("おはよう", "。")); // "おはよう、太郎。"
console.log(boundGreet("こんばんは", "!")); // "こんばんは、太郎!"
// 引数も部分適用できる (カリー化)
const morningGreet = greet.bind(person, "おはよう");
console.log(morningGreet("。")); // "おはよう、太郎。"
bindされた関数に対して再度 call や apply で this を変更しようとしても、bindが優先されるため変更できない。
call / apply / bindの比較
下表に、3つのメソッドの違いを示す。
| メソッド | 実行時期 | 引数の形式 | 主な用途 |
|---|---|---|---|
call |
即座に実行 | 個別に指定 | 単発の関数呼び出し |
apply |
即座に実行 | 配列として指定 | 配列を引数として展開 |
bind |
新しい関数を返す | 個別に指定 | イベントハンドラ、コールバック |
3つのメソッドを使用した同一処理の比較を以下に示す。
function introduce(job, city) {
return this.name + "は" + job + "で、" + city + "に住んでいます";
}
const person = { name: "太郎" };
// call: 即座に実行、引数を個別に渡す
introduce.call(person, "エンジニア", "東京");
// apply: 即座に実行、引数を配列で渡す
introduce.apply(person, ["エンジニア", "東京"]);
// bind: 新しい関数を返す、後から呼び出す
const boundIntroduce = introduce.bind(person);
boundIntroduce("エンジニア", "東京");
アロー関数のthis
アロー関数は、通常の関数とは異なる this の動作を持つ。
アロー関数は自身の this バインディングを持たず、定義された場所の外側のスコープの this を参照する。
レキシカルthis
アロー関数の this は、定義時のスコープ (レキシカルスコープ) に基づいて決定される。
この性質を レキシカルthis と呼ぶ。
通常の関数とアロー関数の this の違いを以下に示す。
const person = {
name: "太郎",
hobbies: ["読書", "映画", "料理"],
// 通常の関数: thisはコールバック内で消失する
listHobbiesWithFunction: function() {
this.hobbies.forEach(function(hobby) {
// thisはwindow (またはundefined) を参照してしまう
console.log(this.name + "は" + hobby + "が好き"); // undefinedは映画が好き
});
},
// アロー関数: 外側のthis (= person) を参照する
listHobbiesWithArrow: function() {
this.hobbies.forEach(hobby => {
// thisは外側のメソッドのthis (= person) を引き継ぐ
console.log(this.name + "は" + hobby + "が好き"); // 太郎は映画が好き
});
}
};
person.listHobbiesWithArrow();
// "太郎は読書が好き"
// "太郎は映画が好き"
// "太郎は料理が好き"
アロー関数には call、apply、bind で this を変更することはできない。
常に定義時のレキシカルスコープの this を使用する。
const arrowFn = () => {
console.log(this);
};
const obj = { name: "太郎" };
// call / apply / bind を使用しても thisは変わらない
arrowFn.call(obj); // グローバルオブジェクト (thisは変わらない)
arrowFn.apply(obj); // グローバルオブジェクト (thisは変わらない)
arrowFn.bind(obj)(); // グローバルオブジェクト (thisは変わらない)
メソッドでのアロー関数の注意点
オブジェクトのメソッドとしてアロー関数を使用すると、this がそのオブジェクトを参照しない問題が発生する。
アロー関数が定義された時点の外側のスコープ (多くの場合はグローバルスコープ) の this を引き継ぐためである。
const obj = {
name: "太郎",
// アロー関数をメソッドとして定義した場合 (非推奨)
greetArrow: () => {
// thisはグローバルオブジェクトを参照する (objではない)
console.log(this.name); // undefined
},
// 通常の関数をメソッドとして定義した場合 (推奨)
greetFunction: function() {
// thisはobjを参照する
console.log(this.name); // "太郎"
}
};
obj.greetArrow(); // undefined
obj.greetFunction(); // "太郎"
オブジェクトのメソッドを定義する場合は、通常の関数 (または ES2015のメソッド短縮記法) を使用することを推奨する。
アロー関数は、メソッド内のコールバックとして使用する場面に適している。
クラスにおけるthis
ES2015で導入されたクラス構文においても、this の挙動は通常の関数と同様である。
クラスメソッド内の this はインスタンスを参照するが、メソッドをコールバックとして渡した場合は this が消失する問題が発生する。
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(this.count);
}
}
const counter = new Counter();
counter.increment(); // 1 (thisはcounterを参照する)
// メソッドをコールバックとして渡すとthisが消失する
const fn = counter.increment;
fn(); // エラー: Cannot read properties of undefined (reading 'count')
この問題を解決する方法は以下の3つがある。
- 方法1 : コンストラクタでbindする
- コンストラクタ内で
thisを束縛した新しい関数を生成して、同名のプロパティに代入する。 class Counter { constructor() { this.count = 0; // コンストラクタ内でbindする this.increment = this.increment.bind(this); } increment() { this.count++; console.log(this.count); } } const counter = new Counter(); const fn = counter.increment; fn(); // 1 (thisはcounterを参照する)
- コンストラクタ内で
- 方法2 : クラスフィールドでアロー関数を使用する (推奨)
- クラスフィールドとアロー関数を組み合わせると、インスタンスごとに
thisが固定されたメソッドを定義できる。 class Counter { count = 0; // クラスフィールド // アロー関数でメソッドを定義するとthisが固定される increment = () => { this.count++; console.log(this.count); }; } const counter = new Counter(); const fn = counter.increment; fn(); // 1 (thisは常にcounterを参照する)
- クラスフィールドとアロー関数を組み合わせると、インスタンスごとに
- 方法3 : インラインアロー関数でラップする
- コールバックを渡す箇所でアロー関数でラップする。
class Counter { count = 0; increment() { this.count++; console.log(this.count); } } const counter = new Counter(); // アロー関数でラップすることでthisを維持する document.addEventListener("click", () => counter.increment());
Reactでthisが問題にならない理由
現代のReact開発では、this に関する問題を意識する場面は大幅に減少している。
その背景にあるクラスコンポーネント時代の問題と、関数コンポーネントによる解決を以下に示す。
クラスコンポーネント時代のthis問題
React 16.8以前では、クラスコンポーネントが主流であった。
クラスコンポーネントでは、イベントハンドラを onClick 等のPropsに渡す際に this が消失する問題が頻繁に発生していた。
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick() {
// thisが消失しているためthis.setStateはundefinedになる
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<button onClick={this.handleClick}>
{/* this.handleClickをonClickに渡すとthisが消失する */}
カウント: {this.state.count}
</button>
);
}
}
この問題に対する解決策を以下に示す。
- 解決策1 : コンストラクタでbindする
class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; // コンストラクタでbindする this.handleClick = this.handleClick.bind(this); } handleClick() { this.setState({ count: this.state.count + 1 }); } render() { return ( <button onClick={this.handleClick}> カウント: {this.state.count} </button> ); } }
- 解決策2 : クラスフィールドでアロー関数を使用する (推奨)
class Counter extends React.Component { state = { count: 0 }; // クラスフィールドとアロー関数でthisを固定する handleClick = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <button onClick={this.handleClick}> カウント: {this.state.count} </button> ); } }
関数コンポーネントでの解決
React 16.8で導入されたHooksにより、関数コンポーネントで状態管理が可能になった。
関数コンポーネントは this を全く使用しないため、this の消失問題が根本的に解消される。
import React, { useState } from "react";
// 関数コンポーネント (thisが不要)
function Counter() {
const [count, setCount] = useState(0);
// thisを使用しないため消失の問題が発生しない
const handleClick = () => {
setCount(count + 1);
};
return (
<button onClick={handleClick}>
カウント: {count}
</button>
);
}
下表に、クラスコンポーネントと関数コンポーネントの this に関する比較を示す。
| 項目 | クラスコンポーネント | 関数コンポーネント |
|---|---|---|
| thisの使用 | 必要 | 不要 |
| 状態管理 | this.state / this.setState |
useState Hook
|
| thisの消失問題 | 発生する | 発生しない |
| 解決策 | bind または クラスフィールド | Hooks (そもそも問題なし) |
| 現在の推奨 | レガシーコード | 現在のベストプラクティス |
現在のReactのベストプラクティスは関数コンポーネントとHooksを使用することであるが、
既存のレガシーコードを保守する場面ではクラスコンポーネントの this の動作を理解しておく必要がある。
関連情報
- JavaScriptの基礎 - アロー関数
- アロー関数の構文、暗黙のreturn、レキシカルthis
- JavaScriptの基礎 - コールバック関数
- コールバック関数、高階関数、タイマ、イベントリスナー
- JavaScriptの基礎 - オブジェクトリテラル
- オブジェクトの作成と操作、プロパティアクセス、メソッド