咱們已經討論過了前端與計算機基礎的不少話題,諸如SQL、面向對象、多線程,本篇將討論數據結構與算法,以我接觸過的一些例子作爲說明。javascript
遞歸就是本身調本身,遞歸在前端裏面算是一種比較經常使用的算法。假設如今有一堆數據要處理,要實現上一次請求完成了,才能去調下一個請求。一個是能夠用Promise,就像《前端與SQL》這篇文章裏面提到的。可是有時候並不想引入Promise,能簡單處理先簡單處理。這個時候就能夠用遞歸,以下代碼所示:css
var ids = [34112, 98325, 68125]; (function sendRequest(){ var id = ids.shift(); if(id){ $.ajax({url: "/get", data: {id}}).always(function(){ //do sth. console.log("finished"); sendRequest(); }); } else { console.log("finished"); } })(); 複製代碼
上面代碼定義了一個sendRequest的函數,在請求完成以後再調一下本身。每次調以前先取一個數據,若是數組已經爲空,則說明處理完了。這樣就用簡單的方式實現了串行請求不堵塞的功能。html
再來說另一個場景:DOM樹。前端
因爲DOM是一棵樹,而樹的定義自己就是用的遞歸定義,因此用遞歸的方法處理樹,會很是地簡單天然。例如用遞歸實現一個查DOM的功能document.getElementById。java
function getElementById(node, id){ if(!node) return null; if(node.id === id) return node; for(var i = 0; i < node.childNodes.length; i++){ var found = getElementById(node.childNodes[i], id); if(found) return found; } return null; } getElementById(document, "d-cal");複製代碼
document是DOM樹的根結點,通常從document開始往下找。在for循環裏面先找document的全部子結點,對全部子結點遞歸查找他們的子結點,一層一層地往下查找。若是已經到了葉子結點了尚未找到,則在第二行代碼的判斷裏面返回null,返回以後for循環的i加1,繼續下一個子結點。若是當前結點的id符合查找條件,則一層層地返回。因此這是一個深度優先的遍歷,每次都先從根結點一直往下直到葉子結點,再從下往上返回。node
最後在控制檯驗證一下,執行結果以下圖所示:git
使用遞歸的優勢是代碼簡單易懂,缺點是效率比不上非遞歸的實現。Chrome瀏覽器的查DOM是使用非遞歸實現。非遞歸要怎麼實現呢?github
以下代碼:web
function getByElementId(node, id){ //遍歷全部的Node while(node){ if(node.id === id) return node; node = nextElement(node); } return null; }複製代碼
仍是依次遍歷全部的DOM結點,只是這一次改爲一個while循環,函數nextElement負責找到下一個結點。因此關鍵在於這個nextElement如何非遞歸實現,以下代碼所示:ajax
function nextElement(node){ if(node.children.length) { return node.children[0]; } if(node.nextElementSibling){ return node.nextElementSibling; } while(node.parentNode){ if(node.parentNode.nextElementSibling) { return node.parentNode.nextElementSibling; } node = node.parentNode; } return null; }複製代碼
仍是用深度遍歷,先找當前結點的子結點,若是它有子結點,則下一個元素就是它的第一個子結點,不然判斷它是否有相鄰元素,若是有則返回它的下一個相鄰元素。若是它既沒有子結點,也沒有下一個相鄰元素,則要往上返回它的父結點的下一個相鄰元素,至關於上面遞歸實現裏面的for循環的i加1.
在控制檯裏面運行這段代碼,一樣也能夠正確地輸出結果。無論是非遞歸仍是遞歸,它們都是深度優先遍歷,這個過程以下圖所示。
實際上getElementById瀏覽器是用的一個哈希map存儲的,根據id直接映射到DOM結點,而getElementsByClassName就是用的這樣的非遞歸查找。
上面是單個選擇器的查找,按id,按class等,多個選擇器應該如何查找呢?
如實現一個document.querySelector:
document.querySelector(".mls-info > div .copyright-content")複製代碼
首先把複雜選擇器作一個解析,序列爲如下格式:
//把selector解析爲 var selectors = [ {relation: "descendant", matchType: "class", value: "copyright-content"}, {relation: "child", matchType: "tag", value: "div"}, {relation: "subSelector", matchType: "class", value: "mls-info"}];複製代碼
從右往左,第一個selector是.copyright-content
,它是一個類選擇器,因此它的matchType是class,它和第二個選擇器是祖先和子孫關係,所以它的relation是descendant;同理第二個選擇器的matchType是tag,而relation是child,表示是第三個選擇器的直接子結點;第三個選擇器也是class,可是它沒有下一個選擇器了,relation用subSelector表示。
matchType的做用就在於用來比較當前選擇器是否match,以下代碼所示:
function match(node, selector){ if(node === document) return false; switch(selector.matchType){ //若是是類選擇器 case "class": return node.className.trim().split(/ +/).indexOf(selector.value) >= 0; //若是是標籤選擇器 case "tag": return node.tagName.toLowerCase() === selector.value. toLowerCase(); default: throw "unknown selector match type"; } }複製代碼
根據不一樣的matchType作不一樣的匹配。
在匹配的時候,從右往左,依次比較每一個選擇器是否match. 在比較下一個選擇器的時候,須要找到相應的DOM結點,若是當前選擇器是下一個選擇器的子孫時,則須要比較當前選擇器全部的祖先結點,一直往上直到document;而若是是直接子元素的關係,則比較它的父結點便可。因此須要有一個找到下一個目標結點的函數:
function nextTarget(node, selector){ if(!node || node === document) return null; switch(selector.relation){ case "descendant": return {node: node.parentNode, hasNext: true}; case "child": return {node: node.parentNode, hasNext: false}; case "sibling": return {node: node.previousSibling, hasNext: true}; default: throw "unknown selector relation type"; //hasNext表示當前選擇器relation是否容許繼續找下一個節點 } }複製代碼
有了nextTarge和match這兩個函數就能夠開始遍歷DOM,以下代碼所示:
最外層的while循環和簡單選擇器同樣,都是要遍歷全部DOM結點。對於每一個結點,先判斷第一個選擇器是否match,若是不match的話,則繼續下一個結點,若是不是標籤選擇器,對於絕大多數結點將會在這裏判斷不經過。若是第一個選擇器match了,則根據第一個選擇器的relation,找到下一個target,判斷下一個targe是否match下一個selector,只要有一個target匹配上了,則退出裏層的while循環,繼續下一個選擇器,若是全部的selector都能匹配上說明匹配成功。若是有一個selecotr的全部target都沒有match,則說明匹配失敗,退出selector的for循環,直接從頭開始對下一個DOM結點進行匹配。
這樣就實現了一個複雜選擇器的查DOM。寫這個的目的並非要你本身寫一個查DOM的函數拿去用,而是要明白查DOM的過程是怎麼樣的,能夠怎麼實現,瀏覽器又是怎麼實現的。還有能夠怎麼遍歷DOM樹,當明白這個過程的時候,遇到相似的問題,就能夠觸類旁通。
最後在瀏覽器上運行一下,以下圖所示:
如今有個問題,以下圖所示:
當地圖往下拖的時候要更新地圖上的房源標籤數據,上圖綠框表示不變的標籤,而黃框表示新加的房源。
後端每次都會把當前地圖可見區域的房源返回給我,當用戶拖動的時候須要知道哪些是原先已經有的房源,哪些是新加的。把新加的房源畫上,而把超出區域的房源刪掉,已有的房源保持不動。所以須要對比當前房源和新的結果哪些是重複的。由於若是不這樣作的話,改爲每次都是所有刪掉再從新畫,已有的房源標籤就會閃一下。所以爲了不閃動作一個增量更新。
把這個問題抽象一下就變成:給兩個數組,須要找出第一個數組裏面的重複值和非重複值。即有一個數組保存上一次狀態的房源,而另外一個數組是當前狀態的新房源數據。找到的重複值是須要保留,找到非重複值是要刪掉的。
最直觀的方法是使用雙重循環。
以下代碼所示:
var lastHouses = []; filterHouse: function(houses){ if(lastHouses === null){ lastHouses = houses; return { remainsHouses: [], newHouses: houses }; } var remainsHouses = [], newHouses = []; for(var i = 0; i < houses.length; i++){ var isNewHouse = true; for(var j = 0; j < lastHouses.length; j++){ if(houses[i].id === lastHouses[j].id){ isNewHouse = false; remainsHouses.push(lastHouses[j]); break; } } if(isNewHouse){ newHouses.push(houses[i]); } } lastHouses = remainsHouses.concat(newHouses); return { remainsHouses: remainsHouses, newHouses: newHouses }; } 複製代碼
上面代碼有一個雙重for循環,對新數據的每一個元素,判斷老數據裏面是否已經有了,若是有的話則說明是重複值,若是老數據循環了一遍都沒找到,則說明是新數據。因爲用到了雙重循環,因此這個算法的時間複雜度爲O(N2),對於百級的數據還好,對於千級的數據可能會有壓力,由於最壞狀況下要比較1000000次。
以下代碼所示:
var lastHouses = new Set(); function filterHouse(houses){ var remainsHouses = [], newHouses = []; for(var i = houses.length - 1; i >= 0; i--){ if(lastHouses.has(houses[i].id)){ remainsHouses.push(houses[i]); } else { newHouses.push(houses[i]); } } for(var i = 0; i < newHouses.length; i++){ lastHouses.add(newHouses[i].id); } return {remainsHouses: remainsHouses, newHouses: newHouses}; }複製代碼
老數據的存儲lastHouses從數組改爲set,但若是一開始就是數組呢,就像問題抽象裏面說的給兩個數組?那就用這個數組的數據初始化一個Set.
使用Set和使用Array的區別在於能夠減小一重循環,調用Set.prototype.has的函數。Set通常是使用紅黑樹實現的,紅黑樹是一種平衡查找二叉樹,它的查找時間複雜度爲O(logN)。因此時間上進行了改進,從O(N)變成O(logN),而整體時間從O(N2)變成O(NlogN)。實際上,Chrome V8的Set是用哈希實現的,它是一個哈希Set,查找時間複雜度爲O(1),因此整體的時間複雜度是O(N).
無論是O(NlogN)仍是O(N),表面上看它們的時間要比O(N2)的少。但實際上須要注意的是它們前面還有一個係數。使用Set在後面更新lastHouses的時候也是須要時間的:
for(var i = 0; i < newHouses.length; i++){ lastHouses.add(newHouses[i].id); }複製代碼
若是Set是用樹的實現,這段代碼是時間複雜度爲O(NlogN),因此總的時間爲O(2NlogN),可是因爲大O是不考慮係數的,O(2NlogN) 仍是等於O(NlogN),當數據量比較小的時侯,這個係數會起到很大的做用,而數據量比較大的時候,指數級增加的O(N2)將會遠遠超過這個係數,哈希的實現也是一樣道理。因此當數據量比較小時,如只有一兩百可直接使用雙重循環處理便可。
上面的代碼有點冗長,咱們能夠用ES6的新特性改寫一下,變得更加的簡潔:
function filterHouse(houses){ var remainsHouses = [], newHouses = []; houses.map(house => lastHouses.has(house.id) ? remainsHouses.push(house) : newHouses.push(house)); newHouses.map(house => lastHouses.add(house.id)); return {remainsHouses, newHouses}; }複製代碼
代碼從16行變成了8行,減小了一半。
使用Map也是相似的,代碼以下所示:
var lastHouses = new Map(); function filterHouse(houses){ var remainsHouses = [], newHouses = []; houses.map(house => lastHouses.has(house.id) ? remainsHouses.push(house) : newHouses.push(house)); newHouses.map(house => lastHouses.set(house.id, house)); return {remainsHouses, newHouses}; }複製代碼
哈希的查找複雜度爲O(1),所以總的時間複雜度爲O(N),Set/Map都是這樣,代價是哈希的存儲空間一般爲數據大小的兩倍
最後作下時間比較,爲此得先造點數據,比較數據量分別爲N = 100, 1000, 10000的時間,有N/2的id是重複的,另一半的id是不同的。用如下代碼生成:
var N = 1000; var lastHouses = new Array(N); var newHouses = new Array(N); var data = new Array(N); for(var i = 0; i < N / 2; i++){ var sameNumId = i; lastHouses[i] = newHouses[i] = {id: sameNumId}; } for(; i < N; i++){ lastHouses[i] = {id: N + i}; newHouses[i] = {id: 2 * N + i}; }複製代碼
而後須要將重複的數據隨機分佈,可用如下函數把一個數組的元素隨機分佈:
//打散 function randomIndex(arr){ for(var i = 0; i < arr.length; i++){ var swapIndex = parseInt(Math.random() * (arr.length - i)) + i; var tmp = arr[i]; arr[i] = arr[swapIndex]; arr[swapIndex] = tmp; } } randomIndex(lastHouses); randomIndex(newHouses);複製代碼
Set/Map的數據:
var lastHousesSet = new Set(); for(var i = 0; i < N; i++){ lastHousesSet.add(lastHouses[i].id); } var lastHousesMap = new Map(); for(var i = 0; i < N; i++){ lastHousesMap.set(lastHouses[i].id, lastHouses[i]); }複製代碼
分別重複100次,比較時間:
console.time("for time"); for(var i = 0; i < 100; i++){ filterHouse(newHouses); } console.timeEnd("for time"); console.time("Set time"); for(var i = 0; i < 100; i++){ filterHouseSet(newHouses); } console.timeEnd("Set time"); console.time("Map time"); for(var i = 0; i < 100; i++){ filterHouseMap(newHouses); } console.timeEnd("Map time");複製代碼
使用Chrome 59,當N = 100時,時間爲: for < Set < Map,以下圖所示,執行三次:
當N = 1000時,時間爲:Set = Map < for,以下圖所示:
當N = 10000時,時間爲Set = Map << for,以下圖所示:
能夠看出,Set和Map的時間基本一致,當數據量小時,for時間更少,但數據量多時Set和Map更有優點,由於指數級增加仍是挺恐怖的。這樣咱們會有一個問題,究竟Set/Map是怎麼實現的。
咱們來研究一下Chrome V8對Set/Map的實現,源碼是在chrome/src/v8/src/js/collection.js這個文件裏面,因爲Chrome一直在更新迭代,因此有可能之後Set/Map的實現會發生改變,咱們來看一下如今是怎麼實現的。
以下代碼初始化一個Set:
var set = new Set(); //數據爲20個數 var data = [3, 62, 38, 42, 14, 4, 14, 33, 56, 20, 21, 63, 49, 41, 10, 14, 24, 59, 49, 29]; for(var i = 0; i < data.length; i++){ set.add(data[i]); } 複製代碼
這個Set的數據結構究竟是怎麼樣的呢,是怎麼進行哈希的呢?
哈希的一個關鍵的地方是哈希算法,即對一堆數或者字符串作哈希運算獲得它們的隨機值,V8的數字哈希算法是這樣的:
function ComputeIntegerHash(key, seed) { var hash = key; hash = hash ^ seed; //seed = 505553720 hash = ~hash + (hash << 15); // hash = (hash << 15) - hash - 1; hash = hash ^ (hash >>> 12); hash = hash + (hash << 2); hash = hash ^ (hash >>> 4); hash = (hash * 2057) | 0; // hash = (hash + (hash << 3)) + (hash << 11); hash = hash ^ (hash >>> 16); return hash & 0x3fffffff; }複製代碼
把數字進行各類位運算,獲得一個比較隨機的數,而後對這個數對行散射,以下所示:
var capacity = 64; var indexes = []; for(var i = 0; i < data.length; i++){ indexes.push(ComputeIntegerHash(data[i], seed) & (capacity - 1)); //去掉高位 } console.log(indexes)複製代碼
散射的目的是獲得這個數放在數組的哪一個index。
因爲有20個數,容量capacity從16開始增加,每次擴大一倍,到64的時候,能夠保證capacity > size * 2,由於只有容量是實際存儲大小的兩倍時,散射結果重複值才能比較低。
計算結果以下:
能夠看到散射的結果仍是比較均勻的,可是仍然會有重複值,如14重複了3次。
而後進行查找,例如如今要查找key = 56是否存在這個Set裏面,先把56進行哈希,而後散射,按照存放的時候一樣的過程:
function SetHas(key){ var index = ComputeIntegerHash(56, seed) & this.capacity; //可能會有重複值,因此須要驗證命中的index所存放的key是相等的 return setArray[index] !== null && setArray[index] === key; } 複製代碼
上面是哈希存儲結構的一個典型實現,可是Chrome的V8的Set/Map並非這樣實現的,略有不一樣。
哈希算法是同樣的,可是散射的時候用來去掉高位的並非用capacity,而是用capacity的一半,叫作buckets的數量,用如下面的data作說明:
var data = [9, 33, 68, 57];複製代碼
因爲初始化的buckets = 2,計算的結果以下:
因爲buckets很小,因此散射值有不少重複的,4個數裏面1重複了3次。
如今一個個的插入數據,觀察Set數據結構的變化。
以下圖所示,Set的存儲結構分紅三部分,第一部分有3個元素,分別表示有效元素個數、被刪除的個數、buckets的數量,前兩個數相加就表示總的元素個數,插入9以後,元素個數加1變成1,初始化的buckets數量爲2. 第二部分對應buckets,buckets[0]表示第1個bucket所存放的原始數據的index,源碼裏面叫作entry,9在data這個數組裏面的index爲0,因此它在bucket的存放值爲0,而且bucket的散射值爲0,因此bucket[0] = 0. 第三部分是記錄key值的空間,9的entry爲0,因此它放在了3 + buckets.length + entry * 2 = 5的位置,每一個key值都有兩個元素空間,第一個存放key值,第二個是keyChain,它的做用下面將提到。
如今要插入33,因爲33的bucket = 1,entry = 1,因此插入後變成這樣:
68的bucket值也爲1,和33重複了,由於entry = buckets[1] = 1,不爲空,說明以前已經存過了,entry爲1指向的數組的位置爲3 + buckets.length + entry * 2 = 7,也就是說以前的那個數是放在數組7的位置,因此68的相鄰元素存放值keyChain爲7,同時bucket[1]變成68的entry爲2,以下圖所示:
插入57也是一樣的道理,57的bucket值爲1,而bucket[1] = 2,所以57的相鄰元素存放3 + 2 + 2 * 2 = 9,指向9的位置,以下圖所示:
如今要查找33這個數,經過一樣的哈希散射,獲得33的bucket = 1,bucket[1] = 3,3指向的index位置爲11,可是11放的是57,不是要找的33,因而查看相鄰的元素爲9,非空,可繼續查找,位置9存放的是68,一樣不等於33,而相鄰的index = 10指向位置7,而7存放的就是33了,經過比較key值相等,因此這個Set裏面有33這個數。
這裏的數據總共是4個數,可是須要比較的次數比較多,key值就比較了3次,key值的相鄰keyChain值比較了2次,總共是5次,比直接來個for循環還要多。因此數據量比較小時,使用哈希存儲速度反而更慢,可是當數據量偏大時優點會比較明顯。
再繼續插入第5個數的時候,發現容量不夠了,須要繼續擴容,會把容量提高爲2倍,bucket數量變成4,再把全部元素再從新進行散射。
Set的散射容量即bucket的值是實際元素的一半,會有大量的散射衝突,可是它的存儲空間會比較小。假設元素個數爲N,須要用來存儲的數組空間爲:3 + N / 2 + 2 * N,因此佔用的空間仍是挺大的,它用空間換時間。
和Set基本一致,不一樣的地方是,map多了存儲value的地方,以下代碼:
var map = new Map(); map.set(9, "hello");複製代碼
生成的數據結構爲:
固然它不是直接存的字符串「hello」,而是存放hello的指針地址,指向實際存放hello的內存位置。
JSObject主要也是採用哈希存儲,具體我在《從Chrome源碼看JS Object的實現》這篇文件章裏面已經討論過。
和JS Map不同的地方是,JSObject的容量是元素個數的兩倍,就是上面說的哈希的典型實現。存儲結構也不同,有一個專門存放key和一個存放value的數組,若是能找到key,則拿到這個key的index去另一個數組取出value值。當發生散列值衝突時,根據當前的index,直接計算下一個查找位置:
inline static uint32_t FirstProbe(uint32_t hash, uint32_t size) { return hash & (size - 1); } inline static uint32_t NextProbe( uint32_t last, uint32_t number, uint32_t size) { return (last + number) & (size - 1); }複製代碼
一樣地,查找的時候在下一個位置也是須要比較key值是否相等。
上面討論的都是數字的哈希,實符串如何作哈希計算呢?
以下所示,依次對字符串的每一個字符的unicode編碼作處理:
uint32_t AddCharacterCore(uint32_t running_hash, uint16_t c) { running_hash += c; running_hash += (running_hash << 10); running_hash ^= (running_hash >> 6); return running_hash; } uint32_t running_hash = seed; char *key = "hello"; for(int i = 0; i < strlen(key); i++){ running_hash = AddCharacterCore(running_hash, key[i]); }複製代碼
接着討論一個經典話題
以下,給一個數組,去掉裏面的重複值:
var a = [3, 62, 3, 38, 20, 42, 14, 5, 38, 29, 42]; 輸出 [3, 62, 38, 20, 42, 14, 5, 29]; 複製代碼
以下代碼所示:
function uniqueArray(arr){ return Array.from(new Set(arr)); }複製代碼
在控制檯上運行:
優勢:代碼簡潔,速度快,時間複雜度爲O(N)
缺點:須要一個額外的Set和Array的存儲空間,空間複雜度爲O(N)
以下代碼所示:
function uniqueArray(arr){ for(var i = 0; i < arr.length - 1; i++){ for(var j = i + 1; j < arr.length; j++){ if(arr[j] === arr[i]){ arr.splice(j--, 1); } } } return arr; }複製代碼
優勢:不須要使用額外的存儲空間,空間複雜度爲O(1)
缺點:須要頻繁的內存移動,雙重循環,時間複雜度爲O(N2)
注意splice刪除元素的過程是這樣的,這個我在《從Chrome源碼看JS Array的實現》已作過詳細討論:
它是用的內存移動,並非寫個for循環一個個複製。內存移動的速度仍是很快的,最快1s能夠達到30GB,如下圖所示:
以下代碼所示:
function uniqueArray(arr){ var retArray = []; for(var i = 0; i < arr.length; i++){ if(retArray.indexOf(arr[i]) < 0){ retArray.push(arr[i]); } } return retArray; }複製代碼
時間複雜度爲O(N2),空間複雜度爲O(N)
下面代碼是goog.array的去重實現:
和方法三的區別在於,它再也不是使用Array.indexOf判斷是否已存在,而是使用Object[key]進行哈希查找,因此它的時間複雜度爲O(N),空間複雜爲O(N).
最後作一個執行時間比較,對N = 100/1000/10000,分別重複1000次,獲得下面的表格:
Object + Array最省時間,splice的方式最耗時(它比較省空間),Set + Array的簡潔方式在數據量大的時候時間將明顯少於須要O(N2)的Array,一樣是O(N2)的splice和Array,Array的時間要小於常常內存移動操做的splice。
實際編碼過程當中一、二、4都是能夠可取的
方法1 一行代碼就能夠搞定
方法2 能夠用來添加一個Array.prototype.unique的函數
方法4 適用於數據量偏大的狀況
上面已經討論了哈希的數據結構,再來討論下棧和堆
棧的特色是先進後出,只有push和pop兩個函數能夠操做棧,分別進行壓棧和彈棧,還有top函數查看棧頂元素。棧的一個典型應用是作開閉符號的處理,如構建DOM。有如下html:
<html> <head></head> <body> <div>hello, world</div> <p>goodbye, world</p> </body> </html>複製代碼
將會構建這麼一個DOM:
上圖省略了document結點,而且這裏咱們只關心DOM父子結點關係,省略掉兄弟節點關係。
首先把html序列化成一個個的標籤,以下所示:
1 html ( 2 head ( 3 head ) 4 body ( 5 div ( 6 text 7 div ) 8 p ( 9 text 10 p ) 11 body ) 12 html)
其中左括號表示開標籤,右括號表示閉標籤。
以下圖所示,處理html和head標籤時,它們都是開標籤,因此把它們都壓到棧裏面去,並實例一個HTMLHtmlElement和HTMLHeadElement對象。處理head標籤時,因爲棧頂元素是html,因此head的父元素就是html。
處理剩餘其它元素以下圖所示:
遇到第三個標籤是head的閉標籤,因而進行彈棧,把head標籤彈出來,棧頂元素變成了html,因此在遇到第一個標籤body的時候,html元素就是body標籤的父結點。其它節點相似處理。
上面的過程,我在《從Chrome源碼看瀏覽器如何構建DOM樹》已經作過討論,這裏用圖表示意,可能會更加直觀。
函數執行的時候會把局部變量壓到一個棧裏面,以下函數:
function foo(){ var a = 5, b = 6, c = a + b; } foo();複製代碼
a, b, c三個變量在內存棧的結構以下圖所示:
先遇到a把a壓到棧裏面,而後是b和c,對函數裏面的局部變量不斷地壓棧,內存向低位增加。棧空間大小8MB(可以使用ulimit –a查看),假設一個變量用了8B,一個函數裏面定義了10個變量,最多遞歸8MB / (8B * 10) = 80萬次就會發生棧溢出stackoverflow
這個在《WebAssembly與程序編譯》這篇文章裏面作過討論。
數據結構裏的堆一般是指用數組表示的二叉樹,如大堆排序和小堆排序。內存裏的堆是指存放new出來動態建立變量的地方,和棧相對,以下圖所示:
討論完了棧和堆,再分析一個比較實用的技巧。
節流是前端常常會遇到的一個問題,就是不想讓resize/mousemove/scroll等事件觸發得太快,例如說最快每100ms執行一次回調就能夠了。以下代碼不進行節流,直接兼聽resize事件:
$(window).on("resize", adjustSlider);複製代碼
因爲adjustSlider是一個很是耗時的操做,我並不想讓它執行得那麼快,最多500ms執行一次就行了。那應該怎麼作呢?以下圖所示,藉助setTimout和一個tId的標誌位:
最後再討論下圖像和圖形處理相關的。
假設要在前端作一個濾鏡,如用戶選擇了本地的圖片以後,點擊某個按鈕就能夠把圖片置成灰色的:
效果以下:
一個方法是使用CSS的filter屬性,它支持把圖片置成灰圖的:
img{ filter: grayscale(100%); }複製代碼
因爲須要把真實的圖片數據傳給後端,所以須要對圖片數據作處理。咱們能夠用canvas獲取圖片的數據,以下代碼所示:
<canvas id="my-canvas"></canvas>複製代碼
JS處理以下:
var img = new Image(); img.src = 「/test.jpg」; //經過FileReader等 img.onload = function(){ //畫到canvas上,位置爲x = 10, y = 10 ctx.drawImage(this, 10, 10); } function blackWhite() { var imgData = ctx.getImageData(10, 10, 31, 30); ctx.putImageData(imgData, 50, 10); console.log(imgData, imgData.data); }複製代碼
這個的效果是把某張圖片原封不動地再畫一個,以下圖所示:
如今對imgData作一個灰化處理,這個imgData是什麼東西呢?它是一個一維數組,存放了從左到右,從上到下每一個像素點的rgba值,以下圖所示:
這張圖片尺寸爲31 * 30,因此數組的大小爲31 * 30 * 4 = 3720,而且因爲這張圖片沒有透明通道,因此a的值都爲255。
經常使用的灰化算法有如下兩種:
(1)平均值
Gray = (Red + Green + Blue) / 3
(2)按人眼對三原色的感知度:綠 > 紅 > 藍
Gray = (Red * 0.3 + Green * 0.59 + Blue * 0.11)
第二種方法更符合客觀實際,咱們採用第二種方法,以下代碼所示:
function blackWhite() { var imgData = ctx.getImageData(10, 10, 31, 30); var data = imgData.data; var length = data.length; for(var i = 0; i < length; i += 4){ var grey = 0.3 * data[i] + 0.59 * data[i + 1] + 0.11 * data[i + 2]; data[i] = data[i + 1] = data[i + 2] = grey; } ctx.putImageData(imgData, 50, 10); }複製代碼
執行的效果以下圖所示:
其它的濾鏡效果,如模糊、銳化、去斑等,讀者有興趣可繼續查找資料。
還有一種是圖形算法
以下須要計算兩個多邊形的交點:
這個就涉及到圖形算法,能夠認爲圖形算法是對矢量圖形的處理,和圖像處理是對所有的rgba值處理相對。這個算法也多種多樣,其中一個可參考《A new algorithm for computing Boolean operations on polygons》
綜合以上,本篇討論了幾個話題:
本篇從前端的角度對一些算法作一些分析和總結,只列了一些我認爲比較重要,其它的還有不少沒有說起。算法和數據結構是一個永恆的話題,它的目的是用最小的時間和最小的空間解決問題。可是有時候不用太拘泥於必定要最優的答案,可以合適地解決問題就是好方法,並且對於不一樣的應用場景可能要採起不一樣的策略。反之,若是你的代碼裏面動不動就是三四重循環,還有嵌套了不少if-else,你可能要考慮下采用合適的數據結構和算法去優化你的代碼。