1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

javascriptにおけるthisの決定の仕組みと対策

Last updated at Posted at 2020-03-13

javascriptのthisってなんなんですか?と聞かれたときに明確な回答ができず、先輩風を吹かせ損なったので備忘録的に復習。めんどくせえこと聞きやがって。俺も知らねえよ。

thisが決定される仕組み

javascriptのthisは呼びされ方や呼び出されるタイミングによって、参照されるものが変更される。
実際にログを出しながら見た方がわかりやすい気がするので、適当にhtmlとjsを用意。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        div.text-color { color: red}
    </style>
</head>
<body>
    <script src="main.js"></script>
    <div id="container">
        <div class="text-color">
            text color
        </div>
    </div>
</body>
</html>

これに対してthisを出力するjs↓

//domの読み込みが終了したらthisを出力
document.addEventListener('DOMContentLoaded', function() {
  console.log(`addEventListener: ${this}`);
});
//thisを出力
console.log(`global: ${this});

結果

undefined
addEventListener: [object HTMLDocument]

globalにthisを出力した方はwindowが返ってくる想定だったけど、undefined。
一方addEventListenerでthisを出力した方はHTMLDocumentが出力。
ここからthisは呼び出される場所によって左右されることがわかる。

class内で出力

もうちょっと踏み込んで今度はclassのconstructor内でthisを出力。

class TextChange {
  constructor(element) {
    this.element = document.querySelector(element);
    console.log(this);
  }
}

const textChange = new TextChange('.text-color)

結果

TextChange

想定どおり、class内で呼び出したのでthisはTextChangeクラスを参照してくれている。

class内のメソッドで出力

さらに踏み込んでclass内のメソッド内でthisを出力
メソッド内で普通に出力と、setTimeoutの中で出力

class TextChange {
  constructor(element) {
    this.element = document.querySelector(element);
  }
  log() {
    console.dir(this);
    setTimeout(function() {
      console.dir(this);
    }, 500);
  }
}

const textChange = new TextChange('.text-color');
textChange.log();

結果

TextChange
Window

setTimeoutを挟んでthisを出力した方はclass内ではあるが、setTimeoutのオブジェクトであるWindowから呼び出されることになるので、同じクラス内の同じメソッドの中でもthisの参照が異なるものとなってしまう。

以上、いくつかパターンに分けてthisの参照の決定を見てみたが、thisは直近で呼び出されたオブジェクトによって定まることがわかった。

thisをコントロールする方法

上述の通り、thisは直近で呼び出されたオブジェクトによって定まるので、それを意識してコードを記述しないと想定どおりの結果を得られない。
例えば、以下のテキストを書き換えるコードは一見正しく動作しそうだが、動作しない。

document.addEventListener('DOMContentLoaded', function() {
  const textChange = new TextChange('.text-color');
  textChange.change();
});

class TextChange {
  constructor(element) {
    this.element = document.querySelector(element);
    console.log(this.element);
  }
  change() {
    setTimeout(function() {
      this.element.innerHTML = 'quitjob';
    }, 500);
  }
}

結果

Uncaught TypeError: Cannot set property 'innerHTML' of undefined

これは先程確認した通り、setTimeout内で呼び出されたthisはsetTimeoutのオブジェクトであるWindowオブジェクトを参照しているので、本来参照してほしいclassのプロパティであるelementにアクセスができていないからである。

解消方法

  • thisをbindする。
  • thisを変数化する。

thisをbindする

setTimeout内のコールバック関数内でもthisの参照はTextChangeオブジェクトを参照して欲しいので、thisをbindによって決定する。

document.addEventListener('DOMContentLoaded', function() {
  const textChange = new TextChange('.text-color');
  textChange.change();
});

class TextChange {
  constructor(element) {
    this.element = document.querySelector(element);
    console.log(this.element);
  }
  change() {
    setTimeout(
      function() {
        this.element.innerHTML = 'quitjob';
      }.bind(this),
      500
    );
  }
}

bind(this)によってthisをwindowオブジェクトから呼び出されたものではなくTextChangeオブジェクトから呼び出されたものとして認識させることができる。
結果としてinnerHTMLを'quitjob'に書き換えることができる。
仕事やめたい

ES6ではアロー関数を用いても同じ挙動にさせることができる。

document.addEventListener('DOMContentLoaded', function() {
  const textChange = new TextChange('.text-color');
  textChange.change();
});

class TextChange {
  constructor(element) {
    this.element = document.querySelector(element);
    console.log(this.element);
  }
  change() {
    setTimeout(() => {
      this.element.innerHTML = 'quitjob';
    }, 500);
}

thisを変数化する

thisを変数に格納し、参照して欲しいところでthisの代わりにその変数を使用する。変数名はselfとかthatが多い印象

document.addEventListener('DOMContentLoaded', function() {
  const textChange = new TextChange('.text-color');
  textChange.change();
});

class TextChange {
  constructor(element) {
    this.element = document.querySelector(element);
    console.log(this.element);
  }
  change() {
    const _self = this;
    setTimeout(function() {
      _self.element.innerHTML = 'quitjob';
    }, 500);
  }
}

selfに予めthisを格納し、無名関数内でthisの代わりにselfを参照することで参照して欲しいthisを確定させる。

所感

thisに関しては他にもcallやapplyで決定させるなど色々奥が深いが、よく見るパターンはこの二つのような気がする。
復習してみて、thisをなんとなく理解しているだけでほとんど理解していないことがわかったのでいい機会だった。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?