原文:Understanding the "this" keyword in JavaScript
許多人被JavaScript中的this
關(guān)鍵字給困擾住了厌处,我想混亂的根源來自人們理所當(dāng)然地認(rèn)為JavaScript中的this
應(yīng)該像Java中的this
或Python中的self
一樣工作态蒂。盡管JavaScript的this
有時(shí)有類似的效果,但它跟Java或其他語言中的this
是完全不同的丹壕。盡管有點(diǎn)難理解捌木,但它的原理并不神秘。事實(shí)上职辅,this
遵循一些簡單的小規(guī)則,這篇文章就對這些規(guī)則作出了解釋聂示。
但首先域携,我想給出一些專業(yè)術(shù)語和一些JavaScript運(yùn)行時(shí)環(huán)境的相關(guān)信息,希望能幫助你構(gòu)建一個(gè)心智模型來更好的理解this
鱼喉。
執(zhí)行上下文
每一行JavaScript代碼都運(yùn)行在一個(gè)“執(zhí)行上下文”中秀鞭。JavaScript運(yùn)行時(shí)環(huán)境用一個(gè)棧維持著這些上下文,棧頂?shù)囊粋€(gè)就是當(dāng)前正在運(yùn)行的執(zhí)行上下文扛禽。
有三類可執(zhí)行代碼:全局代碼(global code)锋边,函數(shù)代碼(function code),eval代碼(eval code)编曼。大概的說豆巨,全局代碼是應(yīng)用程序最頂層的代碼,不被包含在任何方法中掐场,函數(shù)代碼是在函數(shù)(function)體中的代碼往扔,eval代碼是被eval
解釋的全局代碼。
this
對象每次控制進(jìn)入一個(gè)新的執(zhí)行上下文時(shí)會被重新確定指向熊户,直到控制切換到一個(gè)不同的上下文萍膛。this
的值取決于兩件事:被執(zhí)行的代碼的類型(global,function,eval)和調(diào)用代碼的對象。
全局對象
所有的JavaScript運(yùn)行時(shí)都有一個(gè)唯一的全局對象嚷堡。他的屬性包括內(nèi)置的對象如Math
和String
卦羡,以及其他由主環(huán)境變量定義的屬性。
在瀏覽器中,全局對象是window
對象绿饵。在Node.js中,它就叫作“global object”瓶颠。(我假定你將在瀏覽器中運(yùn)行代碼拟赊,然而,我所說的一切也同樣適用于Node.js粹淋。)
確定this
的值
第一條規(guī)則很簡單:this
指向全局對象在所有全局代碼中吸祟。由于所有的程序都是由執(zhí)行全局代碼開始的,并且this
在給定的執(zhí)行上下文中會被修正桃移,所以屋匕,默認(rèn)的this
指全局對象。
當(dāng)控制進(jìn)入一個(gè)新的執(zhí)行上下文時(shí)發(fā)生了什么呢借杰?只有三種this
的值發(fā)生改變的情況:方法調(diào)用过吻,new一個(gè)函數(shù)對象,函數(shù)被call
和apply
調(diào)用蔗衡。我將逐一解釋纤虽。
方法調(diào)用
當(dāng)我們通過點(diǎn)(例obj.foo()
)或者方括號(例obj["foo"]
)把一個(gè)方法作為一個(gè)對象的屬性來調(diào)用時(shí),this
將指向方法體的父對象:
var counter = {
val: 0,
increment: function () {
this.val += 1;
}
};
counter.increment();
console.log(counter.val); // 1
counter['increment']();
console.log(counter.val); // 2
這是第二條規(guī)則:函數(shù)被當(dāng)作父對象的屬性來調(diào)用時(shí)绞惦,在函數(shù)代碼中的this
指向函數(shù)的父對象逼纸。
注意,如果我們直接調(diào)用相同的方法济蝉,即杰刽,不作為父對象的屬性,this
將不會指向counter
對象:
var inc = counter.increment;
inc();
console.log(counter.val); // 2
console.log(val); // NaN
當(dāng)inc
被調(diào)用時(shí)王滤,這里的this
不會改變贺嫂,所以它還是指向全局對象。
當(dāng)inc
執(zhí)行
this.val += 1;
它等效于執(zhí)行:
window.val += 1;
window.val
是undefined淑仆,且undefined
加1
得到NaN
涝婉。
new運(yùn)算符
任何JavaScript函數(shù)都可以通過new
運(yùn)算符當(dāng)成構(gòu)造函數(shù)使用。new
運(yùn)算符創(chuàng)建一個(gè)新對象并且設(shè)置函數(shù)中的this
指向調(diào)用函數(shù)的新對象蔗怠。舉個(gè)栗子:
function F (v) {
this.val = v;
}
var f = new F("Woohoo!");
console.log(f.val); // Woohoo!
console.log(val); // ReferenceError
這就是我們的第三條規(guī)則:在用new
運(yùn)算符調(diào)用的函數(shù)代碼中的this
指向新創(chuàng)建的對象墩弯。
注意F
沒有任何特殊。如果不通過new
調(diào)用寞射,this
將指向全局對象:
var f = F("Oops!");
console.log(f.val); // undefined
console.log(val); // Oops!
在這個(gè)例子中渔工,F("Oops!")
是一個(gè)通常調(diào)用,this
沒有指向新的對象桥温,因?yàn)闆]有用new
創(chuàng)建新的對象引矩,this
繼續(xù)指向全局對象。
Call & Apply
所有JavaScript函數(shù)都有兩個(gè)方法,call
和apply
旺韭,讓你能夠調(diào)用函數(shù)并且清楚的設(shè)置this
指向的對象氛谜。apply
函數(shù)有兩個(gè)參數(shù):一個(gè)是this
指向的對象,一個(gè)(可選)傳進(jìn)函數(shù)的參數(shù)的數(shù)組:
var add = function (x, y) {
this.val = x + y;
},
obj = {
val: 0
};
add.apply(obj, [2, 8]);
console.log(obj.val); // 10
call
方法和apply
方法是完全一樣的区端,只不過要逐個(gè)的傳遞參數(shù)值漫,而不是用一個(gè)數(shù)組:
add.call(obj, 2, 8);
console.log(obj.val); // 10
這是第四條規(guī)則:當(dāng)使用call
或apply
調(diào)用函數(shù)時(shí),函數(shù)代碼中的this
被設(shè)置為call
或apply
中的第一個(gè)參數(shù)织盼。
總結(jié)
- 默認(rèn)的杨何,
this
指向全局對象 - 當(dāng)一個(gè)函數(shù)被作為一個(gè)父對象的屬性調(diào)用時(shí),函數(shù)中的
this
指向父對象 - 當(dāng)一個(gè)函數(shù)被
new
運(yùn)算符調(diào)用時(shí)沥邻,函數(shù)中的this
指向新創(chuàng)建的對象 - 當(dāng)使用
call
或apply
調(diào)用函數(shù)時(shí)危虱,函數(shù)代碼中的this
被設(shè)置為call
或apply
中的第一個(gè)參數(shù)。如果第一個(gè)參數(shù)是null
或不是個(gè)對象唐全,this
指向全局對象埃跷。
如果你理解并接受了上面的4條規(guī)則,你就能明白this
指的是什么了芦瘾。
補(bǔ)充:eval
打破上面所有規(guī)則
Remember when I said that code evaluated inside eval
is its own type of executable code? Well, that’s true, and it means that the rules for determining what this
refers to inside of eval code are a little more complex.
As a first pass, you might think that this
directly inside eval refers to the same object as it does in eval
’s caller’s context. For example:
var obj = {
val: 0,
func: function() {
eval("console.log(this.val)");
}
};
obj.func(); // 0
That works likely as you expect it to. However, there are many cases with eval
where this
will probably not work as you expect:
eval.call({val: 0}, "console.log(this.val)"); // depends on browser
The output of the above code depends on your browser. If your JavaScript runtime implements ECMAScript 5, this
will refer to the global object and the above should print undefined
, because it’s an “indirect” call of eval
. That’s what the latest versions of Chrome and Firefox do. Safari 5.1 actually throws an error (“The ‘this’ value passed to eval must be the global object from which eval originated”), which is kosher according to ECMAScript 3.
If you want know how this
works with eval
, you should read Juriy Zaytsev’s excellent, “Global eval. What are the options?” You’ll learn more about eval
, execution contexts, and direct vs. indirect calls than you probably ever wanted to know.
In general, you should just avoid using eval
, in which case the simple rules about this
given previously will always hold true.