詳解this
原文:https://www.cnblogs.com/Wayou/p/all-this.html
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。