読み込み中...
読み込み中...
読み込み中...
読み込み中...
読み込み中...
JavaScriptの this は、関数が呼び出された方法によって値が決まります。他の多くの言語とは異なり、定義時ではなく実行時にバインドされる点が特徴です。this の値を決定するルールは4つあり、優先順位があります。
| 優先順位 | ルール | 例 | this の値 |
|---|---|---|---|
| 1(最高) | new バインディング | new Foo() | 新しく生成されたオブジェクト |
| 2 | 明示的バインディング | fn.call(obj) | 指定されたオブジェクト |
| 3 | 暗黙的バインディング | obj.fn() | メソッドを呼び出したオブジェクト |
| 4(最低) | デフォルトバインディング | fn() | グローバルオブジェクト(strict mode では undefined) |
上位のルールが下位のルールを上書きします。例えば、暗黙的バインディングと明示的バインディングが競合する場合、明示的バインディングが優先されます。
3つのバインディングルールの例
// デフォルトバインディング
function showThis() {
"use strict";
console.log(this);
}
showThis(); // undefined(strict mode)
// 暗黙的バインディング
const user = {
name: "太郎",
greet() {
console.log("こんにちは、" + this.name);
}
};
user.greet(); // "こんにちは、太郎"
// new バインディング
function Person(name) {
this.name = name;
}
const p = new Person("花子");
console.log(p.name); // "花子"チャレンジ
オブジェクト calculator に value プロパティ(初期値 0)と add メソッドを実装してください。add メソッドは引数 n を受け取り、this.value に加算して this(calculator自身)を返すことでメソッドチェーンを可能にしてください。
ポイント
this の値は関数の呼び出し方で決まります。4つのルール(new > 明示的 > 暗黙的 > デフォルト)の優先順位を覚えましょう。暗黙的バインディングではドット演算子の左側のオブジェクトが this になります。
call、apply、bind は関数の this を明示的に指定するためのメソッドです。すべての関数は Function.prototype からこれらのメソッドを継承しています。
| メソッド | 構文 | 実行タイミング | 引数の渡し方 |
|---|---|---|---|
call | fn.call(thisArg, arg1, arg2) | 即座に実行 | カンマ区切り |
apply | fn.apply(thisArg, [arg1, arg2]) | 即座に実行 | 配列 |
bind | fn.bind(thisArg, arg1) | 新しい関数を返す | カンマ区切り(部分適用可) |
call と apply は関数を即座に呼び出しますが、bind は this が固定された新しい関数を返します。bind で返された関数は後から何度でも呼び出せます。
call と apply の違いは引数の渡し方だけです。スプレッド構文の登場以降、apply の出番は減りましたが、レガシーコードでは頻繁に見かけます。
call / apply / bind の使い分け
function introduce(greeting, punctuation) {
return greeting + "、" + this.name + punctuation;
}
const person = { name: "田中" };
// call: 引数をカンマ区切りで渡す
console.log(introduce.call(person, "こんにちは", "!"));
// "こんにちは、田中!"
// apply: 引数を配列で渡す
console.log(introduce.apply(person, ["やあ", "。"]));
// "やあ、田中。"
// bind: this を固定した新しい関数を返す
const tanakaSays = introduce.bind(person);
console.log(tanakaSays("おはよう", "!"));
// "おはよう、田中!"call / apply を使うと、あるオブジェクトのメソッドを別のオブジェクトに対して実行できます。これをメソッド借用と呼びます。よくある例として、配列風オブジェクト(arguments など)に対して配列メソッドを適用するケースがあります。
bind は this の固定だけでなく、引数の一部を事前に固定する**部分適用(Partial Application)**にも使えます。
メソッド借用と部分適用
// メソッド借用
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
const arr = Array.prototype.slice.call(arrayLike);
console.log(arr); // ["a", "b", "c"]
// 部分適用
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10
console.log(double(10)); // 20チャレンジ
関数 greet(greeting) は this.name と greeting を組み合わせて "greeting, name" の形式で文字列を返します。bind を使って this を user に固定し、さらに greeting を "Hi" に部分適用した関数 greetUser を作成してください。
ポイント
call と apply は this を指定して関数を即座に実行します。bind は this が固定された新しい関数を返し、部分適用にも使えます。メソッド借用は配列風オブジェクトの操作などで実用的です。
アロー関数 () => {} は通常の関数と異なり、自身の this を持ちません。代わりに、定義された場所の外側スコープの this をそのまま使います。これをレキシカル thisと呼びます。
この特性により、アロー関数内の this は call、apply、bind で変更できません。また new 演算子でコンストラクタとして使うこともできません。
アロー関数のレキシカル this は、特にコールバック関数で威力を発揮します。従来は const self = this; や .bind(this) で回避していた問題を、アロー関数は自然に解決します。
通常の関数とアロー関数での this の違い
// 通常の関数: this はコールバック内で変わる
const timer1 = {
seconds: 0,
start() {
const self = this; // this を退避
setInterval(function() {
self.seconds++; // self 経由でアクセス
}, 1000);
}
};
// アロー関数: レキシカル this でシンプルに書ける
const timer2 = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // アロー関数は外側の this を使う
}, 1000);
}
};| 場面 | 推奨 | 理由 |
|---|---|---|
| コールバック(map, filter等) | アロー関数 | 外側の this を保持できる |
| イベントハンドラ(DOM要素の this が必要) | 通常の関数 | this が DOM 要素を指す必要がある場合 |
| オブジェクトのメソッド | 通常の関数 | this がオブジェクト自身を指す必要がある |
| コンストラクタ | 通常の関数 / class | アロー関数は new で使えない |
チャレンジ
オブジェクト team に members 配列と getNames メソッドを実装してください。getNames は this.members を map で変換して名前の配列を返します。コールバックにはアロー関数を使ってください。
ポイント
アロー関数は自身の this を持たず、定義時の外側スコープの this(レキシカル this)を使います。コールバック関数では便利ですが、オブジェクトのメソッドやコンストラクタには不向きです。
JavaScript の this は多くの開発者を悩ませる機能の一つです。ここでは実践で遭遇しやすい落とし穴と、その回避策を紹介します。
オブジェクトのメソッドを変数に代入すると、暗黙的バインディングが失われます。関数の参照だけがコピーされ、呼び出し時に this がデフォルトバインディング(グローバル or undefined)になります。
コールバックとしてメソッドを渡す場合も同様の問題が起きます。
DOM のイベントハンドラでは、this がイベントを発火した DOM 要素を指します。クラスのメソッドをイベントハンドラに渡す場合は注意が必要です。
メソッド抽出と setTimeout の this 問題
// 落とし穴1: メソッド抽出
const user = {
name: "太郎",
greet() {
return "Hello, " + this.name;
}
};
const greetFn = user.greet; // メソッドを変数に代入
// greetFn(); // this === undefined (strict mode)
// 解決策: bind で固定
const boundGreet = user.greet.bind(user);
console.log(boundGreet()); // "Hello, 太郎"
// 落とし穴2: setTimeout
const counter = {
count: 0,
start() {
// NG: this が counter を指さない
// setTimeout(this.increment, 1000);
// OK: アロー関数で包む
setTimeout(() => this.increment(), 1000);
// OK: bind で固定
// setTimeout(this.increment.bind(this), 1000);
},
increment() {
this.count++;
console.log(this.count);
}
};クラスフィールドとアロー関数による this の固定
// 落とし穴3: クラスとイベントハンドラ
class Button {
constructor(label) {
this.label = label;
this.count = 0;
}
// アロー関数でメソッドを定義すると this が固定される
handleClick = () => {
this.count++;
console.log(`${this.label}: ${this.count}回クリック`);
};
}
const btn = new Button("送信");
// イベントリスナに渡しても this が btn を指す
// document.querySelector("#btn").addEventListener("click", btn.handleClick);チャレンジ
logger オブジェクトの log メソッドを setTimeout で1秒後に呼び出したいのですが、this が失われてしまいます。bind を使って this を固定し、正しく "[INFO] テスト" が出力されるようにしてください。
ポイント
メソッドを変数に代入したりコールバックに渡すと this の暗黙的バインディングが失われます。bind による固定、アロー関数の活用、クラスフィールドでのアロー関数定義が主な回避策です。
実務での活用
オブジェクトのメソッドをコールバックとして渡すとき、this が意図と異なるオブジェクトを指してしまうのは非常によくあるバグです。bind で固定する方法と、アロー関数で定義時のスコープに固定する方法の2つを覚えておけば、ほとんどの場面に対応できます。
用語