B-Teck!

お仕事からゲームまで幅広く

【JavaScript】thisの種類

JavaScriptthisにはいくつかの種類があり、状況によって動作が変わる。
現在はおおまかに分けて5つ?

コンストラクタ呼出し

function Func1(arg1, arg2){
  this.arg1 = arg1;
  this.arg2  = arg2;
}

let obj1 = new Func1('test', 123456);
// obj1.arg1 => 'test'
// obj1.arg2 => 123456

Functionオブジェクトのコンストラクタをnewで呼び出してインスタンスを生成する場合。
この文脈でのthisインスタンス自身を指す。

メソッド呼出し

let obj = {
  val : 'test',
  func1 : function(){
    console.log(this.val);
    // =>'test'
  }  
}
obj.func1();

オブジェクトの中のメソッドからthisを参照した場合。
メソッドの存在するオブジェクトをthisとする。

関数呼び出し

function func1(arg1, arg2){
  this.arg1 = arg1;
  this.arg2  = arg2;
}

func1('test', 123456);
console.log(window.arg1);
//windowオブジェクトにプロパティがセットされる。
//windowはグローバルオブジェクトのため、プロパティもグローバルになってしまう。

Functionオブジェクトをnewをつけずにそのまま呼び出した場合。
普通に呼び出される関数の中でthisを使用すると、Function内にスコープが限定されず、
windowオブジェクトを参照してしまう。

Strictモードを使用した場合はエラーが発生して利用できなくなるため安全。

'use strict';
function func1(arg1, arg2){
  this.arg1 = arg1;
  this.arg2  = arg2;
}

func1('test', 123456);
console.log(window.arg1);
//Exception: TypeError: this is undefined

関数呼び出しの注意点

let obj = {
  val : 'obj prop',
  func1 : function(){
    // メソッド呼び出し
    // obj.val
    console.log(this.val);
    
    function nestedFunc(){
      // 関数呼び出し
      // window.val
      console.log(this.val);      
    }
    nestedFunc();
  }  
}

window.val = 'window.prop';
obj.func1();

メソッド呼出しの中で関数呼出しを行った場合でも、単体で関数呼出しを行った場合と同様にthiswindowを指してしまう。
ECMASript6(ECMAScript2015)の場合は、後述するアロー関数を用いることで、thisを一意にすることができる。
それ以外の環境の場合、下記のような対処法がある。

  • self
let obj = {
  val : 'obj prop',
  func1 : function(){
    let self = this;
    console.log(self.val);
    // =>'obj prop'
    
    function nestedFunc(){
      console.log(self.val);
    // =>'obj prop'
    }
    nestedFunc();
  }  
}

window.val = 'window.prop';
obj.func1();

参照するべきthisを変数に保持してしまう方法。
慣例的にselfthatthisなどの変数名が利用される。
単純だがわかりやすい。

  • bind()
let obj = {
  val : 'obj prop',
  func1 : function(){
    console.log(this.val);
    
    function nestedFunc(){
      console.log(this.val);      
    };
    // thisをbindしたfunctionを定義
    let bindFunc = nestedFunc.bind(this);
    bindFunc();
    //またはそのまま実行
    nestedFunc.bind(this)();
  }  
}
window.val = 'window.prop';
obj.func1();

参照されるべきthisの値をbind()で束縛した新しいFunctionオブジェクトを作成する。 作成した関数を実行することで別のものを参照しないようにする方法。
部分適用はしやすいが、少し可読性が良くない。

個人的には、どちらかと言えばselfの方が嬉しい気持ちになる。

applyまたはcallで呼び出し時

let obj = {
  val : 'obj prop',
}

function func1(){
  console.log(this.val);
}  

func1();
// => undefined
func1.call(obj);
// => 'obj prop'
func1.apply(obj);
// => 'obj prop'

Function.prototype.apply()Function.prototype.call()は、
引数で与えられたオブジェクトをthisとしてセットして呼び出すことができる。

applycallの違い

let obj = {
  val : 'obj prop',
}

function func1(arg1, arg2){
  console.log(this.val + ' ' + arg1 + ' ' + arg2);
}

func1.call(obj, 'test', 'args');
// => 'obj prop test args'
func1.apply(obj, ['test', 'args']);
// => 'obj prop test args'

両方共第一引数はthisにセットする値だが、下記の点が異なる。
* callは第二引数以降に与えた引数が、呼び出すオブジェクトに引数として渡される。
* applyは第二引数に渡した配列の中身が引数として渡される。

アロー関数で呼び出し時

let obj = {
  val : 'obj prop',
  func1 : function(){
    console.log(this.val);
    // アロー関数は、外側のthisを自身のthisとして扱うため、
    // func1のthis.val = obj.valを参照する。
    let nestedFunc = () => {
      console.log(this.val);      
    }
    nestedFunc();
  }  
}

window.val = 'window.prop';
obj.func1();

ECMASript6(ECMAScript2015)以降では、アロー関数という仕組みが提供されている。 アロー関数は、自分が宣言されているスコープのthisを引き継いで関数内でthisとして扱うため、
直感に反しないthisの扱い方をすることができる。

アロー関数の注意点

window.val = 'window val';
let func = () => {
  console.log(this.val);      
}

let obj2 = {val : 2};
func();
// => windos val
func.call(obj2);
// => windos val
func.apply(obj2);
// => windos val

前述したように、アロー関数のthisは外側のスコープのthisを引き継いでセットされる。
アロー関数の場合、callapplyで呼び出してもこの前提は変わらず、thisを置き換えることが出来ない。