this の振る舞いを整理する — JavaScript Tips
this は定義場所ではなく、呼び出し方(call site) で決まります。メソッド・new・call / bind・アロー(レキシカル)の違いが、コールバックやイベントで this が消える理由になります。
参考(MDN)
目次
- this は呼び出し方で決まる
- 代表的な 4 パターン
- アロー関数の this
- クラスと this
- bind / call / apply
- コールバックで this が失われる問題
- うまくいかないとき
this は呼び出し方で決まる
多くの言語では this は「今のオブジェクト」に固定されますが、JavaScript では 関数がどう呼ばれたか で this が決まります(アロー関数は例外)。定義した場所ではなく、実行した瞬間の文脈 が重要です。
function showThis() { console.log(this);}showThis(); // 非 strict なスクリプトでは window(ブラウザ)や global(Node)同じ関数でも、呼び出し方を変えると this が変わります。
const obj = { name: "田中", show() { console.log(this.name); },};obj.show(); // "田中" — メソッド呼び出しconst fn = obj.show;fn(); // undefined またはエラー — 単なる関数呼び出し代表的な 4 パターン
1. 単なる関数呼び出し
function foo() { "use strict"; return this;}foo(); // strict モードでは undefinedモジュール(ESM)は自動的に strict 相当なので、トップレベル関数の this は undefined になりやすいです。
2. メソッド呼び出し
const counter = { value: 0, increment() { this.value += 1; return this.value; },};counter.increment(); // 1 — this は counter3. new 呼び出し
function Person(name) { this.name = name;}const p = new Person("佐藤");console.log(p.name); // "佐藤" — this は新しいインスタンス4. call / apply / bind による明示指定
function greet(greeting) { return `${greeting}, ${this.name}`;}const user = { name: "鈴木" };console.log(greet.call(user, "こんにちは")); // "こんにちは, 鈴木"アロー関数の this
アロー関数は 自身の this を持ちません。外側のレキシカルスコープの this をそのまま使います(lexical this)。
const timer = { seconds: 0, start() { setInterval(() => { this.seconds += 1; console.log(this.seconds); }, 1000); },};timer.start(); // this は timer を指す通常の関数をコールバックに渡すと this が変わります。
const broken = { seconds: 0, start() { setInterval(function () { // this は broken ではない(ブラウザでは window など) this.seconds += 1; }, 1000); },};アロー関数向き — 外側の this を保ちたいコールバック
通常の関数向き — メソッド定義、new で使うコンストラクタ、動的な this が必要なとき
const obj = { // メソッドは通常の関数が無難 method() { return this; }, // アローは this を外側から借りる — メソッドとしては非推奨なことが多い arrow: () => this,};クラスと this
クラスのメソッド内では、通常 this はインスタンスを指します。
class Stack { constructor() { this.items = []; } push(item) { this.items.push(item); } pop() { return this.items.pop(); }}const s = new Stack();s.push(1);console.log(s.pop()); // 1メソッドを変数に取り出すと メソッド呼び出しではなくなる ため、this が失われます。
const pop = s.pop;// pop(); // TypeError — this が undefined などbind / call / apply
bind — this を固定した新しい関数を返す
const user = { name: "山田", greet() { return `こんにちは、${this.name}`; },};const greet = user.greet.bind(user);console.log(greet()); // "こんにちは、山田"bind は 即座に実行せず、あとから何度でも呼べる関数を返します。イベントリスナーや setTimeout にメソッドを渡すときに使います。
class Button { constructor(label) { this.label = label; this.handleClick = this.handleClick.bind(this); } handleClick() { console.log(`クリック: ${this.label}`); }}call — 即座に実行、引数は個別に渡す
function introduce(age, city) { return `${this.name}、${age}歳、${city}`;}introduce.call({ name: "太郎" }, 30, "東京");apply — 即座に実行、引数は配列で渡す
introduce.apply({ name: "花子" }, [25, "大阪"]);// 可変長引数の最大値(例)const numbers = [3, 7, 2];Math.max.apply(null, numbers); // 7 — 今はスプレッドの Math.max(...numbers) が一般的| 関数 | 実行タイミング | 引数の渡し方 |
|---|---|---|
bind |
後で実行 | 後から渡す |
call |
すぐ実行 | カンマ区切り |
apply |
すぐ実行 | 配列 |
コールバックで this が失われる問題
DOM イベントや配列メソッドにメソッドを渡すと、classic な this 問題が起きます。
class List { constructor(items) { this.items = items; } logAll() { this.items.forEach(function (item) { // this は List インスタンスではない console.log(item, this); }); }}解決策 1 — アロー関数
logAll() { this.items.forEach((item) => { console.log(item, this.items); // 外側の this });}解決策 2 — bind
document.getElementById("btn").addEventListener( "click", this.handleClick.bind(this));解決策 3 — クラスフィールドのアロー(インスタンスごとに 1 関数)
class App { onReady = () => { console.log(this); };}bind は明示的、アローは簡潔、クラスフィールドはコンストラクタでの bind を省略できます。プロジェクトではどれか一つに寄せると迷いにくいです。
うまくいかないとき
オブジェクトリテラルでメソッドを変数にした
const api = { base: "/api", fetch(endpoint) { return `${this.base}${endpoint}`; },};const get = api.fetch;get("/users"); // ESM では this が undefined のため TypeErrorメソッドを変数へ取り出すと、api をレシーバーにした呼び出しではなくなります。ESM のような strict モードでは this 自体が undefined なので、this.base を読んだ時点で例外になります。非 strict なスクリプトでは this がグローバルオブジェクトになるため、base が無ければ文字列に undefined が入ります。
アロー関数をオブジェクトのメソッドにした
const o = { value: 42, getValue: () => this.value, // this は o ではなく外側から引き継ぐ};console.log(o.getValue()); // ESM では this.value の評価時に TypeErrorアロー関数は呼び出し元のオブジェクトを this にしません。ESM のトップレベルでは外側の this が undefined なので、上の例は undefined を返すのではなく例外になります。ブラウザの非モジュールスクリプトなど、外側の this がオブジェクトなら、そのオブジェクトの value を読みます。
bind したあと元のメソッドも呼んで二重登録
element.addEventListener("click", this.onClick);element.addEventListener("click", this.onClick.bind(this));// 別関数として 2 回登録されるコンストラクタで一度だけ bind し、同じ参照を使い回します。
call と apply の取り違え
引数が配列のときは apply(またはスプレッド付き call)。
fn.apply(ctx, [a, b, c]);fn.call(ctx, ...[a, b, c]);厳格モードで意図しない undefined
function alone() { "use strict"; console.log(this); // undefined}モジュール全体が strict なので、グローバル関数の this に依存した古いパターンは動きません。
アロー関数を new しようとしている
const F = () => {};// new F(); // TypeError — アロー関数はコンストラクタに使えない呼び出し方と this(早見)
| 状況 | this の向き先(目安) |
|---|---|
obj.method() |
obj |