1
3

More than 1 year has passed since last update.

JavaScriptのthisについて

Last updated at Posted at 2022-06-19

初めに

今回はthisについて自分なりにまとめたものをアウトプットしたいと思います。
主にこの二つの文章を参考したうえの勉強メモです。

検証を書いたら文章が長くなってしまって、TLDRの方は結論だけ読んでもいいと思います。

thisとは

まずはMDNの説明によると、

ほとんどの場合、this の値はどのように関数が呼ばれたかによって決定されます (実行時結合)。これは実行時に代入によって設定することはできず、関数が呼び出されるたびに異なる可能性があります。
this - JavaScript | MDN

strict モードではないthisはどこに存在するのに関係なく、どのように呼び出されることに関連している。しかしcall()apply()bind()バインドメソッドの導入に連れ、thisの応用範囲が一気に拡大しました。

ES5 では bind() メソッドが導入され、関数がどのように呼ばれたかに関係なく this の値を設定するすることができるようになり、ES2015 では、自身では this の結び付けを行わないアロー関数が導入されました (これは包含する構文上のコンテキストの this の値を保持します)。
this - JavaScript | MDN

ES5で導入されたメソッドによって値を設定することが可能になった。(説明文はbind()を書いてあるが、実際はほかにcall()、apply()もthisのコンテキストを決定することができる)
実際調べたら、thisは扱い方によっていくつの形態があるそうです。まずはthisの概念からまとめていきたいと思います。

Default this context & Object literals{this}

thisの概念は、関数が生成されたとき、その背後にthisというキーワードも創られ、その関数の執行範囲(コンテキスト)を決定する。

そして範囲を決定するというのは、どう呼び出されるのかにつながるので、

// default this context
const myFunction = function () {
  console.log(this)
}
myFunction()
// Object [global]
// this refers to window

myFunction()のようにglobal scopeで呼び出され、ここのthiswindowというオブジェクトにコンテキストを決めるようになる。

// object literals
const a = 1
const myMethod = function () {
  const a = 2
  console.log(this.a)
  console.log(this)
}

const myObject = {
  a: 3,
  myMethod: myMethod
}
myMethod()
// undefined
// Object [global] === window
// this refers to window
// note: there dose not exist a variable called 'a' in window object

myObject.myMethod()
// 3
// { a: 3, myMethod: [Function: myMethod] } === myObject
// this refers to myObject

myMethod()myObjectというブロックスコープ(Block Scope)から呼び出され、myObjectオブジェクトにコンテキストを決める。

そして、もし多重スコープ内で呼び出されたら、

// multiple scopes
const outerObject = {
  name: 'Mick',
  age: 20,
  innerObject: {
    name: 'Lucy',
    age: 24,
    fn: function () {
      console.log(this.name, this.age)
      console.log(this)
    }
  }
}

outerObject.innerObject.fn()
// Lucy 24
// { name: 'Lucy', age: 24, fn: [Function: fn] } === innerObject
// this refers to innerObject

どんな多重スコープであっても、fnのようにthisは一個上のコンテキストに直結している。

これでMDNの説明文、関数がどこで宣言されたかに関係なく、どう呼び出されたに関連しているということがわかるようになりました。
そしてthisのこのような動きが、 Implicit binding(暗黙の結合) とも呼ばれています。

ES5からcall()apply()bind()で強制的にthisのコンテキストを変えることもできるようになって、Explicit binding(明確な結合)Hard binding(強力な結合) というやり方も出てきました。
(日本語と英語が母語ではありませんので、変な和訳してしまったらご容赦ください)

Explicit binding

call()apply()Explicit binding(明確な結合) に属して、

const myMethod = function (a, b, c) {
  console.log(this)
  console.log(a * b * c)
}

const myObject = {
  myMethod: myMethod
}

myMethod() // Object [global] === window // NaN
myMethod.call(myObject, 1, 2, 3) // myObject // 6
myMethod.apply(myObject, [4, 5, 6]) // myObject // 120
myMethod.call(null, 1, 2, 3) // Object [global] === window // 6
myMethod.apply(null, [4, 5, 6]) // Object [global] === window // 120
/* note:
func.call([thisArg[, arg1, arg2, ...argN]])
func.apply(thisArg, [argsArray])
*/

上例のようにcall()apply()は強制的にmyMethodのコンテキストをmyObjectに変えるようになった。またcall()apply() メソッドの違いは後ろのパラメータ arg の引数の形態です。

そしてDOM操作のとき返してきた配列風オブジェクトの処理ではこれらのメソッドよく使われています。(ここは単純にオブジェクトを例として書いてみたが実際はもっと複雑なものになる)

// another example
const DOMObject = {
  length: 2,
  0: 'first',
  1: 'second'
}

console.log(Array.prototype.slice.call(DOMObject, 0, 2)) // [ 'first', 'second' ]
console.log(Array.prototype.slice.apply(DOMObject, [0, 2])) // [ 'first', 'second' ]
console.log([].slice.call(DOMObject, 0, 2)) // [ 'first', 'second' ]
// note: [].slice.call(document.querySelectorAll('...'), arg1, arg2)
console.log(Array.from(DOMObject)) // [ 'first', 'second' ]

Hard binding

bind()Hard binding(強力な結合) として、一番強制力のあるメソッドです。
call()apply()と違って、bind()で一旦thisコンテキスト決めたらcall()apply()使っても変えることができません。

let myMethod = function () {
  console.log(this.a)
  console.log(this)
}

const obj1 = {
  a: 2
}

const obj2 = {
  a: 3
}

myMethod = myMethod.bind(obj1)
console.log(myMethod())
// 2
// { a: 2 } === obj1
// this refers to obj1

myMethod.call(obj2)
// 2
// { a: 2 } === obj1
// this still refers to obj1

// note: bind() -> function currying

また、call()apply()はほかの関数と連携して値を返してもらうことに対して、bind()はコンテキストだけを決めていろいろ応用するのが多いそうです。

// freeze btn 3s
const btn = document.querySelector('button')
btn.addEventListener('click', eventHandler)

function eventHandler() {
  this.disabled = true
  console.log(this) // btn

  setTimeout(function () {
    this.disabled = false
    console.log(this) // window
    // this refers to window
    // btn won't retrieve
  }, 3000)
  console.log(this)  // btn
}
// use bind()
function eventHandler() {
  this.disabled = true
  console.log(this) // btn

  setTimeout(function () {
    this.disabled = false
    console.log(this) // btn
    // this refers to btn
  }.bind(this), 3000)
  console.log(this) // btn
}

// don't know the difference
// btn.addEventListener('click', function eventHandler() {
//   this.disabled = true
//   console.log(this) // btn

//   setTimeout(function () {
//     this.disabled = false
//     console.log(this) // window
//     // this refers to window
//     // but btn will be functional
//   }, 3000)
//   console.log(this)  // btn
// })

setTimeout()WebAPIsなので、bind()使わない限りthisはブラウザwindowに参照している。
(別の話ですけれど、addEventListenerの内(inside)に書いてみると、thiswindowに参照しているが、なぜかbtnは正しく動作してくれました。原因はまだわからないですが...)

そして、bind()の使い方を調べながら試行錯誤のなか、何となく分割代入(Destructuring assignment)と概念が似ている。(ちなみにbind()はオブジェクトとの結合により特定の引数を設定し、関数の土台になるというやり方もあります。)
MDNの例をご参考にしてください↓

function foobar(something) {
  this.a = something
  console.log(this)
}

const obj = {}
const fb = foobar.bind(obj)
fb(2) // { a: 2 }
console.log(obj.a) // 2

// note: it's similar to
// const obj = {}
// const { a } = obj
// obj.a = 2
// console.log(obj.a) // 2

Weight of the binding

Implicit binding < Explicit binding < Hard binding < new binding

New binding

// new instance
function foo(something) {
  this.name = 'foo'
  this.a = something
  console.log(this.name)
  console.log(this.a)
  console.log(this)
}

foo(2)
// foo
// 2
// Object [global] {..., name: 'foo', a: 2} === window
// this refers to window

const f = new foo(5)
// foo
// 5
// foo { name: 'foo', a: 5 }
// this refers to foo itself

// console.log(f.name) // foo
// console.log(f.a) // 5

newは新しいインスタンスを作っていくため、そのなかのthisは常に新しいコンテキストに参照する。
bind()使ってもインスタンス中のthisを変えることができません。

this in bind() & new

// this in bind() & new
function foo(something) {
  this.a = something
}

const obj = {}
const bar = foo.bind(obj)
bar(2)
console.log(obj.a) // 2

const newBar = new bar(3)
console.log(obj.a) // 2
console.log(newBar.a) // 3

this in Arrow function

// arrow function as methods
const myArrowMethod = () => {
  console.log(this.name)
  console.log(this)
}

const arrow = {
  name: 'Luna',
  myArrowMethod: myArrowMethod
}
myArrowMethod()
// undefined
// {} === window or Object [global]

arrow.myArrowMethod()
// undefined
// {} === window or Object [global]

arrow functions don't bind their own scope, but inherit it from the parent one, which in this case is window or the global object.
Arrow functions as methods

アロー関数のスコープ(コンテキスト)は親から継承するので、コンテキストもwindowobject [global]に参照している。(arrow.myArrowMethodの親であるmyArrowMethodのコンテキストはwindowに参照しているから)

そして下のように書き方を変えたら、親であるmyArrowMethodはすでにオブジェクトarrowに入っているので、スコープを継承したmyArrowFunctionも同じコンテキストに指しています。

const arrow = {
  myArrowFunction: null,
  myArrowMethod: function () {
    this.myArrowFunction = () => {
      console.log(this)
    }
  }
}

// call `myArrowMethod` to initialize `myArrowFunction`
arrow.myArrowMethod()
// call `myArrowFunction` to print `this`
arrow.myArrowFunction()
// {
//   myArrowFunction: [Function(anonymous)],
//   myArrowMethod: [Function: myArrowMethod]
// }

const myArrowFunction = arrow.myArrowFunction
myArrowFunction()
// {
//   myArrowFunction: [Function(anonymous)],
//   myArrowMethod: [Function: myArrowMethod]
// }

そしてアロー関数に対し、call()apply()bind()メソッドでコンテキストを変えることができません。

const myArrowFunction = () => {
  console.log(this)
}

const myObject = {}

myArrowFunction.call(myObject) // {} === window or Object [global]
myArrowFunction.apply(myObject) // {} === window or Object [global]

const myFunctionBound = myArrowFunction.bind(myObject)

myFunctionBound() // {} === window or Object [global]

const newFunction = new myArrowFunction()
// TypeError: myArrowFunction is not a constructor

/* note: arrow functions can't be called as constructors, 
and the prototype property doesn't exist for arrow functions
*/

newを使って新しいイスタンスを作ることも不可能です。
詳細についてこちらご参考してください↓

API asynchronous callbacks

例えばsetTimeout()addEventListener()などのasynchronous codeはブラウザが提供した関数なので、常にwindowに指しているが、bind()かアロー関数でコンテキストを変えることが可能です。

// those examples all make btn functional
// example1
btn.addEventListener('click', () => {
  this.disabled = true
  console.log(this) // window

  setTimeout(function () {
    this.disabled = false
    console.log(this) // window
  }, 3000)
  console.log(this) // window
})
// note: it's functional but I don't know why it works

example1のようにsetTimeout()addEventListener()のコンテキストwindowに指している。

// example2
const myObject = {
  a: 1,
  myMethod: function () {
    const a = 5
    console.log(this) // {a: 1, myMethod: ƒ}
    console.log(this.a) // 1
    btn.addEventListener('click', () => {
      this.disabled = true
      const a = 10
      console.log(this) // {a: 1, disabled: true, myMethod: ƒ}
      console.log(this.a) // 1
    })
  },
}

myObject.myMethod()
// {a: 1, myMethod: ƒ}
// {a: 1, disabled: true, myMethod: ƒ}
// {a: 1, disabled: true, myMethod: ƒ}

example2からはちょっと変なことが起こってると見えるかもしれません。

ここからは直接引用できる文章がなかなか見つからないので、自分の考察と合わせて解釈していきたいと思います。
まずmyMethod: function () {...}という無名関数のthisを注目していただきたいと思います。
ここは一般の関数の Object literals {this} とつながって、つまり無名関数はmyObjectオブジェクトというスコープに参照しているということです。

中のbtn.addEventListener('click', () => {...})のほうはアロー関数を使って、アロー関数自体はthis持たないため 一番近いスコープに帰属する(継承する) ので、myMethodと同じmyObjectのスコープに格納されました。

なのでmyObject.myMethod()と、アロー関数使用したaddEventListener()と、this.amyObject.aに指していて、addEventListener()this.disabled = truemyObjectのプロパティになりました。

アロー関数の特性を利用してsetTimeout()のコールバックのスコープをmyObjectに変えることもできる。

// example3
const myObject = {
  a: 1,
  myMethod: function () {
    console.log(this) // {a: 1, myMethod: ƒ}
    btn.addEventListener('click', () => {
      this.disabled = true
      console.log(this) // {a: 1, disabled: true, myMethod: ƒ}

      setTimeout(() => {
        this.disabled = false
        console.log(this) // {a: 1, disabled: false, myMethod: ƒ}
      }, 3000)
      console.log(this) // {a: 1, disabled: true, myMethod: ƒ}
    })
  }
}

myObject.myMethod()
// {a: 1, myMethod: ƒ}
// {a: 1, disabled: true, myMethod: ƒ}
// {a: 1, disabled: true, myMethod: ƒ}
// {a: 1, disabled: false, myMethod: ƒ}

bind()を使ってもいいです。

// example4
const myObject = {
  a: 1,
  myMethod: function () {
    console.log(this) // {a: 1, myMethod: ƒ}
    btn.addEventListener('click', () => {
      this.disabled = true
      console.log(this) // {a: 1, disabled: true, myMethod: ƒ}

      setTimeout(function () {
        this.disabled = false
        console.log(this) // {a: 1, disabled: false, myMethod: ƒ}
      }.bind(this), 3000) // this === myObject
      console.log(this) // {a: 1, disabled: true, myMethod: ƒ}
    })
  }
}
myObject.myMethod()

しかしいったんaddEventListener()のスコープが変わってしまえば、setTimeout()も変わっていく。

const myObject = {
  a: 1,
  myMethod: function () {
    console.log(this) // {a: 1, myMethod: ƒ}
    btn.addEventListener('click', function () {
      this.disabled = true
      console.log(this) // <button class="btn" disabled>Add New Column</button>

      setTimeout(() => {
        this.disabled = false
        console.log(this) // <button class="btn">Add New Column</button>
      }, 3000)
      console.log(this) // <button class="btn" disabled>Add New Column</button>
    })
  }
}
myObject.myMethod()

addEventListener()どうしてwindowに戻るのではなく、btnに変わったんだろう。自分の解釈では、btnもオブジェクトなので、いまbtnglobal scopeではなく、 Object literals {this} の多重スコープのなかにあるので、thisの指向は自分の一個上の親と直結しているわけです。(Implicit binding)
myObject.myMethod.btn.addEventListener()、こんな感じです。

そしてsetTimeout()addEventListener()と違い、アロー関数もbind()も使わなかったら常にwindowに指している。

結論

オブジェクトの中にある暗黙状態のthisは自分の一個上の親に指している。
call()apply()は一時にthisコンテキストを変えることができ、戻り値を別のメソッドに使わせる。
bind()は永久的にthisコンテキストを変え、ほかの関数の土台を作れる。
newのコンテキストは常に新しいイスタンス(つまり新しい自分自身)に指している。

アロー関数は一個上の親のスコープを継承するので、親のスコープを注意すべきです。
WebAPIsのコンテキストは常にwindowを指しているが、bind()やアロー関数でコンテキストを変えることができる。

参考資料まとめ

Understanding "This" in JavaScript | Codementor
Understanding "this" in javascript with arrow functions | Codementor
Anomalies in JavaScript arrow functions

1
3
0

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
1
3