this
虐我千百遍,看完此文效立見!不得不說,這篇文章的總結很地道很全面,適合收藏之用。
原文:all this
習慣了高級語言的你或許覺得JavaScript中的this
跟Java這些面向對象語言相似,保存了實體屬性的一些值。其實不然。將它視作幻影魔神比較恰當,手提一個裝滿未知符文的靈龕。
以下內容我希望廣大同行們能夠了解。全是掏箱底的干貨,其中大部分占用了我很多時間才掌握。
全局this
瀏覽器宿主的全局環境中,this
指的是window
對象。
<script type="text/javascript">
console.log(this === window); //true
</script>
瀏覽器中在全局環境下,使用var
聲明變量其實就是賦值給this
或window
。
<script type="text/javascript">
var foo = "bar";
console.log(this.foo); //logs "bar"
console.log(window.foo); //logs "bar"
</script>
任何情況下,創建變量時沒有使用var
或者let
(ECMAScript 6),也是在操作全局this
。
<script type="text/javascript">
foo = "bar";
function testThis() {
foo = "foo";
}
console.log(this.foo); //logs "bar"
testThis();
console.log(this.foo); //logs "foo"
</script>
Node命令行(REPL)中,this
是全局命名空間。可以通過global
來訪問。
> this
{ ArrayBuffer: [Function: ArrayBuffer],
Int8Array: { [Function: Int8Array] BYTES_PER_ELEMENT: 1 },
Uint8Array: { [Function: Uint8Array] BYTES_PER_ELEMENT: 1 },
...
> global === this
true
在Node環境里執行的JS腳本中,this
其實是個空對象,有別于global
。
console.log(this);
console.log(this === global);
$ node test.js
{}
false
當嘗試在Node中執行JS腳本時,腳本中全局作用域中的var
并不會將變量賦值給全局this
,這與在瀏覽器中是不一樣的。
var foo = "bar";
console.log(this.foo);
$ node test.js
undefined
...但在命令行里進行求值卻會賦值到this
身上。
> var foo = "bar";
> this.foo
bar
> global.foo
bar
在Node里執行的腳本中,創建變量時沒帶var
或let
關鍵字,會賦值給全局的global
但不是this
(譯注:上面已經提到this
和global
不是同一個對象,所以這里就不奇怪了)。
foo = "bar";
console.log(this.foo);
console.log(global.foo);
$ node test.js
undefined
bar
但在Node命令行里,就會賦值給兩者了。
譯注:簡單來說,Node腳本中
global
和this
是區別對待的,而Node命令行中,兩者可等效為同一對象。
函數或方法里的this
除了DOM的事件回調或者提供了執行上下文(后面會提到)的情況,函數正常被調用(不帶new
)時,里面的this
指向的是全局作用域。
<script type="text/javascript">
foo = "bar";
function testThis() {
this.foo = "foo";
}
console.log(this.foo); //logs "bar"
testThis();
console.log(this.foo); //logs "foo"
</script>
foo = "bar";
function testThis () {
this.foo = "foo";
}
console.log(global.foo);
testThis();
console.log(global.foo);
$ node test.js
bar
foo
還有個例外,就是使用了"use strict";
。此時this
是undefined
。
<script type="text/javascript">
foo = "bar";
function testThis() {
"use strict";
this.foo = "foo";
}
console.log(this.foo); //logs "bar"
testThis(); //Uncaught TypeError: Cannot set property 'foo' of undefined
</script>
當用調用函數時使用了new
關鍵字,此刻this
指代一個新的上下文,不再指向全局this
。
<script type="text/javascript">
foo = "bar";
function testThis() {
this.foo = "foo";
}
console.log(this.foo); //logs "bar"
new testThis();
console.log(this.foo); //logs "bar"
console.log(new testThis().foo); //logs "foo"
</script>
通常我將這個新的上下文稱作實例。
原型中的this
函數創建后其實以一個函數對象的形式存在著。既然是對象,則自動獲得了一個叫做prototype
的屬性,可以自由地對這個屬性進行賦值。當配合new
關鍵字來調用一個函數創建實例后,此刻便能直接訪問到原型身上的值。
function Thing() {
console.log(this.foo);
}
Thing.prototype.foo = "bar";
var thing = new Thing(); //logs "bar"
console.log(thing.foo); //logs "bar"
當通過new
的方式創建了多個實例后,他們會共用一個原型。比如,每個實例的this.foo
都返回相同的值,直到this.foo
被重寫。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo);
}
Thing.prototype.setFoo = function (newFoo) {
this.foo = newFoo;
}
var thing1 = new Thing();
var thing2 = new Thing();
thing1.logFoo(); //logs "bar"
thing2.logFoo(); //logs "bar"
thing1.setFoo("foo");
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "bar";
thing2.foo = "foobar";
thing1.logFoo(); //logs "foo";
thing2.logFoo(); //logs "foobar";
在實例中,this
是個特殊的對象,而this
自身其實只是個關鍵字。你可以把this
想象成在實例中獲取原型值的一種途徑,同時對this
賦值又會覆蓋原型上的值。完全可以將新增的值從原型中刪除從而將原型還原為初始狀態。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo);
}
Thing.prototype.setFoo = function (newFoo) {
this.foo = newFoo;
}
Thing.prototype.deleteFoo = function () {
delete this.foo;
}
var thing = new Thing();
thing.setFoo("foo");
thing.logFoo(); //logs "foo";
thing.deleteFoo();
thing.logFoo(); //logs "bar";
thing.foo = "foobar";
thing.logFoo(); //logs "foobar";
delete thing.foo;
thing.logFoo(); //logs "bar";
...或者不通過實例,直接操作函數的原型。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo, Thing.prototype.foo);
}
var thing = new Thing();
thing.foo = "foo";
thing.logFoo(); //logs "foo bar";
同一函數創建的所有實例均共享一個原型。如果你給原型賦值了一個數組,那么所有實例都能獲取到這個數組。除非你在某個實例中對其進行了重寫,實事上是進行了覆蓋。
function Thing() {
}
Thing.prototype.things = [];
var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo");
console.log(thing2.things); //logs ["foo"]
通常上面的做法是不正確的(譯注:改變thing1
的同時也影響了thing2
)。如果你想每個實例互不影響,那么請在函數里創建這些值,而不是在原型上。
function Thing() {
this.things = [];
}
var thing1 = new Thing();
var thing2 = new Thing();
thing1.things.push("foo");
console.log(thing1.things); //logs ["foo"]
console.log(thing2.things); //logs []
多個函數可以形成原型鏈,這樣this
便會在原型鏈上逐步往上找直到找到你想引用的值。
function Thing1() {
}
Thing1.prototype.foo = "bar";
function Thing2() {
}
Thing2.prototype = new Thing1();
var thing = new Thing2();
console.log(thing.foo); //logs "bar"
很多人便是利用這個特性在JS中模擬經典的對象繼承。
注意原型鏈底層函數中對this
的操作會覆蓋上層的值。
function Thing1() {
}
Thing1.prototype.foo = "bar";
function Thing2() {
this.foo = "foo";
}
Thing2.prototype = new Thing1();
function Thing3() {
}
Thing3.prototype = new Thing2();
var thing = new Thing3();
console.log(thing.foo); //logs "foo"
我習慣將賦值到原型上的函數稱作方法。上面某些地方便使用了方法這樣的字眼,比如logFoo
方法。這些方法中的this
同樣具有在原型鏈上查找引用的魔力。通常將最初用來創建實例的函數稱作構造函數。
原型鏈方法中的this
是從實例中的this
開始住上查找整個原型鏈的。也就是說,如果原型鏈中某個地方直接對this
進行賦值覆蓋了某個變量,那么我們拿到 的是覆蓋后的值。
function Thing1() {
}
Thing1.prototype.foo = "bar";
Thing1.prototype.logFoo = function () {
console.log(this.foo);
}
function Thing2() {
this.foo = "foo";
}
Thing2.prototype = new Thing1();
var thing = new Thing2();
thing.logFoo(); //logs "foo";
在JavaScript中,函數可以嵌套函數,也就是你可以在函數里面繼續定義函數。但內層函數是通過閉包獲取外層函數里定義的變量值的,而不是直接繼承this
。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
var info = "attempting to log this.foo:";
function doIt() {
console.log(info, this.foo);
}
doIt();
}
var thing = new Thing();
thing.logFoo(); //logs "attempting to log this.foo: undefined"
上面示例中,doIt
函數中的this
指代是全局作用域或者是undefined
如果使用了"use strict";
聲明的話。對于很多新手來說,理解這點是非常頭疼的。
還有更奇葩的。把實例的方法作為參數傳遞時,實例是不會跟著過去的。也就是說,此時方法中的this
在調用時指向的是全局this
或者是undefined
在聲明了"use strict";
時。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo);
}
function doIt(method) {
method();
}
var thing = new Thing();
thing.logFoo(); //logs "bar"
doIt(thing.logFoo); //logs undefined
所以很多人習慣將this
緩存起來,用個叫self
或者其他什么的變量來保存,以將外層與內層的this
區分開來。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
var self = this;
var info = "attempting to log this.foo:";
function doIt() {
console.log(info, self.foo);
}
doIt();
}
var thing = new Thing();
thing.logFoo(); //logs "attempting to log this.foo: bar"
...但上面的方式不是萬能的,在將方法做為參數傳遞時,就不起作用了。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
var self = this;
function doIt() {
console.log(self.foo);
}
doIt();
}
function doItIndirectly(method) {
method();
}
var thing = new Thing();
thing.logFoo(); //logs "bar"
doItIndirectly(thing.logFoo); //logs undefined
解決方法就是傳遞的時候使用bind
方法顯示指明上下文,bind
方法是所有函數或方法都具有的。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo);
}
function doIt(method) {
method();
}
var thing = new Thing();
doIt(thing.logFoo.bind(thing)); //logs bar
同時也可以使用apply
或call
來調用該方法或函數,讓它在一個新的上下文中執行。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
function doIt() {
console.log(this.foo);
}
doIt.apply(this);
}
function doItIndirectly(method) {
method();
}
var thing = new Thing();
doItIndirectly(thing.logFoo.bind(thing)); //logs bar
使用bind
可以任意改變函數或方法的執行上下文,即使它沒有被綁定到一個實例的原型上。
function Thing() {
}
Thing.prototype.foo = "bar";
function logFoo(aStr) {
console.log(aStr, this.foo);
}
var thing = new Thing();
logFoo.bind(thing)("using bind"); //logs "using bind bar"
logFoo.apply(thing, ["using apply"]); //logs "using apply bar"
logFoo.call(thing, "using call"); //logs "using call bar"
logFoo("using nothing"); //logs "using nothing undefined"
避免在構造函數中返回作何東西,因為返回的東西可能覆蓋本來該返回的實例。
function Thing() {
return {};
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo);
}
var thing = new Thing();
thing.logFoo(); //Uncaught TypeError: undefined is not a function
但,如果你在構造函數里返回的是個原始值比如字符串或者數字什么的,上面的錯誤就不會發生了,返回語句將被忽略。所以最好別在一個將要通過new
來調用的構造函數中返回作何東西,即使你是清醒的。如果你想實現工廠模式,那么請用一個函數來創建實例,并且不通過new
來調用。當然這只是個人建議。
誠然,你也可以使用Object.create
從而避免使用new
。這樣也能創建一個實例。
function Thing() {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo);
}
var thing = Object.create(Thing.prototype);
thing.logFoo(); //logs "bar"
這種方式不會調用該構造函數。
function Thing() {
this.foo = "foo";
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
console.log(this.foo);
}
var thing = Object.create(Thing.prototype);
thing.logFoo(); //logs "bar"
正因為Object.create
沒有調用構造函數,這在當你想實現一個繼承時是非常有用的,隨后你可能想要重寫構造函數。
function Thing1() {
this.foo = "foo";
}
Thing1.prototype.foo = "bar";
function Thing2() {
this.logFoo(); //logs "bar"
Thing1.apply(this);
this.logFoo(); //logs "foo"
}
Thing2.prototype = Object.create(Thing1.prototype);
Thing2.prototype.logFoo = function () {
console.log(this.foo);
}
var thing = new Thing2();
對象中的this
可以在對象的任何方法中使用this
來訪問該對象的屬性。這與用new
得到的實例是不一樣的。
var obj = {
foo: "bar",
logFoo: function () {
console.log(this.foo);
}
};
obj.logFoo(); //logs "bar"
注意這里并沒有使用new
,也沒有用Object.create
,更沒有函數的調用來創建對象。也可以將函數綁定到對象,就好像這個對象是一個實例一樣。
var obj = {
foo: "bar"
};
function logFoo() {
console.log(this.foo);
}
logFoo.apply(obj); //logs "bar"
此時使用this
沒有向上查找原型鏈的復雜工序。通過this
所拿到的只是該對象身上的屬性而以。
var obj = {
foo: "bar",
deeper: {
logFoo: function () {
console.log(this.foo);
}
}
};
obj.deeper.logFoo(); //logs undefined
也可以不通過this
,直接訪問對象的屬性。
var obj = {
foo: "bar",
deeper: {
logFoo: function () {
console.log(obj.foo);
}
}
};
obj.deeper.logFoo(); //logs "bar"
DOM 事件回調中的this
在DOM事件的處理函數中,this
指代的是被綁定該事件的DOM元素。
function Listener() {
document.getElementById("foo").addEventListener("click",
this.handleClick);
}
Listener.prototype.handleClick = function (event) {
console.log(this); //logs "<div id="foo"></div>"
}
var listener = new Listener();
document.getElementById("foo").click();
...除非你通過bind
人為改變了事件處理器的執行上下文。
function Listener() {
document.getElementById("foo").addEventListener("click",
this.handleClick.bind(this));
}
Listener.prototype.handleClick = function (event) {
console.log(this); //logs Listener {handleClick: function}
}
var listener = new Listener();
document.getElementById("foo").click();
HTML中的this
HTML標簽的屬性中是可能寫JS的,這種情況下this
指代該HTML元素。
<div id="foo" onclick="console.log(this);"></div>
<script type="text/javascript">
document.getElementById("foo").click(); //logs <div id="foo"...
</script>
重寫this
無法重寫this
,因為它是一個關鍵字。
function test () {
var this = {}; // Uncaught SyntaxError: Unexpected token this
}
eval
中的this
eval
中也可以正確獲取當前的 this
。
function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
eval("console.log(this.foo)"); //logs "bar"
}
var thing = new Thing();
thing.logFoo();
這里存在安全隱患。最好的辦法就是避免使用eval
。
使用Function
關鍵字創建的函數也可以獲取this
:
function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = new Function("console.log(this.foo);");
var thing = new Thing();
thing.logFoo(); //logs "bar"
使用with
時的this
使用with
可以將this
人為添加到當前執行環境中而不需要顯示地引用this
。
function Thing () {
}
Thing.prototype.foo = "bar";
Thing.prototype.logFoo = function () {
with (this) {
console.log(foo);
foo = "foo";
}
}
var thing = new Thing();
thing.logFoo(); // logs "bar"
console.log(thing.foo); // logs "foo"
正如很多人認為的那樣,使用with
是不好的,因為會產生歧義。
jQuery中的this
一如HTML DOM元素的事件回調,jQuery庫中大多地方的this
也是指代的DOM元素。頁面上的事件回調和一些便利的靜態方法比如$.each
都是這樣的。
<div class="foo bar1"></div>
<div class="foo bar2"></div>
<script type="text/javascript">
$(".foo").each(function () {
console.log(this); //logs <div class="foo...
});
$(".foo").on("click", function () {
console.log(this); //logs <div class="foo...
});
$(".foo").each(function () {
this.click();
});
</script>
傳遞 this
如果你用過underscore.js或者lo-dash你便知道,這兩個庫中很多方法你可以傳遞一個參數來顯示指定執行的上下文。比如_.each
。自ECMAScript 5 標準后,一些原生的JS方法也允許傳遞上下文,比如forEach
。事實上,上文提到的bind
,apply
還有call
已經給我們手動指定函數執行上下文的能力了。
function Thing(type) {
this.type = type;
}
Thing.prototype.log = function (thing) {
console.log(this.type, thing);
}
Thing.prototype.logThings = function (arr) {
arr.forEach(this.log, this); // logs "fruit apples..."
_.each(arr, this.log, this); //logs "fruit apples..."
}
var thing = new Thing("fruit");
thing.logThings(["apples", "oranges", "strawberries", "bananas"]);
這樣可以使得代碼簡潔些,不用層層嵌套bind
,也不用不斷地緩存this
。
一些編程語言上手很簡單,比如Go語言手冊可以被快速讀完。然后你差不多就掌握這門語言了,只是在實戰時會有些小的問題或陷阱在等著你。
而JavaScript不是這樣的。手冊難讀。非常多缺陷在里面,以至于人們抽離出了它好的部分
(The Good Parts)。最好的文檔可能是MDN上的了。所以我建議你看看他上面關于this
的介紹,并且始終在搜索JS相關問題時加上"mdn" 來獲得最好的文檔資料。靜態代碼檢查也是個不錯的工具,比如jshint。
歡迎勘誤及討論,我的推特@bjorntipling。
文章列表