初めに
今回は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で呼び出され、ここのthisはwindowというオブジェクトにコンテキストを決めるようになる。
// 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)に書いてみると、thisはwindowに参照しているが、なぜか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
アロー関数のスコープ(コンテキスト)は親から継承するので、コンテキストもwindowかobject [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.aはmyObject.aに指していて、addEventListener()のthis.disabled = trueもmyObjectのプロパティになりました。
アロー関数の特性を利用して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もオブジェクトなので、いまbtnはglobal 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