棧結構很簡單,咱們能夠經過數組就能模擬出一個棧結構,但僅僅介紹棧結構就太不前端了,本節從棧結構開始延伸到瀏覽器中 JavaScript 運行機制,還有存儲機制上用到的棧結構及相關數據結構,一文吃透全部的前端棧知識。前端
之後再提到棧時,咱們再也不僅限於 LIFO 了,而是一個有深度的棧。git
這部分是前端進階資深必備,若是你想打造高性能的前端應用,也須要了解這塊,同時它也是面試的常見考察點。github
理解棧對於咱們理解 JavaScript 語言相當重要,本文主要從如下幾個方面介紹棧:面試
本節吃透棧原理,以後幾天會每日一題,刷透棧題目,下面進入正文吧👍算法
棧是一種聽從後進先出 (LIFO / Last In First Out) 原則的有序集合,它的結構相似以下:數組
棧的操做主要有: push(e)
(進棧)、 pop()
(出棧)、 isEmpty()
(判斷是不是空棧)、 size()
(棧大小),以及 clear()
清空棧,具體實現也很簡單。瀏覽器
function Stack() { let items = [] this.push = function(e) { items.push(e) } this.pop = function() { return items.pop() } this.isEmpty = function() { return items.length === 0 } this.size = function() { return items.length } this.clear = function() { items = [] } }
查找:從棧頭開始查找,時間複雜度爲 O(n)數據結構
插入或刪除:進棧與出棧的時間複雜度爲 O(1)異步
咱們知道 JavaScript 是單線程的,所謂單線程,是指在 JavaScript 引擎中負責解釋和執行 JavaScript 代碼的線程惟一,同一時間上只能執行一件任務。函數
爲何是單線程的喃?這是由於 JavaScript 能夠修改 DOM 結構,若是 JavaScript 引擎線程不是單線程的,那麼能夠同時執行多段 JavaScript,若是這多段 JavaScript 都修改 DOM,那麼就會出現 DOM 衝突。
爲了不 DOM 渲染的衝突,能夠採用單線程或者死鎖,JavaScript 採用了單線程方案。
但單線程有一個問題:若是任務隊列裏有一個任務耗時很長,致使這個任務後面的任務一直排隊等待,就會發生頁面卡死,嚴重影響用戶體驗。
爲了解決這個問題,JavaScript 將任務的執行模式分爲兩種:同步和異步。
同步
// 同步任務 let a = 1 console.log(a) // 1
異步
// 異步任務 setTimeout(() => { console.log('時間到') }, 1000)
同步任務都在主線程(這裏的主線程就是 JavaScript 引擎線程)上執行,會造成一個 調用棧 ,又稱 執行棧 ;
除了主線程外,還有一個任務隊列(也稱消息隊列),用於管理異步任務的 事件回調 ,在 調用棧 的任務執行完畢以後,系統會檢查任務隊列,看是否有能夠執行的異步任務。
注意:任務隊列存放的是異步任務的事件回調
例如上例:
setTimeout(() => { console.log('時間到') }, 1000)
在執行這段代碼時,並不會馬上打印 ,只有定時結束後(1s)纔打印。 setTimeout
自己是同步執行的,放入任務隊列的是它的回調函數。
下面咱們重點看一下主線程上的調用棧。
咱們從如下兩個方面介紹調用棧:
咱們知道,在 JavaScript 中有不少函數,常常會出現一個函數調用另一個函數的狀況,調用棧就是用來管理函數調用關係的一種棧結構 。
那麼它是如何去管理函數調用關係喃?咱們舉例說明:
var a = 1 function add(a) { var b = 2 let c = 3 return a + b + c } // 函數調用 add(a)
這段代碼很簡單,就是建立了一個 add
函數,而後調用了它。
下面咱們就一步步的介紹整個函數調用執行的過程。
在執行這段代碼以前,JavaScript 引擎會先建立一個全局執行上下文,包含全部已聲明的函數與變量:
從圖中能夠看出,代碼中的全局變量 a
及函數 add
保存在變量環境中。
執行上下文準備好後,開始執行全局代碼,首先執行 a = 1
的賦值操做,
賦值完成後 a 的值由 undefined 變爲 1,而後執行 add
函數,JavaScript 判斷出這是一個函數調用,而後執行如下操做:
至此,整個函數調用執行結束了。
因此說,調用棧是 JavaScript 用來管理函數執行上下文的一種數據結構,它記錄了當前函數執行的位置,哪一個函數正在被執行。 若是咱們執行一個函數,就會爲函數建立執行上下文並放入棧頂。 若是咱們從函數返回,就將它的執行上下文從棧頂彈出。 也能夠說調用棧是用來管理這種執行上下文的棧,或稱執行上下文棧(執行棧)。
在咱們執行 JavaScript 代碼的時候,有時會出現棧溢出的狀況:
上圖就是一個典型的棧溢出,那爲何會出現這種錯誤喃?
咱們知道調用棧是用來管理執行上下文的一種數據結構,它是有大小的,當入棧的上下文過多的時候,它就會報棧溢出,例如:
function add() { return 1 + add() } add()
add
函數不斷的遞歸,不斷的入棧,調用棧的容量有限,它就溢出了,因此,咱們平常的開發中,必定要注意此類代碼的出現。
兩種方式,一種是斷點調試,這種很簡單,咱們平常開發中都用過。
一種是 console.trace()
function sum(){ return add() } function add() { console.trace() return 1 } // 函數調用 sum()
在 JavaScript 開發平常中,前端人員不多有機會了解內存,但若是你想成爲前端的專家,打造高性能的前端應用,你就須要瞭解這一塊,同時它也是面試的常見考察點。
JavaScript 中的內存空間主要分爲三種類型:
代碼空間主要用來存放可執行代碼的。棧空間及堆空間主要用來存放數據的。接下來咱們主要介紹棧空間及堆空間。
JavaScript 中的變量類型有 8
種,可分爲兩種:基本類型、引用類型
基本類型:
undefined
null
boolean
number
string
bigint
symbol
引用類型:
object
其中,基本類型是保存在棧內存中的簡單數據段,而引用類型保存在堆內存中。
基本類型在內存中佔有固定大小的空間,因此它們的值保存在棧空間,咱們經過 按值訪問 。
通常棧空間不會很大。
引用類型,值大小不固定,但指向值的指針大小(內存地址)是固定的,因此把對象放入堆中,將對象的地址放入棧中,這樣,在調用棧中切換上下文時,只須要將指針下移到上個執行上下文的地址就能夠了,同時保證了棧空間不會很大。
當查詢引用類型的變量時, 先從棧中讀取內存地址, 而後再經過地址找到堆中的值。對於這種,咱們把它叫作 按引用訪問 。
通常堆內存空間很大,能存放不少數據,但它內存分配與回收都須要花費必定的時間。
舉個例子幫助理解一下:
var a = 1 function foo() { var b = 2 var c = { name: 'an' } } // 函數調用 foo()
基本類型(棧空間)與引用類型(堆空間)的存儲方式決定了:基本類型賦值是值賦值,而引用類型賦值是地址賦值。
// 值賦值 var a = 1 var b = a a = 2 console.log(b) // 1 // b 不變 // 地址賦值 var a1 = {name: 'an'} var b1 = a1 a1.name = 'bottle' console.log(b1) // {name: "bottle"} // b1 值改變
JavaScript 中的垃圾數據都是由垃圾回收器自動回收的,不須要手動釋放。因此大部分的開發人員並不瞭解垃圾回收,但這部分也是前端進階資深必備!
在 JavaScript 執行代碼時,主線程上會存在 ESP 指針,用來指向調用棧中當前正在執行的上下文,以下圖,當前正在執行 foo
函數:
當 foo
函數執行完成後,ESP 向下指向全局執行上下文,此時須要銷燬 foo
函數。
怎麼銷燬喃?
當 ESP 指針指向全局執行上下文,foo
函數執行上下文已是無效的了,當有新的執行上下文進來時,能夠直接覆蓋這塊內存空間。
即:JavaScript 引擎經過向下移動 ESP 指針來銷燬存放在棧空間中的執行上下文。
V8 中把堆分紅新生代與老生代兩個區域:
V8 對這兩塊使用了不一樣的回收器:
其實不管哪一種垃圾回收器,都採用了一樣的流程(三步走):
副垃圾回收器與主垃圾回收器雖然都採用一樣的流程,但使用的回收策略與算法是不一樣的。
副垃圾回收器
它採用 Scavenge 算法及對象晉升策略來進行垃圾回收
所謂 Scavenge 算法,即把新生代空間對半劃分爲兩個區域,一半是對象區域,一半是空閒區域,以下圖所示:
新加入的對象都加入對象區域,當對象區滿的時候,就執行一次垃圾回收,執行流程以下:
翻轉後,對象區域是沒有碎片的,此時不須要進行第三步(內存整理了)
但,新生代區域很小的,通常1~8M的容量,因此它很容易滿,因此,JavaScript 引擎採用對象晉升策略來處理,即只要對象通過兩次垃圾回收以後依然繼續存活,就會被晉升到老生代區域中。
主垃圾回收器
老生代區域裏除了存在重新生代晉升來的存活時間久的對象,當遇到大對象時,大對象也會直接分配到老生代。
因此主垃圾回收器主要保存存活久的或佔用空間大的對象,此時採用 Scavenge 算法就不合適了。V8 中主垃圾回收器主要採用標記-清除法進行垃圾回收。
主要流程以下:
增量標記
V8 瀏覽器會自動執行垃圾回收,但因爲 JavaScript 也是運行在主線程上的,一旦執行垃圾回收,就要打斷 JavaScript 的運行,可能會或多或少的形成頁面的卡頓,影響用戶體驗,因此 V8 決定採用增量 標記算法回收:
即把垃圾回收拆成一個個小任務,穿插在 JavaScript 中執行。
本節從棧結構開始介紹,知足後進先出 (LIFO) 原則的有序集合,而後經過數組實現了一個棧。
接着介紹瀏覽器環境下 JavaScript 的異步執行機制,即事件循環機制, JavaScript 主線程不斷的循環往復的從任務隊列中讀取任務(異步事件回調),放入調用棧中執行。調用棧又稱執行上下文棧(執行棧),是用來管理函數執行上下文的棧結構。
JavaScript 的存儲機制分爲代碼空間、棧空間以及堆空間,代碼空間用於存放可執行代碼,棧空間用於存放基本類型數據和引用類型地址,堆空間用於存放引用類型數據,當調用棧中執行完成一個執行上下文時,須要進行垃圾回收該上下文以及相關數據空間,存放在棧空間上的數據經過 ESP 指針來回收,存放在堆空間的數據經過副垃圾回收器(新生代)與主垃圾回收器(老生代)來回收。
聊聊就跑遠了🤦♀️,但都是前端進階必會,接下來咱們開始刷棧題目吧!!!每日一刷,進階前端與算法⛽️⛽️⛽️,來道簡單的吧!
設計一個支持 push
,pop
,top
操做,並能在常數時間內檢索到最小元素的棧。
push(x)
—— 將元素 x 推入棧中。pop()
—— 刪除棧頂的元素。top()
—— 獲取棧頂元素。getMin()
—— 檢索棧中的最小元素。示例:
MinStack minStack = new MinStack(); minStack.push(-2); minStack.push(0); minStack.push(-3); minStack.getMin(); --> 返回 -3. minStack.pop(); minStack.top(); --> 返回 0. minStack.getMin(); --> 返回 -2.
歡迎將答案提交到 https://github.com/sisterAn/J...,讓更多人看到,瓶子君也會在明日放上本身的解答。
瀏覽器工做原理與實踐(極客時間)
歡迎關注「前端瓶子君」,回覆「算法」自動加入,從0到1構建完整的數據結構與算法體系!
在這裏,瓶子君不只介紹算法,還將算法與前端各個領域進行結合,包括瀏覽器、HTTP、V八、React、Vue源碼等。
在這裏,你能夠天天學習一道大廠算法題(阿里、騰訊、百度、字節等等)或 leetcode,瓶子君都會在次日解答喲!
⬆️ 掃碼關注公衆號「前端瓶子君」,回覆「算法」便可自動加入 👍👍👍