前言
要正確理解this,首先得理解執行上下文,這裡推薦湯姆大叔的執行上下文,因為this
是在執行程式碼時確認具體指向誰,箭頭函式除外。
全域性作用域中的this
node
: 每個javaScript
檔案都是一個模組,this
指向空物件(module.exports
)
this.a = 1;
console.log(this, module.exports);
// { a: 1 } { a: 1 }
當然也有些意外,比如下面這種情況:
this.a = 1;
module.exports = {}
console.log(this, module.exports);
// { a: 1 } {}
瀏覽器端: this
指向window
。
函式作用域中的this
這裡分為兩種,一種是全域性作用域下直接執行函式,另外一種是被當作某個物件的屬性的時候執行。eval的情況這裡不作討論。
全域性環境下執行
function foo() {
console.log(this); // 此時的執行上下文為全域性物件
}
foo();
// node global, 瀏覽器 window
當然嚴格模式下有不同,具體區別如下:
嚴格模式
this
指向undefined
(node
and 瀏覽器端)
非嚴格模式
瀏覽器端: this
指向全域性變數window
node
: this
指向global
被當作屬性呼叫
當函式作為一個物件的屬性時,node
和瀏覽器端一致,指向呼叫該屬性的物件
var obj = {
name: 'foo',
foo: function foo() {
console.log(this);
}
}
obj.foo();
// { name: 'foo', foo: [Function: foo] }
接下來,做一些升級。
var obj = {
name: 'foo',
foo: function foo() {
console.log(this);
}
}
var objA = obj.foo;
objA();
// node環境指向global,瀏覽器端指向window,嚴格模式下均指向undefined
--------------------------------------------------------------
var obj = {
name: 'foo',
foo: function foo() {
console.log(this);
}
}
var objA = {
name: 'objA',
foo: obj.foo
};
objA.foo();
// { name: 'objA', foo: [Function: foo] }
call、apply、bind
如果想手動更改函式裡的this
指向,可通過上述3個方法。call
和apply
會立即執行,bind
則返回一個繫結好this
指向的函式。
var obj = {
name: 'foo',
foo: function foo() {
console.log(this);
}
}
var objA = {
name: 'objA',
foo: obj.foo
};
obj.foo.call(objA); // 將this指向objA
obj.foo.apply(objA);
obj.foo.bind(objA)(); // bind函式會返回一個繫結好this的函式,可供以後呼叫
/**
{ name: 'objA', foo: [Function: foo] }
{ name: 'objA', foo: [Function: foo] }
{ name: 'objA', foo: [Function: foo] }
*/
這裡對上述3個方法進行更細的說明,方便更好的理解之間的差異。
var obj = {
name: 'foo',
foo: function foo() {
console.log(this, arguments); // 通過arguments物件訪問函式傳入的引數列表,類似陣列但不是陣列,可通過arguments[0]訪問到傳入的Tom
}
}
var objA = {
name: 'objA',
foo: obj.foo
};
obj.foo.call(objA, 'Tom', 'Jerry');
obj.foo.apply(objA, ['Tom', 'Jerry']);
obj.foo.bind(objA, 'Tom', 'Jerry')(1);
/**
{ name: 'objA', foo: [Function: foo] } [Arguments] { '0': 'Tom', '1': 'Jerry' }
{ name: 'objA', foo: [Function: foo] } [Arguments] { '0': 'Tom', '1': 'Jerry' }
{ name: 'objA', foo: [Function: foo] } [Arguments] { '0': 'Tom', '1': 'Jerry', '2': 1 }
可以看到call和bind是按序列傳參,而apply是按陣列傳參,bind不會更改傳參的順序
*/
new構造
當函式被當作建構函式呼叫時,this
指向構造的那個物件。
注:new
呼叫中的this
不會被call
、apply
、bind
改變。
接下來,簡單驗證一下,由於call
和apply
會立即執行,無法被當作建構函式,只能選擇bind
。
function Foo() {
console.log(this);
}
var foo = Foo.bind({ name: 'Tom' });
foo();
// { name: 'Tom' }
new foo();
// Foo {}
箭頭函式中的this
this
在定義時,就已經知道其具體指向,因為在執行到宣告的箭頭函式時,會將this
進行強繫結到外部作用域中的this
,且無法更改。可以理解為繼承了外部作用域中的this
。由於箭頭函式的this
是確定的,無法更改,因此也無法被當作建構函式呼叫。
外部作用域為全域性作用域:
var foo = () => {
console.log(this);
}
this.a = 1;
foo();
// 或者下面程式碼
var obj = {
name: 'obj',
foo: () => {
console.log(this);
}
}
var foo = obj.foo;
obj.foo();
foo();
foo.call({ name: 'Tom' });
/**
因為obj是在全域性作用域下被定義,所以外部作用域為全域性物件
node: 指向module.exports
瀏覽器:指向window
*/
外部作用域為函式作用域:
function foo() {
var a = () => {
console.log(this); // 繼承外部作用域foo函式的this
};
a();
}
foo();
foo.call({ name: 'foo' });
new foo();
/**
這裡foo函式中的this並不確定,由於呼叫方式不同,其this指向也不同
*/
相信寫ES6類的情況很多,本人經常寫React
類元件,剛開始初學者會好奇為什麼在類元件裡寫方法時要用bind
或者箭頭函式來強繫結this
。因為一般類元件裡的方法,都會設計到this
的處理。比如事件處理函式,當觸發相應事件時,呼叫事件對應的處理函式,此時訪問到的this
為undefined
(ES6預設類與模組內就是嚴格模式),這就導致不能正確處理該元件的狀態,甚至出錯(處理函式內可能呼叫this.setState
方法)。所以在類元件內部宣告方法時會需要我們進行強繫結。
接下來我們看看React
元件渲染流程:new
構造一個元件例項instance
,然後呼叫其render
方法進行渲染和事件繫結。new
構造的過程,this
已經確定指向構造的元件例項,所以你可以在constructor
進行bind
或直接使用箭頭函式,這樣函式內部this
就繫結到了instance
。render
函式裡之所以能正常訪問this
,是因為以instance.render()
進行渲染。
當然這裡不特指React
類元件,只要是ES6類,只能用new
構造呼叫,否則會報錯,所以ES6類裡this
指向是確定的,可以放心使用箭頭函式。
迷惑的程式碼
還有一個比較迷惑的地方,遇到的機會很少,程式碼如下:
(function(){
console.log(this); // 執行結果和全域性作用域下執行結果一致
})();
// 由於沒有以物件屬性的方式呼叫,則被認為是全域性環境下呼叫
--------------------------------------------------------
(function(){
console.log(this);
}).call({ name: 'Hello World' });
// { name: 'Hello World' },this指向可以被改變
--------------------------------------------------------
new (function(name){
this.name = name;
console.log(this);
})('Tom');
// { name: 'Tom' },this指向新建立的物件
更好的閱讀體驗在我的github,歡迎?提issue。