一、歷史背景與語法基礎
JavaScript 作為動態弱類型語言,變量聲明機制經歷了從 ES5 到 ES6 的重大變革。在 ES5 及更早版本中,var
是唯一的變量聲明方式,而 ES6(2015 年)引入了 let
和 const
,旨在解決 var
存在的作用域模糊、變量提升隱患等問題,提升語言的嚴謹性和開發體驗。
二、作用域機制:函數作用域 vs 塊級作用域
2.1 var
:函數作用域(Function Scope)
var
聲明的變量屬于函數作用域或全局作用域,塊級作用域({}
代碼塊)對其無效。這意味著在 if
、for
、while
等塊中聲明的 var
變量,會被提升到所在函數的頂部,成為函數作用域內的全局變量。
示例:var
在塊級作用域中的表現
function varTest() {if (true) {var x = 10; // 函數作用域內有效}console.log(x); // 輸出 10(塊外可訪問)
}
varTest();
2.2 let
和 const
:塊級作用域(Block Scope)
let
和 const
聲明的變量屬于塊級作用域,僅在當前代碼塊({}
)內有效。塊級作用域包括:
if
/else
、switch
條件語句的代碼塊for
、while
、do...while
循環語句的代碼塊- 函數聲明中的參數作用域(非函數體本身,函數體仍為函數作用域)
try
/catch
代碼塊
示例:let
在塊級作用域中的表現
function letTest() {if (true) {let y = 20; // 塊級作用域內有效}console.log(y); // 報錯:y is not defined(塊外不可訪問)
}
letTest();
經典場景:循環中的作用域差異
// var 的閉包陷阱
var btnList = [];
for (var i = 0; i < 3; i++) {btnList[i] = function() {console.log(i); // 點擊時均輸出 3(共享同一個 i)};
}
btnList[0](); // 3// let 的正確行為
let btnListLet = [];
for (let j = 0; j < 3; j++) {btnListLet[j] = function() {console.log(j); // 分別輸出 0、1、2(每個迭代有獨立的 j)};
}
btnListLet[0](); // 0
原理:for
循環的頭部使用 let
時,會為每個迭代創建獨立的變量實例,而 var
僅在循環體外部創建一個變量,導致閉包共享同一引用。
三、變量提升(Hoisting)與暫時性死區(TDZ)
3.1 var
的變量提升
var
聲明的變量存在變量提升現象:變量聲明會被提升到作用域頂部,但初始化(賦值)仍在原地。因此,var
變量在聲明前使用會返回 undefined
(而非報錯)。
示例:變量提升的表現
console.log(a); // 輸出 undefined(聲明提升,但未賦值)
var a = 10;
3.2 let
和 const
的暫時性死區
let
和 const
不存在變量提升,聲明前訪問會觸發 ReferenceError(暫時性死區)。暫時性死區(Temporal Dead Zone, TDZ)指從塊級作用域開始到變量聲明處的區域,在此區域內訪問變量會報錯。
示例:暫時性死區的限制
console.log(b); // 報錯:b is not defined(處于 TDZ)
let b = 20;// 函數參數中的 TDZ
function constTest(arg = c) { // c 在此處屬于 TDZlet c = 10; // 報錯:Cannot access 'c' before initialization
}
設計目的:
- 避免
var
因變量提升導致的“先使用后聲明”的隱性問題。 - 強制開發者在使用變量前聲明,提升代碼可讀性和健壯性。
四、重復聲明(Redeclaration)規則
4.1 var
:允許重復聲明
var
可以在同一作用域內多次聲明同一變量,后聲明會覆蓋前聲明(但不會報錯)。
示例:var
重復聲明的隱患
var x = 10;
var x = 20; // 合法,x 的值變為 20
var x; // 合法,無實際效果(聲明提升后相當于冗余聲明)
4.2 let
和 const
:禁止重復聲明
在同一作用域內,let
和 const
禁止聲明已存在的變量(包括 var
聲明的變量),否則會拋出 SyntaxError
。
示例:let
重復聲明的報錯場景
var y = 30;
let y = 40; // 報錯:Identifier 'y' has already been declared(與 var 沖突)let z = 50;
let z = 60; // 報錯:Identifier 'z' has already been declared(同一 let 重復聲明)
例外情況:塊級作用域內允許聲明同名變量(形成“屏蔽”效果)
let a = 10;
{let a = 20; // 合法(內部塊級作用域屏蔽外部變量)console.log(a); // 20
}
console.log(a); // 10
五、值的可變性:常量(const
)與變量(var
/let
)
5.1 var
和 let
:值可變
var
和 let
聲明的變量可以重新賦值(基本類型)或修改引用(引用類型)。
基本類型示例:
let num = 10;
num = 20; // 合法,num 的值變為 20var str = "hello";
str = "world"; // 合法,str 的值變為 "world"
引用類型示例:
let obj = { a: 1 };
obj = { b: 2 }; // 合法,obj 指向新對象(引用改變)
5.2 const
:值不可變(引用固定)
const
聲明的變量為常量,必須在聲明時初始化,且不能重新賦值(即不能改變變量的引用)。但對于引用類型(對象、數組、函數),雖然不能改變引用,但可以修改其內部屬性或元素。
基本類型示例:
const PI = 3.14;
PI = 3.15; // 報錯:Assignment to constant variable.
引用類型示例:
const user = { name: "Alice" };
user.age = 30; // 合法,修改對象屬性
console.log(user); // { name: "Alice", age: 30 }user = { name: "Bob" }; // 報錯:Assignment to constant variable.(不能改變引用)
常見誤區:
- 誤區 1:認為
const
聲明的對象完全不可變。實際上,const
僅保證引用不變,對象內部屬性可修改。 - 誤區 2:聲明
const
時未初始化。const
必須在聲明時賦值,否則報錯。
六、初始化要求與默認值
6.1 var
和 let
:可選初始化
var
和 let
聲明的變量可以不初始化,默認值為 undefined
。
var a; // a = undefined
let b; // b = undefined
6.2 const
:必須初始化
const
聲明的變量必須在聲明時賦值,否則拋出 SyntaxError
。
const c; // 報錯:Missing initializer in const declaration
const d = null; // 合法(初始值可為 null/undefined)
七、實際應用場景與最佳實踐
7.1 優先使用 const
的場景
- 聲明常量:如配置項、枚舉值、數學常數等,確保值不被意外修改。
const API_URL = "https://api.example.com"; const STATUS = { PENDING: 0, SUCCESS: 1 };
- 引用類型的不可變需求:當需要保證變量引用固定(但允許修改內部屬性)時,使用
const
避免誤操作導致引用改變。const list = []; // 允許 push/pop 操作,但禁止 list = new Array()
7.2 使用 let
的場景
- 需要重新賦值的變量:如循環計數器、狀態變量等。
let page = 1; while (page <= 10) {// 業務邏輯page++; }
- 塊級作用域內的臨時變量:避免變量污染外層作用域。
if (userRole === "admin") {let permission = "full"; // 僅在 if 塊內有效 }
7.3 var
的適用場景(盡量避免)
- 舊項目兼容:在無法使用 ES6 的環境中(如某些古老的瀏覽器或工具鏈)。
- 函數作用域的特殊需求:極少數情況下,需要利用變量提升或函數作用域特性(但需謹慎評估代碼可讀性)。
八、常見錯誤與避坑指南
8.1 閉包中誤用 var
導致的邏輯錯誤
// 錯誤示例:點擊按鈕時均輸出 3
for (var i = 0; i < 3; i++) {document.querySelectorAll('button')[i].addEventListener('click', function() {console.log(i); // 預期輸出 0、1、2,實際均為 3});
}// 修正方案:使用 let 或閉包包裹 var
for (let i = 0; i < 3; i++) { /* ... */ } // 推薦
// 或
for (var i = 0; i < 3; i++) {(function(j) {btn.addEventListener('click', () => console.log(j));})(i);
}
8.2 const
聲明對象時的誤操作
const obj = { key: 'value' };
obj = { newKey: 'newValue' }; // 報錯:不能重新賦值
// 正確做法:修改現有對象屬性
obj.key = 'newValue'; // 合法
8.3 塊級作用域屏蔽導致的變量訪問異常
let count = 10;
{count = 20; // 合法(塊內訪問外層 let 變量)let count = 30; // 合法(塊內聲明新變量,屏蔽外層變量)console.log(count); // 30(訪問塊內變量)
}
console.log(count); // 20(訪問外層變量)
九、性能考量與引擎優化
從 JavaScript 引擎的角度看,var
、let
、const
的性能差異微乎其微。現代引擎(如 V8、SpiderMonkey)會對變量聲明進行優化,塊級作用域和暫時性死區的檢查主要在編譯階段完成,運行時開銷可忽略不計。因此,選擇聲明方式時應優先考慮代碼邏輯的清晰性和健壯性,而非性能。
十、總結:核心差異對比表
特性 | var | let | const |
---|---|---|---|
作用域 | 函數作用域/全局作用域 | 塊級作用域 | 塊級作用域 |
變量提升 | 有(聲明提升,值為 undefined) | 無(存在暫時性死區) | 無(存在暫時性死區) |
重復聲明 | 允許(同一作用域內) | 禁止(同一作用域內) | 禁止(同一作用域內) |
值可變性 | 可變(基本類型/引用類型) | 可變(基本類型/引用類型) | 不可變(固定引用,引用類型可改內部) |
初始化要求 | 可選 | 可選 | 必須初始化 |
典型場景 | 舊項目兼容、函數作用域變量 | 塊級作用域變量、需重新賦值 | 常量、固定引用的對象/數組 |
十一、未來趨勢與建議
隨著 ES6 的普及,現代 JavaScript 開發中強烈建議摒棄 var
,優先使用 const
,其次使用 let
。const
能顯著減少因變量意外修改導致的 bug,提升代碼的可維護性,而 let
則用于需要動態賦值的場景。遵循這一原則,可充分利用 ES6 語法特性,編寫更安全、更易讀的代碼。
通過深入理解三者的核心差異,開發者能更精準地選擇變量聲明方式,避免常見陷阱,提升開發效率和代碼質量。