JavaScriptのthisキーワードを整理してみた
ポート株式会社、新卒1年目のruiy03です。現在、実務を通じて様々な技術をキャッチアップしています。その中で、JavaScriptのthisの挙動を勉強中につまずいたので、改めて調べてみました。
すると、ES6以降はthisを避けて書けるケースがかなり増えていて、Reactの関数コンポーネントやVue Composition APIなど、thisを使わない設計が主流になりつつあるようです。
とはいえ、ライブラリの内部コードを読んだり、少し複雑なDOM操作をVanilla JSで書いたり、あるいはクラス構文ベースのレガシーコードを改修する場面では、依然としてthisの理解が求められます。
うっかりハマりやすいthisの挙動について、改めて整理しておきます。
定義場所ではなく「呼び出し方」
JavaScriptのthisが他言語経験者を混乱させる最大の要因は、「どこに定義されたか」ではなく「どう呼び出されたか」によって値が決まる点です。
function showName() {
console.log(this.name);
}
const tanaka = { name: "田中", showName };
const yamada = { name: "山田", showName };
tanaka.showName(); // "田中"
yamada.showName(); // "山田"
同じ関数でも、どのオブジェクトから呼んだかで結果が変わります。
4つのバインディングルール
複雑そうに見えますが、結局は4パターンです。優先順位が高い順に並べると:
-
new呼び出し → 新しく作られたオブジェクト -
call/apply/bind→ 明示的に指定されたオブジェクト -
obj.method()呼び出し → ドットの左側のオブジェクト -
上記以外(単独呼び出し) → グローバルオブジェクト(非厳格モード)または
undefined(厳格モード)
const suzuki = { name: "鈴木" };
function greet() {
console.log(this.name);
}
// ルール4: 関数としての単独呼び出し
// 現代の環境(Strict mode)なら undefined になりエラーになることが多い
greet();
// ルール3: メソッド呼び出し
// ドットの左側(suzuki)がthisになる
suzuki.greet = greet;
suzuki.greet(); // "鈴木"
// ルール2: callで明示的な指定
greet.call({ name: "佐藤" }); // "佐藤"
// ルール1: new 演算子
// 生成されるインスタンス自体がthisになる
function User(name) {
this.name = name;
}
const yamamoto = new User("山本");
console.log(yamamoto.name); // "山本"
call / apply / bind
「どうしてもこのオブジェクトをthisとして扱いたい」という場合に使います。
function introduce(greeting) {
console.log(`${greeting}、${this.name}です`);
}
const member = { name: "伊藤" };
introduce.call(member, "こんにちは"); // "こんにちは、伊藤です"
introduce.apply(member, ["どうも"]); // "どうも、伊藤です"
const boundIntro = introduce.bind(member);
boundIntro("やあ"); // "やあ、伊藤です"
callとapplyは即実行、bindは新しい関数を返します。callは引数を個別に、applyは配列で渡します。
コールバックでthisが消える問題
オブジェクトのメソッドをコールバック関数として渡すと、thisの情報が消失します。
const counter = {
count: 0,
increment() {
this.count++;
console.log(this.count);
}
};
counter.increment(); // 1(これは動く)
// コールバックとして渡すと...
setTimeout(counter.increment, 100); // NaN
なぜ動かないのか?
setTimeout(counter.increment, 100)と書いたとき、渡しているのはincrementという関数そのものです。「counterのメソッドである」という情報は渡されません。そのため、setTimeoutの内部で実行されるときには「単なる関数」として実行され(ルール4)、thisがundefined(またはグローバル)になってしまいます。
回避策
いくつか回避策がありますが、今はアロー関数を使うのが一般的です。
const obj = {
name: "中村",
// ES5以前: 変数にthisを退避
greetOld() {
var self = this;
setTimeout(function() {
console.log(self.name);
}, 100);
},
// ES5: bind()でthisを固定した新しい関数を渡す
greetBind() {
setTimeout(function() {
console.log(this.name);
}.bind(this), 100);
},
// ES6以降: アロー関数を使う(推奨)
greetArrow() {
setTimeout(() => {
console.log(this.name);
}, 100);
}
};
アロー関数のthisは特別
アロー関数には「自身のthisを持たない」という重要な特徴があります。アロー関数内のthisは、関数が定義されたスコープのthisを参照します(レキシカルスコープ)。
定義された場所のthisをそのまま引き継ぎ、後からcallやbindで変えることもできません。
const timer = {
seconds: 0,
start() {
// ここのthisはtimerオブジェクト
setInterval(() => {
// アロー関数なので、外側(startメソッド)のthisをそのまま使う
this.seconds++;
console.log(this.seconds);
}, 1000);
}
};
timer.start(); // 1, 2, 3, ... と正常に動作する
従来のfunctionだと、setIntervalのコールバック内でthisがwindowやundefinedになってしまう。アロー関数なら気にしなくていいです。
クラスとコンストラクター
コンストラクター
newで呼び出すと、thisは新しく作られるオブジェクトを指します。
function User(name) {
this.name = name;
}
const sato = new User("佐藤");
console.log(sato.name); // "佐藤"
クラス
クラスのメソッド内では、thisはそのインスタンスを指します。一方、staticメソッド内では、クラス自体を指します。
class Counter {
constructor(initial) {
this.value = initial;
}
increment() {
this.value++;
}
static create() {
return new this(0); // thisはCounterクラス
}
}
クラスでもコールバック問題は起きる
class Player {
constructor(name) {
this.name = name;
this.score = 0;
}
// 通常のメソッド → コールバックに渡すとthisが消える
addScore() {
this.score += 100;
}
// アロー関数で定義 → thisが固定される
addBonus = () => {
this.score += 50;
};
}
const p = new Player("田村");
// 通常メソッドは関数だけ渡すとthisが消える
setTimeout(p.addScore, 100); // エラーまたは意図しない動作
// アロー関数で定義したプロパティなら安全
setTimeout(p.addBonus, 100); // 正常動作
イベントハンドラとして渡すなら、アロー関数で定義するか、bindしておくのが安全です。
実際の活用例
Vanilla JS
DOMイベントハンドラー
addEventListenerに渡した通常の関数では、thisはイベントが発生した要素になります。
document.querySelectorAll(".tab").forEach(tab => {
tab.addEventListener("click", function() {
// thisはクリックされたtab要素
this.classList.add("active");
});
});
アロー関数を使う場合は、event.currentTargetで要素を取得します。
document.querySelectorAll(".tab").forEach(tab => {
tab.addEventListener("click", (e) => {
e.currentTarget.classList.add("active");
});
});
メソッドチェーン
thisを返せばメソッドチェーンできます。
const calc = {
value: 0,
add(n) {
this.value += n;
return this;
},
multiply(n) {
this.value *= n;
return this;
},
result() {
return this.value;
}
};
const answer = calc.add(5).multiply(3).add(10).result();
console.log(answer); // 25
React
クラスコンポーネント(レガシー)
class Counter extends Component {
state = { count: 0 };
// アロー関数で定義してthisを固定
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return <button onClick={this.handleClick}>{this.state.count}</button>;
}
}
通常のメソッドで書くと、onClickに渡した時点でthisが消えます。アロー関数で定義するか、constructorでbindする必要があります。
関数コンポーネント
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
{count}
</button>
);
}
Hooksの登場により、thisを意識する必要はなくなりました。
TypeScript
thisパラメータ
TypeScriptには、関数の第一引数にthisの型を指定できる機能があります(コンパイル後のJSからは消えます)。
function handleClick(this: HTMLButtonElement, e: MouseEvent) {
console.log(this.textContent); // thisはHTMLButtonElement型
}
noImplicitThis
noImplicitThisはthisの型注釈を必須にするコンパイラオプションです。strictオプションに含まれているため、strictを有効にすると自動的にtrueになります。
このオプションを有効にすると、thisの型が不明な場合にエラーになります。
function lengthOfDiagonal(): number {
// エラー: thisの型が不明(any扱い)
return (this.width ** 2 + this.height ** 2) ** (1 / 2);
}
thisパラメータで型を明示すれば解決できる:
type Area = {
width: number;
height: number;
diagonal(): number;
};
function lengthOfDiagonal(this: Area): number {
return (this.width ** 2 + this.height ** 2) ** (1 / 2);
}
一方、オブジェクトリテラルのメソッド内では、thisの型が推論されるためエラーにならない:
const user = {
name: "田中",
show() {
console.log(this.name); // OK: thisの型が推論される
}
};
まとめ
-
thisは「定義場所」ではなく「呼び出し方」で決まる - 基本優先順位は
new>call/bind>obj.method> 単独 - コールバックに渡すと
thisが消える → アロー関数かbindで対処 - アロー関数は外側の
thisを引き継ぐ - モダンなフレームワークでは、
thisの出番は減っている
補足
コンストラクターでオブジェクトをreturnした場合
コンストラクター内でオブジェクトを明示的にreturnすると、newで作られるオブジェクトの代わりにそちらが返されます。
function User(name) {
this.name = name;
return { name: "上書き" };
}
const user = new User("佐藤");
console.log(user.name); // "上書き"
配列メソッドのthisArg
filter、map、forEachなどは、第2引数でthisを指定できます。
const rule = {
min: 18,
isAdult(user) {
return user.age >= this.min;
}
};
const users = [
{ name: "Aさん", age: 25 },
{ name: "Bさん", age: 16 }
];
// 第2引数でthisを指定
const adults = users.filter(rule.isAdult, rule);
今はアロー関数で書くことが多いですね。
const adults = users.filter(u => rule.isAdult(u));
Discussion
currentTargetは イベントハンドラを登録した要素なのでthisに対比させるならばtargetではないでしょうか?