Masayan tech blog.

  1. ブログ記事一覧>
  2. JavaScriptのthisについて最低限押さえておくべきこと

JavaScriptのthisについて最低限押さえておくべきこと

公開日

TL;DR

  • アロー関数とそうでない関数(※以下、「通常関数」と記述)は、単に記述方法が異なるだけではなく、その違いの一つとして、thisの指す値が挙げられる
  • アロー関数では関数定義時に、通常関数では実行時にthisの値が決まる
  • 通常関数においてthisを決定する要因は実行モード、対象のスクリプトの種別、実行コンテキスト、呼び出し方、関数定義方法など複数あり、これを常に意識するのはつらみがある
  • つまり、基本的にはアロー関数使いましょう
  • アロー関数はthisを持たないため、そのアロー関数自身のより外側のスコープチェーンをたどった結果もっとも近いthisがアロー関数のコンテキスト内で参照できるthisとなる

thisは何を指すのか

thisの初期値

  • thisの値は状況により変動するので、一概に説明できないが初期値が存在する
  • 以下のとおり、strictモードか否かでthisの初期値が変わってくる

非strictモード

strictモード

windowオブジェクト

undefined

※本記事では、原則としてstrictモードの前提でコードを例示する

thisの前提知識

  • thisは読み取り専用のグローバル変数のため、上書き不可
  • プロパティの追加や値の変更は可能
  • 実際の開発におけるthisのユースケースのほとんどは、クラス内のthisである(以下のようなjQueryのthisもあるとは思うが・・・)
document.getElementById("submit-button").onclick = function() {
    // thisはbutton要素
    console.log(this);
};
  • globalThis
    • thisと同様にグローバルオブジェクトを指すが、thisとは異なり、実行環境(ブラウザ・サーバ)に依存しない
    • グローバルオブジェクトを参照したい場合、ブラウザ側ではwindowを、Node.js側ではglobalという別々のキーワードを使用する必要があったが、ES2020で実行環境にとらわれずグローバルオブジェクトを参照するためのキーワードとして導入された

thisはどのように決まるのか

  • 通常関数におけるthisの値は、関数が呼び出された際に動的に決定される(アロー関数はこの限りでない)
  • そのうえで、thisは大きく以下の要因によってその値が変化する

項目

内容

実行モード

strictモードか否か

対象のスクリプトの種別

ScriptかModuleか

実行コンテキスト

グローバルコンテキストと関数コンテキスト

呼び出し方

call, apply

関数定義方法

bindメソッド, アロー関数か否か

実行モード

  • 前述の通り、strictモードではthisの値はundefinedとなり、非strictモードであればwindowオブジェクトとなる(明示的にthisが指定されていなければ)

対象のスクリプトの種別

  • スクリプトとして実行されている場合で非strictモードであればwindowオブジェクト、strictモードの場合はundefinedとなる
<script>
console.log(this); //window(非strict) or undefined
</script>
  • モジュールとして実行されている場合は、strictモード扱いとなるため、undefinedとなる
<script type="module">
console.log(this); // undefined
</script>

実行コンテキスト

  • どのような状況(文脈)でコードが実行されているかという情報

グローバルコンテキスト

  • トップレベル(関数の外側)を指す
console.log(this); // undefined
```
function ...() {
    ///
};

関数コンテキスト

  • 関数が実行される直前に生成される、関数内の環境({ }の中)を指す
  • 通常関数では、thisは関数定義時でなく実行時に決まる
  • 関数コンテキストは、さらにいくつかの種類に分けられ、その内容ごとにthisの値が変わってくる
  • thisの値はオブジェクト.func()オブジェクト["func"]()のようなベースとなるオブジェクトがある場合はそのオブジェクトを、ない場合はundefinedとなる
const ...

function hoge() {
    // 関数コンテキスト({ }の中)
    console.log(this) // 原則undefinedだが、呼び出し方により変動する
};
constructor
  • thisはnew演算子でインスタンス化されたオブジェクトを指す
関数コンテキスト(入れ子)
  • 入れ子関数の場合、少し複雑になる(以下のコード例では、説明の便宜上、入れ子関数をpersonというオブジェクトに持たせている)
  • 入れ子関数の外側のthis(以下の①)personオブジェクトを、入れ子関数の内側のthis(以下の②)はundefinedを指す
    • ②はpersonオブジェクトから呼び出されているわけではないため(ベースオブジェクトがなく、通常関数のコンテキストと同じ要領)
  • なお、入れ子になる関数がいくつ増えても同じ動作となる
const person = {
  printName: function() {
    console.log(this) //personを指す・・・①
 
    const inner = function() {
      console.log(this) //undefinedを指す・・・②
    }()
  }
};

person.printName()
メソッド
  • thisはnew演算子でインスタンス化されたオブジェクトを指す
class Person {
  constructor(name) {
    this.name = name;
  }

  printName() {
    console.log(this.name); //Person のname("tanaka")を指す
  }
}

const person = new Person("tanaka")
person.printName() // "tanaka"
  • ネストしたオブジェクトでも考え方は同じでベースオブジェクトに着目すればよい
const person = {
  name: {
    fullName: {
      print() {
        return this;
      },
    },
  },
};

console.log(person.name.fullName.print() === person.name.fullName); //true
  • なお、以下のようにメソッドを関数の外でいったん別の変数に代入して実行すると、thisはundefinedを指すようになる(通常関数コンテキストと同じ要領)
const person = {
  printName: function() {
    console.log(this) //personを指す
  }
};

// ↓

const printName = person.printName

printName() // undefinedを指す

以下のように,演算子を使用してもおなじ

const person = {
  printName: function () {
    console.log(this); //personを指す
  },
};

//↓

(0, person.printName)(); // undefinedを指す
グローバルオブジェクトがもつメソッド内
  • グローバルオブジェクトがもつメソッドとは、例えばsetTimeoutなどのこと
  • これらのメソッドの中でthisを呼び出す場合は、thisundefinedを指す
  • ※ 後述の通り、setTimeoutに渡す関数をアロー関数にするとこの限りではない
class Person {
  constructor(name) {
    this.name = name;
  }

  printName() {
    console.log(this); //Person {name: "tanaka"}を指す

    setTimeout(function () {
      console.log(this); // undefined
    }, 1000);
  }
}

const person = new Person("tanaka")
person.printName()
コールバック関数
  • コールバック関数内で
  • mapに渡したコールバック関数は、配列の各要素に対して適用され、その際の呼び出され方は単にcallbackFn()イメージで、ベースオブジェクトがないため、thisはundefinedとなる
  • 以下では、mapの引数に渡しているコールバック関数内でthisを参照しているthisがundefinedとなることを知らず自身のオブジェクトを指すと思いnameプロパティを呼び出してエラーになっているケース
class Person {
  name;

  constructor(name) {
    this.name = name;
  }

  isContain(names) {
    names.map(function (name) {
      console.log(this.name === name); //TypeError: Cannot read properties of undefined (reading 'name')
    });
  }
}

new Person('keito').isContain(['keito']);

呼び出し方

  • call, applyを使用すると、thisの値を指定して関数を呼び出すことが可能
  • 以下の①では、通常通りpersonオブジェクトのメソッドを呼び出しているので、thisはpersonを指している
  • 以下の②では、callを使用してメソッドを呼び出しているので、thisはundefinedを指している
const person = {
  printName: function() {
    console.log(this)
  }
};

person.printName() //personを指す・・・①
person.printName.call() //undefinedを指す・・・②

call,applyの違いは第二引数(=関数の引数)の指定方法が、配列かそうでないかの違いのみ

function printProfile(hobby, myfavoriteFood) { //本来、関数コンテキストのthisはundefinedだが、call,applyでthisを指定しているのでpersonを指す
  console.log(this.age); //20
  console.log(hobby); //釣り
  console.log(myfavoriteFood); //ハンバーガー
}

const person = {
  name: 'tanaka',
  age: 20,
};

const hobby = '釣り';
const myfavoriteFood = 'ハンバーガー';

printProfile.call(person, hobby, myfavoriteFood);
printProfile.apply(person, [hobby, myfavoriteFood]);

上記で説明済みの関数コンテキスト(入れ子)の場合も以下の通り、inner関数をcallを使ってthisを指定してあげると、personを指すようになる

const person = {
  printName: function () {
    console.log(this); //personを指す

    const inner = function () {
      console.log(this); //personを指す
    };

    inner.call(this);
  },
};

person.printName();

関数定義方法

bind
  • bindメソッドを使用することで、関数定義時にthisを固定することが可能
  • 以下のようにすることで、setTimeout内のthisをundefinedからpersonオブジェクトに変更(固定)することが可能
  • bindの返り値は、オブジェクト束縛後の生成された新しい関数が返る
class Person {
  constructor(name) {
    this.name = name;
  }

  printName() {
    console.log(this); //Person {name: "tanaka"}

    setTimeout(function () {
      console.log(this); //Person {name: "tanaka"} こちらもPersonを指すようになる
    }.bind(this), 1000);
  }
}

const person = new Person("tanaka")
person.printName()

なお、以下のようアロー関数の形式でsetTimeOutに関数を渡すと、thisが自身のpersonオブジェクトを指すようになる(※アロー関数についての詳細は下部で説明)

class Person {
  constructor(name) {
    this.name = name;
  }

  printName() {
    setTimeout(() => {
      console.log(this); //Person {name: "tanaka"}を指すようになる
    }, 1000);
  }
}

const person = new Person('tanaka');
person.printName();
アロー関数
  • thisは実行時ではなく、宣言時に静的に決まる(=レキシカルスコープ)
  • thisはそのアロー関数自身の外側のスコープチェーンをたどった結果としてもっとも近いthisを指す(常にアロー関数の外側の関数のthisをそのまま利用)
class Person {
  name;

  constructor(name) {
    this.name = name;
  }

  isContain(names) {
    names.map((name) => {
      console.log(this.name === name); //true
    });
  }
}

new Person('keito').isContain(['keito']);
  • イベントハンドラー内のthisは、イベントの発生元(要素)を表す
  • 以下のようなイベントリスナーの登録時にアロー関数を使うかそうでないかで、thisの値が変わってくる
    (=なんでもかんでもアロー関数を使用すればよいということではない)
document.getElementById("submit-button").onclick = function() {
    // thisはbutton要素
    console.log(this);
};
document.getElementById("submit-button").onclick = () => {
    // thisはundefined
    console.log(this);
};
  • アロー関数の外側の関数のthisをアロー関数内で参照できるのみで、アロー関数自体はthisを保有していない
  • アロー関数に対してcall、apply、bindを使ったthisの指定はできない(指定しても無視される)
const arrow = () => {
  return this;
};

console.log(arrow()); // undefined

// callを使用しても、thisは変わらない
console.log(arrow.call({ name: 'hoge' })); // undefined

まとめ

いかがでしたでしょうか。本記事では、JavaScriptのthisについて最低限押さえておくべきことについてサンプルコードを提示しながら説明しました。特に、通常関数とアロー関数におけるthisの差異や、thisを固定する方法については重要性が高いので、ぜひ押さえてください。