LoginSignup
153

More than 3 years have passed since last update.

JavaScriptのthisの挙動を完全に理解した話

Posted at

はじめに

今回はJavaScriptのthisの挙動について改めてまとめていきたいと思います。

このthisの挙動については多くのエンジニアが頭を悩ませていると思います。

何を隠そう、私もその一人です。

多くの時間を捧げたおかげで、ある程度thisの挙動について理解できた気がするので記事にまとめていきたいと思います。

「完全に理解した」と書いてありますが、自分の勘違いである可能性も十分にあリます。コメント欄で優しく指摘して頂ければ幸いです。

thisの挙動を頑張って理解していきましょう。頑張りましょう。

thisとは

JavaScriptにおけるthisを一言で説明すると、関数の実行コンテキストのオブジェクト自身への参照が格納された暗黙の引数であるということができます。

以下の記事が参考になりました。

JavaScript の this を理解する多分一番分かりやすい説明

PythonやRubyなどの一般の言語においては、メソッドの実行においてその実行コンテキストのオブジェクトがthisselfとして渡されています。

Pythonの場合

Pythonの場合、実行コンテキストのオブジェクトへの参照をselfという引数で明示的に受け取ります。

class UnReact:
    def __init__(self, name):
        self.name = name

    def some_function(self):
        print(self.name)


organization = UnReact('unreact')
print(organization.name)

Pythonではクラスからインスタンスを作成した場合、そのインスタンス自身への参照をクラス内のメソッドの第一引数に受け取ります。

変数名は何でも構いませんが、一般的にはselfという変数名で受け取ります。

organization.nameという形でインスタンスの属性へアクセスした際にunreactという文字列にアクセスできるということは、コンストラクタ関数内の処理self.name = nameのselfがorganaization自身を表しているということを示しています。

このように、Pythonではメソッドの実行においてその実行コンテキストのオブジェクトをselfという形で明示的に受け取ります。

実行コンテキストのオブジェクトがメソッドの第一引数に格納されるという性質を確かめるため、以下のコードを実行してください。

class UnReact:
    def __init__(self, name):
        self.name = name


def some_function(this):
    print(this.name)


organization = UnReact('unreact')
some_function(organization)  # unreact
UnReact.some_function = some_function
organization.some_function()  # unreact

unreact
unreact

some_function(organization)の例では、インスタンスであるorganizationがそのままthisとして使用されています。

また、UnReact.some_function = some_functionの後にorganization.some_function()をした例では、引数にインスタンスを設定していないにも関わらずインスンタンスであるorganaizationsome_functionの第一引数thisに渡されていることが確認されます。

つまり、Pythonのメソッドの実行においては実行コンテキストのオブジェクト、今回の例では.の前のorganization自身がthisとして渡されていることが確認できます。

Pythonはその設計哲学に「Explicit is better than implicit」を掲げているため、他の言語(RubyやJava)では暗黙の引数として渡されるthisをクラスのメソッドの第一引数として明示的に記述することが求められます。

JavaScriptでは、このthisが暗黙の内に渡されているだけです。

このことを頭に入れて、実際の具体例に進んでいきましょう。

thisのパターン4種類

それでは具体的にthisの4つのパターンについて見ていきましょう。

[パターン1] new演算子をつけて呼び出したとき

JavaScriptにおいてクラスの生成に用いられるnew演算子ですが、このnew演算子を用いたときは、new演算子により新規生成されるオブジェクトがthisとして渡されることになります。

というかJavaScriptにおいて、厳密な意味でのクラス定義は存在せず、ただのオブジェクトを生成するシンタックスシュガーです。

とりあえずその話は気にせずに、new演算子が使用されたときのthisの挙動について実際のコードを用いて解説していきます。

const unreact = function () {
  console.log("Hello UnReact, this is", this);
};

const obj = new unreact();

Hello UnReact, this is unreact {}

クラス定義に使用されるnew演算子ですが、実は任意の関数に実行することができます。

new演算子の挙動ですが、これは非常に難解なので以下の記事に目を通すことをおすすめします。

さすがにそろそろ JavaScript の new (あと継承も)について理解したいと思っているあなたに。

new演算子の挙動をざっくり説明すると、先にnew unreactが実行されて、new演算子によりunreactのプロトタイプオブジェクト、この場合はfunctionのプロトタイプのため空のオブジェクト{}がコピーされて新規にオブジェクトが作られます。

その後にnew unreact()()の部分が実行されて、空のオブジェクト{}を実行コンテキストにしてfunctionの中身が実行されることになります。

今回は関数宣言をunreactに代入しているので、thisにはunreact {}が格納されており、new unreact()()により実行されるために結果が出力されています。

つまり、new演算子をつけて呼び出したときには、thisにはその新規生成されるオブジェクトがかくのう されることになります。

もう少し分かりやすく説明するために、以下のようにコードを書き換えました。

const unreact = function (name) {
  console.log("Hello UnReact, this is", this); //Hello UnReact, this is unreact {}
  this.name = name;
  console.log(this.name); //UnReact
  console.log(this); //unreact { name: 'UnReact' }
};

const obj = new unreact("UnReact");

functionの中のコードはnew unreact("UnReact")("UnReact")が実行されて段階で実行されます。

new unreactの部分でfunctionのプロトタイプオブジェクトである{}:空のオブジェクトが新規作成されて渡されることになります。

その空のオブジェクトを実行コンテキストにして、functionの中身が実行されます。つまり、this.name = nameの部分は{}.name = 'UnReact'というコードが実行されることになるため、オブジェクトの中身が{ name: 'UnReact' }になります。

ここまでで、new演算子を用いた際のthisの挙動についての解説は終わりです。

[パターン2] メソッドとして実行したとき

これは最もシンプルなパターンですが、JavaScriptにおいてthisがメソッド内で使用された場合は、そのthisはメソッドが所属するオブジェクトになります。

そもそもJavaScriptにおいてメソッドとはオブジェクトのプロパティに存在する関数のことであり、それ以上でもそれ以下でもありません。

そのメソッド内でthisを使用した際に、そのthisにメソッドが所属するオブジェクト自体が格納されるというのは、他の言語と共通する当然の挙動であるといえるでしょう。

const unreact = {
  name: "UnReact",
  greet: function () {
    console.log("Hello", this);
  },
};
unreact.greet();

unreact.greetの部分で、オブジェクトのプロパティであるfunctionにアクセスして、()を用いることでその関数を実行しています。

メソッド内におけるthisはそのメソッドが所属するオブジェクト、つまりはunreactオブジェクト自体を表すため、上記のような挙動になります。

Hello { name: 'UnReact', greet: [Function: greet] }

[パターン3] 1,2以外の関数

thisの4つもパターンと書いたんですが、このパターン3に至っては2つに場合分けされます。「いやもうそれ4つじゃないやん!」と言いたくなる気持ちも分かりますが、ぐっとこらえて読んで下さい。

というのもES5から追加されたStrictモードにより、グローバル環境へのthisへのアクセスの挙動が分岐するからです。

StrictモードはJavaScriptの古来から続くヤバい挙動を防ぐためにES5から追加されました。そのStrictモードによりグローバル変数thisへのアクセスが禁止されてしまったのです。

冷静に考えて、グローバル変数thisへ簡単にアクセスできてしまう初期の実装が悪いのですけれども、このStrictモードにより余計にthisが分かりにくくなってしまったのは事実だと思います。

ここでは一旦Strictモードが無い場合と有る場合に分けてthisの挙動を確認していきましょう。

1,2以外の関数のthisの挙動 [非Strictモード]

それでは最初に非Strictモードにおける挙動をみていきましょう。

こちらの方はJavaScriptの基本設計に沿った挙動をするため、今までの話の延長線上にあります。

というかこの基本設計がヤバかったため、Strictモードが導入されて禁止されることになったんですが。

メソッド内のthisや、new演算子を用いた際のthis以外の関数内で使われるthisは実行コンテキストがグローバルオブジェクトになります。これは処理系によっても異なっていて、Node.jsならglobalオブジェクトであり、ブラウザであればWindowオブジェクトになります。

thisは実行環境のオブジェクトであるという基本原則が守られており、分かりやすいですよね。

以下のコードで具体例をみていきましょう。

const unreact = function (name) {
  this.name = name;
};
unreact("UnReact");
console.log(name);

UnReact

この仕様は設計方針的には正しいですが、こんなに簡単に汚染が起きてしまうのは良くないですよね。

ちなみに以下のようにすればグローバル環境の汚染が起きてしまったことが確認できます。

const unreact = function (name) {
  this.name = name;
};
unreact("UnReact");
console.log(name);
console.log(global);

...
setTimeout: [Function],
console: [Getter],
name: 'UnReact' }

このように、メソッドやnew演算子が無い関数の中で呼び出されるthisはグローバルオブジェクトになります。

1,2以外の関数のthisの挙動 [Strictモード]

それでは次にStrictモードにおける挙動を見てみましょう。

Strictモードとは、ES5から追加されたJavaScriptの危険な挙動を回避するためのものでしたね。

Strictモードにおいて、関数定義の中で呼ばれるthisはグローバルオブジェクトではなくundefinedになります。

const unreact = function () {
  console.log(this)
};
unreact();

undefined

この挙動は安全ではありますが、thisは実行コンテキストのオブジェクトであるという基本原則から外れてしまっていますね。

この辺りはStrictモードによる例外としてしっかりと抑えておきましょう。

apply, callによる呼び出し

以下の記事を参考にしました。

JavaScriptの「this」は「4種類」??

callやapplyはthisを呼び出し側から任意のオブジェクトに指定して関数を実行する方法です。

第一引数に渡したオブジェクトをthisに強制的に束縛して、callやapplyを実行した関数を実行します。

言葉で説明しても何がなんだか分からないと思うので、実際にコードで解説していきます。

const unreact = {
  name: "UnReact",
  say: function () {
    console.log(this.name);
  },
};

const react = {
  name: "React",
};

unreact.say.call(react);  //React
unreact.say.apply(react);  //React

React
React

上記の例では、unreactオブジェクトのsayメソッドを、reactオブジェクトを主体として呼び出しています。

callメソッドとapplyメソッドは、第一引数に実行の主体となるオブジェクトを渡し、第2引数にメソッドの引数を渡します。

この例ではunreact.sayメソッドを実行する際に、主体をreactオブジェクトに切り替えて実行しています。

unreactオブジェクトのsayメソッドの中で用いられるthisは、実行コンテキストがreactオブジェクトに切り替えられているので、この場合はreactオブジェクト自身が格納されることになります。

もう少し分かりやすく説明するために、引数にオブジェクトを直接渡してみましょう。

const unreact = {
  say: function (organization) {
    this.organization = organization;
    console.log(this);
  },
};

unreact.say.call({ name: "React" }, "UnReact");
unreact.say.apply({ name: "React" }, ["UnReact"]);

{ name: 'React', organization: 'UnReact' }
{ name: 'React', organization: 'UnReact' }

上記の例では、{ name: "React" }を実行コンテキストとして、unreactオブジェクトのsayメソッドを呼び出しています。

callやapplyの第2引数は、sayメソッドの引数organaizationに代入されます。それによりthis.organization = organization;の部分が{ name: "React" }.organaization = "UnReact"になるため、上記のような結果になります。

callとapplyの違いは、第2引数のとり方です。applyは第2引数に配列をとり、その配列の中身が引数として渡されますが、callは第2引数以降がそのまま引数として渡されます。

thisの問題点を回避しよう

ここまでで、4つのパターンのthisの挙動が理解できたと思います。

それでは次に、このthisの挙動が引き起こす問題とその回避方法についてまとめていきます。

よくあるthisの挙動により起こる問題は、クラスのメソッドの中で新たに関数を定義した際のthisの挙動でしょうか。

クラスのメソッドで関数を定義した場合

結論から書きますと、クラスのメソッドで関数を定義して、その中でthisを使おうとした場合、そのthisはグローバルオブジェクトになります。

Strictモードにおいてはグローバルオブジェクトのthisへのアクセスは禁止されているので、undefinedが返ってくることになります。

下記のコードです。

class Organization {
  constructor(name) {
    this.name = name;
  }
  say() {
    const hello = function () {
      console.log(`Hello ${this.name}`);
    };
    hello();
  }
}

const Company = new Organization("UnReact");
Company.say();

TypeError: Cannot read property 'name' of undefined

上記の例において、sayメソッド内で新たに定義したhelloという関数はただの関数であり、オブジェクトの実行コンテキストにありません。そのため、非Strictモードではthisはグローバルオブジェクトになるのですが、今回はStrictモードで実行しているためthisにundefinedが格納されています。

そのundefinedに対してプロパティアクセスを実行しているため、TypeErrorが発生しています。

この問題を回避するためにいくつかの方法が考えられます。

  • callやapplyを使ってthisを強制的に指定する
  • bindを使って関数にthisを束縛する
  • thisの値を一時変数に避難させる
  • アロー関数式を使用する

各々の方法について解説していきます。

callやapplyを使用してthisを強制的に指定する

callやapplyを使ってthisを指定してみましょう。

以下のコードです。

class Organization {
  constructor(name) {
    this.name = name;
  }
  say() {
    const hello = function () {
      console.log(`Hello ${this.name}`);
    };
    hello.call(this);
  }
}

const Company = new Organization("UnReact");
Company.say();

hello()で実行していた部分をhello.call(this)とすることで、thisを主体としてhello関数を実行しています。

この場合のthisはメソッドの中のthisなので、実行コンテキストがオブジェクト自身、この場合はCompanyインスタンス自身になります。

applyを使用しても同様の挙動になります。

bindを使って関数にthisを束縛する

それでは次にbindを使って関数にthisを束縛する方法をみていきましょう。

class Organization {
  constructor(name) {
    this.name = name;
  }
  say() {
    const hello = function () {
      console.log(`Hello ${this.name}`);
    };
    const bindHello = hello.bind(this);
    bindHello();
  }
}

const Company = new Organization("UnReact");
Company.say();

Hello UnReact

const bindHello = hello.bind(this)を使用することでthisをhello関数に束縛しています。このthisはメソッドの中のthisであるため、実行コンテキストがオブジェクト自身、つまりCompanyインスタンスになっています。

そのため、Company.say()を実行するとthisがCompany自身となり、this.nameにUnReactが格納されています。

thisの値を一時変数に避難させる

次はthisの値を一時変数に避難させる方法です。

メソッド内でのthisは実行コンテキストがオブジェクトになっているため、このthisを一時変数_thisに避難させて、この_thisをメソッド内で定義した関数内で使用します。

class Organization {
  constructor(name) {
    this.name = name;
  }
  say() {
    const _this = this;
    const hello = function () {
      console.log(`Hello ${_this.name}`);
    };
    hello();
  }
}

const Company = new Organization("UnReact");
Company.say();

Hello UnReact

const _this = thisの部分で、実行コンテキストがオブジェクト自身であるthisを一時変数_thisに格納しています。

この_thisをhello関数の中で使用することで、thisの挙動を制御しています。

アロー関数式を使用する

最後はアロー関数式です。

メソッド内で定義する関数のみをアロー関数式で定義する場合と、メソッド自身もアロー関数式で定義する場合があります。

この2つを場合分けして解説します。

メソッド内で定義する関数のみをアロー関数式で定義する

アロー関数式で関数を定義すると、functionで関数を宣言した場合とは異なる挙動をします。

以下のコードです。

class Organization {
  constructor(name) {
    this.name = name;
  }
  say() {
    const hello = () => {
      console.log(`Hello ${this.name}`);
    };
    hello();
  }
}

const Company = new Organization("UnReact");
Company.say();

Hello UnReact

上記のコードでは、hello関数をアロー関数式で定義しています。

アロー関数式はES2015から追加された書き方です。そのため、一般的なオブジェクト指向言語、PythonやRubyなどに慣れ親しんだ開発者向けに配慮されています。

具体的には、アロー関数式を使用した場合は暗黙の引数としてのthisを持ちません。

つまり、上記の例ではhello関数は暗黙の引数thisを持ちません。その代わり、外のスコープのthis、この場合はメソッド内のthisが使用されることになります。

そのため、上記のようにhello関数の中のthisはCompany自体となっています。

基本的には、このアロー関数式を使った方法でthisの問題を回避することになります。

メソッド自身もアロー関数式で定義する場合

最後はメソッド自身もアロー関数式で定義する場合です。

以下のコードになります。

class Organization {
  constructor(name) {
    this.name = name;
  }
  say = () => {
    const hello = () => {
      console.log(`Hello ${this.name}`);
    };
    hello();
  };
}

const Company = new Organization("UnReact");
Company.say();

Hello UnReact

メソッド自身もアロー関数式で定義したパターンです。

本来なら、say関数内のthisは実行コンテキストがグローバルになるので、Strictモードでundefinedになるはずです。

しかし、クラスのインスタンスのメソッドとしてアロー関数式が定義された場合には、例外的にthisの一時変数による移し替えが発生します。

それにより、メソッド自身もアロー関数式で定義した場合も、メソッドをfunctionで定義した場合と同様の挙動をします。

終わりに

今回の記事はここまでになります。

ここまで読んで頂きありがとうございました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
153