初めに
今回は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