摘要: 從記憶體角度理解 let 和 const 的意義。
Fundebug經授權轉載,版權歸原作者所有。
這是專門探索 JavaScript 及其所構建的元件的系列文章的第 21 篇。
如果你錯過了前面的章節,可以在這裡找到它們:
- JavaScript 是如何工作的:引擎,執行時和呼叫堆疊的概述!
- JavaScript 是如何工作的:深入 V8 引擎&編寫優化程式碼的 5 個技巧!
- JavaScript 是如何工作的:記憶體管理+如何處理 4 個常見的記憶體洩漏!
- JavaScript 是如何工作的:事件迴圈和非同步程式設計的崛起+ 5 種使用 async/await 更好地編碼方式!
- JavaScript 是如何工作的:深入探索 websocket 和 HTTP/2 與 SSE +如何選擇正確的路徑!
- JavaScript 是如何工作的:與 WebAssembly 比較 及其使用場景!
- JavaScript 是如何工作的:Web Workers 的構建塊+ 5 個使用他們的場景!
- JavaScript 是如何工作的:Service Worker 的生命週期及使用場景!
- JavaScript 是如何工作的:Web 推送通知的機制!
- JavaScript 是如何工作的:使用 MutationObserver 跟蹤 DOM 的變化!
- JavaScript 是如何工作的:渲染引擎和優化其效能的技巧!
- JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全!
- JavaScript 是如何工作的:CSS 和 JS 動畫底層原理及如何優化它們的效能!
- JavaScript 是如何工作的:解析、抽象語法樹(AST)+ 提升編譯速度 5 個技巧!
- JavaScript 是如何工作的:深入類和繼承內部原理+Babel 和 TypeScript 之間轉換!
- JavaScript 是如何工作的:儲存引擎+如何選擇合適的儲存 API!
- JavaScript 是如何工作的:Shadow DOM 的內部結構+如何編寫獨立的元件!
- JavaScript 是如何工作的:WebRTC 和對等網路的機制!
- JavaScript 是如何工作的:編寫自己的 Web 開發框架 + React 及其虛擬 DOM 原理!
- JavaScript 是如何工作的:模組的構建以及對應的打包工具
// 宣告一些變數並初始化它們
var a = 5;
let b = "xy";
const c = true;
// 分配新值
a = 6;
b = b + "z";
c = false; // 型別錯誤:不可對常量賦值
複製程式碼
作為程式設計師,宣告變數、初始化變數(或不初始化變數)以及稍後為它們分配新值是我們每天都要做的事情。
但是當這樣做的時候會發生什麼呢? JavaScript 如何在內部處理這些基本功能? 更重要的是,作為程式設計師,理解 JavaScript 的底層細節對我們有什麼好處。
下面,我打算介紹以下內容:
- JS 原始資料型別的變數宣告和賦值
- JavaScript 記憶體模型:呼叫堆疊和堆
- JS 引用型別的變數宣告和賦值
let
vsconst
JS 原始資料型別的變數宣告和賦值
讓我們從一個簡單的例子開始。下面,我們宣告一個名為myNumber
的變數,並用值23
初始化它。
let myNumber = 23;
複製程式碼
當執行此程式碼時,JS 將執行:
- 為變數(
myNumber
)建立唯一識別符號(identifier)。 - 在記憶體中分配一個地址(在執行時分配)。
- 將值
23
儲存在分配的地址。
雖然我們通俗地說,“myNumber 等於 23”
,更專業地說,myNumber
等於儲存值 23 的記憶體地址,這是一個值得理解的重要區別。
如果我們要建立一個名為 newVar
的新變數並把 myNumber
賦值給它。
let newVar = myNumber;
複製程式碼
因為 myNumber
在技術上實際是等於 “0012CCGWH80
”,所以 newVar
也等於 “0012CCGWH80
”,這是儲存值為23
的記憶體地址。通俗地說就是 newVar
現在的值為 23
。
因為 myNumber
等於記憶體地址 0012CCGWH80
,所以將它賦值給 newVar
就等於將0012CCGWH80
賦值給 newVar
。
現在,如果我這樣做會發生什麼:
myNumber = myNumber + 1;
複製程式碼
myNumber
的值肯定是 24。但是newVar
的值是否也為 24 呢?,因為它們指向相同的記憶體地址?
答案是否定的。由於 JS 中的原始資料型別是不可變的,當 myNumber + 1
解析為24
時,JS 將在記憶體中分配一個新地址,將24
作為其值儲存,myNumber
將指向新地址。
這是另一個例子:
let myString = "abc";
myString = myString + "d";
複製程式碼
雖然一個初級 JS 程式設計師可能會說,字母d
只是簡單在原來存放adbc
記憶體地址上的值,從技術上講,這是錯的。當 abc
與 d
拼接時,因為字串也是 JS 中的基本資料型別,不可變的,所以需要分配一個新的記憶體地址,abcd
儲存在這個新的記憶體地址中,myString
指向這個新的記憶體地址。
下一步是瞭解原始資料型別的記憶體分配位置。
JavaScript 記憶體模型:呼叫堆疊和堆
JS 記憶體模型可以理解為有兩個不同的區域:呼叫堆疊(call stack)和堆(heap)。
呼叫堆疊是存放原始資料型別的地方(除了函式呼叫之外)。上一節中宣告變數後呼叫堆疊的粗略表示如下。
在上圖中,我抽象出了記憶體地址以顯示每個變數的值。 但是,不要忘記實際上變數指向記憶體地址,然後儲存一個值。 這將是理解 let vs. const
一節的關鍵。
堆是儲存引用型別的地方。跟呼叫堆疊主要的區別在於,堆可以儲存無序的資料,這些資料可以動態地增長,非常適合陣列和物件。
JS 引用型別的變數宣告和賦值
讓我們從一個簡單的例子開始。下面,我們宣告一個名為myArray
的變數,並用一個空陣列初始化它。
let myArray = [];
複製程式碼
當你宣告變數“myArray
”併為其指定非原始資料型別(如“[]”)時,以下是在記憶體中發生的情況:
- 為變數建立唯一識別符號(“
myArray
”) - 在記憶體中分配一個地址(將在執行時分配)
- 儲存在堆上分配的記憶體地址的值(將在執行時分配)
- 堆上的記憶體地址儲存分配的值(空陣列[])
從這裡,我們可以 push
, pop
,或對陣列做任何我們想做的。
myArray.push("first");
myArray.push("second");
myArray.push("third");
myArray.push("fourth");
myArray.pop();
複製程式碼
程式碼部署後可能存在的 BUG 沒法實時知道,事後為了解決這些 BUG,花了大量的時間進行 log 除錯,這邊順便給大家推薦一個好用的 BUG 監控工具 Fundebug。
let vs
const
一般來說,我們應該儘可能多地使用const
,只有當我們知道某個變數將發生改變時才使用let
。
讓我們明確一下我們所說的**“改變”**是什麼意思。
let sum = 0;
sum = 1 + 2 + 3 + 4 + 5;
let numbers = [];
numbers.push(1);
numbers.push(2);
numbers.push(3);
numbers.push(4);
numbers.push(5);
複製程式碼
這個程式設計師使用let
正確地宣告瞭sum
,因為他們知道值會改變。但是,這個程式設計師使用let
錯誤地宣告瞭陣列 numbers
,因為他將把東西推入陣列理解為改變陣列的值。
解釋**“改變”**的正確方法是更改記憶體地址
。let
允許你更改記憶體地址。const
不允許你更改記憶體地址。
const importantID = 489;
importantID = 100; // 型別錯誤:賦值給常量變數
複製程式碼
讓我們想象一下這裡發生了什麼。
當宣告importantID
時,分配了一個記憶體地址,並儲存489
的值。記住,將變數importantID
看作等於記憶體地址。
當將100
分配給importantID
時,因為100
是一個原始資料型別,所以會分配一個新的記憶體地址,並將100
的值儲存這裡。
然後 JS 嘗試將新的記憶體地址分配給 importantID
,這就是丟擲錯誤的地方,這也是我們想要的行為,因為我們不想改變這個 importantID
的值。
當你將100
分配給importantID
時,實際上是在嘗試分配儲存100
的新記憶體地址,這是不允許的,因為importantID
是用const
宣告的。
如上所述,假設的初級 JS 程式設計師使用let
錯誤地宣告瞭他們的陣列。相反,他們應該用const
宣告它。這在一開始看起來可能令人困惑,我承認這一點也不直觀。
初學者會認為陣列只有在我們可以改變的情況下才有用,const
使陣列不可變,那麼為什麼要使用它呢? 請記住:“改變”是指改變記憶體地址。讓我們深入探討一下為什麼使用const宣告陣列是完全可以的。
const myArray = [];
複製程式碼
在宣告 myArray
時,將在呼叫堆疊上分配記憶體地址,該值是在堆上分配的記憶體地址。堆上儲存的值是實際的空陣列。想象一下,它是這樣的:
如果我們這麼做:
myArray.push(1);
myArray.push(2);
myArray.push(3);
myArray.push(4);
myArray.push(5);
複製程式碼
執行 push
操作實際是將數字放入堆中存在的陣列。而 myArray
的記憶體地址沒有改變。這就是為什麼雖然使用const
宣告瞭 myArray,但沒有丟擲任何錯誤。
myArray
仍然等於 0458AFCZX91
,它的值是另一個記憶體地址22VVCX011
,它在堆上有一個陣列的值。
如果我們這樣做,就會丟擲一個錯誤:
myArray = 3;
複製程式碼
由於 3
是一個原始資料型別,因此生成一個新的呼叫堆疊上的記憶體地址,其值為 3
,然後我們將嘗試將新的記憶體地址分配給 myArray
,由於 myArray 是用 const 宣告的,所以這是不允許的。
另一個會丟擲錯誤的例子:
myArray = ["a"];
複製程式碼
由於[a]
是一個新的引用型別的陣列,因此將分配呼叫堆疊上的一個新記憶體地址,並儲存堆上的一個記憶體地址的值,其它值為 [a]
。然後,我們嘗試將呼叫堆疊記憶體地址分配給 myArray
,這會丟擲一個錯誤。
對於使用const
宣告的物件(如陣列),由於物件是引用型別,因此可以新增鍵,更新值等等。
const myObj = {};
myObj["newKey"] = "someValue"; // 這不會丟擲錯誤
複製程式碼
為什麼這些知識對我們有用呢
JavaScript 是世界上排名第一的程式語言(根據 GitHub 和 Stack Overflow 的年度開發人員調查)。 掌握併成為“JS 忍者”是我們所有人都渴望成為的人。
任何質量好的的 JS 課程或書籍都提倡使用let, const
來代替 var
,但他們並不一定說出原因。 對於初學者來說,為什麼某些 const 變數在“改變”其值時會丟擲錯誤而其他 const變數卻沒有。 對我來說這是有道理的,為什麼這些程式設計師預設使用let到處避免麻煩。
但是,不建議這樣做。谷歌擁有世界上最好的一些程式設計師,在他們的 JavaScript 風格指南中說,使用 const 或 let 宣告所有本地變數。預設情況下使用 const,除非需要重新分配變數,不使用 var 關鍵字(原文)。
雖然他們沒有明確說明原因,但據我所知,有幾個原因
- 先發制人地限制未來的 bug。
- 使用
const
宣告的變數必須在宣告時初始化,這迫使程式設計師經常在範圍方面更仔細地放置它們。這最終會導致更好的記憶體管理和效能。 - 要通過程式碼與任何可能遇到它的人交流,哪些變數是不可變的(就 JS 而言),哪些變數可以重新分配。
希望上面的解釋能幫助你開始明白為什麼或者什麼時候應該在程式碼中使用 let 和 const 。
關於Fundebug
Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了10億+錯誤事件,付費客戶有Google、360、金山軟體、百姓網等眾多品牌企業。歡迎大家免費試用!
版權宣告
轉載時請註明作者Fundebug以及本文地址: blog.fundebug.com/2019/04/15/…