JavaScript
 Computer >> コンピューター >  >> プログラミング >> JavaScript

JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

JavaScriptの「this」キーワードを徹底解説

プログラマーを目指すなら、どの分野に進むとしても、ほぼ確実にJavaScriptと向き合うことになるでしょう。ささやかな始まりから進化を遂げたJavaScriptは、今やフロントエンドはもちろん、バックエンド、モバイルアプリ、ゲーム、デスクトップアプリ、さらには機械学習までカバーする強力な言語へと成長しました。

ただし、JavaScriptマスターへの道のりでは、まず基礎を固めることが何よりも重要です。その学習リストの筆頭に挙げるべきなのが、thisキーワードです。

そもそも「this」とは何か?

シンプルに言えば、thisとはポインターであり、JavaScriptのコード内で「どのオブジェクトが関数やメソッドを呼び出したのか」を参照するための仕組みです。平たく表現するなら、thisは「ドット(.)の左側にあるもの」と覚えると直感的に理解できます。

JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

thisには主に2つの用途があります。具体的な例を見ながら確認していきましょう。

thisでオブジェクトを構築する

function Book(title, author, genre) {
  this.title = title;
  this.author = author;
  this.genre = genre;
}
const book1 = new Book("Lord Of The Rings", "J.R.R. Tolkien", "Fantasy");
const book2 = new Book("Tools Of Titans", "Tim Ferriss", "Self-help");
 
console.log(book1);
console.log(book2);

上記のコードを実行すると、次のような出力が得られます。

JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

ここではコンストラクタ関数を定義しています。この関数から、タイトル(title)、著者(author)、ジャンル(genre)をパラメータとして受け取る本(Book)オブジェクトを作成できます。

コンストラクタ関数とは、いわばオブジェクトの「設計図」となる関数です。これを使うことで、同じ名前(この場合はBook)のオブジェクトをいくつでも生成できます。

new演算子で新しいオブジェクトを作成し、必要な引数を渡します(関数を定義するときは「パラメータ」、呼び出すときは「引数」と呼ばれる点に注意しましょう)。

最後にconsole.logで出力すると、渡した値を持つプロパティを備えたオブジェクトが返ってきます。この仕組みにより、新しく作成するすべてのBookオブジェクトに、それぞれ異なる値を割り当てられるのです。

thisでプロパティにアクセスする

const girl = {
  name: "Sarah",
  age: 26,
  introduce() {
    console.log(
      "Hi! " + "I'm " + this.name + " and I'm " + this.age + " years old."
    );
  },
};
JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

ここでは、nameとageというプロパティ、そしてintroduce関数を持つオブジェクトを変数girlとして定義しました。

introduce関数は、this.wanted_property の形式でオブジェクト自身のプロパティにアクセスし、自己紹介文を出力します。このように、thisを使うことでオブジェクトの内部データを安全に参照できるのです。

JavaScriptにおけるthisとスコープ

デフォルトでは、グローバルスコープ(定義された関数やオブジェクトの外)でthisを使用すると、グローバルオブジェクトを参照します。ブラウザ環境では、windowがグローバルオブジェクトです。

JavaScriptでプリミティブ型のデータ(文字列、数値、真偽値など)、オブジェクト、関数を宣言すると、それらはすべてグローバル(window)オブジェクトに紐付けられます。

つまり、「むき出し」で書かれたものはすべて、頭に window. が付いているものとして読み取ることができます。次のコードを見てみましょう。

function returnThis() {
  return this;
}

まず、グローバルスコープにreturnThisという関数を定義し、thisの値を返すようにします。そして returnThis() と書いて呼び出します。

この関数はグローバルスコープにあるため、window.returnThis() と見なせます。「ドットの左側」はwindowなので、戻り値もwindowオブジェクトになるわけです。

JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

オブジェクトの関数、いわゆるメソッド

今度は、同じ関数を内部に持つ新しいオブジェクト(オブジェクト内の関数は「メソッド」と呼ばれます)を定義して呼び出してみると、今度はそのオブジェクト自体(波括弧 {} で示されるもの)が返されます。

const object = {
  returnThis() {
    return this;
  },
};
object.returnThis();
JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

thisの罠と、その3つの解決策

ここで注意すべきポイントがあります。関数がオブジェクトに直接紐付いていない場合、その関数内のthisはグローバルオブジェクトを参照してしまうのです。

const weirdBehaviorObject = {
        directlyTiedFunction() {
          return this;
        },
        directlyTiedOuterFunction() {
          return function indirectNestedFunction() {
            return this;
          };
        },
      };

ブラウザで実行して結果を確認してみましょう。

JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

このオブジェクトには、thisを返す関数と、内部関数を返す関数(その内部関数もthisを返す)が定義されています。

indirectNestedFunctionを呼び出すには、まず最初の () で外側の関数を呼び出し、返された内部関数を取得します。続けてもう一度 () を呼ぶと、結果としてwindowが返ってきます。

これはJavaScript特有のミステリアスな挙動の一つで、予期しないバグの原因になりがちです。

幸い、この問題には解決方法があります。1つ目はES6以前から使われていたクロージャ、2つ目はアロー関数、3つ目はcall・applyメソッドと密接に関連するbindメソッドです。順番に見ていきましょう。

#1 クロージャでthisを解決する

クロージャとは、永続的なメモリ領域のようなもので、関数の実行が終わっても変数が消えずに残る仕組みです。ネストされた内部関数が、外側の関数で定義された変数を参照しているとき、JavaScriptエンジンによって作成され、外側の関数の実行終了後もその変数にアクセスできます。thisの例に当てはめると、次のようになります。

const objectWithClosure = {
        closureFunction() {
          const self = this;
          return function enclosedFunction() {
            return self;
          };
        },
      };
JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

closureFunctionを持つオブジェクトを定義し、その中でselfという変数にthisの値を保存しています。closureFunctionはオブジェクトに直接紐付いているため、この時点でのthisはそのオブジェクトを指します。

通常、enclosedFunctionの中のthisはwindowオブジェクトを参照してしまいますが、クロージャ経由で外側の関数のthisにアクセスすることで、呼び出し元のオブジェクトを値として保持できるのです。

#2 アロー関数でthisを解決する

2つ目の解決策は、ES6で導入された最も優れた機能の一つ、アロー関数を使う方法です。

アロー関数は、より簡潔な構文で関数を書けるだけでなく、thisを探す際にスコープを1階層上(自分を含んでいる関数)へ遡るという特徴を持っています。

そこで、オブジェクトを次のように定義してみます。

const arrowFunctionObject = {
        outerFunc() {
          return () => this;
        },
      };
JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

アロー関数がwindowではなく、実際のオブジェクトを返していることがわかります。

これは、アロー関数が自分を呼び出したouterFuncのスコープを辿り、そこでのthisがarrowFunctionObjectを指しているためです。つまり、アロー関数には一種の組み込みクロージャがあると言えます。

ネストされたアロー関数の場合

ここでもう一つの落とし穴があります。技術面接以外で遭遇することはほぼないかもしれませんが、知っておく価値は十分にあります。

通常の関数の中にアロー関数を入れ、さらにその中に関数をネストするとどうなるでしょうか?

const arrowNestedInRegularFunctionObject = {
        outerFunction() {
          return function regularFunction() {
            return () => this;
          };
        },
      };
JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

このオブジェクトのouterFunctionを呼び出すと、内側のregularFunctionが返されます。さらにregularFunctionを呼び出すと、最も内側のアロー関数が返されます。そして、それを呼び出すと、windowオブジェクトが返ってきてしまうのです。

理由は、アロー関数がスコープを1階層しか遡らないためです。アロー関数を包んでいるregularFunctionはオブジェクトに直接紐付いていないため、代わりにwindowが返されてしまいます。

一方、通常の関数の中にアロー関数を入れ、さらにその中にもう一つのアロー関数をネストすると、期待通りの結果——つまり実際のオブジェクト——が得られます。

const nestedArrowFunctionObject = {
        outerFunction() {
          return () => {
            return () => this;
          };
        },
      };
JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

この場合、内側のアロー関数は外側のアロー関数を辿り、外側のアロー関数もさらに1階層上を辿ります。regularFunctionであるouterFunctionは呼び出し元のオブジェクトに直接紐付いているため、結果としてオブジェクトがthisとして返されるのです。

#3 bindメソッドでthisを解決する

最後の3つ目の解決策は、bindメソッドを使う方法です。bindメソッドは、thisが何を参照すべきかを明示的に宣言するために使います。対象のオブジェクトを引数として渡すことで、そのオブジェクトが関数のthisキーワードに結び付けられます。

const arrowNestedInRegularFunctionObject = {
        outerFunction() {
          return function regularFunction() {
            return () => this;
          }.bind(arrowNestedInRegularFunctionObject);
        },
      };
JavaScriptの「this」を一度で完全理解!基本から落とし穴の解決策まで

この例では、通常ならwindowオブジェクトをthisとして返してしまう関数にbindをチェーンしています。引数としてthisに参照させたいオブジェクトを渡せば、問題は解決です。

理解してしまえば、thisは難しくない

お疲れ様でした!かなり深く掘り下げてきましたね。「なぜthisはこんなに混乱を招くのか?」と疑問に思った方もいるでしょう。

その理由は、JavaScriptの他の要素と違って、thisだけが動的スコープだからです。つまり、JavaScriptエンジンはコード実行時(ランタイム)に、「どこから」「誰が」呼び出したのかを見て、thisが何を表すのかを決定します。

対照的に、JavaScriptのその他の要素はすべて静的スコープ(レキシカルスコープ)です。これは、コードを書いた時点で、変数が定義された場所によって値が決まることを意味します。

thisのおかげで、オブジェクトの「設計図」を一度だけ作成し、先ほどのBookの例のように、異なるプロパティ値を持つオブジェクトを動的に生成できます。

また、this.wanted_property の形式で、オブジェクトのプロパティを公開・操作するメソッドも作れます。

JavaScriptの癖を理解して回避すれば、その挙動を自信を持ってコントロールし、期待通りの結果を得られるようになります。

thisの仕組みと、いくつかの重要な基礎概念を理解できた今こそ、実際に手を動かして実験し、練習を重ねながら、JavaScriptマスターへの道を着実に進んでいきましょう。

You got this!(あなたなら、きっとできます!)

  1. JavaScriptの分割代入を使った関数パラメータの受け渡し方法

    JavaScriptでは、関数にオブジェクトを引数として渡す際、「分割代入(デストラクチャリング)」と呼ばれる構文を利用することで、オブジェクトの各プロパティを個別の変数として受け取ることができます。 さらに、各プロパティにデフォルト値を指定しておけば、対応するプロパティが存在しない場合でもエラーにならず、あらかじめ設定した値が使用されます。これにより、安全で柔軟な関数設計が可能になります。 サンプルコード 以下は、分割代入を使って関数のパラメータを受け取るJavaScriptのコード例です。 <!DOCTYPE html> <html lang="en&quo

  2. Twitchエラー6000の解決方法!原因と対処法をわかりやすく解説

    Twitchはライブ配信で絶大な人気を誇る世界最大級のストリーミングプラットフォームです。しかし、人気が高いからといってバグや不具合がないわけではありません。他のアプリケーションと同様に、Twitchにもさまざまなエラーや問題が存在します。本記事では、その中でも多くのユーザーが報告しているTwitchエラー6000について詳しく解説します。Windows PCでTwitchを利用中にこのエラーに遭遇した方は、ぜひ本記事のトラブルシューティングガイドを参考にして問題を解決してください。 お使いのブラウザはこの動画の再生に対応していません。(エラー #6000) Twitchエラー6000の解決方