蒂莫西一臉茫然地盯著瀏覽器控制台。他寫的明明是簡單的JavaScript程式碼,但執行結果卻毫無道理。
var x = 5;
if (true) {
var x = 10;
}
console.log(x); // 10 - why?!
“這說不通,”他嘟囔道,“我習慣了程式碼塊保護變數的語言。但 JavaScript 竟然…直接覆蓋了它?”
他再次嘗試使用let :
let y = 5;
if (true) {
let y = 10;
}
console.log(y); // 5 - now it works!
「等等,什麼?」提摩西驚呼道,一邊喊著正在諮詢台整理新到書的瑪格麗特。 “瑪格麗特,快來看看。為什麼let和var用法不一樣?”
瑪格麗特放下書,走過去,看著他螢幕上的程式碼。她心領神會地笑了。
「啊,你發現了 JavaScript 最令人困惑的特性之一——變數作用域。但這並非 bug,Timothy。這是歷史的產物。這門語言在發展過程中始終保持著完美的向後相容性。”
“但是它們都聲明了變數啊!為什麼它們不一樣?”
“因為,”瑪格麗特說著,拉過一把椅子坐下,“它們來自 JavaScript 的不同時代。要理解其中的區別,我們需要了解 JavaScript 是如何看待作用域的。”
瑪格麗特打開她那本破舊的筆記本。 “JavaScript 誕生於 1995 年,當時聲明變數只有一種方式: var 。而var有個怪癖。讓我來給你解釋一下。”
她寫道:
function greet(name) {
if (name) {
var message = "Hello, " + name;
}
console.log(message); // Prints the message!
}
greet("Alice"); // "Hello, Alice"
提摩西皺起了眉頭。 “ if語句區塊內所建立的變數在語句區塊外也可見嗎?”
“沒錯。在 JavaScript 中, var是函數作用域的,而不是程式碼塊作用域的。if if塊不會建立新的作用域——只有函數才會。所以message在整個greet函數中都存在。”
“這……很奇怪。像 C 和 Java 這樣的語言會為程式碼區塊建立一個新的作用域。”
「確實如此。JavaScript 的var並非如此。二十年來,JavaScript 一直都是這樣工作的。直到 2015 年,JavaScript 迎來了一次名為 ES6 的重大更新,引入了let和const 。”
瑪格麗特又舉了一個例子:
function greet(name) {
if (name) {
let message = "Hello, " + name;
}
console.log(message); // ReferenceError: message is not defined
}
greet("Alice"); // Error!
「現在這個變數的作用域真正局限於程式碼區塊內了,」瑪格麗特解釋。 “它只存在於if語句內部。在 if 語句外部, message並不存在。”
蒂莫西向後靠去。 “所以, let只是var ,但固定不變?”
「基本上來說,是的。let let var應該有的作用。它是塊級作用域的,它會遵守if語句、循環和其他程式碼塊的邊界。”
“那為什麼現在還有人用var呢?”
「問得好。在新程式碼中,它們不應該被使用。但是數百萬行 JavaScript 程式碼都使用了var 。而 JavaScript 從來不會破壞舊程式碼。所以var仍然存在,仍然以舊的方式工作,並且仍然會讓人們感到困惑。”
瑪格麗特拿出三張索引卡片,放在桌上。
“JavaScript 提供了三種宣告變數的方式。每一種方式都向 JavaScript 傳達了不同的變數使用規則。”
卡片一: var
var x = 5;
var x = 10; // Allowed - you can re-declare
x = 15; // Allowed - you can reassign
console.log(x); // 15
“使用var ,你可以重新聲明同一個變數並重新賦值。它很靈活。靈活有利於編寫簡單的腳本。但靈活不利於大型程序,因為你可能會意外地覆蓋掉一個你不想修改的變數。”
第二張牌: let
let y = 5;
// let y = 10; // NOT ALLOWED - can't re-declare
y = 15; // Allowed - you can reassign
console.log(y); // 15
“使用let ,你只需聲明一次變數。你可以更改它的值,但不會意外地再次使用let聲明它。這樣更安全。”
第三張牌: const
const z = 5;
// const z = 10; // NOT ALLOWED - can't re-declare
// z = 15; // NOT ALLOWED - can't reassign
console.log(z); // 5
“使用const ,你只需聲明一次變數,它的值就會永遠保持不變。這是最安全的選擇。”
蒂莫西仔細研究了這三張卡片。 “所以const就像 Python 裡的常數一樣?”
「類似的想法,但並不完全相同。在 Python 中,常數只是一種約定——一個全部大寫的變數名,程式設計師們約定不要更改它。而在 JavaScript 中, const是由語言強制執行的。如果你嘗試更改它,就會收到錯誤提示。”
所以我們應該總是使用const嗎?
瑪格麗特向後靠去。 “大多數 JavaScript 開發者都這麼認為。他們的理念是:先用const 。如果之後發現需要修改值,就換成let 。至於var ……就當它不存在。”
“為什麼不一直使用const呢?”
「因為有時候你確實需要改變一個變數的值,」瑪格麗特說。她寫道:
// Counting through numbers
for (let i = 0; i < 10; i++) {
console.log(i);
}
// This won't work with const because i changes
// for (const i = 0; i < 10; i++) { // Error!
「這裡, i從 0 開始,每次循環遞增。如果你把它聲明為const ,你就無法重新賦值,循環就會中斷。”
“所以let用於表示會變化的變數, const用於表示不會變化的變數?”
「沒錯,這就是模式。它能讓你的程式碼更清晰。當有人讀到const x = 5時,他們立刻就知道:這個值永遠不會改變。而當他們讀到let y = 0時,他們就知道:這個值可能會隨著程式的執行而改變。”
蒂莫西向前傾身。 “但我還是不太明白var 。當我在函數中使用它,然後在if語句塊中使用它時,它實際存在於哪裡呢?”
瑪格麗特笑了。她喜歡蒂莫西提出的那些恰到好處的後續問題。
“這就是關鍵所在。var var在函數作用域內建立變數。讓我來解釋一下。”
她寫道:
function example() {
console.log(typeof x); // undefined (not an error!)
if (true) {
var x = 5;
}
console.log(x); // 5
}
example();
“請注意, console.log(typeof x) 輸出的是undefined而不是拋出錯誤。為什麼呢?因為 JavaScript 會將所有var`聲明移到函數頂部。這叫做‘變數提升’。”
她重寫了這段程式碼,以展示 JavaScript 內部的實際運作原理:
function example() {
var x; // JavaScript moves this to the top
console.log(typeof x); // undefined - x exists but has no value
if (true) {
x = 5; // This just assigns to the already-declared x
}
console.log(x); // 5
}
example();
「所以, var聲明會被『提升』到函數作用域,但它們的賦值仍然保留在你編寫的位置。這就是為什麼你可以在賦值之前存取x原因——它存在,但它是undefined 。”
蒂莫西眨了眨眼。 “這……真是令人困惑。”
“確實如此。而且let不會這樣做。使用let時,變數只有在你聲明它們的行中才會存在。”
function example() {
// console.log(y); // ReferenceError: y is not defined
let y = 5;
console.log(y); // 5
}
example();
“如果在 y 聲明之前嘗試存取y ,就會報錯。這樣就清楚多了。”
瑪格麗特打開了一個新文件。 “讓我們用一個實際的例子來看看這三個函數是如何運作的。一個簡單的計數器函數。”
var方法(老舊、危險): ❌
function createCounter() {
var count = 0;
return function() {
count = count + 1;
console.log(count);
}
}
const counter1 = createCounter();
counter1(); // 1
counter1(); // 2
counter1(); // 3
“這種方法可行,但使用var很容易在程式碼的其他地方意外覆蓋count 。而且作用域規則也令人困惑。”
更好let方法: ✅
function createCounter() {
let count = 0;
return function() {
count = count + 1;
console.log(count);
}
}
const counter2 = createCounter();
counter2(); // 1
counter2(); // 2
counter2(); // 3
“使用let ,可以更清楚地表明count的值僅限於該函數的作用域。而且你也不會意外地重複聲明它。”
最const方法(最佳): ✅
function createCounter() {
const count = 0;
return function() {
count = count + 1; // ERROR! Can't reassign const
console.log(count);
}
}
const counter3 = createCounter();
counter3(); // TypeError: Assignment to constant variable
「等等,這樣不行,」提摩西說。 “我們需要改變count 。”
瑪格麗特點點頭。 “觀察得很仔細。在這種情況下,我們必須使用let ,因為我們要重新賦值變數。但還有另一種模式可以有效地使用const 。”
她重寫了它:
function createCounter() {
let count = 0;
return {
increment() {
count = count + 1;
},
get() {
return count;
}
};
}
const counter4 = createCounter();
counter4.increment();
counter4.increment();
console.log(counter4.get()); // 2
“這裡,我們返回一個帶有方法的物件。物件本身是const (永遠不會改變),但這些方法會修改閉包內部的count變數。這樣既保證了安全性,又實現了功能性。”
蒂莫西仔細地分析了程式碼。 “所以count被隱藏在函數內部,只能通過返回物件的方法來修改它?”
“沒錯。這叫做‘封裝’。返回對像上的const關鍵字表示‘此物件引用永遠不會改變’。但該物件的方法可以修改其內部的內容。”
let一切變得有意義)提摩西拿出了他之前那個令人困惑的例子。 “現在讓我用我們學到的知識來理解一下。”
var x = 5;
if (true) {
var x = 10;
}
console.log(x); // 10
“使用var ,兩個聲明都在同一個作用域內——全域作用域。因此,第二個聲明會覆寫第一個聲明。”
瑪格麗特點點頭。 “ if程式碼區塊不會為var建立一個新的作用域。所以這和以下程式碼完全一樣:”
var x = 5;
var x = 10; // Re-declaring the same variable
console.log(x); // 10
“現在是let版本:”
let y = 5;
if (true) {
let y = 10; // New variable in the block's scope
}
console.log(y); // 5
「這裡, if程式碼區塊建立了一個新的作用域。第二個let y = 10建立了一個不同的y ,該值僅存在於程式碼區塊內部。在程式碼區塊外部,原來的y仍然是 5。”
蒂莫西最終點了點頭。 “所以let尊重邊界,而var忽略它們。”
“沒錯。這就是為什麼現代 JavaScript 使用let和const原因。它們提供了真正有意義的作用域。”
瑪格麗特站起身,在提摩西的筆記型電腦上調出了另一個例子。
“在結束之前,關於const還有一個容易讓初學者犯錯的關鍵點。”
她寫道:
const myArray = [1, 2, 3];
myArray.push(4); // ✅ Allowed - modifying the array's contents
console.log(myArray); // [1, 2, 3, 4]
myArray = [5, 6, 7]; // ❌ Error: Assignment to constant variable
“注意,”瑪格麗特說,“你可以修改陣列裡的內容,但你不能重新賦值變數本身。 const保護的是綁定關係,而不是內容。”
蒂莫西緩緩點頭。 “所以const意思是‘這個變數始終指向同一個物件’,而不是‘這個物件永遠不會改變’。”
“完全正確。這是一個至關重要的區別。”
瑪格麗特合上筆記本。 “以後你需要記住這些。”
預設使用const 。除非有特殊原因,否則所有內容都應使用const聲明。
需要重新賦值時,請使用let 。如果變數的值在宣告後發生變化,也請使用let 。
新程式碼中永遠不要使用var 。它是舊版 JavaScript 的產物。你會在遺留程式碼中看到它,但請忽略它的存在。
提摩西把這些寫下來。 “那我在開發實際應用程式的時候會遇到什麼問題?我會遇到特殊情況嗎?”
「當然。但就目前而言,這三個規則對你很有幫助。遵循它們,你就能避免JavaScript初學者最常犯的錯誤。”
她站起身,走回辦公桌。 “明天,我們將學習一個看似同樣簡單的東西: this關鍵字。你會發現,JavaScript 與this的關係甚至比變數的關係還要複雜。”
提摩西呻吟了一聲。 “更糟嗎?”
瑪格麗特回頭笑了笑。 “哦,蒂莫西,你根本不知道。”
var是函數作用域的-可以在函數內部的任何位置存取它,並且聲明會被提升到頂部。
let是區塊級作用域的-它只存在於宣告它的區塊中(if、迴圈、函數等)。
const會阻止重新賦值-變數一旦被賦值,其值就不能改變。
這三者都聲明了變數——區別在於變數存在的位置以及它們是否可以改變。
變數提升會影響var -宣告會移到函數頂部,但賦值語句保持不變。
let和const會被提升但不會被初始化——它們會被提升到其程式碼區塊作用域的頂部,但在聲明行之前會處於「暫時死區」。在聲明之前存取它們會拋出ReferenceError 。
現代 JavaScript 更傾向於使用const和let —— var存在只是為了向後相容。
區塊級作用域更安全—它可以防止意外覆蓋,並使程式碼更容易理解。
const保護的是綁定關係,而不是內容const會阻止變數被重新賦值,但物件和陣列仍然可以在內部被修改。 const arr = [1]; arr.push(2)可以正常運作,但arr = [3]卻會失敗。
作用域建立封裝-使用let和const函數將變數對外部世界隱藏起來。
你認為 JavaScript 為什麼一開始就採用區塊級作用域而不是var這種方式?
什麼時候會選擇let而不是const ,為什麼?
「不能重新賦值」( const )和「不可變」之間有什麼區別?
變數提升如何改變你對變數宣告的看法?
在反例中,為什麼使用方法的物件模式比只使用let能提供更好的封裝性?
請在下方留言區分享你對變數的探索!
《JavaScript的秘密生活》揭示了JavaScript的實際運作原理——而非你希望它如何運作。本書透過提摩西(一位好奇的開發者,正在學習他的第一門新語言)和瑪格麗特(一位經驗豐富的JavaScript專家)在倫敦維多利亞時代圖書館的對話,探索了JavaScript奇特語法背後的理念。
每篇文章都是獨立成篇的,但又環環相扣,逐步加深理解。無論你是 JavaScript 新手,還是只是好奇它的運作原理,本系列文章都能為你指引方向。
接下來是《JavaScript 的秘密生活:理解this 》 ——在這裡,上下文變得混亂,你最終會明白為什麼this並不總是意味著你所想的那樣。
Aaron Rose 是一位軟體工程師,也是tech-reader.blog的技術作家,著有《像天才一樣思考》一書。