JavaScript の this を理解する多分一番分かりやすい説明

  • 285
    Like
  • 4
    Comment

JavaScript の this は、(他のプログラム言語から見ると) ちょっと面白い挙動に見えることがあります。

先日、この this の挙動について、会社の同僚が説明してくれたのですが、これまで聞いた説明の中で一番分かりやすいと感じたので、頑張って日本語で説明してみます。

分かりにくかったら、多分それは私の技量不足。

thisfunction の関係

function が基準スコープになるのがまず一点。

その function をどう呼ぶかで変わるのかがもう一点。

それを踏まえて……

thisfunction を呼んだ時の . の前についているオブジェクトを指している

と理解できるというのが、同僚の説明でした。

. が省略された場合はグローバルオブジェクトになります (non-strict モード時)。 strict モードでは undefined になります。

@ryo511 さん、ご指摘感謝。

js_this_and_func.png

サンプルコード

これだけだとナンノコッチャかもしれないので、具体例を……

function test() {
    console.log(this)
}

this の内容をコンソールに表示するだけの、なんてことない関数です。

これをブラウザ上で呼び出すと……

test() // => Window {frames: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}

のようになります。

呼び出し時に . がないので、 this はグローバルオブジェクト、ブラウザでは Window オブジェクトになります (non-strict モード)。

この関数 test を特定のオブジェクトに結びつけます。

function test() {
    console.log(this)
}
var obj = {}
obj.test = test

その上で、 obj.test() を呼び出すと、 thisobj になります。

obj.test() // => {test: ƒ}

関数 test を呼び出す際、 . がついていて、 obj. となっているので、関数 test の中では this = obj となるという訳です。

逆に、あるオブジェクトに結びついているものをグローバルスコープで呼び出すこともできます。

var obj = { test: function() { console.log(this) } }
var test = obj.test
test() // => Window {frames: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}

test()obj の関数 test を呼び出していることになりますが、呼び出し時に . がついていないので、グローバルオブジェクトが this になります (non-strict モード)。

上のサンプルコードでは、分かりやすくしていますが、例えば、setTimeout などで obj.test をコールバックとして渡すような場合でも同様のことが起きています。

メソッドチェーン

複数の function を数珠つなぎで呼び出すメソッドチェーンは . の前が関数になりますが、この場合、その関数が返すオブジェクトを参照することになります。

関数で return を省略したり、 return 単独で呼ぶと undefined が返ってくるので、メソッドチェーンは利用できません。

var obj = {
  test: function() { return this },
  alert: function(msg) { console.log(msg) }
}
var test = obj.test
obj.test().alert("hello") // => hello とコンソールに表示
test().alert("hello") // => アラート表示

関数 alert の呼び出しに . があって、その前に関数 test があります。

関数 test の返り値は this なので、その test の呼び出し方によって参照されるオブジェクトによって関数 alert の結果が変わります。

callapply

callapply を使って関数を呼び出すと . に前につけるオブジェクトを指定することができます。

function test() {
    console.log(this)
}
var obj = { name: "obj" }
test() // => Window {frames: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
test.call(obj) // => {name: "obj"}

test.call(obj)obj.test() に等しくなります (ただし、 objtest というメソッドは追加されません)。

コンストラクタ

function に対して、 new 構文を利用して、オブジェクトを作成することができます。

これは大雑把に言ってしまえば、 call の変形構文と考えることができます。

例えば、以下のコード……

var obj = new function() {
    this.name = "obj"
    console.log(this) // => {name: "obj"}
}

この時、 function が呼ばれますが、 this はグローバルオブジェクトでも undefined でもありません。

new を使うと、新規にオブジェクトを作成して、それに対して、 call を使って function を呼び出して、関数内部で return this するような動作になります。

つまり……

var obj = function() {
    this.name = "obj"
    console.log(this) // => {name: "obj"}
    return this
}.call({})

と同じ動作と考えることができます。

これは無名関数の場合ですが、名前付き関数 (例えば function Test()) の場合、内部でもう少し追加処理が入ります。 ただし、 this の挙動に関しては、基本的に同様に考えることができると思います。

bind

bind はこの . ルールの挙動を変化させて、強制的にあるオブジェクトと結びつけます。

function test() {
    console.log(this)
}
var obj = { name: "obj" }
var check = test.bind(obj)
check() // => {name: "obj"}

関数 check の呼び出しには . がついていませんが、 bind されているので、呼び出し時に obj. が付く形となり、関数 test の中で this = obj になります。

関数の中の関数

. はあくまでも関数呼び出し時に参照されるものです。

var obj = {
    test: function() {
        console.log(this) // *1
        function test() {
            console.log(this) // *2
        }
        test()
    }
}
obj.test()

のようなコードがあった時、

*1 では obj がコンソールに表示され、 *2 ではグローバルオブジェクト (例えば、 Window) が表示されます。

*2 の関数呼び出しに . がないからですね。

var obj = {
    test: function() {
        console.log(this) // *1
        function test() {
            console.log(this) // *2
        }
        test.call(this)
    }
}
obj.test()

call を使えば (apply でもいいです)、どっちも obj になります。

まとめ

thisfunction を呼んだ時の . の前についているオブジェクトを指している

  1. 関数呼び出しの . の前のオブジェクトが this になる
  2. . を省略するとグローバルオブジェクトを参照する (non-strict モード)
    • strict モードでは undefined になる
  3. call, apply を使うと . の前のオブジェクトを指定できる (コンストラクタはこの変形型)
  4. bind を使うと . の前のオブジェクトを強制できる

(補足)アロー関数について

当記事では、あえて ES2015 から導入されたアロー関数について触れていませんでした。

アロー関数での this の振る舞いは (他のプログラム言語から見て) 比較的自然で分かりやすい気がします。

最初に触れたとおり

  • function が基準スコープになる

のが JavaScript の大きな特徴です (var のスコープもこれに準じます)。

そのため、function を入れ子にした途端 this の指すものが変わってきてしまうのが一番の混乱の要因です。当記事では、そこに焦点を当てて説明しています。

アロー関数は入れ子にしても、アロー関数の中で参照されている this が指すものは変わりません。そのアロー関数がキャプチャされたときの this を保持します。