かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Javascript 関数とthisについてのメモ

以前書いた記事でオブジェクトの関数内でsetTimeout()に無記名関数を渡した際のthisの値について理解が不明瞭だったのでjavascriptの関数とthisについて調べたメモです。

関数について

関数宣言 (function declaration)

function 関数名(){}
関数の巻き上げが発生する

!function() {
  var res = add(3, 4); // 7
  function add(n1, n2) {
    return n1 + n2;
  }
}();

関数宣言はスコープの先頭まで巻き上げられるので、上記の記述は下記の解釈となる

!function() {
  function add(n1, n2) {
    return n1 + n2;
  }
  var res = add(3, 4); // 7
}();

関数式 (funtion expression)

var 関数名 = function(){}
※ 関数式の場合関数名は必須ではない。(無記名関数)
関数の巻き上げは発生しない

!function() {
  var res = add(3, 4); // TypeError: add is not a function
  var add = function(n1, n2) {
    return n1 + n2;
  }
}();

thisについて

Javascriptの関数は その関数を呼び出しているオブジェクトを表すthisオブジェクト を持っている。
[出展]: オブジェクト指向JavaScriptの原則

Strictモードでの違い

thisの値にnullundefinedが設定されている時

  • 標準モード this = グローバルオブジェクト
  • Strictモード this = nullundefinedの値のまま

標準モード

function showThis() {
  console.log(this);
}
showThis(); // => global Object (Window)

Strictモード

'use strict';
function showThis() {
  console.log(this);
}
showThis(); // => undefined

グローバススコープではthisはグローバルオブジェクトと指す。= WEBブラウザではWindow
グローバル変数はグローバルオブジェクのプロパティとして扱われる。

"use strict";
console.log(this); // Window

// window.a = 'Hibiki'; と同じ意味
var a = 'Hibiki';

function say() {
  var a = 'Abukuma';
  console.log(a); // "Abukuma"
  console.log(window.a); // "Hibiki"
}

say();

オブジェクトのメソッドとして定義してある関数内のthisは通常そのオブジェクト自身

var obj = {
  name: 'サーバル',
  say: function() {
    // `this` is obj 
    console.log(this, this.name + 'ちゃんすごーい!');
  }
};

obj.say(); // Object, "サーバルちゃんすごーい!"

thisの値は関数が呼び出される際に設定される

function sayThisName() {
  console.log(this.name);
}

var obj1 = {
  name: 'かばん',
  sayName: sayThisName
};

var obj2 = {
  name: 'サーバル',
  sayName: sayThisName
};

var name = 'ミライ';

obj1.sayName(); // かばん
obj2.sayName(); // サーバル

// 標準モード: `this` is Window => "ミライ"
// Strict Mode: Error `this` is undefined
sayThisName();

setTimeoutのcallback関数のthis

setTimeout()windowオブジェクトが持っているメソッドなので省略せずに書くとwindow.setTimeout()

var obj = {
  name: 'MyObject',
  fnc: function() {
    var self = this; // MyObject
    window.setTimeout(function() {
      console.log(self); // => MyObject
      console.log(this);
    }, 1000);
  }
};

setTimeoutが書かれているのと同じスコープにある変数が実行されるcallback関数の中でも使えるので、 setTimeoutの第一引数に無記名関数を渡すのは👇と同じなのではないかと考えています。(厳密にはどうか解りませんが…)

var obj = {
  name: 'MyObject',
  fnc: function() {
    var self = this; // MyObject
    var callback = function() {
      console.log(self); // => MyObject
      console.log(this);
    };
    window.setTimeout(callback, 1000);
  }
};

なので、setTimeoutに渡されるコールバック関数は通常の関数と同じだと考えていました。

オブジェクト内の関数内に関数を作成して呼び出した場合のthis

var obj = {
  name: 'MyObjct',
  fnc: function() {
    console.log(this); // MyObject
    // 関数宣言
    function objFnc () {
      console.log(this);
      // 標準モード =>  Global Object[window]
      // Strict Mode => undefined
    }
    objFnc();
    // 関数式
    var myFnc = function() {
      console.log(this);
      // 標準モード =>  Global Object[window]
      // Strict Mode => undefined
    };
    myFnc();

    // 即時関数
    (function() {
      console.log(this);
      // 標準モード =>  Global Object[window]
      // Strict Mode => undefined
    })();
  }
};
obj.fnc();

オブジェクト内の関数内に関数を作成して実行しても、その関数を呼び出しているオブジェクトは無いので関数内のthisはStrictモードならundefined、標準モードならグローバルオブジェクト(WEBブラウザならwindow)になるので、Strictモードならcallback関数のthisundefinedになるのではないかと思っていたのですが、実際は👇

var obj = {
  name: 'MyObject',
  fnc: function() {
    var self = this; // MyObject
    var callback = function() {
      console.log(self); // => MyObject
      console.log(this);
      // 標準モード =>  Global Object[window]
      // Strict Mode => Global Object[window]
    };
    window.setTimeout(callback, 1000);
  }
};

コールバック関数内のthisは標準モードでもStrictモードでも windowとなっていました。
これはsetTimeout関数のcallbackはwindowがcallback関数を呼び出す仕様(applyやcallで実行されるなどthiswindowになる仕様)になっているという事なのだと思います。

setTimeout() によって実行されるコードは、setTimeout() が呼び出された関数とは別の実行コンテキスト内で実行されます。結果的に、呼び出された関数の this キーワードは window (または global) オブジェクトに設定され、setTimeout が呼び出された関数の this 値と同じにはなりません。
window.setTimeout - Web API インターフェイス | MDN

ドキュメント見に行けば済んでいた話でした。
Strictモードと標準モードで違いがあることを下手に知っていたために混乱してしまっていましたorz  

追記
window.setTimeoutで実行されるthisがwindowになるのはsetTimeoutを呼出しているオブジェクトが指定されるからではないか?と思い、callapplysetTimeoutを呼び出すとどうなるのか試して見たところ…

var fnc = function() { console.log(this); };
window.setTimeout.call(null, fnc, 100); // Uncaught TypeError: Illegal invocation
window.setTimeout.apply(null, [fnc, 100]); // Uncaught TypeError: Illegal invocation
var _setTimeout = window.setTimeout;
_setTimeout.call(null, fnc, 100); // Uncaught TypeError: Illegal invocation
_setTimeout.apply(null, [fnc, 100]); // Uncaught TypeError: Illegal invocation

どうやらこの動作は許可されない様です…


[参考]

オブジェクト指向JavaScriptの原則

オブジェクト指向JavaScriptの原則