前端也要會的資料結構 (不定期更新篇)

酸楚與甘甜發表於2018-08-06

前端的軟肋

一說到前端大家腦子裡只有,佈局、展示資料、修改樣式等等。可是資料是哪裡來的呢?後端給的後端給的。資料的結構呢?後端給啥用啥。

這就是前端的一個軟肋。我們的業務讓我們並不需要過深入的瞭解資料結構,資料結構和演算法是一個程式設計師的基礎。無論是前端開發還是後端開發、還是AI機器學習大資料,我認為都需要一定的資料結構和演算法知識(除了前端,其餘的都是強烈的剛需。。。),前端的夥伴們學會資料結構有什麼好處呢?改變思考方式,深入瞭解js執行的一些過程,在程式碼中不知不覺考慮程式碼層面的效能優化。用處很多,接下來開始吧。

什麼是資料結構?

資料結構是計算機儲存、組織資料的方式。資料結構是指相互之間存在一種或多種特定關係的資料元素的集合。(來自百度百科)

計算機儲存是什麼意思呢?就是我們常說的儲存結構。

組織資料的方式與特定關係呢?就是我們的邏輯結構。

一堆按一定的儲存結構和邏輯結構組合起來的資料集合就是資料結構。

這是我的一些理解。所以當一種資料結構擺在我的面前我就會考慮,它是以哪種形式儲存起來,它有什麼特殊的邏輯組合起來。這種方式有哪些好處呢?當我明白這些的時候,這種資料結構我就算基本瞭解了。

首先我們先說一下線性表,線性表是資料的邏輯結構,元素之間是一對一的關係,你個線性表中,任何一個元素的前一個或者後一個都只有一個元素,而不會出現任何一個元素的前一個或者後一個元素對應多個元素的情況。

線性表分為一般性的線性表、與受限的線性表。一般性的線性表就好比陣列。就是最常見的一般性線性表,而受限的線性表就是棧與佇列。

好了好了 我們開始正題了

你好 神奇的棧結構

棧型結構,最大的特點是什麼?先進後出,先進入的元素要比後進入的元素更晚的離開這個容器,這像什麼一堆摞起來的書籍。你這能拿走書的最上面的。(你要是給書推倒了,那就只能說明你比較睿智)

前端也要會的資料結構 (不定期更新篇)

所以我們要實現一個棧型結構很自然的想到了

容器:陣列

方法:陣列的push與pop方法 下面開始實現一個棧的建構函式

function Stack(){
    // 用let建立一個私有容器,無法用this選擇到dataStore;
    let dataStore = [];
    // 模擬進棧的方法 
    this.push = function(element){
        dataStore.push(element);
    };
    // 模擬出棧的方法,返回值是出棧的元素。
    this.pop = function(){
        return dataStore.pop();
    };
    // 返回棧頂元素
    this.peek = function(){
        return dataStore[dataStore.length-1];  
    };
    // 是否為空棧
    this.isEmpty = function(){
        return dataStore.length === 0 
    };
    // 獲取棧結構的長度。
    this.size = function(){
        return dataStore.length;
    };
    //  清除棧結構內的所有元素。
    this.clear = function(){
        dataStore = [];
    }
}
複製程式碼

好了夥伴們 棧的建構函式我們已經寫好了。

// 一個單獨的棧生成了。
let stack = new Stack();
stack.push(1);
stack.push(2);
stack.push(5);
stack.peek(); // return 5
stack.size(); // return 3
stack.clear();
stack.peek(); // undefined
複製程式碼

一個基本的棧型結構就實現了。 真的很是簡單。但是這個東西在js中有什麼用處呢?

在js中的用處

用處很大,首先我們都知道遞迴如果沒有設定好邊界值,就會報堆疊溢位。 什麼意思??? 我們的js程式碼在執行的時候,會生成一個呼叫棧(裡面裝滿了所有執行中的函式)

前端也要會的資料結構 (不定期更新篇)
在這個呼叫棧第一個進棧的也就是壓在最下面的就是全域性函式,這個全域性函式只會在瀏覽器關閉後才會出棧,瀏覽器關閉了這個堆疊也就隨之消失了。 當瀏覽器執行下去的時候執行一個函式的時候,就會把這個執行中的函式加入到呼叫棧中,被呼叫了就要進呼叫棧(比較好理解吧),同時控制權也會轉交給這個函式,這個函式中如果有別的函式,執行到別的函式時,做著一樣的事情,進棧。當任何一個函式執行結束之後,那麼不好意思,他就要退棧了。離開這個呼叫棧了。因為呼叫棧中內容過多,代表著垃圾資源沒有回收,從而導致瀏覽器卡頓,這是不合理的,執行完畢就會退棧。

前端也要會的資料結構 (不定期更新篇)

退棧之後,那麼執行權就要交回到包含著它的函式了。

堆疊的三種場景

1:遞迴是一種怎樣的情況呢?

當我們沒有考慮遞迴的出口的時候,
簡化函式
function fn(){
    let a = 1; fn()
}
fn()  // 報錯!!!! Maximum call stack size exceeded
      // 最大呼叫堆疊大小超過
複製程式碼

當我沒有設定出口時,並沒有任何一個函式會出棧,在不斷的迴圈呼叫後,你的堆疊肯定不會是無限的,那麼就只好提醒你堆疊溢位,程式報錯。

2: 你知道redux的洋蔥模型圖嗎?

所謂redux的洋蔥模型(其實redux我沒用過,但是在公司分享會上聽過一段對於這個洋蔥模型圖的分享),大家也可以理解一下express框架的寫介面時的next函式。

前端也要會的資料結構 (不定期更新篇)
在我們洋蔥在最外層執行完畢後就會進入裡面,到最內部後再循壞退出來。

function fn1(){
    console.log('fn1 first');
    fn2()
    console.log('fn1 last');
}
function fn2(){
    console.log('fn2 first');
    fn3()
    console.log('fn2 last');
}
function fn3(){
    console.log('fn3 first');
    console.log('fn3 last');
}
fn1()
複製程式碼

前端也要會的資料結構 (不定期更新篇)
列印結果中我們可以看出,fn1執行的時候在,遇到fn2執行進棧後,將控制權轉交給fn2,fn2執行遇到fn3執行進棧,將控制權交給fn3,fn3執行完畢後退棧,控制權還給fn2,那麼fn2後面的程式碼會繼續執行,fn2的程式碼執行完畢,退棧。繼續執行fn1後面的程式碼直到退棧。可能這種簡單的模式大家看起來比較清晰,如果有比較複雜的內容,大家記得畫圖不要弄錯了。

3: 不說你就會忘的閉包

為什麼閉包使用過多會導致程式卡頓,效能不好???

這個問題很讓人費解,但是扯上呼叫棧後,我覺得可以解釋一波(程式碼我就不上了,大家可以去找找閉包得程式碼看一看) 閉包得私有變數怎麼產生的? 在一個函式執行後,它執行完畢就一定會退棧。

function A(){
  var count = 0;
  function B(){
    count ++;
    console.log(count);
  }
  return B;
}
var C = A();
C();// 1
C();// 2
C();// 3
複製程式碼

不好意思我食言了,方便大家理解還是上一波程式碼吧。

在A函式執行得時候,我們發現執行過程中,遇到B函式,好了它開始呼叫進棧執行,執行完畢後,控制權迴歸A函式,然後把B函式return出去了。B函式中保持對count變數的引用,你就把它return出去了????好吧你願意你就這麼幹。

B函式被推出去到外面的世界(外面的函式體內);將B賦值給C,好了C需要count變數的支援,count就不能離開記憶體(也就是不能被垃圾回收);拿咋辦? A函式執行完了我也該離開了(函式執行完畢後,函式內部的變數會被回收掉)。不好意思,外面執行著的函式還有對你的引用,那不好意思你別退棧了,並不允許離開呼叫棧,因為要保留count變數的環境。

好吧每一個這種情況就會有幾個函式無法退棧,呼叫棧裡面的內容越堆越多。就會越加卡頓。一輛公交車,我們們的乘客到站就下車了,但是總有幾個乘客死活不下車,車上人越來越多,車也越來越沉,好吧跑起來就越來越慢了。

是時候結個尾了

在不理解棧的時候,我很難去想以上幾種情況,資料結構真的在改變我的思路,一切的知識點都在這些基礎中有著驗證,夯實基礎,我認為資料結構也不應該是前端的加分項,而是比會項。最後的最後,我用我聽過的一句很經典的話來結尾好了

我們寫的程式碼不是為了更好的和人去溝通,而是去更好的和機器溝通

最後打個廣告(我們一起維護的學習公眾號)

公眾號主要面向的是初級/應屆生。內容包含我們從應屆生轉換為職場開發所踩過的坑,以及我們每週的學習計劃和學習總結。 內容會涉及計算機網路演算法等基礎;也會涉及前端,後臺,Android等內容~

求關注,不迷路

我們基友團其他朋友的文章:

Android基友

Java基友

相關文章