JavaScript上級 #2 this バインディングと呼び出しパターン

読了 6分

JavaScript の this呼び出し方によって変わります。他の言語ではクラスのインスタンスが自然に this になる場面で、JavaScript は呼び出しの形によって決まります。この記事ではそのルールを一度に整理します。

this が決まる 4 つのルール #

呼び出し方によって優先順位があります。上から順に。

ルール呼び出しの形this になるもの
1new Func()新しく作られたインスタンス
2obj.method()obj
3func.call(x) / apply / bind明示した x
4func()グローバルオブジェクト(または strict では undefined)

上の 4 ルールに対する例外 — アロー関数はこれらをすべて無視して、自分が定義された場所の this をそのまま使います(後述)。

ルール 4 — そのまま呼び出し #

基本呼び出し
function show() {
  console.log(this);
}

show();   // strict モード: undefined / 非 strict: globalThis (window)

最もシンプルな場面です。strict モード (基礎 #2let/const / モジュールは自動的に strict)では undefined。非 strict ではグローバルオブジェクト。

ES Modules とクラス本体は自動 strict なので、モダンなコードではほぼ undefined だと考えてよいです。

ルール 2 — メソッド呼び出し #

メソッド呼び出し — ドットの前が this
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 — 即時呼び出し #

call
function greet(greeting) {
  console.log(`${greeting}, ${this.name}`);
}

greet.call({ name: 'カーティス' }, 'こんにちは');
// こんにちは, カーティス

第一引数が this、その後が関数の引数。関数を即座に呼び出します。

apply — 引数を配列で #

apply
greet.apply({ name: 'カーティス' }, ['こんにちは']);
// こんにちは, カーティス

call と同じですが引数を配列で受け取ります。spread が登場してからは apply を直接使うことはほとんどありません — fn(...args) のほうが短いです。

bind — this が固定された新しい関数を返す #

bind
const boundGreet = greet.bind({ name: 'カーティス' });
boundGreet('こんにちは');   // こんにちは, カーティス

boundGreet.call({ name: '別の人' }, 'こんにちは');
// こんにちは, カーティス  ← bind で固定した this が優先

bind は呼び出さず、this が固定された新しい関数を返します。一度固定されると、その後どのように呼び出しても this は変わりません。コールバック登録の場面でよく使います。

bind で引数も先に固定 #

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 呼び出し #

new 呼び出し
function User(name) {
  this.name = name;
}

const u = new User('カーティス');
u.name;   // 'カーティス'

new を付けて呼び出すと — 新しいオブジェクトを作り、そのオブジェクトが this になり、関数の終わりに自動でそのオブジェクトを返します。クラスの呼び出しは結局この動作です。

new 呼び出しは他のすべてのルールよりも優先されます。

アロー関数 — 上のルールを破る例外 #

アロー関数は自分の this を持ちません。定義された場所の 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 は無視される #

bind もアローには通用しない
const arrow = () => this;
const bound = arrow.bind({ name: 'カーティス' });
bound();   // 依然として外側の this

アロー関数は自分の this を持たないので — バインドする対象がありません。bind が静かに無視されます。

よく出会う混乱しやすい場面 #

1) forEach のコールバック #

forEach の中の this
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 値)を渡すこともできます。

forEach の thisArg
this.items.forEach(function(x) {
  console.log(this.prefix + x);
}, this);   // 第二引数が thisArg

アロー関数が登場してからは thisArg パターンはほとんど使いません。

2) DOM のイベントハンドラ #

イベントハンドラ — this は currentTarget
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 の落とし穴一つ。

非 strict — this がグローバル
function f() {
  console.log(this);   // window または global
}
f();
strict — undefined
'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 プロトタイプチェーン)ではクラスの本当の正体 — プロトタイプとそのチェーンがどう動作するかを扱います。

X