其實說到底,在js中棧更像是一種變種的數組,只是沒有數組那麼多的方法,也沒有數組那麼靈活。可是棧和隊列這兩種數據結構比數組更加的高效和可控。而在js中要想模擬棧,依據的主要形式也是數組。html
從這篇文章開始,可能會接觸到一些原型,原型鏈,類,構造函數等相關的js概念,可是這裏並不會過多的介紹這些概念,必要的時候會進行一些簡要的說明,推薦你們去看看湯姆大叔的深刻理解Javascript系列,王福朋大神的深刻理解Javascript原型和閉包系列。都是極爲不錯的深度好文,推薦你們能夠深刻學習。es6
要想實現一個數據結構,首先你要明白它的基本原理,那麼棧是什麼?又是如何工做的呢?數組
棧(stack)是一種遵循後進先出(Last In First Out)原則的有序集合。新添加的元素和待刪除的元素都保存在棧的同一端,稱爲棧頂,另外一端就叫作棧底。在棧裏,新元素都接近棧頂,舊元素都靠近棧底。其實能夠把棧簡單理解成往一個木桶裏堆疊的放入物品,最後放進去的在桶的頂端,也是能夠最早拿出來的,而最早放進去的卻在桶的底部,只有把全部上面的物品拿出來以後才能夠拿走底部的物品。數據結構
對於數組來講,能夠添加元素,刪除元素,獲取數組的長度以及返回對應下標獲得值,那麼在開始構造一個棧以前,咱們須要瞭解一下棧都有哪些基本操做。閉包
一、壓棧,也稱之爲入棧,也就是把元素加入棧中。就像是數組中的push同樣。函數
二、出棧,移除棧頂的元素。就像是數組中的pop同樣。學習
三、獲取棧頂的元素,不對棧作任何其餘操做。就像是在數組中經過下標獲取對應的值同樣。測試
四、判斷棧是否爲空。就像是判斷數組的長度是否爲0同樣。this
五、清空棧,也就是移除棧裏的全部元素。就像是把數組的長度設置爲0同樣。spa
六、獲取棧裏的元素個數。就像是數組的length屬性同樣。
那麼,我相信我你們已經對棧有了一個基本的瞭解,那麼咱們接下來就看看如何經過構造函數來實現一個本身的js棧。
function Stack () { var items = []; //首先,咱們來實現一個入棧的方法,這個方法負責往棧里加入元素,要注意的是,該方法只能添加元素到棧頂,也就是棧的尾部。 this.push = function (ele) { items.push(ele) } } var stack = new Stack();
咱們聲明一個構造函數,而且在構造函數中生命一個私有變量items,做爲咱們Stack類儲存棧元素的基本支持。而後,加入一個push方法,經過this來使其指向調用該方法的實例。下面咱們還會經過這樣的方式依次添加其餘的方法。
function Stack () { var items = []; //首先,咱們來實現一個入棧的方法,這個方法負責往棧里加入元素,要注意的是,該方法只能添加元素到棧頂,也就是棧的尾部。 this.push = function (ele) { items.push(ele); } //而後咱們再添加一個出棧的方法,一樣的,咱們只能移除棧頂的元素。 this.pop = function (ele) { return items.pop(); } //查看棧頂,也就是棧的尾部元素是什麼 this.peek = function () { return items[items.length - 1]; } //檢查棧是否爲空 this.isEmpty = function () { return items.length == 0; } //檢查棧的長度 this.size = function () { return items.length; } //清空棧 this.clear = function () { items = []; } //打印棧內元素 this.print = function () { console.log(items.toString()) } }
這樣咱們就經過構造函數完整的建立了一個棧。咱們能夠經過new命令實例化一個Stack對象來測試一下咱們的棧好很差用。
var stack = new Stack(); console.log(stack.isEmpty());//true stack.push(1); stack.print(); stack.push(3); stack.print(); console.log(stack.isEmpty());//false console.log(stack.size());//2 stack.push(10); stack.print(); stack.pop(); stack.print(); stack.clear(); console.log(stack.isEmpty());//true
咱們發現咱們的Stack類執行的還算不錯。那麼還有沒有其餘的方式能夠實現Stack類呢?在ES6以前我可能會遺憾懵懂的對你Say No。可是如今咱們能夠一塊兒來看看ES6帶咱們的一些新鮮玩意。
在開始改造咱們的Stack類以前,須要先說一下ES6的幾個概念。Class語法,Symbol基本類型和WeakMap。簡單解釋一下,以對後面的改造不會一臉懵逼,而你們想要更深刻的瞭解ES6新增的各類語法,能夠去自行查閱。
Class語法簡單來講就是一個語法糖,它的功能ES5也是徹底能夠實現的,只是這樣看寫起來更加清晰可讀,更像是面向對象的語法。
Symbol是ES6新增的一個基本類型,前面幾篇文章說過,ES5只有6中數據類型,可是在ES6中又新增一種數據類型Symbol,它表示獨一無二的值。
WeakMap,簡單來講就是用於生成鍵值對的集合,就像是對象({})同樣,WeakMap的一個重要用處就是部署私有屬性。
固然,上面的簡單介紹可不只僅是這樣的,真正的內容要比這些多得多。
那麼在你們知道了它們的一些基本意義。我們開始改造一下Stack類
class Stack { constructor() { this.items = []; } push(element) { this.items.push(element); } pop() { return this.items.pop(); } peek() { return this.items[this.items.length - 1]; } isEmpty() { return this.items.length === 0; } size() { return this.items.length; } clear() { this.items = []; } toString() { return this.items.toString(); } print() { console.log(this.items.toString()) } }
這是用class來實現的Stack類,其實咱們能夠看一下,除了使用了constructor構造方法之外,其實並無什麼本質上的區別。
那麼咱們還可使用Symbol數據類型來實現,簡單改造一下:
const _items = Symbol('stackItems'); class Stack { constructor() { this[_items] = []; } push(element) { this[_items].push(element); } pop() { return this[_items].pop(); } peek() { return this[_items][this[_items].length - 1]; } isEmpty() { return this[_items].length === 0; } size() { return this[_items].length; } clear() { this[_items] = []; } print() { console.log(this.toString()); } toString() { return this[_items].toString(); } }
使用Symbol也沒有大的變化,只是聲明瞭一個獨一無二的_items來代替構造方法中的數組。
可是這樣的實現方式有一個弊端,那就是ES6新增的Object.getOwnPropertySymbols方法能夠讀取到類裏面聲明的全部Symbols屬性。
const stack = new Stack(); const objectSymbols = Object.getOwnPropertySymbols(stack); stack.push(1); stack.push(3); console.log(objectSymbols.length); // 1 console.log(objectSymbols); // [Symbol()] console.log(objectSymbols[0]); // Symbol() stack[objectSymbols[0]].push(1); stack.print(); // 1, 3, 1
不知道你們注意沒有,咱們定義的Symbol是在構造函數以外的,所以誰均可以改動它。因此這樣的方式還不是很完善的。那麼咱們還可使用ES6的WeakMap,而後用閉包實現私有屬性。
//經過閉包把聲明的變量變成私有屬性 let Stack = (function () { //聲明棧的基本依賴 const _items = new WeakMap(); //聲明計數器 const _count = new WeakMap(); class Stack { constructor() { //初始化stack和計數器的值,這裏的set是WeakMap的自身方法,經過set和get來設置值和取值,這裏用this做爲設置值的鍵名,那this又指向啥呢?自行console! _count.set(this, 0); _items.set(this, {}); } push(element) { //在入棧以前先獲取長度和棧自己 const items = _items.get(this); const count = _count.get(this); //這裏要注意_count但是從0開始的噢 items[count] = element; _count.set(this, count + 1); } pop() { //若是爲空,那麼則沒法出棧 if (this.isEmpty()) { return undefined; } //獲取items和count,使長度減小1 const items = _items.get(this); let count = _count.get(this); count--; //從新爲_count賦值 _count.set(this, count); //刪除出棧的元素,並返回該元素 const result = items[count]; delete items[count]; return result; } peek() { if (this.isEmpty()) { return undefined; } const items = _items.get(this); const count = _count.get(this); //返回棧頂元素 return items[count - 1]; } isEmpty() { return _count.get(this) === 0; } size() { return _count.get(this); } clear() { /* while (!this.isEmpty()) { this.pop(); } */ _count.set(this, 0); _items.set(this, {}); } toString() { if (this.isEmpty()) { return ''; } const items = _items.get(this); const count = _count.get(this); let objString = `${items[0]}`; for (let i = 1; i < count; i++) { objString = `${objString},${items[i]}`; } return objString; } print() { console.log(this.toString()); } } return Stack; })() const stack = new Stack(); stack.push(1); stack.push(3); stack.print(); // 1, 3, 1
這是最終比較完善的版本了。那麼不知道你們注沒注意到一個小細節,前面咱們只是聲明一個變量,先無論他是否是私有的,就是數組,整個Stack構造函數都是基於items數組來進行各類方法的。
可是這裏經過WeakMap做爲基本,咱們卻多用了一個_count,前面說了_count是計數器,那麼爲啥要用計數器?由於WeakMap是鍵值對的「對象類型」,自己是沒有像數組這樣的長度之說的,因此須要一個計數器來代替數組的下標,以實現基於Stack的各類方法。
到這裏基本上就完成了咱們的棧,下一篇文章會看看如何用咱們寫好的棧去作一些有趣事情。
最後,因爲本人水平有限,能力與大神仍相差甚遠,如有錯誤或不明之處,還望你們不吝賜教指正。很是感謝!