JavaScript上級 #2 this バインディングと呼び出しパターン
JavaScript の this は呼び出し方によって変わります。他の言語ではクラスのインスタンスが自然に this になる場面で、JavaScript は呼び出しの形によって決まります。この記事ではそのルールを一度に整理します。
this が決まる 4 つのルール
#
呼び出し方によって優先順位があります。上から順に。
| ルール | 呼び出しの形 | this になるもの |
|---|---|---|
| 1 | new Func() | 新しく作られたインスタンス |
| 2 | obj.method() | obj |
| 3 | func.call(x) / apply / bind | 明示した x |
| 4 | func() | グローバルオブジェクト(または strict では undefined) |
上の 4 ルールに対する例外 — アロー関数はこれらをすべて無視して、自分が定義された場所の this をそのまま使います(後述)。
ルール 4 — そのまま呼び出し #
function show() {
console.log(this);
}
show(); // strict モード: undefined / 非 strict: globalThis (window)
最もシンプルな場面です。strict モード (基礎 #2 の let/const / モジュールは自動的に strict)では undefined。非 strict ではグローバルオブジェクト。
ES Modules とクラス本体は自動 strict なので、モダンなコードではほぼ undefined だと考えてよいです。
ルール 2 — メソッド呼び出し #
const obj = {
name: 'カーティス',
greet() {
console.log(this.name);
},
};
obj.greet(); // 'カーティス'
ドットの左側のオブジェクトが this になります。最もよくある場面です。
メソッドを切り離すと失う #
これが JavaScript で最もよくある落とし穴。
const greet = obj.greet;
greet(); // undefined (またはグローバル)
greet 変数には関数自体しか入っていません。ドット呼び出しではないのでルール 4 が適用され、this が消えます。
これがコールバックとしてメソッドを渡したり、イベントハンドラとして登録するときによく事故を起こします。
class Counter {
constructor() {
this.count = 0;
}
increment() {
this.count++;
console.log(this.count);
}
}
const c = new Counter();
button.addEventListener('click', c.increment);
// ✗ click 時 this が button (または undefined)
解決方法は二つ。
// 1. アロー関数で包む
button.addEventListener('click', () => c.increment());
// 2. bind で固定
button.addEventListener('click', c.increment.bind(c));ルール 3 — call / apply / bind
#
this を明示的に指定する 3 つのメソッド。
call — 即時呼び出し
#
function greet(greeting) {
console.log(`${greeting}, ${this.name}`);
}
greet.call({ name: 'カーティス' }, 'こんにちは');
// こんにちは, カーティス
第一引数が this、その後が関数の引数。関数を即座に呼び出します。
apply — 引数を配列で
#
greet.apply({ name: 'カーティス' }, ['こんにちは']);
// こんにちは, カーティス
call と同じですが引数を配列で受け取ります。spread が登場してからは apply を直接使うことはほとんどありません — fn(...args) のほうが短いです。
bind — this が固定された新しい関数を返す
#
const boundGreet = greet.bind({ name: 'カーティス' });
boundGreet('こんにちは'); // こんにちは, カーティス
boundGreet.call({ name: '別の人' }, 'こんにちは');
// こんにちは, カーティス ← bind で固定した this が優先
bind は呼び出さず、this が固定された新しい関数を返します。一度固定されると、その後どのように呼び出しても this は変わりません。コールバック登録の場面でよく使います。
bind で引数も先に固定
#
function add(a, b) {
return a + b;
}
const add5 = add.bind(null, 5); // this は null, 第一引数は 5 で固定
add5(3); // 8
add5(10); // 15
#1 クロージャ で見た部分適用パターンが bind でも可能です。
ルール 1 — new 呼び出し
#
function User(name) {
this.name = name;
}
const u = new User('カーティス');
u.name; // 'カーティス'
new を付けて呼び出すと — 新しいオブジェクトを作り、そのオブジェクトが this になり、関数の終わりに自動でそのオブジェクトを返します。クラスの呼び出しは結局この動作です。
new 呼び出しは他のすべてのルールよりも優先されます。
アロー関数 — 上のルールを破る例外 #
アロー関数は自分の this を持ちません。定義された場所の this をそのまま引き継ぎます。
const obj = {
name: 'カーティス',
greetRegular: function() {
console.log(this.name);
},
greetArrow: () => {
console.log(this.name); // 外側の this — 普通は undefined
},
};
obj.greetRegular(); // 'カーティス'
obj.greetArrow(); // undefined (オブジェクトの外の this を見る)
greetArrow はオブジェクトの中で定義されていても、アロー関数の this はオブジェクトの外側の環境(普通はモジュールのトップレベル = undefined)をそのままキャプチャします。
つまり — メソッドは通常関数、コールバックはアロー #
class Counter {
constructor() {
this.count = 0;
}
increment() { // メソッド → 通常関数
setTimeout(() => { // コールバック → アロー
this.count++; // 外側の this (Counter インスタンス) を保持
}, 100);
}
}React のフックのコールバック、setTimeout/setInterval、fetch の .then もすべて同じパターンです。
アロー関数に bind は無視される
#
const arrow = () => this;
const bound = arrow.bind({ name: 'カーティス' });
bound(); // 依然として外側の this
アロー関数は自分の this を持たないので — バインドする対象がありません。bind が静かに無視されます。
よく出会う混乱しやすい場面 #
1) forEach のコールバック
#
const obj = {
prefix: '> ',
items: ['a', 'b', 'c'],
log() {
this.items.forEach(function(x) {
console.log(this.prefix + x); // ✗ this が obj ではない
});
},
};forEach のコールバックが通常の関数なので — ドット呼び出しではないただの呼び出しでルール 4 (undefined)。アロー関数に変えれば解決。
log() {
this.items.forEach((x) => {
console.log(this.prefix + x); // 外側の this を保持
});
}または forEach が受け取る第二引数(this 値)を渡すこともできます。
this.items.forEach(function(x) {
console.log(this.prefix + x);
}, this); // 第二引数が thisArg
アロー関数が登場してからは thisArg パターンはほとんど使いません。
2) DOM のイベントハンドラ #
button.addEventListener('click', function(e) {
console.log(this); // イベントが付けられた要素 (currentTarget)
});
button.addEventListener('click', (e) => {
console.log(this); // 外側 (普通は undefined または window)
});古いコードの this はイベントが付けられた要素を指すように意図されていました。アロー関数を使うとその意図が崩れます。DOM ハンドラで要素が必要なら e.currentTarget を使うのが、どの関数の形でも安全です。
this と strict モード
#
古い JavaScript の落とし穴一つ。
function f() {
console.log(this); // window または global
}
f();'use strict';
function f() {
console.log(this); // undefined
}
f();ES Modules、クラス本体、ES2015+ コードは自動的に strict。モダンなコードでは this がただ呼び出されたときは undefined だと考えてよいです。
まとめ #
この記事で整理した内容:
thisは呼び出し方によって決まる — 4 つのルール- ただの呼び出し → undefined / メソッド呼び出し → ドットの前 / call・apply・bind → 明示 / new → 新しいインスタンス
- メソッドを切り離してコールバックとして渡すと
thisが消える call/applyは即時呼び出し、bindは固定された新しい関数- アロー関数は自分の
thisを持たない — 外側をキャプチャ - メソッドは通常関数、コールバックはアロー(最も一般的なガイド)
- DOM 要素が必要なら
e.currentTargetが最も安全
次の記事(#3 プロトタイプチェーン)ではクラスの本当の正体 — プロトタイプとそのチェーンがどう動作するかを扱います。