🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

第一章:通往王國的三把鑰匙

蒂莫西一臉茫然地盯著瀏覽器控制台。他寫的明明是簡單的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!

「等等,什麼?」提摩西驚呼道,一邊喊著正在諮詢台整理新到書的瑪格麗特。 “瑪格麗特,快來看看。為什麼letvar用法不一樣?”

瑪格麗特放下書,走過去,看著他螢幕上的程式碼。她心領神會地笑了。

「啊,你發現了 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 的重大更新,引入了letconst 。”

瑪格麗特又舉了一個例子:

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 使用letconst原因。它們提供了真正有意義的作用域。”

瑪格麗特站起身,在提摩西的筆記型電腦上調出了另一個例子。

“在結束之前,關於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意思是‘這個變數始終指向同一個物件’,而不是‘這個物件永遠不會改變’。”

“完全正確。這是一個至關重要的區別。”

三的法則

瑪格麗特合上筆記本。 “以後你需要記住這些。”

  1. 預設使用const除非有特殊原因,否則所有內容都應使用const聲明。

  2. 需要重新賦值時,請使用let如果變數的值在宣告後發生變化,也請使用let

  3. 新程式碼中永遠不要使用var它是舊版 JavaScript 的產物。你會在遺留程式碼中看到它,但請忽略它的存在。

提摩西把這些寫下來。 “那我在開發實際應用程式的時候會遇到什麼問題?我會遇到特殊情況嗎?”

「當然。但就目前而言,這三個規則對你很有幫助。遵循它們,你就能避免JavaScript初學者最常犯的錯誤。”

她站起身,走回辦公桌。 “明天,我們將學習一個看似同樣簡單的東西: this關鍵字。你會發現,JavaScript 與this的關係甚至比變數的關係還要複雜。”

提摩西呻吟了一聲。 “更糟嗎?”

瑪格麗特回頭笑了笑。 “哦,蒂莫西,你根本不知道。”


重點總結

  1. var是函數作用域的-可以在函數內部的任何位置存取它,並且聲明會被提升到頂部。

  2. let是區塊級作用域的-它只存在於宣告它的區塊中(if、迴圈、函數等)。

  3. const會阻止重新賦值-變數一旦被賦值,其值就不能改變。

  4. 這三者都聲明了變數——區別在於變數存在的位置以及它們是否可以改變。

  5. 變數提升會影響var -宣告會移到函數頂部,但賦值語句保持不變。

  6. letconst會被提升但不會被初始化——它們會被提升到其程式碼區塊作用域的頂部,但在聲明行之前會處於「暫時死區」。在聲明之前存取它們會拋出ReferenceError

  7. 現代 JavaScript 更傾向於使用constlet —— var存在只是為了向後相容。

  8. 區塊級作用域更安全—它可以防止意外覆蓋,並使程式碼更容易理解。

  9. const保護的是綁定關係,而不是內容const會阻止變數被重新賦值,但物件和陣列仍然可以在內部被修改。 const arr = [1]; arr.push(2)可以正常運作,但arr = [3]卻會失敗。

  10. 作用域建立封裝-使用letconst函數將變數對外部世界隱藏起來。


討論問題

  1. 你認為 JavaScript 為什麼一開始就採用區塊級作用域而不是var這種方式?

  2. 什麼時候會選擇let而不是const ,為什麼?

  3. 「不能重新賦值」( const )和「不可變」之間有什麼區別?

  4. 變數提升如何改變你對變數宣告的看法?

  5. 在反例中,為什麼使用方法的物件模式比只使用let能提供更好的封裝性?

請在下方留言區分享你對變數的探索!


關於本系列

《JavaScript的秘密生活》揭示了JavaScript的實際運作原理——而非你希望它如何運作。本書透過提摩西(一位好奇的開發者,正在學習他的第一門新語言)和瑪格麗特(一位經驗豐富的JavaScript專家)在倫敦維多利亞時代圖書館的對話,探索了JavaScript奇特語法背後的理念。

每篇文章都是獨立成篇的,但又環環相扣,逐步加深理解。無論你是 JavaScript 新手,還是只是好奇它的運作原理,本系列文章都能為你指引方向。

接下來是《JavaScript 的秘密生活:理解this ——在這裡,上下文變得混亂,你最終會明白為什麼this並不總是意味著你所想的那樣。


Aaron Rose 是一位軟體工程師,也是tech-reader.blog的技術作家,著有《像天才一樣思考》一書


原文出處:https://dev.to/aaron_rose_0787cc8b4775a0/the-secret-life-of-javascript-let-const-and-why-variables-are-complicated-2gkk


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝17   💬10   ❤️5
425
🥈
我愛JS
📝2   💬8   ❤️4
91
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付