JS變數和執行環境

前端小醬發表於2023-02-08

1. 資料型別

1.1 基本型別和引用型別的值

在JavaScript中,變數的資料型別分為基本型別和引用型別。
基本型別指那些簡單的資料段,包括Boolean、Number、String、Undefined、Null、Symbol,基本資料型別是直接按值訪問的,可以操作儲存在變數中的實際的值。
引用型別的值是儲存在記憶體中的物件,JavaScript不能直接訪問記憶體中的位置,而是透過指標將變數與記憶體中的物件聯絡起來。因此在操作物件時,實際上是在操作物件的引用。

1.2 棧記憶體和堆記憶體

為了更加深入的理解JS變數基本型別和引用型別的區別,我們還需要了解JS變數的值在記憶體中的儲存方式。

1.2.1 棧記憶體

棧,是一種資料結構,有著先進後出、後進先出的特點。就像一個羽毛球筒,只有一個口(即是出口也是入口),最先進入的球只能最後拿出來:

image.png

如上圖,入棧的順序:1、2、3,出棧的順序:3、2、1

1.2.2 棧記憶體中的資料儲存

棧記憶體用來儲存基本型別的變數或變數的指標,舉個例子:

var num1 = 3;

變數 num1 在記憶體中的儲存形式為:

image.png

對於基本型別的變數,當我們進行復制操作時,比如這個例子:

var num1 = 3;
vat num2 = num1;

此時JS會建立一個新值並將新值分配給新的變數,記憶體中的變數物件表示為:

image.png

變數 num2 得到的是一個全新的值,與變數 num1 中的值無關。

1.2.3 堆記憶體

堆記憶體與棧記憶體不同,對於變數值的儲存沒有需要遵循的規律。

image.png

1.2.4 堆記憶體中的資料儲存

堆記憶體用來儲存物件型別的變數值,物件的內容和大小也會隨時變化。而此時變數物件中的變數儲存的是一個指向堆記憶體中物件的指標,由於存在這種引用關係,物件也被稱為引用型別。例:

var num1 = 3;
var obj = {
  a: 1
}

image.png

當我們對物件型別的值進行復制操作時,實際上copy的是物件的指標,因此兩個變數指向的是同一個物件,例:

var obj 1 = { a: 1 }
var obj2 = obj1;

在記憶體中表現為:

image.png

此時,修改變數 obj1 的屬性等同於修改變數 obj2,例:

var obj1 = { a: 1 };
vat obj2 = obj1;
obj1.b = 2;
alert(obj2.b); // 2

1.3 包裝物件

對於引用型別的值,我們可以為其新增屬性和方法,也可以改變和刪除其屬性和方法:

var person = new Object();
person.name = 'Ian';
alert(person.name); // 'Ian'

但是,我們不能給基本型別的值新增屬性,儘管這樣做不會導致錯誤:

var name = 'Ian';
name.age = 29;
alert(name.age); // undefined

我們可能聽過一種說法,基本型別的值是沒有屬性或方法的。可有意思的是,上述給基本型別的值新增屬性的操作不會報錯,我們還能讀取某些基本型別的值,比如我們可以讀到String型別值的length屬性:

var name = 'Ian';
name.length; // 3
那麼基本型別值的屬性是從哪裡來的呢?

其實,當我們操作基本型別值的屬性時,JS會建立一個臨時的包裝物件,我們操作的實際上是這個包裝物件的屬性,而這個包裝物件是用完立即銷燬的。在上面的例子中,name.length 的 length 屬性實際上來自包裝物件。

var name = 'Ian';
name.length;
var nameObj = new String(name); // 包裝物件
nameObj.length; // 3

由於包裝物件用完立即銷燬的特性,我們對基本型別值的屬性修改都是“無效”的:

var name = 'Ian';
name.length = 4;
name.length; // 3

上面的例子中,當我們再次讀取 name.length 時,實際上又建立了一個新的包裝物件,讀取的是新包裝物件的 length 屬性。

2. 執行環境

執行環境(也稱環境)是JavaScript重要的概念,執行環境定義了變數或函式有權訪問的資料。每個執行環境都有一個相關聯的變數物件,這個變數物件中儲存了當前執行環境中定義的所有變數和函式。

2.1 全域性執行環境

JS程式碼執行時最外層的執行環境稱為全域性執行環境,由於宿主環境的不同,表示全域性執行環境的物件也不同,在web瀏覽器中將window物件作為全域性執行環境,全域性的變數和函式都是作為window物件的屬性和方法宣告的。

2.2 執行環境棧

函式也有執行環境,當開始執行一個函式時,函式執行環境會被壓入一個執行環境棧中,在函式執行性完成後,執行環境棧將函式環境彈出,將主導權交給上層環境。我們透過一段程式碼來展示執行環境棧的變化過程:

var outerName = 'Ian';
function changeName() {
  var innerName = 'Jack';
  outerName = innerName;
}
alert(outerName); // 'Jack'

在初始狀態下,執行環境棧是這樣的:

image.png

當開始執行函式 changeName 時,函式的環境被壓入環境棧:

image.png

當函式 changeName 執行完成後,該環境被彈出環境棧並銷燬(環境棧恢復到上一步的狀態),儲存在其中的變數和函式的定義也隨之銷燬。全域性環境只到程式退出,在web瀏覽器中關閉網頁或瀏覽器程式時才會銷燬。
JS這種執行機制也引出了另一個重要的概念——作用域鏈。

2.3 作用域鏈

JS程式碼在進入一個新的執行環境時會建立一個作用域鏈(scope chain),用來規定對當前執行環境有權訪問的變數或函式的訪問順序。作用域鏈是由執行環境的變數物件組成的,如果是函式環境就將函式的活動物件作為變數物件。
作用域鏈的最前端永遠是當前執行環境的變數物件,上一級變數物件來自上一層執行環境,直到全域性環境的變數物件。當我們訪問一個變數時,會先在當前的變數物件中查詢,如果找不到就會沿著作用域鏈逐級向上查詢,直到返回變數的值或報錯(變數未宣告)。
下面我們透過一個例子來理解這種結構:

var outerName = 'Ian';
function changeName() {
  var innerName = 'Jack';
  outerName = innerName;
  function alertName() {
    alert(outerName); // 'Jack'
  }
}
alert(innerName); // Uncaught ReferenceError: innerName is not defined

在上面這段程式碼中,函式可以訪問到外部定義的變數 outerName,而在全域性環境中不能訪問函式內部定義的變數 innerName。當程式碼進入 alertName 執行環境時,作用域鏈如下圖所示:

image.png

3. 小結

在JavaScript中,變數的資料型別分為基本型別和引用型別,他們具有以下特點:

  • 基本型別的值大小固定儲存在棧記憶體中,當複製基本型別的值時,會建立一個值的副本;
  • 引用型別的值是物件,儲存在堆記憶體中,而變數儲存的是物件的引用,當複製引用型別的值時,複製的是物件的引用,兩個變數都指向同一個物件;
  • 全域性執行環境和函式執行環境;
  • 在進入一個新的執行環境時會建立一個作用域鏈(scope chain),用來規定對當前執行環境有權訪問的變數或函式的訪問順序;
  • 函式區域性環境能訪問父級(直到全域性)環境的變數,而全域性(父級)環境不能訪問函式的區域性變數;

相關文章