
溫故而知新,這些JS基礎知識你都知道嗎?
今天和大家分享的是 JavaScript 中有關變量的知識,希望這篇文章能讓你對JS中的變量有新的認識.
目錄:
- 變量的執行環境(執行上下文)
- 執行上下文的生命周期
- 創建變量對象
- 變量的數據存儲
- 變量的內存空間
- 變量的垃圾回收
- let/const/var的區別
執行環境(執行上下文)
javascript的運行環境主要包括以下三種:
- 全局環境:代碼運行起來后會首先進入全局環境.
- 函數環境:當函數被調用時,會進入當前函數中執行代碼.
- eval環境:不建議使用,這里不做介紹.
js運行環境也叫做執行上下文,因此在一個JavaScript程序中,必定會出現多個執行上下文.
JS引擎會以棧(遵循后進先出的數據存儲方式)的方式來處理執行上下文,也就是我們通常所說的函數調用棧。棧底永遠是全局上下文,棧頂則是當前正在執行的上下文. 處于棧頂的執行上下文執行完畢后,會自動出棧.
看個例子可能會更形象些:
function declare() {var a = 1;function update() {a = 2;}update();
}
declare();
下面是上方用例中的執行上下文對應的進出棧流程示意圖:

假如上方的 declare 函數處于全局環境中,那么代碼運行時會經歷以下幾步:
- 首先第一步就是全局上下文入棧.
- 全局上下文入棧后,遇到的第一個可執行代碼就是 declare() 函數的調用,此函數一旦調用,就會創建自己的執行上下文,此時declare EC入棧.
- 在新開辟的declare EC執行上下文中,執行內部的可執行代碼,直到遇到 update() 函數調用時,又會創建一個新的執行上下文,此時update EC入棧.
- 當update EC中的可執行代碼執行完畢之后,發現不再有其他執行上下文生成的情況,此上下文會自動從棧中彈出.
- update EC執行上下文彈出后,會繼續執行 declare EC執行上下文中的可執行代碼,直到順利執行完畢,且沒有遇到其他執行上下文,則自動從棧中彈出.
- 最后執行棧中只剩下全局上下文,若瀏覽器不關閉,全局上下文會一直存在,直到瀏覽器窗口關閉,全局上下文才會最終出棧.
執行上下文的生命周期
說完執行上下文的入棧、出棧情況,下面說說執行上下文的生命周期.
當一個函數調用時,一個新的執行上下文就會被創建,一個執行上下文的生命周期可分為兩個階段:
- 創建階段 --- 此階段執行上下文會分別創建變量對象、確認作用域鏈、以及確定this指向.
- 執行階段 --- 執行代碼,這個時候會完成變量賦值、函數引用、以及執行其他可執行代碼等工作.
畫個圖看起來會更直觀:

這篇文章接下來主要介紹變量的生命周期,所以我們主要針對執行上下文中 創建變量對象、內存空間、變量賦值階段 展開講解。
創建變量對象
JS中聲明的所有變量都保存在變量對象中, 變量對象的創建依次經歷以下步驟:
1. 首先獲得函數的參數變量及其值.
2. 依次獲取當前上下文中所有的函數聲明. 在變量對象中會以函數名建立一個屬性,屬性值指向該函數所在的內存地址.
3. 依次獲取當前上下文中的變量聲明,每找到一個變量聲明,就在變量對象中以變量名建立一個屬性。 如果是var聲明,則屬性值會初始化為undefined.
變量的數據存儲
變量對象創建完成后,接下來就是對數據的存儲,基礎數據類型往往會保存在棧內存中(特殊情況除外),而引用數據類型的值是保存在堆內存中的對象,在js語言中,不允許直接訪問堆內存空間中的數據. 當我們操作對象時實際上是在操作對象的引用,而不是實際的對象. 因此,引用數據類型都是按引用訪問的,這里的引用可以理解為保存在變量對象中的一個地址,該地址與堆內存中的對象相關聯.
下邊通過一個列子,來看下變量對象的存儲:
function fun(){var a = 1;var b = 'hello world';var c = {x: 100};var d = {y: [1,2]};
}
當fun()函數調用時,會創建一個執行上下文,在當前上下文中創建變量對象,變量對象存放格式如下:

如果是基本數據類型,在棧中存儲數據本身; 如果是引用數據類型,在棧中存儲的是堆中對象的引用;
變量的內存空間
說完變量對象的創建與存儲之后,接下來再說說變量的內存空間的使用過程. 內存空間的使用同樣也具有自己的生命周期,包含:
- 分配內存階段
- 使用分配到的內存
- 不需要時釋放內存
再來看個例子:
var a = 20; // 分配內存
console.log(a + 1); // 使用內存
a = null; // 釋放內存
上邊的三行代碼分別對應著分配內存、使用分配到的內存、以及釋放內存三個過程。其中,分配和使用應該很好理解,最終要的是釋放的過程,涉及到垃圾回收機制的實現原理。
變量的垃圾回收
Javascript中具有自動垃圾收集機制,也就是說,執行環境會負責管理代碼執行過程中使用的內存。所以,在日常開發中,開發人員很少再關心內存使用的問題.
垃圾回收機制的原理就是:找出那些不再繼續使用的變量,垃圾收集器會按照固定的時間間隔,周期性的釋放其占用的內存.
最常用的垃圾收集方式是標記清除算法. 主要依靠 "引用" 的概念,當一塊內存空間中的數據能夠被訪問時,垃圾回收器就會認為 "該數據能夠被獲得",不能夠被獲得的數據,就會被打上標記,并回收內存空間,這種方式叫做 標記 --- 清除算法.
注: 在局部作用域中,當函數執行完畢后,垃圾收集器會很容易做出判斷并做局部變量做回收操作。但在全局作用域中,變量什么時候需要自動釋放內存空間則很難判斷,所以,我們在做開發時,應盡量避免全局變量的使用。如果使用了全局變量,建議通過 a = null 這樣的方式釋放引用,以確保能夠及時的回收內存空間.
介紹完了執行環境、變量對象的創建、內存、垃圾回收后。我們再來看一個最常見的問題:let/const/var 關鍵字聲明變量的方式,來熟悉下變量的生命周期以及它們之間存在的區別.
----------------- 我是分割線 ----------------
let/const/var的區別
關于var、let、const的各自特性,想必大家都已經很清楚了,這里簡單羅列:

使用以上三個關鍵詞定義的變量,之所以會有不同的使用特性,這跟他們的生命周期有直接的關系。 JS引擎下,變量的生命周期包含四個階段:
- - 聲明階段
- - 初始化階段
- - 賦值階段
- - 釋放階段
下邊來分別分析下它們生命周期中存在的區別:
- var 聲明變量的生命周期

當使用var聲明一個變量時:
// 第一個特性
function fun() {// 變量提升 -- 不存在暫時性死區console.log(a); // undefinedvar a = 1;
}// 第二個特性 --- 不支持塊級作用域
console.log(a); // undefined
{var a = 1;
}
console.log(a); // 1
以函數作用域為例,在函數作用域中遇到 var a = 1
這段代碼,編譯器會首先查找當前作用域下是否已經聲明過該變量,若已經存在,則直接忽略這次聲明;若不存在,則在當前作用域中聲明一個名字為 a
的變量,并進行初始化操作(變量 a
的值初始化為 undefined
),這也是為什么使用 var
可以進行重復聲明的原因. 這里的聲明和初始化均被提升至作用域的最頂端.
當函數調用執行時,JS引擎會在當前的函數作用域為該變量進行賦值操作,在 var a = 1
這條語句之前的任何位置訪問變量 a
,它的值將會是 undefined
. 因為初始化操作也被進行了提升.
當代碼執行至 var a = 1
時,引擎也會從當前作用域下查找是否存在變量 a
, 若存在,直接進行賦值;若不存在會強制在當前作用域聲明一個變量 a
, 并進行賦值操作.
注: 不要隨意在代碼塊中使用var聲明變量,因為它會直接掛到全局變量window對象上,污染全局環境,且不能及時的進行垃圾回收釋放內存.
----------------- 我是分割線 ----------------
2. let 聲明變量的生命周期

使用let進行變量聲明:
// 第一個特性 --- 塊級作用域
{let a = 1;
}
console.log(a); // a is not defined// 第二個特性 --- 暫時性死區
console.log(a); // a is not defined
let a = 1;// 第三個特性 --- 不可重復聲明
let a;
let a = 1; // Uncaught SyntaxError: Identifier 'a' has already been declared
let
的生命周期與 var
的主要區別在于,聲明和初始化階段沒有同時進行提升,只有聲明階段被提升至當前作用域的最上方,所以在聲明和初始化階段之間就出現了暫時性死區現象,此階段不可訪問該變量,否則會拋出異常.
let 不可重復聲明,遇到let聲明的變量時,編譯器同樣會先查找當前作用域下是否已經聲明過該變量,若已經存在,引擎就會拋出異常(Uncaught SyntaxError: Identifier 'a' has already been declared);若不存在,則在當前作用域中聲明一個名字為 a 的變量,并把聲明操作提升至作用域最上方.
最重要的是 let
具備了塊級作用域,它很好的規避了 var
身上的缺點,在代碼塊內聲明變量,避免污染全局變量.
----------------- 我是分割線 ----------------
3. const 聲明變量的生命周期

使用const聲明變量:
// 第一個特性 --- 一旦聲明不可修改
const a = 1;
a = 2; // Uncaught TypeError: Assignment to constant variable.// 第二個特性 --- 存在暫時性死區
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization (初始化前無法訪問a)
const a = 1;
與 let
最大的區別是 const
是用來定義常量的,它創建一個值得只讀引用,一旦定義不可修改. const
也存在暫時性死區, 如果我們在聲明之前訪問該變量的值,則會拋出異常(初始化前無法訪問該變量),這也很好的證明了,const
的生命周期同樣存在聲明提升,并且它的初始化和賦值操作必須一起完成.
注: 使用 const
聲明變量時,實際上保證的并不是變量的值不得改動,而是變量指向的那個內存地址所保存的數據不得改動。對于簡單類型的數據(數值、字符串、布爾值),值就保存在變量指向的那個內存地址,因此等同于常量。但對于復合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指向實際數據的指針,const只能保證這個指針是固定的(即總是指向另一個固定的地址),至于它指向的數據結構是不是可變的,就完全不能控制了。
一個常量不能和它所在作用域內的其他變量或函數擁有相同的名稱. 更多const示例描述,可參考:
const?developer.mozilla.org
不管是var、let、還是const聲明的變量,在變量使用完畢后,最終都會隨著執行上下文的出棧、瀏覽器的關閉、或者手動釋放內存的環節進行垃圾回收. 由于JS中自動垃圾回收機制的存在,使我們往往在開發時忽略了內存使用的問題,但這個是所有變量都要經歷的過程.
以上就是今天的所有分享啦~ 感謝您認真讀完,共勉!
參考文獻:
《Javascript高級程序設計》
《Javascript核心技術開發解密》