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
      var 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);

// ---

var obj = {
    bar: "hello"
};

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

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

// ---

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

var func = obj.foo;

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

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

// ---

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

obj.foo();

// ---

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

obj.foo();

// ---

function func() {
    // 回到一般對執行 function 的判斷
    // func 不屬於任何物件的方法
    // 所以這邊的 this 指向 window
    console.log(this);
}
var 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);
}

var 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);
}

var obj = {
    bar: 'bar'
};

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

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

function personContainer() {
    var 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) {
    var fn = this;
    
    return function() {
        fn.apply(ctx, arguments);
    };
};

例子:

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

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

// 81
module.getX();

var retrieveX = module.getX;

// 9
retrieveX();   

// 建立一個新的函數,將其 this 綁定 (bind) 到 module 物件
var 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!');
};

var 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()。