JavaScript this Keyword 關鍵字

JavaScript 有一個很 tricky 的問題,就是 function 中的 this 關鍵字指向哪個物件?其實掌握住幾個原則,這個問題就蠻簡單。

JavaScript interpreter (直譯器) 在執行程式碼時,會維護一個執行環境 (execution context),其中有所謂的 ThisBinding 儲存著 this 應該指向哪一個物件。

在幾個執行情況下,ThisBinding 的值會被改變:

  1. 在全域初始的執行環境下 (initial global execution context):

    像是程式進入 <script> 中,會直接被執行到的 code,JavaScript interpreter 會將 ThisBinding 指向全域物件 (global object) window

    例如:

    <script>
      alert('我是會被直接執行的 initial global execution context');
    
      setTimeout(function () {
        alert('我不是會直接被執行到的 initial global execution context');
      }, 100);
    </script>
    
  2. 執行 eval() function 時:

    執行 eval() 又分兩種情況:

    1. 直接執行 eval 函數 (direct call)

      // 直接 call eval
      eval(...);
      

      這時 ThisBinding 的值會維持原本的值不會改變。

    2. 間接執行 eval 函數 (indirect call)

      // 像是透過引用的變數 call eval
      const indirectEval = eval;
      indirectEval(...);
      
      // 或像是這樣,間接的 call eval
      (0, eval)(...);
      

      ThisBinding 的值會被指到 global object window

  3. 當執行函數 (function) 時:

    如果 function 是屬於某物件 (object) 的方法 (method),例如 obj.myMethod() 或 obj'myMethod',則 ThisBinding 會指向物件 obj 本身。

    在其他情況下,大多是指向 global object window,除了下面這些特殊的 function call:

    Function.prototype.apply( thisArg, argArray )
    Function.prototype.call( thisArg [ , arg1 [ , arg2, ... ] ] )
    Function.prototype.bind( thisArg [ , arg1 [ , arg2, ... ] ] )
    
    Array.prototype.every( callbackfn [ , thisArg ] )
    Array.prototype.some( callbackfn [ , thisArg ] )
    Array.prototype.forEach( callbackfn [ , thisArg ] )
    Array.prototype.map( callbackfn [ , thisArg ] )
    Array.prototype.filter( callbackfn [ , thisArg ] )
    

    Function.prototype.* 類的函數,你可以傳入 thisArg 聲明 this 要指向哪個物件。

    Array.prototype.* 類的函數,你也可以傳入 thisArg 聲明 this 要指向哪個物件,或 thisArg 參數留空表示指向 global object window

舉幾個實際的例子來瞧瞧:

if (true) {
  // initial global execution context
  // this 指向 window
  console.log(this);
}

// ---

// 不屬於 initial global execution context
// 依執行一般 function 的方式判斷
setTimeout(function () {
  // 執行不屬於任何物件方法的一般函數
  // this 指向 window
  console.log(this);
}, 100);

// ---

const obj = {
  bar: 'hello',
};

function foo() {
  // 執行 obj 物件的方法
  // this 指向 obj
  console.log(this);
}

obj.foo = foo;
obj.foo();

// ---

const obj = {
  foo: function () {
    console.log(this);
  },
};

const func = obj.foo;

// 執行 obj 物件的方法
// this 指向 obj
obj.foo();

// 執行一般的函數,不屬於任何物件的方法
// this 指向 window
func();

// ---

const obj = {
  foo: function () {
    // 直接執行 eval 函數,ThisBinding 不變
    // this 指向 obj
    console.log(eval('this'));
  },
};

obj.foo();

// ---

const obj = {
  foo: function () {
    // 間接執行 eval 函數
    // this 指向 window
    const eval2 = eval;
    console.log(eval2('this'));
  },
};

obj.foo();

// ---

function func() {
  // 回到一般對執行 function 的判斷
  // func 不屬於任何物件的方法
  // 所以這邊的 this 指向 window
  console.log(this);
}
const obj = {
  foo: function () {
    // 雖然 eval code 裡面的 this 會指向 obj
    // 但 eval 裡面執行的 function 就不是了
    eval('func()');
  },
};

obj.foo();

Function.prototype.call()

function 的 call() 方法 (method),可以用來改變 this 指向的物件。

語法:

fun.call(thisArg, arg1, arg2, ...)

其中 thisArg 是 this 要指向的物件;arg1, arg2, ... 則是要傳進函數的參數。

範例:

function greet() {
  var reply = [this.person, 'Is An Awesome', this.role].join(' ');
  console.log(reply);
}

const obj = {
  person: 'Douglas Crockford',
  role: 'Javascript Developer',
};

// 將 greet function 中的 this 指向 obj 物件
// 輸出 Douglas Crockford Is An Awesome Javascript Developer
greet.call(obj);

Function.prototype.apply()

apply() 跟 call() 目的是一樣的,可以用來改變 this 指向的物件,只是傳入參數的方式不太一樣。

語法:

fun.apply(thisArg, [arg1, arg2, ...])

其中 thisArg 是 this 要指向的物件;第二個參數是一個陣列,其元素 arg1, arg2, ... 表示要傳進函數的參數。

例如:

function foo(a, b) {
  console.log(this.bar, a, b);
}

const obj = {
  bar: 'bar',
};

// 將 foo function 中的 this 指向 obj 物件
// 輸出 bar 1 2
foo.apply(obj, [1, 2]);

因為第二個參數是一個 array,apply 也常拿來結合 arguments 做運用:

function personContainer() {
    const person = {
        name: 'Mike',
        hello: function() {
            console.log(this.name + ' says hello ' + arguments[1]);
        }
  }

  person.hello.apply(person, arguments);
}

// 輸出 Mike says hello mars
personContainer('world', 'mars');

Function.prototype.bind()

bind() 也是用來綁定 this 指向的物件,跟 call() 和 apply() 的差異在於,call() 和 apply() 是直接執行一個 function,但 bind() 是用來建立一個新的 function。

語法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

其中 thisArg 是返回的 function 中的 this 要指向的物件;其餘參數 arg1, arg2, ... 表示當返回的函數被執行時,要先傳進函數的前面幾個參數。

bind() 返回的新函數,有點像是原來函數,但其中的 this 被綁定到特定的物件上。

// bind() 的實作方式有點像是這樣子
Function.prototype.bind = function (ctx) {
  const fn = this;

  return function () {
    fn.apply(ctx, arguments);
  };
};

例子:

// 這邊的 this 指向 window
this.x = 9;

const module = {
  x: 81,
  getX: function () {
    return this.x;
  },
};

// 81
module.getX();

const retrieveX = module.getX;

// 9
retrieveX();

// 建立一個新的函數,將其 this 綁定 (bind) 到 module 物件
const boundGetX = retrieveX.bind(module);

// 81
boundGetX();

配合 setTimeout 很好用:

function LateBloomer() {
  this.petalCount = 8;
}

LateBloomer.prototype.bloom = function () {
  // 將 declare callback function 的 this 綁定到 LateBloomer (this) 物件本身
  // 不然 setTimeout 執行 function 時,會將 this 指向 window
  setTimeout(this.declare.bind(this), 1000);
};

LateBloomer.prototype.declare = function () {
  console.log('I am a beautiful flower with ' + this.petalCount + ' petals!');
};

const flower = new LateBloomer();
flower.bloom();

// 一秒後會輸出 I am a beautiful flower with 8 petals!

// 如果沒有 bind 好 this 則會輸出 I am a beautiful flower with undefined petals!
IE 瀏覽器在 IE9 開始才有支援 bind()。