首頁 > 軟體

深入理解函數執行上下文 this

2022-10-26 14:02:12

JavaScript 中的 this 是什麼

關於 this,我們得先從執行上下文說起。我們知道:執行上下文中包含了變數環境、詞法環境、外部環境,當然也包括 this,具體你可以參考下圖:

從圖中可以看出,this 是和執行上下文繫結的,也就是說每個執行上下文中都有一個 this。執行上下文主要分為三種——全域性執行上下文、函數執行上下文和 eval 執行上下文,所以對應的 this 也只有這三種——全域性執行上下文中的 this、函數中的 this 和 eval 中的 this。

不過由於 eval 我們使用的不多,所以本文我們對此就不做介紹了,如果你感興趣的話,可以自行搜尋和學習相關知識。

那麼接下來我們就重點講解下全域性執行上下文中的 this和函數執行上下文中的 this。

全域性執行上下文中的 this

首先我們來看看全域性執行上下文中的 this 是什麼。

你可以在控制檯中輸入console.log(this)來列印出來全域性執行上下文中的 this,最終輸出的是 window 物件。所以你可以得出這樣一個結論:全域性執行上下文中的 this 是指向 window 物件的。這也是 this 和作用域鏈的唯一交點,作用域鏈的最底端包含了 window 物件,全域性執行上下文中的 this 也是指向 window 物件。

函數執行上下文中的 this

現在你已經知道全域性物件中的 this 是指向 window 物件了,那麼接下來,我們就來重點分析函數執行上下文中的 this。還是先看下面這段程式碼:

function foo() {
  console.log(this);
}
foo();

我們在 foo 函數內部列印出來 this 值,執行這段程式碼,列印出來的也是 window 物件,這說明在預設情況下呼叫一個函數,其執行上下文中的 this 也是指向 window 物件的。估計你會好奇,那能不能設定執行上下文中的 this 來指向其他物件呢?答案是肯定的。通常情況下,有下面三種方式來設定函數執行上下文中的 this 值。

1. 通過函數的 call 方法設定

你可以通過函數的call方法來設定函數執行上下文的 this 指向,比如下面這段程式碼,我們就並沒有直接呼叫 foo 函數,而是呼叫了 foo 的 call 方法,並將 bar 物件作為 call 方法的引數。

let bar = {
  myName: " name1 ",
  test1: 1,
};
function foo() {
  this.myName = " name2 ";
}
foo.call(bar);
console.log(bar);
console.log(myName);

執行這段程式碼,然後觀察輸出結果,你就能發現 foo 函數內部的 this 已經指向了 bar 物件,因為通過列印 bar 物件,可以看出 bar 的 myName 屬性已經由“name1”變為“name2”了,同時在全域性執行上下文中列印 myName,JavaScript 引擎提示該變數未定義。

其實除了 call 方法,你還可以使用bind和apply方法來設定函數執行上下文中的 this,僅僅是語法稍有不同。

2. 通過物件呼叫方法設定

要改變函數執行上下文中的 this 指向,除了通過函數的 call 方法來實現外,還可以通過物件呼叫的方式,比如下面這段程式碼:

var myObj = {
  name: " name ",
  showThis: function () {
    console.log(this);
  },
};
myObj.showThis();

在這段程式碼中,我們定義了一個 myObj 物件,該物件是由一個 name 屬性和一個 showThis 方法組成的,然後再通過 myObj 物件來呼叫 showThis 方法。執行這段程式碼,你可以看到,最終輸出的 this 值是指向 myObj 的。

所以,你可以得出這樣的結論:使用物件來呼叫其內部的一個方法,該方法的 this 是指向物件本身的。

其實,你也可以認為 JavaScript 引擎在執行myObject.showThis()時,將其轉化為了:

myObj.showThis.call(myObj)

接下來我們稍微改變下呼叫方式,把 showThis 賦給一個全域性物件,然後再呼叫該物件,程式碼如下所示:

var myObj = {
  name: " time ",
  showThis: function () {
    this.name = " bang ";
    console.log(this);
  },
};
var foo = myObj.showThis;
foo();

執行這段程式碼,你會發現 this 又指向了全域性 window 物件。

所以通過以上兩個例子的對比,你可以得出下面這樣兩個結論:

  • 在全域性環境中呼叫一個函數,函數內部的 this 指向的是全域性變數 window。
  • 通過一個物件來呼叫其內部的一個方法,該方法的執行上下文中的 this 指向物件本身。

3. 通過建構函式中設定

你可以像這樣設定建構函式中的 this,如下面的範例程式碼:

function CreateObj() {
  this.name = " time ";
}
var myObj = new CreateObj();

在這段程式碼中,我們使用 new 建立了物件 myObj,那你知道此時的建構函式 CreateObj 中的 this 到底指向了誰嗎?

其實,當執行 new CreateObj() 的時候,JavaScript 引擎做了如下四件事:

  • 首先建立了一個空物件 tempObj;
  • 接著呼叫 CreateObj.call 方法,並將 tempObj 作為 call 方法的引數,這樣當 CreateObj 的執行上下文建立時,它的 this 就指向了 tempObj 物件;
  • 然後執行 CreateObj 函數,此時的 CreateObj 函數執行上下文中的 this 指向了 tempObj 物件;
  • 最後返回 tempObj 物件。

這樣,我們就通過 new 關鍵字構建好了一個新物件,並且建構函式中的 this 其實就是新物件本身。

this 的設計缺陷以及應對方案

就我個人而言,this 並不是一個很好的設計,因為它的很多使用方法都衝擊人的直覺,在使用過程中存在著非常多的坑。下面咱們就來一起看看那些 this 設計缺陷。

1. 巢狀函數中的 this 不會從外層函數中繼承

我認為這是一個嚴重的設計錯誤,並影響了很多開發者。

至於如何解決?你可以在函數中宣告一個變數 self 用來儲存 this。當然也可以使用 ES6 中的箭頭函數來解決這個問題。

2. 普通函數中的 this 預設指向全域性物件 window

上面我們已經介紹過了,在預設情況下呼叫一個函數,其執行上下文中的 this 是預設指向全域性物件 window 的。

不過這個設計也是一種缺陷,因為在實際工作中,我們並不希望函數執行上下文中的 this 預設指向全域性物件,因為這樣會打破資料的邊界,造成一些誤操作。如果要讓函數執行上下文中的 this 指向某個物件,最好的方式是通過 call 方法來顯示呼叫。

這個問題可以通過設定 JavaScript 的“嚴格模式”來解決。在嚴格模式下,預設執行一個函數,其函數的執行上下文中的 this 值是 undefined,這就解決上面的問題了。

總結

回顧下內容:

首先,在使用 this 時,為了避坑,你要謹記以下三點:

  • 當函數作為物件的方法呼叫時,函數中的 this 就是該物件;
  • 當函數被正常呼叫時,在嚴格模式下,this 值是 undefined,非嚴格模式下 this 指向的是全域性物件 window;
  • 巢狀函數中的 this 不會繼承外層函數的 this 值。

最後,我們還提了一下箭頭函數,因為箭頭函數沒有自己的執行上下文,所以箭頭函數的 this 就是它外層函數的 this。

以上就是深入理解函數執行上下文 this的詳細內容,更多關於函數執行上下文 this的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com