Qiitaに書いたやつ

JavaScriptのthisは難しくない

JavaScriptTypeScriptVue.jsReact
2020年04月12日

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 - JavaScript | MDN

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

普段このような書き方はしないと思いますが

hello.ts
class Hello {
  functionWorld = function() {
     console.log(this)
  }

  arrowFunctionWorld = () => {
     console.log(this)
  }
}
./node_modules/.bin/tsc hello.ts

出力されるファイルを見ると

hello.js
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は難しくはないが気持ち悪い。

同じタグの投稿

2020 churabou