const 的騙局

2024年2月6日 20点热度 0人点赞

本期《前端翻譯計劃》共享的是 —— 探討 JS 中賦值(assignment)和變更(mutation)之間的區別。

堅持閱讀,每天一篇,進步一點

JS 中的 const 關鍵字用於聲明常量。常量通常被認為是“無法更改的變量”:

const like = 9
like = 99 // 報錯
console.log(like) // -> 9

奇葩的是,當我使用 const 創建對象時,我可以隨意更改它:

const cat = {
  name: '薛定諤'
}
cat.name = '龍貓' // 目測有效
console.log(cat) // -> { name: '龍貓' }

為何我可以更改 cat 變量呢?我明明用了 const

為了弄懂這種明顯的悖論,我們需要了解賦值和變更之間的區別。此乃 JS 中的核心概念之一,當您對該區別心領神會時,其他知識圖譜亦能融會貫通。

目標受眾

本文假設您已經使用 JS 至少幾周了,並且熟悉基本語法。

作為標簽的變量名

這是一個有效的 JS 程序:

9
;['點贊', '投幣', '收藏']

在這兩個例子中,我創建了一個數字和一個數組。當代碼運行時,這些數據會被創建,並存儲在計算機的內存中。

雖然但是,該程序並不科學。我創建了某些數據,但卻無法讀寫它們!

變量允許我們在創建的東東上貼標簽,這樣我們以後可以通過該標簽引用它們:

// 先創建變量,由數據和標簽組成
const bilibili = ['點贊', '投幣', '收藏']
// 之後再通過標簽讀寫數據
console.log(bilibili)
// -> ['點贊', '投幣', '收藏'];

當我初學編程時,私以為代碼是從左到右執行的 —— 我們首先創建一個 bilibili 變量,它就像一個空盒子,然後我們在該盒子中填充數組。

事實證明,我的思想出了問題。真相隻有一個,我們首先創建了數組,然後將 bilibili 標簽指向該數組。

標簽重新賦值

當我們使用 let 關鍵字創建變量時,我們可以更改該標簽引用的“東東”。

舉個栗子,我們可以將 bilibili 標簽指向一個新值:

let bilibili = ['點贊', '投幣', '收藏']
// 標簽重定向
bilibili = ['like', 'bitcoin', 'star']

這稱為重新賦值(re-assignment)。我們代碼的語義是,bilibili 標簽應該引用一個完全不同的值。

我們不是在修改數據,而是在修改標簽。我們讓該標簽和原數組“和平分手”,並讓它和一個新數組“牽手成功”。

相比之下,使用 const 創建的變量則無法重新賦值

這是 letconst 的根本區別。當我們使用 const 時,我們在變量名和數據之間創建了至死不渝的鏈接。

但還有一件事:我們仍然可以修改數據本身!隻要標簽完好無損。

舉個栗子,對於一個數組,我們可以理直氣壯地添加其中的元素。bilibili 變量仍然連接到同一個數組:

bilibili.push('關註')
// -> ['點贊', '投幣', '收藏', '關註'];

這稱為變更(mutation)。我們通過添加元素來編輯數組的值。

重新賦值和變更存在根本區別:

  • 重新賦值:將變量名指向新事物
  • 變更:編輯事物中的數據

當我們使用 const 創建常量時,我們可以 100% 確定該變量永遠不會被重新賦值,但涉及變更時,我們無法做出任何承諾。const 根本無法阻止變更

還有一件事:字符串和數字等“原始”數據類型(primitive)是不可變的。

凍結對象和數組

令人憂鬱的是,const 關鍵字無法確保我們規避變更。那有沒有其他方案可以保證我們的數據不會被編輯呢?

當然有!這是一個名為 Object.freeze() 的便捷方法:

// 凍結數組
const bilibili = Object.freeze(['點贊', '投幣', '收藏'])
bilibili.push('關註')
console.log(bilibili)
// -> ['點贊', '投幣', '收藏']

Object.freeze() 可用於對象和數組,防止所有形式的變更。唯一的 bug 在於,該方法能且僅能淺層凍結頂層屬性;它無法凍結任何嵌套對象。

另外,如果您是 TS 愛好者,那可以使用 as const 斷言,實現類似的效果:

const bilibili = ['點贊', '投幣', '收藏'] as const
bilibili.push('關註') // 報錯

與所有靜態類型一樣,當代碼編譯為 JS 時,這些保護措施就會人間蒸發,因此這並無法提供與 Object.freeze() 等效的保護。不過,根據您的使用場景,這可能恰到好處!

原始數據類型

目前為止,我們看到的所有示例都涉及對象和數組。但如果我們有一個“原始”數據類型,比如字符串、數字或佈爾值呢?

我們以一個數字為例:

let girlFans = 9
girlFans = 99

對此我們該如何解釋呢?我們是否將 girlFans 標簽重新賦值為新值,或者是否變更了該數字,將 9 編輯為 99 呢?

事情是這樣的:JS 中的所有原始數據類型都是不可變的。“編輯”數字的值乃不可能事件。我們能且僅能將變量重新賦值為不同的值。

我的“科學推理”是:假設有一個包含所有可能數字的大列表。我們已將 girlFans 變量賦值為數字 9,但我們可以將其指向列表中的任意其他數字:

需要明確的是,瀏覽器實際上並沒有包含所有可能的數字的巨大索引。我再重申一遍,數字本身無法變更。我們能且僅能更改標簽指向的數字。

舉一反一,所有原始值類型同理可得,包括但不限於字符串、佈爾值、null 等。

思想實驗

如上所述,原始值在 JS 中是“不可變的” —— 它們無法被編輯。

但假設原始值可以變更呢?如果數字本身可以變更,那 JS 的語法會變成什麼樣子?

它看起來會像這樣:

// 編輯數字 9 的值
9 = 99
// 數字 9 不再存在!
console.log(9) // 99

顯然 JS 並不支持上述語法,反證法可得,JS 的原始值是不可變的。

“變更”的整體思路是,它從根本上改變了該值。當我們變更一個對象時,我們改變了該對象的“本質”,當我們引用它們時,我們可以看到這些變化:

const cat = { girlFans: 9 }
cat.girlFans = 99
console.log(cat)
// -> { girlFans: 99 }

因此,如果我們可以變更 JS 的原始值,這實際上意味著,這會覆蓋某些數字,這樣它們永遠不會被再次引用!

這顯然令人頭大且毫無卵用,這就是為何 JS 的原始值是不可變的。