- 原文地址:The many faces of
this
in javascript- 原文作者:Michał Witkowski
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:lsvih
- 校對者:lampui, zerosrat
Javascript 中多樣的 this
本文將盡量解釋清楚 JavaScript 中最基礎的部分之一:執行上下文(execution context)。如果你經常使用 JS 框架,那理解 this
更是錦上添花。但如果你想更加認真地對待程式設計的話,理解上下文無疑是非常重要的。
我們可以像平常說話一樣來使用 this
。例如:我會說“我媽很不爽,這(this)太糟糕了”,而不會說“我媽很不爽,我媽很不爽這件事太糟糕了”。理解了 this
的上下文,才會理解我們為什麼覺得很糟糕。
現在試著把這個例子與程式語言聯絡起來。在 Javascript 中,我們將 this
作為一個快捷方式,一個引用。它指向其所在上下文的某個物件或變數。
現在這麼說可能會讓人不解,不過很快你就能理解它們了。
全域性上下文
如果你和某人聊天,在剛開始對話、沒有做介紹、沒有任何上下文時,他對你說:“這(this)太糟糕了”,你會怎麼想?大多數情況人們會試圖將“這(this)”與周圍的事物、最近發生的事情聯絡起來。
對於瀏覽器來說也是如此。成千上萬的開發者在沒有上下文的情況下使用了 this
。我們可憐的瀏覽器只能將 this
指向一個全域性物件(大多數情況下是 window)。
var a = 15;
console.log(this.a);
// => 15
console.log(window.a);
// => 15複製程式碼
[以上程式碼需在瀏覽器中執行]
函式外部的任何地方都為全域性上下文,this
始終指向全域性上下文(window 物件)。
函式上下文
以真實世界來類比,函式上下文可以看成句子的上下文。“我媽很不爽,這(this)很不妙。”我們都知道這句話中的 this
是什麼意思。其它句子中同樣可以使用 this
,但是由於其處於所處上下文不同因而意思全然不同。例如,“風暴來襲,這(this)太糟糕了。”
JavaScript 的上下文與物件有關,它取決於函式被執行時所在的物件。因此 this
會指向被執行函式所在的物件。
var a = 20;
function gx () {
return this;
}
function fx () {
return this.a;
}
function fy () {
return window.a;
}
console.log(gx() === window);
// => True
console.log(fx());
// => 20
console.log(fy());
// => 20複製程式碼
this
由函式被呼叫的方式決定。如你所見,上面的所有函式都是在全域性上下文中被呼叫。
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f());
// => 37複製程式碼
當一個函式是作為某個物件的方法被呼叫時,它的 this
指向的就是這個方法所在的物件。
function fx () {
return this;
}
var obj = {
method: function () {
return this;
}
};
var x_obj = {
y_obj: {
method: function () {
return this;
}
}
};
console.log(fx() === window);
// => True — 我們仍處於全域性上下文中。
console.log(obj.method() === window);
// => False — 函式作為一個物件的方法被呼叫。
console.log(obj.method() === obj);
// => True — 函式作為一個物件的方法被呼叫。
console.log(x_obj.y_obj.method() === x_obj)
// => False — 函式作為 y_obj 物件的方法被呼叫,因此 `this` 指向的是 y_obj 的上下文。複製程式碼
例 4
function f2 () {
'use strict';
return this;
}
console.log(f2() === undefined);
// => True複製程式碼
在嚴格模式下,全域性作用域的函式在全域性作用域被呼叫時,this
為 undefined
。
例 5
function fx () {
return this;
}
var obj = {
method: fx
};
console.log(obj.method() === window);
// => False
console.log(obj.method() === obj);
// => True複製程式碼
與前面的例子一樣,無論函式是如何被定義的,在這兒它都是作為一個物件方法被呼叫。
例 6
var obj = {
method: function () {
return this;
}
};
var sec_obj = {
method: obj.method
};
console.log(sec_obj.method() === obj);
// => False
console.log(sec_obj.method() === sec_obj);
// => True複製程式碼
this
是動態的,它可以由一個物件指向另一個物件。
例 7
var shop = {
fruit: "Apple",
sellMe: function() {
console.log("this ", this.fruit);
// => this Apple
console.log("shop ", shop.fruit);
// => shop Apple
}
}
shop.sellMe()複製程式碼
我們既能通過 shop
物件也能通過 this
來訪問 fruit
屬性。
例 8
var Foo = function () {
this.bar = "baz";
};
var foo = new Foo();
console.log(foo.bar);
// => baz
console.log(window.bar);
// => undefined複製程式碼
現在情況不同了。new
操作符建立了一個物件的例項。因此函式的上下文設定為這個被建立的物件例項。
Call、apply、bind
依舊以真實世界舉例:“這(this)太糟糕了,因為我媽開始不爽了。”
這三個方法可以讓我們在任何期許的上下文中執行函式。讓我們舉幾個例子看看它們的用法:
例 1
var bar = "xo xo";
var foo = {
bar: "lorem ipsum"
};
function test () {
return this.bar;
}
console.log(test());
// => xo xo — 我們在全域性上下文中呼叫了 test 函式。
console.log(test.call(foo));
// => lorem ipsum — 通過使用 `call`,我們在 foo 物件的上下文中呼叫了 test 函式。
console.log(test.apply(foo));
// => lorem ipsum — 通過使用 `apply`,我們在 foo 物件的上下文中呼叫了 test 函式。複製程式碼
這兩種方法都能讓你在任何需要的上下文中執行函式。
apply
可以讓你在呼叫函式時將引數以不定長陣列的形式傳入,而 call
則需要你明確引數。
例 2
var a = 5;
function test () {
return this.a;
}
var bound = test.bind(document);
console.log(bound());
// => undefined — 在 document 物件中沒有 a 這個變數。
console.log(bound.call(window));
// => undefined — 在 document 物件中沒有 a 這個變數。在這個情況中,call 不能改變上下文。
var sec_bound = test.bind({a: 15})
console.log(sec_bound())
// => 15 — 我們建立了一個新物件 {a:15},並在此上下文中呼叫了 test 函式。複製程式碼
bind
方法返回的函式的下上文會被永久改變。
在使用 bind 之後,其上下文就固定了,無論你再使用 call、apply 或者 bind 都無法再改變其上下文。
箭頭函式(ES6)
箭頭函式是 ES6 中的一個新語法。它是一個非常方便的工具,不過你需要知道,在箭頭函式中的上下文與普通函式中的上下文的定義是不同的。讓我們舉例看看。
例 1
var foo = (() => this);
console.log(foo() === window);
// => True複製程式碼
當我們使用箭頭函式時,this
會保留其封閉範圍的上下文。
例 2
var obj = {method: () => this};
var sec_obj = {
method: function() {
return this;
}
};
console.log(obj.method() === obj);
// => False
console.log(obj.method() === window);
// => True
console.log(sec_obj.method() === sec_obj);
// => True複製程式碼
請注意箭頭函式與普通函式的不同點。在這個例子中使用箭頭函式時,我們仍然處於 window 上下文中。
我們可以這麼看:
x => this.y equals function (x) { return this.y }.bind(this)
可以將箭頭函式看做其始終 bind
了函式外層上下文的 this
,因此不能將它作為建構函式使用。下面的例子也說明了其不同之處。
例 3
var a = "global";
var obj = {
method: function () {
return {
a: "inside method",
normal: function() {
return this.a;
},
arrowFunction: () => this.a
};
},
a: "inside obj"
};
console.log(obj.method().normal());
// => inside method
console.log(obj.method().arrowFunction());
// => inside obj複製程式碼
當你瞭解了函式中動態(dynamic) this
與詞法(lexical)this
,在定義新函式的時候請三思。如果函式將作為一個方法被呼叫,那麼使用動態 this
;如果它作為一個子程式(subroutine)被呼叫,則使用詞法 this
。
譯註:瞭解動態作用域與詞法作用域可閱讀此文章
相關閱讀
- www.joshuakehn.com/2011/10/20/…
- ryanmorr.com/understandi…
- hackernoon.com/execution-c…
- 2ality.com/2012/04/arr…
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、React、前端、後端、產品、設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。