JavaScript this Keyword 關鍵字
JavaScript 有一個很 tricky 的問題,就是 function 中的 this
關鍵字指向哪個物件?其實掌握住幾個原則,這個問題就蠻簡單。
JavaScript interpreter (直譯器) 在執行程式碼時,會維護一個執行環境 (execution context),其中有所謂的 ThisBinding 儲存著 this 應該指向哪一個物件。
在幾個執行情況下,ThisBinding 的值會被改變:
在全域初始的執行環境下 (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>
執行
eval()
function 時:執行 eval() 又分兩種情況:
直接執行 eval 函數 (direct call)
// 直接 call eval eval(...);
這時 ThisBinding 的值會維持原本的值不會改變。
間接執行 eval 函數 (indirect call)
// 像是透過引用的變數 call eval var indirectEval = eval; indirectEval(...); // 或像是這樣,間接的 call eval (0, eval)(...);
ThisBinding 的值會被指到 global object window。
當執行函數 (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!