其實說到底,在js中棧更像是一種變種的數組,只是沒有數組那么多的方法,也沒有數組那么靈活。但是棧和隊列這兩種數據結構比數組更加的高效和可控。而在js中要想模擬棧,依據的主要形式也是數組。
從這篇文章開始,可能會接觸到一些原型,原型鏈,類,構造函數等相關的js概念,但是這里並不會過多的介紹這些概念,必要的時候會進行一些簡要的說明,推薦大家去看看湯姆大叔的深入理解Javascript系列,王福朋大神的深入理解Javascript原型和閉包系列。都是極為不錯的深度好文,推薦大家可以深入學習。
要想實現一個數據結構,首先你要明白它的基本原理,那么棧是什么?又是如何工作的呢?
棧(stack)是一種遵循后進先出(Last In First Out)原則的有序集合。新添加的元素和待刪除的元素都保存在棧的同一端,稱為棧頂,另一端就叫做棧底。在棧里,新元素都接近棧頂,舊元素都靠近棧底。其實可以把棧簡單理解成往一個木桶里堆疊的放入物品,最后放進去的在桶的頂端,也是可以最先拿出來的,而最先放進去的卻在桶的底部,只有把所有上面的物品拿出來之后才可以拿走底部的物品。
對於數組來說,可以添加元素,刪除元素,獲取數組的長度以及返回對應下標得到值,那么在開始構造一個棧之前,我們需要了解一下棧都有哪些基本操作。
1、壓棧,也稱之為入棧,也就是把元素加入棧中。就像是數組中的push一樣。
2、出棧,移除棧頂的元素。就像是數組中的pop一樣。
3、獲取棧頂的元素,不對棧做任何其他操作。就像是在數組中通過下標獲取對應的值一樣。
4、判斷棧是否為空。就像是判斷數組的長度是否為0一樣。
5、清空棧,也就是移除棧里的所有元素。就像是把數組的長度設置為0一樣。
6、獲取棧里的元素個數。就像是數組的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的各種方法。
到這里基本上就完成了我們的棧,下一篇文章會看看如何用我們寫好的棧去做一些有趣事情。
最后,由於本人水平有限,能力與大神仍相差甚遠,若有錯誤或不明之處,還望大家不吝賜教指正。非常感謝!