🟡

[JavaScript]thisを徹底理解! 8本ノック付き

2024/04/07に公開

エンジニアとして2年目になり、以前と比べてコードも書けるようになってきたなと思う反面、いまだに「JavaScriptのthisってなんか複雑…わかりづらい…」と感じることが多く。

どうせなら徹底的に理解してやろうと思い、記事にしてみました。

記事の構成

本記事では、まず「thisはどのように決められるのか」について触れた後、いくつかの問題を経て理解を深めていただく構成になっています。また、thisを固定するbindやcallメソッドについては取り上げませんので、あらかじめご了承ください。

対象読者

  • JavaScript初心者
  • thisに苦手意識を感じている
  • thisが何を指しているか分からなくなる時がある


thisはどのように決められるか

thisは以下のような特性があります。

  • コンテキストによって変わる
  • 呼び出し元に依存する

上記が基本的な特性ですが、function関数とアロー関数とで定義されている時のみ、挙動が変わるため、こちらも踏まえて解説します。


コンテキストによって変わる

thisは固定で特定の値を指しているわけではありません。thisがどのコンテキストで使用されるかで、thisの値は変わってきます。以下は、一覧にまとめたものです

コンテキスト this
グローバル グローバルオブジェクト
function関数 グローバルオブジェクト
function関数(strictモード) undefined
アロー関数 thisを持たない
イベントリスナー イベントの発生源
コンストラクター 生成したインスタンス
メソッド 呼び出し元のオブジェクト
静的メンバー 呼び出し元のクラス


呼び出し元に依存する

MDNには、以下の記述があります

ほとんどの場合、this の値はどのように関数が呼ばれたかによって決定されます (実行時結合)

前節でコンテキストによって変わると挙げましたが、上記を踏まえて以下のように捉えられます。


例を挙げてみます

globalThis.name = '佐藤太郎'

const man = {
  name: '田中太郎',
  greet() {
    console.log(`私の名前は${this.name}です。`)
  }
}

function callGreet(greet) {
  greet()
}

man.greet()
// => "私の名前は田中太郎です。"

callGreet(man.greet)
// => "私の名前は佐藤太郎です。"
  • man.greet()では、単純にmanオブジェクトのメソッドとして実行されています。
    この時thisは「呼び出し元のオブジェクト」であるmanが設定され、this.nameには、'田中太郎'さんが参照されます。
  • callGreet(man.greet)では、man.greetをコールバック関数として、関数内で呼び出されています。この時、コンテキストは関数となり、thisはグローバルオブジェクトが参照されるため、this.nameには’佐藤太郎’さんが参照されます。

上記の例では同じman.greetを実行していますが、どのコンテキストで関数を実行しているかによってthisが変化していることがわかります。

同じ関数やメソッドでも、呼び出し元によってthisは変化するものであることをまずは抑えていただければと思います。


アロー関数内でのthis

基本的にはこれまで挙げたものを理解していただければ問題ないのですが、アロー関数は例外です。まとめると以下のような点で例外です。

  • thisを持たない
  • どのように関数が呼ばれたかは関係ない。定義されているスコープに基づいて、事前にthisを固定する

こちらも例を挙げてみます


globalThis.name = '佐藤太郎'

const man = {
  name: '田中太郎',
  greet() {
    console.log(this === globalThis)
    console.log(`私の名前は${this.name}です。`)
  },
  greet2:() => {
    console.log(this === globalThis)
    console.log(`私の名前は${this.name}です。`)
  }
}

man.greet()
// => false
// => "私の名前は田中太郎です。"

man.greet2()
// => true
// => "私の名前は佐藤太郎です。"

  • man.greet()では先ほどと同じ構文なので割愛します。
  • man.greet2()では、アロー関数で定義されています。このとき、thisはgreet2メソッドを包含しているmanオブジェクト上のコンテキストとなります。manオブジェクトはグローバルで定義されているので、thisはグローバルオブジェクトとなり、佐藤太郎さんの名前が出力されるわけです。

上記のように、アロー関数で定義してしまうとthisの参照先が変わってしまうため、MDNでは、オブジェクト内でアロー関数を使用することは推奨していません。

アロー関数式は自分自身で this を持たないので、メソッドではない関数にのみ使用してください。




以上ここまでがthisに関する解説になります。理解が怪しい点は再度読み返していただき、ぜひこの後の問題に取り組んでみてください。




8本ノック

thisの理解を深めていただくために、問題を用意しました。コンソール上に出力される値を、選択肢の中から選んでください。

1問目

globalThis.a = 43

function setA() {
  this.a = 21
}

setA() 
console.log(globalThis.a)

回答

43

21

⭕️


2問目

globalThis.age = 28

function greet() {
  this.age = 21
  console.log(`私は${this.age}歳です`)
}

greet()

回答

私は28歳です

私は21歳です

⭕️

Uncaught TypeError


3問目

globalThis.age = 28

function greet() {
  'use strict';
  this.age = 21
  console.log(`私は${this.age}歳です`)
}

greet()

回答

私は28歳です

私は21歳です

Uncaught TypeError

⭕️


4問目

globalThis.name = 'James'
globalThis.age = 24

class Student {
  constructor(name, age) {
     this.name = name
     this.age = age
     console.log(this)
  }
}

const name = 'Bob'
const age = 26

new Student(this.name, this.age)

回答

{ name: “James”, age: 24}

⭕️

{ name: “Bob”, age: 26}


5問目

globalThis.name = 'James'
globalThis.age = 24

class Student {
  constructor(name, age) {
     this.name = name
     this.age = age
  }

  greet() {
     console.log(`私の名前は${this.name}です。年齢は${this.age}です`)
  }
}

const student = new Student('Bob', 26)
student.greet()

回答

"私の名前はJamesです。年齢は24です"

“私の名前はBobです。年齢は26です”

⭕️


6問目

globalThis.name = 'James'
globalThis.age = 24

const student = {
  name: 'Bob',
  age: 26,
  greet() {
    console.log(`私の名前は${this.name}です。年齢は${this.age}です`)
  }
}

function fn(callback) {
  callback()
}

fn(student.greet)

回答

"私の名前はJamesです。年齢は24です"

⭕️

"私の名前はBobです。年齢は26です"

Uncaught TypeError


7問目

globalThis.name = 'James'
globalThis.age = 24

class Student {
  constructor(name, age) {
    this.name = name
      this.age = age
  }

   greet() {
    console.log(`私の名前は${this.name}です。年齢は${this.age}です`)
   }
}

function fn(callback) {
  callback()
}

const student = new Student('Bob', 26)
fn(student.greet)

回答

"私の名前はJamesです。年齢は24です"

"私の名前はBobです。年齢は26です"

Uncaught TypeError

⭕️
クラスのメソッドは、strictモードであるため
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Strict_mode#クラスでの厳格モード


8問目

globalThis.name = 'James'
globalThis.age = 24

const student = {
   name: 'James',
   age: 28,
   information: {
       introduce:() => {
           return `名前: ${this.name} 年齢: ${this.age}`
       }
   }
}

console.log(student.information.introduce())

回答

"名前: James 年齢: 24"

⭕️

"名前: Bob 年齢: 28"

Uncaught TypeError




まとめ

いかがでしたでしょうか?

関数の定義方法によって、またコンテキストやstrictモードかによっても変化するthisですが、整理してみると、さほど複雑ではない印象を持ちました。何事もきちんと整理して、理解を深めるのは大切ですね、、これからも曖昧な点は記事に残していきたいと思います。

最後まで読んでいただきありがとうございました!JavaScriptのthisついて、少しでも理解が深まれば幸いです。ではまた!




参考

Discussion