かもメモ

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

JS call apply vs bind アロー関数

javascriptの関数とthisとの関係のメモの続きです。
関数のthisを決めることができるcall()apply()で実行される関数がFunction.prototype.bindやアロー関数でthisが決められているものだった場合どうなるのか気になったので調べてみました。

call と apply

call()apply()this を指定して関数を実行するメソッド。
違いは関数に渡す引数の指定方法だけ。

call() は 関数に渡す引数を1つ1つ列挙していく

myFnc.call(this, param1, param2, param3);

apply() は 関数に渡す引数を第二引数に配列で渡す。実行される関数には引数は展開されて渡される。(argumentsでも参照できる)

myFnc.apply(this, [param1, param2, param3]);

bind (ES5)

Function.prototype.bind()新たな関数(a bound function)を生成して返すメソッド。

var fnc = function() { console.log(this); };
var bindFnc = fnc.bind(null);
console.log(bindFnc); // => [Function: bound fnc]

bind() 関数は、新たな関数(束縛された関数 = a bound function)を生成して返します。この新たな関数の本体(ECMAScript 5 の観点では内部の call プロパティ)は、call() 先の関数(束縛された関数のターゲット関数)【訳注: fun.bind(thisArg) の fun 】 と同じです。ターゲット関数の this の値が bind() に与えた第 1 引数になり(束縛され)、それを上書きすることはできません。
[出典] Function.prototype.bind() - JavaScript | MDN

bindで生成された関数は実行時に内部的にcallが呼ばれるという事だと考えられるので、この関数をcall()apply()で呼出しても、実行される関数内でthisbind()で指定したものに置き換えられるので、bind()で指定したものが実行される関数内のthisになる。

'use strict';
var global = Function("return this")(); // global オブジェクトを取得
global.name = 'global';
var obj1 = { name: "obj1" };
var obj2 = { name: "obj2" };

var callback = function() {
  console.log('> callback this is ', this.name);
};

var applyFnc = function(f) {
  if(typeof(f) === 'function') {
    let _this = this || global;
    console.log('this is', _this.name);
    f.apply(_this);
  }
};

applyFnc( callback );
// => this is global
// => > callback this is  global

// Function.prototype.bind()
applyFnc( callback.bind(obj1) );
// => this is global
// => > callback this is  obj1

applyFnc.call( obj2, callback.bind(obj1) );
// => this is obj2
// => > callback this is  obj1

アロー関数 ()=> (ES6)

アロー関数式 は、function 式 と比べてより短い構文を持ち、this の値を語彙的に束縛します (ただし、自身の this や arguments, super, new.target は束縛しません)。アロー関数は、常に 匿名関数 です。
call や apply からの呼び出し
this はすでに語彙的に束縛されているため、call() や apply() メソッドを介してアロー関数が呼ばれた場合、引数で渡されるだけなので this は影響を受けません
[出典] アロー関数 - JavaScript | MDN

アロー関数は記述が出てきた場所のスコープにあるthisを関数のthisに決定するというイメージです。
引用元にも書かれているようにcall()apply()thisの値を渡されようと、アロー関数作成時に決定したthisが実行される関数のthisになります。

'use strict';
var global = Function("return this")();
global.name = 'global';
var obj1 = { name: "obj1" };
var obj2 = { name: "obj2" };

var applyFnc = function(f) {
  if(typeof(f) === 'function') {
    let _this = this || global;
    console.log('this is', _this.name);
    f.apply(_this);
  }
};

obj1.fnc1 = function() {
  applyFnc(() => {
    console.log('> callback this is ', this.name);
  });
};
obj1.fnc1();
// => this is global
// => > callback this is  obj1


obj1.fnc2 = function() {
  applyFnc.call(obj2, () => {
    console.log('> callback this is ', this.name);
  });
};
obj1.fnc2();
// => this is obj2
// => > callback this is  obj1

 

概ね予測通りの結果でした。
.bind()が新しい関数を生成するというのは知らなかったので、この機会に知ることができて良かったです。同時にデフォルト引数を設定できることも初めて知り寧ろこちらの方に興味惹かれるものがありましたw
こんな事をしているからなかなか本が読み進められないわけです...


[参考]

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

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

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 値と同じにはなりません。
WindowOrWorkerGlobalScope.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の原則

JS オブジェクトのキーがあるかチェックしたい。

オブジェクト({})のキーが存在しているか調べる時、if文を使うと値によっては上手くいかない。

var obj = {
  A: 0,
  B: 1,
  C: 2,
  D: ""
};
var foo;
obj.E = foo;

["A", "B", "C", "D", "E", "F"].forEach(function(key) {
  if( obj[key] ) {
    console.log( obj[key] );
  }
});

👇 出力

B 1
C 2

key in object を使えばOK

["A", "B", "C", "D", "E", "F"].forEach(function(key) {
  if( key in obj ) {
    console.log( key, obj[key] );
  }
});

👇 出力

A 0
B 1
C 2
D ""
E undefined

配列の値の有無をチェックしたい場合

配列(Array)はindexOfをを使うと値があればそのインデックスを返してくれるので、結果が -1 でなければ値があると判定することができる。

var arr = [2, true, false, '', -1, null, undefined];

arr.indexOf(2); // 0
arr.indexOf(false); // 2

// 空文字もOK
arr.indexOf(''); // 3 

// null や undefined も大丈夫
arr.indexOf(null); // 5
arr.indexOf(undefined); // 6

// 暗黙の型変換はしないっぽい
arr.indexOf("2"); // -1