JavaScriptのthisは難しくない
JavaScriptのthisを完全に理解したのでまとめます。
JavaScriptのthisは難しくない
最初はキモすぎて理解不能でしたが要するにこうゆうことだと思います。
- 呼び出し元によってthisの値は変わる。
- thisの値は固定することができる。
突然undifinedになるthis
JavaScriptを少し触り始めるとthisが想像とは違う値になっている事に気づきます。
class Dog {
constructor(name) {
this.name = name;
}
bark() {
console.log(`私は ${this.name} です。`);
}
}
const dog = new Dog('Bob');
dog.bark() // 私は Bob です
const bark = dog.bark
bark() // Uncaught TypeError: Cannot read property 'name' of undefined
これは他言語とは異なる動きだと思います。
一例としてSwiftを挙げています。
final class Dog {
private let name: String
init(name: String) {
self.name = name
}
func bark() {
print("私は \(self.name) です。")
}
}
let dog = Dog(name: "Bob")
dog.bark() // 私は Bob です
let bark = dog.bark
bark() // 私は Bob です
Reactなどのフレームワークをを使っていても、突然thisがundifinedになってしまい最初は困惑していまします。
thisの呼び出しパターン
thisは呼び出しかたによって変わります。
関数呼び出し
通常の呼び出しではグローバルオブジェクトを指します。 ブラウザ上ではwindowオブジェクトでです。
function hello() {
console.log(this); // Window {parent: Window,
console.log(this === window); // true
}
ただストリクトモードだとundifinedになります。
function hello() {
'use strict';
console.log(this); // undifined
console.log(this === window); // false
}
メソッド呼び出し
メソッドとして呼ばれるときは呼び出しもとがthisになります。 つまりドットの前です。
function hello() {
console.log(this.name)
}
const obj1 = {
name: 'taro',
hello
}
const obj2 = {
name: 'jiro',
hello
}
hello() // window.name (関数呼び出し)
obj1.hello() // taro (thisはobj1)
obj2.hello() // jiro (thisはobj2)
クラスのメソッドもprototypeチェーンに追加されるためこれと同様である。
コンストラクタ呼び出し
new を演算子を使う場合はthisは生成されるオブジェクトを指します。
以下のUser関数があるとして
function User(name) {
this.name = name;
}
普通に呼び出すとthisはグローバルオブジェクトを指します。
console.log(window.name)
User('taro')
console.log(window.name) // 'taro'
new 演算子を使うと
const user = new User('taro')
user.name // taro
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/new
DOM イベントハンドラ
DOMのイベントハンドラに登録するとthisの値は対象のDOM要素を指します。 ブラウザによって多少の違いがあるようです。
document.body.addEventListener('click', function(e) {
console.log(this === e.currentTarget) // true
})
MDNを見る限りこんな感じです。DOMのパターンは知りませんでした。
thisを固定する方法
thisは呼び出しかたによって変わってしまいますが、thisを固定する方法はあります。
bind call apply
これらのメソッドを使うとthisを固定することができます。
bindはthisを固定した関数を返しcall,applyは実行します。
function hello() {
console.log(this.name)
}
const obj = { name: 'taro' }
const binded = hello.bind(obj)
binded(); // taro
hello.call(obj) // taro
hello.apply(obj) // taro
arrow function
Arrow関数もthisを束縛することができます。
const hello = () => console.log(this.name)
const obj1 = {
name: 'taro',
hello
}
obj1.hello() // window.name
定義時にthisがグローバルオブジェクトに固定されたので、メソッドとして呼び出したのにthisはグローバルオブジェクトのままです。
補足
thisの退避
数年前に作られたライブラリのソースを読んでいるとよく使われているが、
that
や_this
といった変数にいれることで対処をしている。
function User(name) {
this.name = name
}
User.prototype.hello = function() {
setTimeout(function() {
console.log(this.name)
}, 100)
}
100ms後に挨拶をしようとするとsetTimeoutの引数のfunctionの呼び出し元は不明です。 もし内部でuserを呼ぶことになっていればうまく動くのですがそうではないようです。 なのでthisを一旦別の変数に対比しておくことでやりたいこと実現します。
User.prototype.hello = function() {
const _this = this
setTimeout(function() {
console.log(_this.name)
}, 100)
}
arrow関数のトランスパイル後
今回は出力が短くなる理由でtscを使っています。内部でthisを退避していることがわかるかと思います。
echo "{}" > package.json
npm i -D typescript
普段このような書き方はしないと思いますが
class Hello {
functionWorld = function() {
console.log(this)
}
arrowFunctionWorld = () => {
console.log(this)
}
}
./node_modules/.bin/tsc hello.ts
出力されるファイルを見ると
var Hello = /** @class */ (function () {
function Hello() {
var _this = this;
this.functionWorld = function () {
console.log(this);
};
this.arrowFunctionWorld = function () {
console.log(_this);
};
}
return Hello;
}());
このようにthisを退避している。
まとめ
JavaScriptのthisは難しくはないが気持ち悪い。