天天看點

像Lisp一樣寫JavaScript--建構棧

老子有言:“道生一,一生二,二生三,三生萬物!”說來慚愧,我始終未能領會其中奧義。直到最近學習lisp,雖隻略知其皮毛,卻隐約感到Lisp中蘊藏了高深莫測的思想,驚喜和感慨之餘,便在前寫下了《Javascript實作Lisp清單(list)及操作》的筆記。但仔細想來這僅僅隻是讓JavaScript具有像Lisp一樣的能力,并沒有像Lisp一樣去使用這些能力,然而後者才是Lisp真正的奧義所在。

與老子的名言不同,Lisp的奧義并非深不可測,反而是簡單到了極緻。鬥膽總結一下便是:“道生原子,原子生清單,清單生萬物!”。Lisp做到這些隻用了三個操作cons、car、cdr。cons操作用兩個值來構成一個cons對象,car和cdr則是通路兩個值的方法。Lisp的能力很大一部分要歸于cons的簡單但是強大的資料描述能力。 一個cons對象包含兩個值,這個值可以是原子對象,也可以是cons對象本身,cons對象就像是一個可以分裂的細胞,細胞複生細胞,通過不同的組合構成了Lisp程式所需要的資料結構。下面的各種結構各異的結構僅僅是常用的一些資料結構:

像Lisp一樣寫JavaScript--建構棧
像Lisp一樣寫JavaScript--建構棧

圖一

像Lisp一樣寫JavaScript--建構棧
像Lisp一樣寫JavaScript--建構棧

圖二

像Lisp一樣寫JavaScript--建構棧
像Lisp一樣寫JavaScript--建構棧

圖三

  • 用Lisp清單來實作棧操作

實作資料結構棧是個說明Lisp好處的很好例子, 棧的特點是後進先出(last in first out) ,  我們很容易發現它是由棧頂元素和剩下的部分組成,這和清單的特點驚人的相似,如圖一中,一個清單有兩部分組成分别通過car和cdr,那麼我們可以用清單直接來表示棧,car表示棧頂cdr表示剩餘部分。對于棧push操作,隻需要通過cons來建立一個新清單,即(cons obj stack) 傳回新清單,一個cons 相當于一次push操作。如(cons 42 (cons 69 (cons 613 nil))) 相當與 連續把613、69、42 壓入空堆棧,構成如圖二中的清單。Lisp可以用宏很容易的定義出這兩個操作:

(defmacro our-push (obj lst)
  `(setf ,lst (cons ,obj ,lst)))

(defmacro our-pop (lst)
	`(let ((x (car ,lst)))
		(setf ,lst (cdr ,lst))
		x))
           
  • 用JavaScript實作棧

在JavaScript去實作一個棧并無必要,因為Array已經包含了push,pop操做完全滿足了需要。 但是本着沒有困難也要創造困難的精神,我們才有機會探索JavaScript這麼語言充滿想象力的語言。在上一篇筆記《Javascript實作Lisp清單(list)及操作》 介紹了Javascript中實作cons、car、cdr的操作, 它們的實作實際上并未依賴任何JavaScript提供的資料結構,而是是通過JavaScript閉包的特性,把資料放在function對象中。 是以可以說,JavaScript中function可以用來表示清單。今天我們将故技重演,讓它來為我們表示棧, Stack類的代碼如下: 

var Stack = (function() {
    function Stack() {
        this.stack;
    };

    Stack.cons = function(x, y) {
        return function(lambda) {
            return lambda(x, y);
        }
    }

    Stack.prototype.push = function(obj) {
        this.stack = Stack.cons(obj, this.stack);
        return this;
    }

    Stack.prototype.pop = function() {
        if (this.stack === undefined) {
            return undefined;
        }
        var top = this.stack(function(x, y) {
            return x;
        });
        this.stack = this.stack(function(x, y) {
            return y;
        });
        return top;
    }
    return Stack;
})();
           

代碼解釋:

如前所訴,我們仍然利用閉包,利用function對象來表示棧, 函數Stack.cons用于傳回一個新的棧。像在Lisp中的棧一樣,棧就是棧頂和剩餘部分的組合,是以棧push操作就是把新元素和舊棧組合傳回一個新的棧

Stack.cons = function(x, y) {
        return function(lambda) {
            return lambda(x, y);
        }
    }
Stack.prototype.push = function(obj) {
        this.stack = Stack.cons(obj, this.stack);
        return this;
    }
           

我們大可以把Stack.cons函數看成Lisp中的cons,把它傳回值function對象看成是lisp中的cons對象,首先它像(cons x y)一樣包含了兩個參數,其次是function本身可以當成參數作為Stack.cons的參數以構成更複雜的對象。

有了棧的建立方法,自然還要可以讀取棧頂和剩下部分的方法,就像是有了cons操作還有對于的car和cdr一樣。pop操作就提供了通路堆棧中的棧頂和剩餘部分的方法, 這裡有有意思的是,棧被定義成是一個高階函數,傳遞一個函數給它,棧就可以傳回所包含的資料(x, y), 如果不這樣做将很難取到它隐藏的秘密。

Stack.prototype.pop = function() {
        if (this.stack === undefined) {
            return undefined;
        }
        var top = this.stack(function(x, y) {
            return x;
        });
        this.stack = this.stack(function(x, y) {
            return y;
        });
        return top;
    }
           

總結:在使用JavaScript實作清單操作和剛剛構造的棧的實作中,都是function來描述所需的資料結。這樣的function有兩個特點: 閉包和高階函數。 閉包的作用是儲存資料,而高階函數的作用是為了通路資料。

繼續閱讀