你們都知道ES6中的Map
是新增的一種數據結構。它相似對象,可是對象的鍵只能是字符串,Map
的鍵不限定是字符串,Map
的鍵能夠是一個
對象,能夠是布爾值等。Map
提供"值-值"的對應關係,是一種Hash
結構,但實際上ES6又比傳統Hash
多了一些特性。javascript
經過下面這個小例子,咱們看一下Map
比傳統Hash
多的特性是什麼:java
let m = new Map() m.set(1,1).set(2,2).set(3,3) //Map(3) {1 => 1, 2 => 2, 3 => 3} m.delete(2) //true Map(2) {1 => 1, 3 => 3} m.set(1,'1') //Map(2) {1 => "1", 3 => 3} m.set(4,4) //Map(3) {1 => "1", 3 => 3, 4 => 4}
我簡單羅列一下 Map
的api(但並非重點)node
這篇文章的重點不是講解api的,而是透過Map提供的這些api,可以體會到Map
底層的數據結構是什麼樣的c++
上面的例子,先初始化一個Map m,而後一次性set 3 個值,能夠看到此時的m是Map(3) {1 => 1, 2 => 2, 3 => 3}
正好和咱們
set的順序是對應的,而後刪除key=2的鍵值對,而後m是Map(2) {1 => 1, 3 => 3}
,而後從新m.set(1,'1')
,
這時m是Map(2) {1 => "1", 3 => 3}
,再m.set(4,4)
,能夠看到m是Map(3) {1 => "1", 3 => 3, 4 => 4}
。key=4的鍵值對是
在最後的,這標明瞭一種順序,在set新值的時候,保持着一種前後順序。es6
這個特性就是,Map
的插入老是保持着前後順序(刪除了中間元素也會保持順序),這和傳統Hash
並不同。傳統Hash
是一種散列結構,元素並不具有順序性,而Map很明顯,
後插入的元素就在最後,保持着這種前後順序。算法
Hash
本是哈希函數,表示爲index = fn(key)
,這裏的fn
就是哈希函數,它表明一種生成地址的規則,通常有直接定製法、數組分析法、
平方取中法等等,這離本篇文章跑遠了,哈希算法哪一種好,已經在計算機算法中有所研究,能夠看https://blog.csdn.net/u011109...
咱們模擬Map
的數據結構,就須要本身制定一個Hash函數。api
Hash是一段連續的有限的內存空間,根據散列函數H(key)和處理衝突的方法將一組關鍵字映射到一個有限的連續的地集(區間)上,並以關鍵字
在地址集中的"象"做爲記錄在表中的存儲位置,這種表就是散列表,這一映象過程稱爲散列造表或散列,所得的額存儲位置稱散列地址。
咱們能夠捋捋思路了,根據上文,Map
底層使用了Hash毋庸置疑,但保持順序這個特性,我猜想Map
的底層使用了鏈表數據結構(鏈表是前一個結點的next指針指向後一個結點),因此Map
的底層數據結構,使用了Hash
+ 鏈表實現。Map
能夠保證順序,也可使用O(1)的時間複雜度來找到某個元素,因此咱們初步方案,Hash的散列表存儲Map中的數據,同時每一個結點存入的前後順序使用鏈表的形式表示。數組
我畫了兩張示意圖,表示數據結構的一種基本實現和刪除某個結點時的實現,以下:數據結構
圖中,我爲了簡化默認散列表的長度是6,插入節點時,咱們使用Hash()
函數,根據傳入的key,獲取一個應該插入的位置index,好比第一個結點①,
得出index=1,那麼放入位置,再放一個結點②,得出index,這裏要有一個指針(next),從①指向②,這樣才能保證Map中數據的順序,而後是插入結點③。插入結點④
的時候,經過Hash
函數,得出和①結點相同的位置,這種現象叫作Hash碰撞,碰撞是不可避免的,好的Hash函數應該儘量少的出現碰撞,更加平均的使用散列表。
出現了碰撞,怎麼處理呢,這裏我選擇使用指針,鏈接①結點和④結點,這樣在同一個位置,會映射多個結點。(理論上應該儘量少的碰撞,不然影響查找速度)
,按照這樣的規律,依次建立告終點⑤和結點⑥dom
刪除某個結點時,傳入要刪除的key,經過Hash
函數,很是快的找到所在位置,不須要遍歷。然而咱們說了Map的值的插入是有順序的,好比刪除結點②,
爲了保持這種順序性,咱們使用鏈表的特性,把結點②的前一個結點①的next指針指向結點③。這裏不須要移動結點什麼,只是改變一下指針的指向,因此操做很是快
像Map的其餘操做,好比get(key)
就很容易了,關鍵是理解上面的兩個圖模型。好了,接下來用Javascript原生來模擬一個Map
數據結構,上代碼:
class ListNode { constructor(key, value) { this.key = key; this.value = value; this.next = null; //記錄插入順序 this.ne = null; //記錄Hash碰撞後的結點 } } function myMap() { this.init() } myMap.prototype.init = function(){ this.collection = new Array(6) //map底層用了hash算法。假如使用collection容器存放map中的數據 for (let i = 0; i < this.collection.length; i++) { this.collection[i] = Object.create(null) this.collection[i].ne = null; this.collection[i].next = null; } this.size = 0 this.head = null; //頭指針,老是指向第一個 this.tail = null; //尾指針,老是指向最後一個 } //插入或更新key結點 myMap.prototype.set = function (key, value) { let index = this.hash(key) //獲取容器中的位置 let node = this.collection[index] //得到index位置處的對象 while (node.ne) { if (node.ne.key === key) { node.ne.value = value //更新 return this //注意返回當前對象this } else { node = node.ne } } //map中沒有該key,就在鏈表尾部插入 let new_node = new ListNode(key, value) node.ne = new_node if (!this.tail) { this.tail = new_node } if (!this.head) { this.head = new_node //若是是第一個結點,頭指針指向它 } this.tail.next = new_node //尾指針 this.tail = new_node this.size++ return this //注意返回當前對象this } //獲取key結點的值 myMap.prototype.get = function (key) { let index = this.hash(key) let node = this.collection[index] //獲取容器相應位置處的對象 while (node) { if (node.key === key) { return node.value } else { node = node.ne } } return undefined } //刪掉key結點 myMap.prototype.delete = function (key) { if (!this.head) { return false } //從容器中刪除 let index = this.hash(key) let node = this.collection[index] let pre = null; while (node.ne) { if (node.ne.key === key) { let _prev = node; //從鏈表中刪除,須要前置結點 let _node = node.ne //保存要刪除的結點 _prev.ne = node.ne.ne//從容器中刪除,前置結點的指針指向要刪除結點的指針 //從鏈表中刪除 if (this.head === _node) { //若是要刪除的結點是頭結點 this.head = _node.next; } else if (this.tail === _node) { //若是要刪除的結點時尾結點 this.tail = _prev; //將尾指針指爲前置結點 _prev.next = null; //將前置結點的指針置爲空 } else { let cur = this.head; while (cur) { if (cur.key !== key) { pre = cur; cur = cur.next; } else { break } } pre.next = cur.next } this.size-- return true; } else { node = node.ne } } return false } myMap.prototype.has = function (key) { let index = this.hash(key) let node = this.collection[index] while (node.ne) { if (node.ne.key === key) { return true } else { node = node.ne } } return false } //返回鍵名的遍歷器 myMap.prototype.keys = function* () { let head = this.head // 遍歷鏈表,把鏈表中全部Key放入生成器中 while (head) { if (head.key) { yield head.key } head = head.next } } //返回鍵值的遍歷器 myMap.prototype.values = function* () { let head = this.head; while (head) { if (head.value) { yield head.value } head = head.next } } //返回全部成員 遍歷器 myMap.prototype.entries = function* () { let head = this.head; while (head) { if (head.key) { yield [head.key, head.value] } head = head.next } } myMap.prototype[Symbol.iterator] = myMap.prototype.entries //默認遍歷器接口,for of使用 //返回Map的全部成員,接受一個函數做爲第一個參數,第二個參數是thisArg 若是省略了 thisArg 參數,或者其值爲 null 或 undefined,this 則指向全局對象。 myMap.prototype.forEach = function (callbackFn, thisArg) { let head = this.head while (head) { if (head.key) { callbackFn.call(thisArg, head.key, head.value, this) head = head.next } } } myMap.prototype.clear = function () { this.init() } /** * @param any key * @return {number} */ //Hash 方法,Hash的速算法我本身模擬一個,真正實踐中的Hash算法確定十分複雜 myMap.prototype.hash = function (key) { let index = 0; if (typeof key === 'string') { //字符串的話取前10位,也不必所有遍歷完字符串,會影響性能、計算時間 for (let i = 0; i < 10; i++) { index += isNaN(key.charCodeAt(i)) ? 0 : key.charCodeAt(i) } } else if (typeof key === 'number') { index = isNaN(key) ? this.collection.length - 1 : key % this.collection.length } else if (typeof key === 'object') { // 若是傳入的是一個對象做爲鍵,es6中的Map,底層Hash算法必定跟它的內存地址有關,由於取值時,比較的是是不是同一個引用。就算給了一個字面量相同的值,也不能取到值,必須試試引用相同的值才能取到 // 這裏我只能模擬key爲對象時,經Hash算法獲得的都是index=0了 index = 0 } else if (typeof key === 'undefined') { index = 1 } else if (typeof key === 'boolean') { index = 2 } return index % this.collection.length }
因爲要注意的細節太多,因此我加上了註釋,必定要好好看註釋啊~
先說一句前提Javascript底層是用c/c++寫的,爲何這麼說呢?由於ES6的Map
能夠用對象做爲鍵,那麼在計算機底層,確定是和傳入對象的內存地址有關係的,Map
取值時,若是是引用類型,必須是相同的內存地址才能夠取到。
綜上,模擬的主要思路是,用Hash
思想來存儲數據,達到O(1)
的查找時間,用鏈表思想來維持插入數據的前後順序。細節點是,Hash
的碰撞處理,我是本身模擬Hash函數,由於Javascript底層是用
c/c++寫的,因此用js寫只能是叫"模擬"。我還用了單鏈表來處理碰撞,經過ne指針,能夠取到碰撞的後續值,這樣有個隱藏問題是,當碰撞多了,Map
的查找速度會慢,
其實也能夠用數組來處理碰撞,好處是Map
查找速度會快那麼點。
多說一句,在計算機領域Hash
碰撞算是一個課題,若是要深刻研究均可以寫篇論文了。Hash函數也有不少種,但以哪一種算法爲準,則要看使用場景,不過最終目標是同樣的,就是減小碰撞,均勻使用每一個存儲地址。
來驗證一下結果吧:
let m = new myMap(); m.set('a', 1).set('b', 2) console.log(m.get('a'), m.get('b')) //1 2 let obj = {name: 'lolita'} let obj2 = {name: 'obj2'} m.set(obj, 'obj') m.set(obj2, 'obj2') console.log(m.get(obj)) //obj console.log(m.size) //4 console.log(m.delete('a')) //true console.log(m.delete('a')) //false,由於'a'已經被刪除過了 console.log(m.delete(obj)) //true console.log(m.get(obj2)) // obj2 for (let key of m.keys()) { console.log('key:', key) //key: b key: { name: 'obj2' } } for (let value of m.values()) { console.log('value:', value) //value: 2 value: obj2 } for (let item of m.entries()) { console.log(item[0], item[1]) //b 2 { name: 'obj2' } 'obj2' } for (let [key, value] of m) { console.log(key, value); //b 2 { name: 'obj2' } 'obj2' } let ooo = {name: 'Jack'} m.forEach(function (key, value, map) { console.log("key: %s, value: %s", key, value) console.log(this) //forEach第二個參數不寫或者null或者undefined,this都會是全局對象 }, ooo) //key: b, value: 2 { name: 'Jack' } // key: [object Object], value: obj2 { name: 'Jack' } console.log(m.clear()) //undefined
通過驗證,徹底符合預期結果,能夠說模擬是到位的,歐耶~
Question:
不行,對象雖然也能夠達到get set的效果,可是在遍歷時,使用 for ... in 或者Object.keys()都會按升序自動排序問題
let generator = function* RandomRefFun() { const random = Math.random().toString(16).slice(2,8) yield `0x${random}`; } let RandomRefFun = generator() let randomRef = RandomRefFun().next().value
參考連接:
https://es6.ruanyifeng.com/#d...
https://dev.to/arthurbiensur/...