ウェブエンジニア問題集
第11章

thisの正体 — 呼び出しパターンごとの挙動を整理する

13
この章の目次開く

this は、JavaScriptで最も誤解されているキーワードです。「クラスの中では自分自身のこと」と覚えていると、コールバックに渡した途端に undefined になって裏切られます。他の言語の経験者ほど、この挙動に混乱します。

学習者学習者

this って「そのオブジェクト自身」のことじゃないんですか? 同じ関数なのに、動かし方によって this の中身が変わることがあって、もう信用できない…。

その「動かし方によって変わる」こそが、this の本質です。先に結論を言ってしまいます。this の中身は、関数が「どこに書かれたか」ではなく「どう呼び出されたか」で、呼び出しのたびに決まります(唯一の例外がアロー関数)。

つまり this は暗記の対象ではなく、呼び出しパターンの見分け方の問題です。パターンは実質4つしかありません。この章で1つずつ潰していきましょう。

大原則 — thisは「呼び出し時」に決まる

普通の変数は、クロージャの章で学ぶとおり「どこで定義されたか」(レキシカルスコープ)で参照先が決まります。this はその真逆で、定義時には何も決まっていません

function showThis() {
  console.log(this);
}
// この時点では、thisが何になるかは決まっていない。
// showThisが「どう呼ばれるか」で毎回変わる。
js

同じ関数でも、呼び出し方が違えば this は別物になります。

const user = { name: '太郎', showThis };
 
showThis(); // グローバルオブジェクト(またはundefined)← パターン1
user.showThis(); // userオブジェクト ← パターン2
new showThis(); // 新しく作られたオブジェクト ← パターン3
js
呼び出し方によって応答が変わるイメージ
thisは関数の性質ではなく「呼び出し方」の性質。同じ関数でも呼び方次第で変わる

判定の全体像を先に図で示します。以降の節は、この分岐を1つずつ確認していく構成です。

パターン1: 単独の関数呼び出し — グローバル or undefined

fn() のように、何にも付けずに関数を呼び出すと、thisグローバルオブジェクト(ブラウザでは window)になります。ただしstrictモードでは undefined です。

function showThis() {
  console.log(this);
}
 
showThis();
// 非strictモード: window(グローバルオブジェクト)
// strictモード:   undefined
js

このパターンで this を意図的に使うことはまずありません。重要なのは、後述の「thisが失われる」事故の落下先がここだという点です。メソッドとして呼ばれるはずだった関数が単独呼び出しに変わってしまうと、thisundefined になり、this.name のようなアクセスが TypeError で落ちます。

パターン2: メソッド呼び出し — 「ドットの左」を見る

obj.fn() の形で呼び出すと、thisドットの左にあるオブジェクトになります。最頻出パターンにして、判定はいちばん簡単です。

const user = {
  name: '太郎',
  greet() {
    console.log(`こんにちは、${this.name}です`);
  },
};
 
user.greet(); // "こんにちは、太郎です" ← thisはuser(ドットの左)
js

メソッド呼び出しの this は「ドットの左のオブジェクト」。それ以上でも以下でもありません。「そのメソッドを定義したオブジェクト」ではない、という点が重要です。

const admin = { name: '管理者' };
admin.greet = user.greet; // userのメソッドをadminに付け替える
 
admin.greet(); // "こんにちは、管理者です" ← thisはadmin(呼び出し時のドットの左)
js

同じ greet 関数なのに、user.greet() なら太郎、admin.greet() なら管理者。関数がどこ生まれかは関係なく、呼び出しの瞬間のドットの左がすべてです。

メソッドを「取り出す」とthisは消える

このルールの裏返しとして、メソッドを変数に代入したりコールバックとして渡したりすると、呼び出しからドットが消えるため、this は失われます。

const greet = user.greet; // 関数を取り出した時点で、userとの縁は切れる
 
greet(); // TypeError: Cannot read properties of undefined (reading 'name')
// ← 単独呼び出し(パターン1)になり、thisはundefined
js
学習者学習者

えっ、user.greet を代入しただけなのに! 関数は「userのもの」って情報ごとコピーされるんじゃないんだ…。

先生先生

そこが最大の誤解ポイント。関数はどのオブジェクトにも属していない、独立した値なんだ。user.greet は「userが持つ関数への参照」にすぎない。だから取り出したら、ただの関数に戻る。this が決まるのはあくまで呼び出す瞬間だよ。

この「thisの喪失」は実務で最も踏む罠なので、「よくあるハマりどころ」でも改めて扱います。

パターン3: コンストラクタ呼び出し — newが作る新しいオブジェクト

関数を new 付きで呼び出すと、その関数はコンストラクタとして動作し、this新しく作られる空のオブジェクトを指します。

function User(name) {
  // newで呼ばれると、thisは新品の空オブジェクト
  this.name = name;
  // return を書かなければ、thisが自動的に返される
}
 
const taro = new User('太郎');
console.log(taro.name); // "太郎"
js

new が裏でやっていることを分解すると、次の3ステップです。

  1. 新しい空のオブジェクトを作る
  2. そのオブジェクトを this として関数を実行する
  3. 関数が明示的にオブジェクトを return しなければ、this を返す

クラス構文の constructor の中の this も、これとまったく同じ仕組みです。クラスは new 専用になっているぶん、事故が起きにくくなっています。

class User {
  constructor(name) {
    this.name = name; // thisは作られつつあるインスタンス
  }
}
js

パターン4: アロー関数 — thisを持たない

最後のパターンは、これまでと毛色が違います。アロー関数は自分の this を持ちません。アロー関数の中で this を書くと、それが定義された場所の外側の this がそのまま使われます。

const timer = {
  seconds: 0,
  start() {
    // startはメソッド呼び出しなので、ここのthisはtimer
    setInterval(() => {
      // アロー関数は自分のthisを持たない
      // → 外側(start)のthis、つまりtimerをそのまま使う
      this.seconds += 1;
      console.log(this.seconds);
    }, 1000);
  },
};
 
timer.start(); // 1, 2, 3, ... と正しくカウントされる
js

もしこのコールバックを function () { ... } で書くと、setInterval が呼び出す時点で「ドットの左」が存在しないため、this はパターン1に落ちてしまい、this.seconds は動きません。アロー関数は、この**「コールバックの中でも外側のthisを使い続けたい」**という頻出ニーズへの言語側の回答です。

アロー関数の this は呼び出し方の影響を一切受けず、「定義された場所の外側の this」で固定されます。変数がレキシカルスコープで解決されるのと同じ仕組みなので、「レキシカルな this」とも呼ばれます。

学習者学習者

じゃあ全部アロー関数で書けば this に悩まなくて済むのでは…?

先生先生

コールバックは基本アロー関数でOK。ただし「thisを使いたいオブジェクトのメソッド」をアロー関数で定義すると、今度は逆にドットの左を無視してしまうから逆効果なんだ。使い分けはこの後のハマりどころで見ていくよ。

thisを自分で指定する — call・apply・bind

4パターンの自動決定に任せず、this明示的に指定する手段も用意されています。すべての関数が持つ call / apply / bind メソッドです。

メソッド引数戻り値
fn.call(thisArg, arg1, arg2, ...)this にする値 + 関数への引数を個別に関数を即実行した結果
fn.apply(thisArg, argsArray)this にする値 + 関数への引数を配列で関数を即実行した結果
fn.bind(thisArg, ...args?)this にする値(+ 先頭の引数を固定も可)this を固定した新しい関数(実行はしない)

callapply は「thisを指定して、その場で実行する」、bind は「thisを縛り付けた別の関数を作っておく」という違いです。

function greet(greeting) {
  console.log(`${greeting}、${this.name}です`);
}
 
const user = { name: '太郎' };
 
greet.call(user, 'こんにちは'); // "こんにちは、太郎です"(即実行)
greet.apply(user, ['こんばんは']); // "こんばんは、太郎です"(引数は配列で)
 
const greetTaro = greet.bind(user); // thisをuserに固定した新しい関数を作る
greetTaro('おはよう'); // "おはよう、太郎です"
js

bind で作った関数の this は、その後どう呼び出しても変わりません。「メソッドをコールバックとして渡すと this が消える」問題の、アロー関数と並ぶ解決策です。

const button = document.querySelector('button');
 
// ❌ 渡した時点でthisが失われる
button.addEventListener('click', user.greet);
 
// ⭕ thisをuserに固定してから渡す
button.addEventListener('click', user.greet.bind(user));
js

呼び出しパターン早見表

早見表で確認するイメージ
呼び出し方this
単独呼び出しfn()グローバルオブジェクト(strictモードは undefined
メソッド呼び出しobj.fn()ドットの左のオブジェクト(obj
コンストラクタ呼び出しnew fn()新しく作られるオブジェクト
アロー関数() => {}外側の this を継承(呼び出し方は無関係)
明示指定fn.call(x) / fn.apply(x) / fn.bind(x)指定した x

迷ったら、呼び出している行(定義ではなく)を見て、この表に当てはめてください。

よくあるハマりどころ

コールバックに渡したメソッドのthisが消える

パターン2の節で見た「thisの喪失」は、コールバックの形で最も頻繁に起きます。

const user = {
  name: '太郎',
  greet() {
    console.log(`こんにちは、${this.name}です`);
  },
};
 
setTimeout(user.greet, 1000);
// "こんにちは、undefinedです"(またはTypeError)
// ← setTimeoutが後で呼ぶときは greet() 単独呼び出しになっている
js

setTimeout(user.greet, ...) と書いた時点で渡っているのは関数そのものだけで、「userの」という情報は付いてきません。対策は2つ、どちらでも構いません。

// 対策1: アロー関数で包む(呼び出しの形を user.greet() のまま保つ)
setTimeout(() => user.greet(), 1000);
 
// 対策2: bindでthisを固定した関数を渡す
setTimeout(user.greet.bind(user), 1000);
js

オブジェクトのメソッドをアロー関数で書いてしまう

「アロー関数は便利」を拡大解釈すると、逆パターンの事故が起きます。

const counter = {
  count: 0,
  // ❌ メソッドをアロー関数で定義
  increment: () => {
    this.count += 1; // このthisはcounterではない!
  },
};
 
counter.increment();
console.log(counter.count); // 0 ← 増えていない
js

アロー関数はドットの左を無視して外側の this(ここではモジュールのトップレベル)を使うため、counter.increment() と呼んでも thiscounter になりません。

使い分けの軸はシンプルです——オブジェクトの「メソッド」は通常の関数(短縮記法 increment() {})で、その中の「コールバック」はアロー関数で書く。

メソッドの中のネストした関数でthisが変わる

メソッドの中で function を定義すると、その関数の呼び出しは単独呼び出し(パターン1)なので、this は外側のメソッドから引き継がれません。

const team = {
  name: '開発チーム',
  members: ['太郎', '花子'],
  introduce() {
    this.members.forEach(function (member) {
      // ❌ このfunctionは単独呼び出し → thisはundefined
      console.log(`${member}は${this.name}の一員です`); // TypeError
    });
  },
};
js

this は関数の境界を越えるたびにリセットされる」と考えてください。コールバックをアロー関数にすれば、外側(introduce)の this がそのまま届きます。

introduce() {
  this.members.forEach((member) => {
    console.log(`${member}は${this.name}の一員です`); // ⭕ thisはteam
  });
}
js

ちゃんと使うためのポイント

  • this は「どこで定義したか」ではなく「どう呼び出されたか」で毎回決まる。迷ったら呼び出している行を見る
  • メソッド呼び出しの thisドットの左。関数を取り出したり、コールバックとして渡したりした瞬間にその縁は切れる
  • new 付きの呼び出しでは、this は新しく作られるオブジェクト。クラスの constructor も同じ仕組み
  • アロー関数は this を持たず、外側の this を継承する。呼び出し方の影響を受けない唯一のパターン
  • メソッドは通常の関数で、コールバックはアロー関数で書くのが基本の使い分け
  • this を明示的に決めたいときは call(即実行)/ apply(引数を配列で即実行)/ bind(固定した関数を作る)

「アロー関数は外側の this を使う」「コールバックは周囲の変数を覚えている」——この章で何度も出てきた**「外側のものを参照し続ける」仕組み**の正体が、次章のクロージャです。this とセットで理解すると、JavaScriptの関数の挙動はほぼ説明がつくようになります。

参考リンク

JavaScriptクイズに挑戦するこの章で学んだJavaScriptの知識を、4択クイズでアウトプットして定着させよう