JavaScript 是如何工做的:JavaScript 的內存模型

阿里雲最近在作活動,低至2折,有興趣能夠看看:
https://promotion.aliyun.com/...

爲了保證的可讀性,本文采用意譯而非直譯。html

這是專門探索 JavaScript 及其所構建的組件的系列文章的第 21 篇。前端

想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等着你!git

若是你錯過了前面的章節,能夠在這裏找到它們:程序員

  1. JavaScript 是如何工做的:引擎,運行時和調用堆棧的概述!
  2. JavaScript 是如何工做的:深刻V8引擎&編寫優化代碼的5個技巧!
  3. JavaScript 是如何工做的:內存管理+如何處理4個常見的內存泄漏!
  4. JavaScript 是如何工做的:事件循環和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
  5. JavaScript 是如何工做的:深刻探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
  6. JavaScript 是如何工做的:與 WebAssembly比較 及其使用場景!
  7. JavaScript 是如何工做的:Web Workers的構建塊+ 5個使用他們的場景!
  8. JavaScript 是如何工做的:Service Worker 的生命週期及使用場景!
  9. JavaScript 是如何工做的:Web 推送通知的機制!
  10. JavaScript 是如何工做的:使用 MutationObserver 跟蹤 DOM 的變化!
  11. JavaScript 是如何工做的:渲染引擎和優化其性能的技巧!
  12. JavaScript 是如何工做的:深刻網絡層 + 如何優化性能和安全!
  13. JavaScript 是如何工做的:CSS 和 JS 動畫底層原理及如何優化它們的性能!
  14. JavaScript 是如何工做的:解析、抽象語法樹(AST)+ 提高編譯速度5個技巧!
  15. JavaScript 是如何工做的:深刻類和繼承內部原理+Babel和 TypeScript 之間轉換!
  16. JavaScript 是如何工做的:存儲引擎+如何選擇合適的存儲API!
  17. JavaScript 是如何工做的:Shadow DOM 的內部結構+如何編寫獨立的組件!
  18. JavaScript 是如何工做的:WebRTC 和對等網絡的機制!
  19. JavaScript 是如何工做的:編寫本身的 Web 開發框架 + React 及其虛擬 DOM 原理!
  20. JavaScript 是如何工做的:模塊的構建以及對應的打包工具
// 聲明一些變量並初始化它們
var a = 5
let b = 'xy'
const c = true

// 分配新值
a = 6
b = b + 'z'
c = false //  類型錯誤:不可對常量賦值

做爲程序員,聲明變量、初始化變量(或不初始化變量)以及稍後爲它們分配新值是咱們天天都要作的事情。github

可是當這樣作的時候會發生什麼呢? JavaScript 如何在內部處理這些基本功能? 更重要的是,做爲程序員,理解 JavaScript 的底層細節對咱們有什麼好處。web

下面,我打算介紹如下內容:編程

  • JS 原始數據類型的變量聲明和賦值
  • JavaScript內存模型:調用堆棧和堆
  • JS 引用類型的變量聲明和賦值
  • let vs const

JS 原始數據類型的變量聲明和賦值

讓咱們從一個簡單的例子開始。下面,咱們聲明一個名爲myNumber的變量,並用值23初始化它。segmentfault

let myNumber = 23

當執行此代碼時,JS將執行:數組

  1. 爲變量(myNumber)建立惟一標識符(identifier)。
  2. 在內存中分配一個地址(在運行時分配)。
  3. 將值 23 存儲在分配的地址。

clipboard.png

雖然咱們通俗地說,「myNumber 等於 23」,更專業地說,myNumber 等於保存值 23 的內存地址,這是一個值得理解的重要區別。安全

若是咱們要建立一個名爲 newVar 的新變量並把 myNumber 賦值給它。

let newVar = myNumber

由於 myNumber 在技術上實際是等於 「0012CCGWH80」,因此 newVar 也等於 「0012CCGWH80」,這是保存值爲23的內存地址。通俗地說就是 newVar 如今的值爲 23

clipboard.png

由於 myNumber 等於內存地址 0012CCGWH80,因此將它賦值給 newVar 就等於將0012CCGWH80 賦值給 newVar

如今,若是我這樣作會發生什麼:

myNumber = myNumber + 1

myNumber的值確定是 24。可是newVar的值是否也爲 24 呢?,由於它們指向相同的內存地址?

答案是否認的。因爲JS中的原始數據類型是不可變的,當 myNumber + 1 解析爲24時,JS 將在內存中分配一個新地址,將24做爲其值存儲,myNumber將指向新地址。

clipboard.png

這是另外一個例子:

let myString = 'abc'
myString = myString + 'd'

雖然一個初級 JS 程序員可能會說,字母d只是簡單在原來存放adbc內存地址上的值,從技術上講,這是錯的。當 abcd 拼接時,由於字符串也是JS中的基本數據類型,不可變的,因此須要分配一個新的內存地址,abcd 存儲在這個新的內存地址中,myString 指向這個新的內存地址。

clipboard.png

下一步是瞭解原始數據類型的內存分配位置。

JavaScript 內存模型:調用堆棧和堆

JS 內存模型能夠理解爲有兩個不一樣的區域:調用堆棧(call stack)堆(heap)

clipboard.png

調用堆棧是存放原始數據類型的地方(除了函數調用以外)。上一節中聲明變量後調用堆棧的粗略表示以下。

clipboard.png

在上圖中,我抽象出了內存地址以顯示每一個變量的值。 可是,不要忘記實際上變量指向內存地址,而後保存一個值。 這將是理解 let vs. const 一節的關鍵。

是存儲引用類型的地方。跟調用堆棧主要的區別在於,堆能夠存儲無序的數據,這些數據能夠動態地增加,很是適合數組和對象。

JS 引用類型的變量聲明和賦值

讓咱們從一個簡單的例子開始。下面,咱們聲明一個名爲myArray的變量,並用一個空數組初始化它。

let myArray = []

當你聲明變量「myArray」併爲其指定非原始數據類型(如「[]」)時,如下是在內存中發生的狀況:

  1. 爲變量建立惟一標識符(「myArray」)
  2. 在內存中分配一個地址(將在運行時分配)
  3. 存儲在堆上分配的內存地址的值(將在運行時分配)
  4. 堆上的內存地址存儲分配的值(空數組[])

clipboard.png

clipboard.png

從這裏,咱們能夠 push, pop,或對數組作任何咱們想作的。

myArray.push("first")
myArray.push("second")
myArray.push("third")
myArray.push("fourth")
myArray.pop()

clipboard.png

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看做等於內存地址。

clipboard.png

當將100分配給importantID時,由於100是一個原始數據類型,因此會分配一個新的內存地址,並將100的值存儲這裏。

而後 JS 嘗試將新的內存地址分配給 importantID,這就是拋出錯誤的地方,這也是咱們想要的行爲,由於咱們不想改變這個 importantID的值。

clipboard.png

當你將100分配給importantID時,其實是在嘗試分配存儲100的新內存地址,這是不容許的,由於importantID是用const聲明的。

如上所述,假設的初級JS程序員使用let錯誤地聲明瞭他們的數組。相反,他們應該用const聲明它。這在一開始看起來可能使人困惑,我認可這一點也不直觀。

初學者會認爲數組只有在咱們能夠改變的狀況下才有用,const 使數組不可變,那麼爲何要使用它呢? 請記住:「改變」是指改變內存地址。讓咱們深刻探討一下爲何使用const聲明數組是徹底能夠的。

const myArray = []

在聲明 myArray 時,將在調用堆棧上分配內存地址,該值是在堆上分配的內存地址。堆上存儲的值是實際的空數組。想象一下,它是這樣的:

clipboard.png

clipboard.png

若是咱們這麼作:

myArray.push(1)
myArray.push(2)
myArray.push(3)
myArray.push(4)
myArray.push(5)

clipboard.png

執行 push 操做實際是將數字放入堆中存在的數組。而 myArray 的內存地址沒有改變。這就是爲何雖然使用const聲明瞭myArray,但沒有拋出任何錯誤。

myArray 仍然等於 0458AFCZX91,它的值是另外一個內存地址22VVCX011,它在堆上有一個數組的值。

若是咱們這樣作,就會拋出一個錯誤:

myArray = 3

因爲 3 是一個原始數據類型,所以生成一個新的調用堆棧上的內存地址,其值爲 3,而後咱們將嘗試將新的內存地址分配給 myArray,因爲myArray是用const聲明的,因此這是不容許的。

clipboard.png

另外一個會拋出錯誤的例子:

myArray = ['a']

因爲[a]是一個新的引用類型的數組,所以將分配調用堆棧上的一個新內存地址,並存儲上的一個內存地址的值,其它值爲 [a]。而後,咱們嘗試將調用堆棧內存地址分配給 myArray,這會拋出一個錯誤。

clipboard.png

對於使用const聲明的對象(如數組),因爲對象是引用類型,所以能夠添加鍵,更新值等等。

const myObj = {}
myObj['newKey'] = 'someValue' // 這不會拋出錯誤

爲何這些知識對咱們有用呢

JavaScript 是世界上排名第一的編程語言(根據GitHub和Stack Overflow的年度開發人員調查)。 掌握併成爲「JS忍者」是咱們全部人都渴望成爲的人。

任何質量好的的 JS 課程或書籍都提倡使用let, const 來代替 var,但他們並不必定說出緣由。 對於初學者來講,爲何某些 const 變量在「改變」其值時會拋出錯誤而其餘 const變量卻沒有。 對我來講這是有道理的,爲何這些程序員默認使用let處處避免麻煩。

可是,不建議這樣作。谷歌擁有世界上最好的一些程序員,在他們的JavaScript風格指南中說,使用 constlet 聲明全部本地變量。默認狀況下使用 const,除非須要從新分配變量,不使用 var 關鍵字(原文)。

雖然他們沒有明確說明緣由,但據我所知,有幾個緣由

  1. 先發制人地限制將來的 bug。
  2. 使用 const 聲明的變量必須在聲明時初始化,這迫使程序員常常在範圍方面更仔細地放置它們。這最終會致使更好的內存管理和性能。
  3. 要經過代碼與任何可能遇到它的人交流,哪些變量是不可變的(就JS而言),哪些變量能夠從新分配。

但願上面的解釋能幫助你開始明白爲何或者何時應該在代碼中使用 letconst

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

你的點贊是我持續分享好東西的動力,歡迎點贊!

交流

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

https://github.com/qq44924588...

我是小智,公衆號「大遷世界」做者,對前端技術保持學習愛好者。我會常常分享本身所學所看的乾貨,在進階的路上,共勉!

關注公衆號,後臺回覆福利,便可看到福利,你懂的。

clipboard.png

相關文章
相關標籤/搜索