JavaScriptにおけるthisの挙動とバインディング戦略

JavaScriptにおけるthisは、関数が実行される瞬間に決定される特殊な参照値であり、その値は「どこで呼び出されたか」に依存します。宣言時のスコープや定義位置とは無関係です(アロー関数を除く)。この挙動は、実行時における実行コンテキストの構築プロセスと密接に関連しています。

実行コンテキストとthisの関係

JavaScriptエンジンはコードを実行する前に、各関数呼び出しやグローバル実行に対して「実行コンテキスト」を作成します。このコンテキストには、レキシカル環境(変数スコープ)とthisの値が含まれます。実行コンテキストはスタック構造で管理され、現在実行中のコンテキストがスタックトップに位置します。

コンテキストの種類は主に以下の3つです:

  • グローバル実行コンテキスト:スクリプト全体の最上位で生成され、ブラウザではwindow、Node.jsではglobalthisに持つ。1つの実行環境につき1つだけ存在。
  • 関数実行コンテキスト:関数が呼び出されるたびに新規作成され、複数同時に存在可能。
  • Evalコンテキスト:実用性が低く、本稿では扱いません。

4つのthisバインディングルール

thisの値は、関数呼び出しの文脈によって次の4つのルールのいずれかで決定されます。これらには明確な優先順位があります。

1. デフォルトバインディング(独立呼び出し)

関数がオブジェクトのプロパティとしてではなく、単独で呼び出された場合に適用されます。

function checkThis() {
  console.log(this);
}

// 非厳格モード
checkThis(); // window(ブラウザ)

// 厳格モード
function strictCheck() {
  'use strict';
  console.log(this);
}
strictCheck(); // undefined

注意点:オブジェクト内に定義されたメソッドを変数に代入して呼び出すと、デフォルトバインディングが発動します。

const user = {
  id: 42,
  getName: function() { return this.id; }
};

const extracted = user.getName;
console.log(extracted()); // undefined(厳格モード)または window.id(非厳格)

2. アンプリシットバインディング(オブジェクト経由呼び出し)

関数がオブジェクトのプロパティとして呼び出された場合、thisはその直近のオブジェクトを指します。

const vehicle = {
  brand: 'Tesla',
  model: 'Model S',
  getSpec: function() {
    return `${this.brand} ${this.model}`;
  }
};

console.log(vehicle.getSpec()); // "Tesla Model S"

ネストされたオブジェクトでも、thisは「最終的に呼び出したオブジェクト」を指します。

const device = {
  type: 'laptop',
  info: function() { return this.type; }
};

const inventory = {
  type: 'server',
  fetch: function() { return device.info(); }
};

console.log(inventory.fetch()); // "laptop"(device.infoが直接呼び出されたため)

3. エクスプリシットバインディング(call/apply/bind)

callapplybindメソッドを用いてthisを明示的に指定できます。

  • call(obj, arg1, arg2):引数を個別に渡す
  • apply(obj, [arg1, arg2]):引数を配列で渡す
  • bind(obj, arg1, arg2):新しいバインド済み関数を返す(実行はしない)
function introduce(greeting, year) {
  return `${greeting}, I'm ${this.name} (${year})`;
}

const person = { name: 'Alice' };
console.log(introduce.call(person, 'Hi', 2024)); // "Hi, I'm Alice (2024)"
console.log(introduce.apply(person, ['Hello', 2024])); // 同上

const boundIntro = introduce.bind(person, 'Hey');
console.log(boundIntro(2024)); // "Hey, I'm Alice (2024)"

nullundefinedthisArgとして渡すと、デフォルトバインディングが適用されます(厳格モードではそのまま使用)。

4. newバインディング(コンストラクタ呼び出し)

new演算子で関数を呼び出すと、新たな空オブジェクトが作成され、そのオブジェクトがthisとして設定されます。

function Car(make, year) {
  this.make = make;
  this.year = year;
  this.toString = () => `${this.make} (${this.year})`;
}

const myCar = new Car('Toyota', 2023);
console.log(myCar.toString()); // "Toyota (2023)"

内部的には以下のような処理が行われます:

  1. 空のオブジェクトが生成される
  2. そのオブジェクトの[[Prototype]]がコンストラクタのprototypeに設定される
  3. thisがその新オブジェクトに束縛される
  4. コンストラクタ関数が実行される
  5. 明示的なオブジェクト返却がない場合、新オブジェクトが返される

バインディングの優先順位

複数のルールが競合する場合、次の優先順位で解決されます:

  1. newバインディング(最高)
  2. エクスプリシットバインディングbindによる永久バインド含む)
  3. アンプリシットバインディング
  4. デフォルトバインディング(最低)

例えば、bindでバインドされた関数をnewで呼び出すと、newが優先され、バインドされたthisは無視されます。

タグ: this javascript execution-context bind call

5月17日 21:15 投稿