🐣

【JS】 thisの参照先まとめ

2021/05/25に公開

はじめに

JavaScript における"this"の参照先に関して混乱したので、自身の頭の整理も兼ねてまとめます。
(2021.6.1 「クラスにおける"this"の参照先」追記)

  • "this"とは
  • オブジェクトメソッドとしての"this"と関数としての"this"
  • コールバック関数における"this"の参照先
  • bind による"this"の束縛
  • bind, call, apply の違い
  • アロー関数は"this"を取らない
  • クラスにおける"this"の参照先

"this"とは

関数コンテキスト内には"this"が JavaScript エンジンによって設定され、関数内で使える状態になっています。
関数コンテキスト

この this は関数が実行される際に値が設定されます。
この値と言うのが、「その関数を保持しているオブジェクトのプロパティもしくはメソッドへの参照」です。

オブジェクトメソッドとしての"this"と関数としての"this"

this は「オブジェクトのメソッドとして実行する場合」と「関数として実行する場合」で参照先が異なります。
それは、前述したように、this の値は関数が実行される際に設定されるからです。
つまり、関数内やメソッド内で this が使われていても、その時点では this の値は決まっていません。

オブジェクトメソッドとしての"this"

例として、コンソールに「こんにちは ○○ さん」と表示する無名関数のメソッドを持つオブジェクト greeting を定義し、実行します。

const greeting = {
  name: "だんぼ",
  message: function () {
    console.log(`こんにちは ${this.name}さん`); //この"this"はgreetingオブジェクトを参照する
  },
};
greeting.message(); //こんにちは だんぼさん

この message()を格納しているのは、"."の前で指定している greeting であり、「message()は greeting オブジェクトに格納されているメソッドとして呼び出されている」ということになります。
そのため、この message メソッド内の this は呼び出し元のオブジェクトである greeting への参照を値として保持します。

オブジェクトとthis

次項で詳しく説明しますが、この greeting.message を新しい関数として定義した場合は this は greeting を参照しません。

関数としての"this"

例として、先程の greeting.message を新しい関数として定義します。
その際に、グローバルスコープである window オブジェクトにも name を追加しておきます。

window.name = "ひよこ太郎";

const greeting = {
  name: "だんぼ",
  message: function () {
    console.log(`こんにちは ${this.name}さん`);
  },
};

const ref = greeting.message;
ref(); //こんにちは ひよこ太郎さん

結果、this はフローバルスコープである window オブジェクトを参照します。

何故こうなるのかというと、
そもそも関数は独立したオブジェクトであって、関数を定義したオブジェクトに所属しません。
ですので、前節の図解は、正しくは以下のようになります。

オブジェクトメソッドとthisの参照

greeting の外に独立したオブジェクトとしてconsole.log(...)が存在しており、greeting の中の message メソッドには、外部の関数オブジェクトへのアドレスを保持している、という構造になります。

つまり、新しい関数として const や let で定義した場合、greeting の外に新しく ref が生成され、その中にconsole.log(...)へのアドレスを保持します。

関数とthisの参照

新しいスクリプトスコープ(ref)にconsole.log(...)へのアドレスがコピーされ、this を参照しようとします。
この時コピーしているのは飽くまでもgreeting.messageが保持しているconsole.log(...)へのアドレスのため、「greeting というオブジェクトから参照しますよ」という情報は一切コピーされません。
そのため、ref には参照するべきオブジェクトが存在せず、更に外側のグローバルスコープを参照することとなります。
this には window オブジェクトのname = "ひよこ太郎"が入ります。

入れ子構造の"this"

では、次に greeting.message の中に更に this を含む関数を追加してみます。

window.name = "ひよこ太郎";

const greeting = {
  name: "だんぼ",
  message: function () {
    console.log(`こんにちは ${this.name}さん`);

    const greeting02 = {
      message: function () {
        console.log(`Hello! ${this.name}さん`);
      },
    };

    greeting02.message();
  },
};

greeting.message();
//こんにちは だんぼさん
//Hello! undefinedさん

const ref = greeting.message;
ref();
//こんにちは ひよこ太郎さん
//Hello! undefinedさん

greeting.message 内の greeting02.message
ref として定義した関数内の greeting02.message
どちらも undefined となってしまいました。

「greeting02 は greeting オブジェクト内にあるため、greeting.message 関数と同じ this の値にならないのか?
前者は greeting.name への参照が値となり、後者は window.name への参照が値となるのでは?」
と思うかもしれません。

ここで、どのような構造になっているのか図解を見てみます。

入れ子構造のthis

このように、greeting02 オブジェクトは独立した関数スコープ内に存在しているため、greeting オブジェクトにも ref にも干渉しません。
そして、greeting02.message 内の this は、単純に、"."の前で指定している greeting02 への参照を値として保持します。
しかし、greeting02 オブジェクト内には name というプロパティが割り当てられていないため、undefined となります。

まとめ:オブジェクトメソッドとしての"this"と関数としての"this"

まとめると、以下のように覚えておくとわかりやすいかと思います。

尚、「関数として実行する場合 window オブジェクトを参照する」というのは strict モードでは異なることに注意してください。
strict モードでは、関数として実行する場合、全て undefined となります。

コールバック関数における"this"の参照先

コールバック関数として greeting.message を実行した場合を考えます。

window.name = "ひよこ太郎";

const greeting = {
  name: "だんぼ",
  message: function () {
    console.log(`こんにちは ${this.name}さん`);
  },
};

greeting.message(); //こんにちは だんぼさん ー①

function fn(ref) {
  greeting.message(); // → こんにちは だんぼさん ー②-1
  ref(); // → こんにちは ひよこ太郎さん ー②-2
}
fn(greeting.message); //ー②
//こんにちは だんぼさん
//こんにちは ひよこ太郎さん

① では前節で説明した通り、this は greeting オブジェクトを参照します。
② では、greeting.message として実行した場合は greeting オブジェクトを参照しますが、
ref()として実行した場合は window オブジェクトを参照します。

② 実行時を図解すると以下のようになります。

コールバック関数と"this"

②-1 では、greeting.message() となっているため、"."の greeting を参照しに行きます。
②-2 では、ref という仮引数に greeting.message という実引数を代入しています。
ref = greeting.message となり、
これはconst ref = greeting.messageと同じような挙動をとります。
よって、message:f(){}への参照のみがコピーされ、関数実行時の this と同じように window オブジェクトを参照します。

bind による"this"の束縛

では、単なる関数やコールバック関数で this の参照先をオブジェクトにしたい時はどうすればいいのでしょうか。

JavaScript には bind というメソッドが存在します。
このメソッドを使用することによって、this の参照先を意図的に変更することが可能です。
これを「bind による this の束縛」といいます。

window.name = "ひよこ太郎";

const greeting = {
  name: "だんぼ",
  message: function () {
    console.log(`こんにちは ${this.name}さん`);
  },
};

greeting.message(); //こんにちは だんぼさん

const ref = greeting.message;
ref(); //こんにちは ひよこ太郎さん

const refBind = greeting.message.bind(greeting);
refBind(); //こんにちは だんぼさん

このように、オブジェクトのメソッドを代入時に .bind() と付け加えます。
この時、refBind にはconsole.log(...)への参照がコピーされ、
bind によって「console.log(...)の中の this は greeting を参照するように」という命令が追加で入ります。
結果、this は window オブジェクトではなく、greeting を参照することになります。

では、name プロパティを持つ piyo オブジェクトを追加し、bind の拘束先を piyo オブジェクトにした refBindPiyo を定義します。

window.name = "ひよこ太郎";

const greeting = {
  name: "だんぼ",
  message: function () {
    console.log(`こんにちは ${this.name}さん`);
  },
};

const piyo = {
  name: "ぴよぴよ丸",
};

greeting.message(); //こんにちは だんぼさん

const ref = greeting.message;
ref(); //こんにちは ひよこ太郎さん

const refBind = greeting.message.bind(greeting);
refBind(); //こんにちは だんぼさん

const refBindPiyo = greeting.message.bind(piyo);
refBindPiyo(); //こんにちは ぴよぴよ丸さん

このように、refBindPiyo に greeting.message を代入しても this の参照先は、bind で拘束した piyo オブジェクトとなります。
これは前述した通り、代入されているのは飽くまでもconsole.log(...)への参照のコピーだからです。
refBindPiyo と greeting は干渉せず、単純に bind での拘束先を参照します。

bind による"this"の束縛

bind, call, apply の違い

bind と似ているメソッドに call, apply があります。
どれも this の参照先を束縛するものですが、少し動きが異なるので注意しましょう。

という違いがあります。

window.name = "ひよこ太郎";

const greeting = {
  name: "だんぼ",
  message: function () {
    console.log(`こんにちは ${this.name}さん`);
  },
};

const refBind = greeting.message.bind(greeting);
refBind(); //こんにちは だんぼさん
//改めて関数を実行する必要がある

greeting.message.call(greeting); //こんにちは だんぼさん
greeting.message.apply(greeting); //こんにちは だんぼさん
//使用と同時にコンソールに表示される

bind, call, apply と関数の引数

また、this の参照先の話からは脱線しますが、bind は第二引数以下を値にすることによって、関数の引数の値を束縛することが出来ます。

function helloFunc(name) {
  console.log(`こんにちは ${name}さん`);
}

const helloFuncBind = helloFunc.bind(null, "ひよこ王子"); //引数の値を第二引数で指定した"ひよこ王子"に固定する
helloFuncBind("だんぼ"); //引数の値が固定されているので、新しく"だんぼ"を指定しても こんにちは ひよこ王子さん と表示される

bind 内で第一引数を null にすることによって、this がない場合でも関数の引数の値の束縛として bind を使うことが出来ます。。
第二引数を"ひよこ王子"にしているため、関数 helloFuncBind は関数 helloFunc の name を"ひよこ王子"で固定した状態になります。
よって、helloFuncBind の実引数を何を入れても
こんにちは ひよこ王子さん
という表示になります。

尚、this がある場合でも引数の値を束縛することはできます。

window.name = "ひよこ太郎";

const greeting = {
  name: "だんぼ",
  message: function (message) {
    console.log(`${message} ${this.name}さん`);
  },
};

const refBind = greeting.message.bind(greeting, "こんにちは");
refBind("こんばんは"); //こんにちは だんぼさん
//"こんにちは"でmessageの値を固定しているため、"こんばんは"は無視される

そして、call と apply でも似たことが可能です。

call は bind と同様、第二引数以下に関数に引数の値を渡すことが出来ます。

function helloFunc(name) {
  console.log(`こんにちは ${name}さん`);
}

const helloFuncBind = helloFunc.call(null, "ひよこ王子"); //こんにちは ひよこ王子さん

call の第二引数に"ひよこ王子"を定義しているため、name の部分が"ひよこ王子"となってコンソールに出力されます。

apply では第二引数に配列を渡します。
配列の値が順番に引数の値として渡されていきます。

function helloFunc(name1, name2, name3, name4) {
  console.log(
    `こんにちは ${name1}さん! いってらっしゃい ${name2}さん! おやすみなさい ${name3}さん! また明日 ${name4}さん!`
  );
}

const array = ["ひよこ王子", "ひよこ姫", "ぴよぴよ騎士", "ぴよぴよ大臣"];
const helloFuncBind = helloFunc.apply(null, array);
// こんにちは ひよこ王子さん! いってらっしゃい ひよこ姫さん! おやすみなさい ぴよぴよ騎士さん! また明日 ぴよぴよ大臣さん!

helloFunc 関数に追加で name2,name3,name4 という仮引数を追加します。
array で配列を定義し、apply の第二引数として渡すと、name1-4 に順番に配列の値が渡されます。

前述した通り、call や apply では使用と同時に関数が実行されるため、改めて関数を実行することは出来ません。
ですので、call や apply では「関数の引数の値を束縛」ではなく、「関数の引数を同時に渡すことができる」と言えるでしょう。

尚、bind と同様、call と apply でも this と併用して使うことが出来ます。

配列を扱うときは apply,引数の値が独立しているときは call を使うと覚えておくといいでしょう。
※現在 ES6 ではスプレット演算子を使うことができるため、apply はあまり使わなくなったようです。

アロー関数は"this"を取らない

今まで使ってきた function()では、関数やメソッド内で this の値は確定しておらず、関数を呼び出した際に this の値が決まります。

ですが、アロー関数では
宣言された時点で this の値を確定します。

アロー関数ではレキシカルスコープを順に参照していく形をとっていきます。
同じスコープに this の値がない場合、次のスコープ、そして更に次のスコープへと、順に this の値を探します。

window.name = "グローバルひよこ";

const allowMessage = () => {
  console.log(`こんにちは ${this.name}さん`);
};

const greeting = {
  name: "だんぼ",
  message: allowMessage,
};

const greeting02 = {
  name: "ひよこ丸",
  message: allowMessage,
};

greeting.message(); //こんにちは グローバルひよこさん
greeting02.message(); // こんにちは グローバルひよこさん

アロー関数 allowMessage はグローバルスコープ内で定義されています。
オブジェクトメソッドとして実行した場合でも、allowMessage 定義時に既にレキシカルスコープである window オブジェクトを値に取ることが決まっているため、greeting, greeting02 どちらもグローバルスコープである window オブジェクトで定義された"グローバルひよこ"がコンソールに出力されることになります。

アロー関数はthisを取らない

アロー関数の場合レキシカルスコープを辿っていくので、通常の変数と同じような挙動を取ります。

window.name = "グローバルひよこ";

const allow = () => {
  console.log(`こんにちは ${this.name}さん`); //スコープ内でthisの値を定義する前にconsole.logしてるため、this="グローバルひよこ"
  this.name = "アウターひよこ";
  console.log(`こんにちは ${this.name}さん`); //allowスコープ内にthisが存在するため、this="アウターひよこ"
  const innerAllow = () => {
    this.name = "インナーひよこ";
    console.log(`こんにちは ${this.name}さん`); //innerAllowスコープ内にthisが存在するため、this="インナーひよこ"
  };
  innerAllow();
};

allow();
//こんにちは グローバルひよこさん
//こんにちは アウターひよこさん
//こんにちは インナーひよこさん

クラスにおける"this"の参照先

こちらの同項目にて詳しく説明しています。
https://zenn.dev/danbo/articles/2b861cb88678bfb473cb

参考文献

MDN
【JS】ガチで学びたい人のための JavaScript メカニズム
【JavaScript】アロー関数式を学ぶついでに this も復習する話
ECMAScript 2015 以降の JavaScript のthisを理解する

Discussion