2018-07-09

JavaScript の this とはつまりなんなのか

info
  • ブラウザが対象です (node もだいたい同じだと思いますが)
  • 実行例を試すときは 前回の実行結果が残らないようにリロードしたりしながらお試しください。

this の示す場所

this とは 自分自身を表すオブジェクトですが、実行場所によって表すものが異なります。

this は実行された関数が 実行された場所 を指します。

info
  • 訂正: 最初 JS の関数スコープは ダイナミックスコープ ではなくレキシカルスコープです。 this のみが固定されません。
  • thx @tell_k

具体的には以下が該当します。

HTML Element

一番わかり易いのは HTML要素のイベントハンドラでしょう。

<button value="1" onclick="alert('valueは' + this.value++)">increment</button>

のように記述すると onclick 内の thisbutton を指し、 this を経由して value を参照、更新できるというわけです。

object

百聞は一見にしかずということで以下をご覧ください。

o = { e: 1, f: function () {return this.e++}, } o.f() // 1 o.f() // 2 a = [function() {return this[1]++}, 1] a[0]() // 1 a[0]() // 2 class C { constructor () {this.e = 1} f () {return this.e++} } c = new C() c.f() // 1 c.f() // 2

関数が オブジェクト に属する場合、関数(メソッド)の this はそのオブジェクトを指します。 (ここでいう 属する とは簡単に言うと . でアクセスできることとします)

info
  • クラス も Array もオブジェクトです

window

(non-strict モードにて) 関数が どこのオブジェクトにも属していない(. の左側がない)場合、 その関数の this は window を指します。

function f () {return this} f() === window // true
info
  • ちなみに ネストされた関数内の this は window を指します。
  • ネストされた関数には . 経由でアクセスできないので当然といえば当然ですね。
  • 同じ理由で即時実行関数の this も window を指します。

constructor

前でも少し登場しましたが new をつけて 関数を呼び出すと関数は return の内容にかかわらず Object のインスタンスを返却するコンストラクタになります。

このときの this はインスタンスを指すため、ここでインスタンスの初期化を行えるんですね。

function A (value) {this.value = value; return null} a = new A(100) a // {value: 100} aa = A(100) aa // null window.value // 100

コンストラクタ用に作った関数で new をつけ忘れるとインスタンスは得られませんし 予想通り window が書き換えられてしまいます。 ES6 が使える場合は class 構文 の constructor メソッドで初期化しましょう。 new を忘れるとエラーになってくれます。

何を指すかを大まかに分けるとこんな感じです。

で、、

getter/setter

いろんな this を想定した関数を定義できるのが this が固定されないことの メリットです。

もっとも単純な例として gettersetter 関数が考えられます。

getter = function (attr) {return this[attr]} setter = function (attr, value) {this[attr] = value} o = {e: 1, getter, setter} o.getter('e') // 1 o.setter('f', 2) o // {e: 1, getter: ƒ, setter: ƒ, f: 2}

o オブジェクトの読み書きができていますね。

Problem

逆に、この性質がなんの問題になるかというと、関数を持ち出すときです。

o = { e: 1, f: function () {return this.e++}, } o.f () // 1

ここまでは OK です

つづいて o.f を移動してみましょう。今回は普通に f として定義してみます。

f = o.f f() // NaN e = 100 f() // 100 f() // 101

f は window に属していますが、 window.e が定義されていないのでインクリメントの結果は NaN となってしまいました。 その後 e (window.e) を定義したら インクリメントできるようになりました。

が、多くの場合、これは期待した動作ではないはずです。

例えばクロージャを使うと以下のようにして this が持ち出せます。

o = { e: 1, f: function () {return this.e++}, f2: function () {that = this; return function () {return that.e++}} } f = o.f2() f() // 1 f() // 2
info
  • thisthat に代入して、ネストした関数内で参照するのはクロージャでよく使われるテクニックだったりします

正確に言うと解決できないほどの問題ではないですが、混乱を招きやすく初学者にとっては壁になってしまうわけです。

arrow function

arrow function と呼ばれる機能が ECMA Script 2015 で登場しました。

構文は (引数) => {} のように記述します。一行で return する場合、 {} は要りません。

arrow function 自身は this を作らないものの、自分が属している関数スコープの this を 閉じ込めます。(束縛するとも言います)

具体的にどうするのか見てみましょう。

o = { e: 1, f: () => this.e++, f2: function () {return () => this.e++}, } f = o.f f() // NaN f2 = o.f2() f2() // 1 f2() // 2

正しく動作するのは o.f2() です。

前述したように arrow function 自体は this を持たないので、 普通の関数スコープの中で定義し、その this を使っていろいろするのが正しい arrow function の使い方です。

bind method

arrow function と同様に ECMA Script 2015 で登場した機能です。

上では 返却方法 を工夫することで this を固定していましたが、 bind メソッドを使うことで持ち出した後に this を固定できます。

o = { e: 1, f: function () {return this.e++}, } f = o.f f() // NaN

ここまでは期待通りです。

これをこうしてこうじゃ

f2 = f.bind(o) // bindメソッドは非破壊メソッドなので代入して使う f2() // 1 f2() // 2

bind メソッドで this を教えてあげるイメージです。

info
  • arrow function にも bind メソッドは定義されていますが使っても意味ありません。
  • bind の第二実引数以降を指定することでバインドされた関数の引数を固定します。
o = { e: 1, f: function (a=2, b=3) {console.log(this.e, a, b)} } f = o.f.bind(o) f(4) // 1 4 3 f2 = o.f.bind(o, 10, 20) f2(4) // 1 10 20

call method

call は 関数やメソッドを呼び出すためのメソッドです。

「いやいや、関数なんて括弧つけて呼び出せばええやん」となるんですが、 第一実引数で this を指定できる というメリットがあるんですよ。

o = {value: 5} add = function (value) {return this.value + value} add(3) // NaN add.call(o, 3) // 8
info
  • call によく似た関数に apply というのがあります。

  • apply は 実引数を Array で受け取るため(可変長な)引数の適用ができるメリットがあったんですが スプレッド構文 (...) の登場により虫の息になりつつあります。

    // 関係ないけど arrow function では arguments は参照できないっぽい sum = function () {return [... arguments].reduce((a, b) => a + b)} // apply での呼び出し sum.apply(null, [1, 4, 5]) // 10 // スプレッド構文での呼び出し sum(... [1, 4, 5]) // 10
  • おまけ: クラスの場合 は Date.apply みたいにできないので こうやって呼び出してたけど.. スプレッド構文超便利!

    args = [2018, 7 - 1, 9, 18, 30] // 月は 0 始まりなので d1 = new (Function.prototype.bind.apply(Date, [null].concat(args))) d1 // Wed Aug 09 2018 18:30:00 GMT+0900 (日本標準時) d2 = new Date(... args) d2 // Wed Aug 09 2018 18:30:00 GMT+0900 (日本標準時)

いろいろ解説しましたが、どれを使うのが正しいということはありません。

用途に応じて必要な機能を使えるように正しく理解しましょう。

参考

this - JavaScript | MDN関数の this キーワード は、JavaScript ではほかの言語と少々異なる動作をします。また、strict モードであるかどうかでも違いがあります。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/this new 演算子 - JavaScript | MDNnew 演算子を使用すると、開発者はユーザー定義のオブジェクト型やコンストラクター関数を持つ組み込みオブジェクト型のインスタンスを作成することができます。https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/new JavaScriptの「this」は「4種類」?? - Qiita#javascriptの「this」は「4種類」??この記事ではベースとなる4種類の「this」を紹介します。実際は4種類ではないのですが、このベースの4種類を理解できれば他もすぐに理解できま…https://qiita.com/takeharu/items/9935ce476a17d6258e27