this解惑

Raion發表於2019-05-18

前言

要正確理解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個方法。callapply會立即執行,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不會被callapplybind改變。

接下來,簡單驗證一下,由於callapply會立即執行,無法被當作建構函式,只能選擇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的處理。比如事件處理函式,當觸發相應事件時,呼叫事件對應的處理函式,此時訪問到的thisundefined(ES6預設類與模組內就是嚴格模式),這就導致不能正確處理該元件的狀態,甚至出錯(處理函式內可能呼叫this.setState方法)。所以在類元件內部宣告方法時會需要我們進行強繫結。

接下來我們看看React元件渲染流程:new構造一個元件例項instance,然後呼叫其render方法進行渲染和事件繫結。new構造的過程,this已經確定指向構造的元件例項,所以你可以在constructor進行bind或直接使用箭頭函式,這樣函式內部this就繫結到了instancerender函式裡之所以能正常訪問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。

相關文章