javascript 匿名函數

這幾年來,有個程式語言的名詞很夯,到處都在喊著 lambda, lambda, lambda ,就連 Java 下一次的改版重點也都放在 lambda 上面。
而其中我最喜歡的語言 Python 也有這個東西,到底什麼是 lambda 呢?

其實 lambda 就是所謂的匿名函式,而其中的始祖就是 functional language 的始祖 - Lisp 所生出來的一個應用。
對 js 而言,許多開發者也都不斷的嫌 js 奇怪、難用、噁心。但是仔細挖掘,會發現 functional language 才是 js 的根基。
就連被詬病的 prototype 的物件導向,其實也很完美的應用了 functional 的思想。

這幾天我仔細挖掘之後才發現到 js 的美,本篇會著重在 js 的匿名函式上。
依照我的習慣,我一樣會先從原始的歷史來源介紹一下。

(以下所稱的「功能」、「函數」、「函式」,我都是指同一個東西 'function')

  • 從 Lisp 開始(使用 Scheme)

    我會大致上講一下 Lisp 主要的結構,不過只會講重點而已,大致上如下:

    (功能 參數1 參數2...) ; 分號開始是註解
                        ; 括號裡參數可以很多,端看功能需要幾個參數
    

    一個正常的 lisp 一定是由一對括號組合起來。每個括號裡第一個參數代表的是你要做的功能 (function) ,緊跟著是這功能的參數。
    快速舉個例子:

    (+ 1 2)
    

    對這一對括號來說,他的功能是 '+' 這個功能,當然就是加法啦!那他的功能就是把參數1 跟參數2 給加起來。
    這就是為甚麼叫做函數式語言的原因,所有的 Lisp 都是以這個為基礎。

    • 定義變數

      來講講其中如何去定義一個變數。

      (define a 5) ; 使用 define 這個功能,其中第一個參數就是變數的名稱,第二個參數是他的值
      

      很好理解吧?使用 define 就宣告出一個變數,當然重度的 Lisp 使用者會說不是如此,對 Lisp 而言只是把 a 跟 5 做個關係的相連而已。
      會用這麼繞口的方式講,是因為 define 也可以拿來定義一個功能、或者大家講的函數

    • 定義函數

      (define (add x y) (+ x y)) ; 第一個參數是這個功能的名字和參數、第二個參數則是主體
      以下為使用 add:
      (add 1 2) ; 螢幕上會顯示回傳 3 這個數字
      

      看了這個例子就是前面為什麼要做特別補充了,因為第一個參數可以不只是單一個變數名稱,也可以是以一個括號定義一個函數(功能)的名稱以及參數,第二個參數也是用括號去定義一個要如何執行的本體。

      因此對於 define 來說,他個功能真的可以總結為把兩者的關係相連。

      (define 名稱 對象) ; 名稱 -> 對象
      
  • Lisp 的匿名函數

    剛剛講了這麼多,單純只是為了匿名函數去做鋪陳,因為我也想讓沒學過 Lisp 的人了解這一層關係。
    在 Lisp 中,宣告函式也是可以不必去宣告名稱的,只需要使用 lambda 這個功能,他就能幫你把一個沒有名字的函數宣告出來。

    (lambda (參數) (函數的主體))
    ex.
    (lambda (x y) (+ x y))
    

    如此一來,就有一個需要參數為 x 跟 y 的加法函數出來,但是目前這意義不大,因為不知道這家伙的名字,無法去呼叫這功能
    像剛剛還有辦法 (add 1 2) ,現在則是無從呼叫起

    但是剛剛有說過 define 可以連結一個名稱,我們可以試著:

    (define add (lambda (x y) (+ x y)))
    以下執行:
    (add 1 2) ;螢幕上會顯示 3 這個結果
    

    這跟宣告函數的方式根本就是同一個效果,只是剛剛是把函數的形體(名稱、參數),對主體(+ x y )做結合
    而這次是宣告一個名稱為 add 的變數去跟後面的沒有名稱的函數做連結
    各位應該還是覺得很白痴,為什麼同樣的事情要分兩種方法?
    除了意義上的不同, lambda 還能這樣做:

    ((lambda (x y) (+ x y)) 1 2) ;螢幕也會顯示結果為 3
    

    這就是 lambda 的妙用呀!還記得剛剛我說過,Lisp 的主軸是 (功能 參數1 參數 2) 吧?
    對最外頭的括號來說,拆了三個部份 (lambda (x y) (+ x y)), 1, 2 那個 lambda 就是功能的部份,接著這個括號會把 1 跟 2 丟下去算

    算完之後,就再也無法呼叫這個臨時做出來的加法功能了。因為他根本沒有名字,無從呼叫起呀!
    對一個好的直譯器來說,就會把他回收掉了。lambda 除了可以省記憶體之外,也能節省取名稱這件事。

    Python 中也已經導入 lambda 這功能了,很多 GUI 的功能你並不想取名字,也能直接與像是按鈕事件結合 有興趣可以參考一下我的 心理測驗計數器

  • 完美結合 functional language 的 js

    如同我一開始所說,許多人誤解 js 醜陋,是因為並不了解 functional language 。對 js 而言,他是使用像是 ALGOL 系語言的方式去宣告函數,而不是用括弧海並且把函數的名稱放在括弧內。

    基本的 function 宣告方式

    function add(x, y) {
        return x + y;
    }
    

    如此一來下次就能以 add(1, 2) 的方式呼叫加法的函數了
    對於 js 而言,他也能像剛剛 Lisp 那樣,可以宣告一個匿名函數跟變數名稱結合

    var add = function(x, y) {
        return x + y;
     }; // 這裡有分號,是因為把他當正常的運算把值丟給變數,事實上 EMCAScript 並沒有強求分號
    

    這樣做除了是把 add 變數跟一個沒有名字的 function 結合之外,兩者最大的不同點在於,前者會先被直譯器給挖出來。 因此前者可以:

    add(1, 2);
    function add(x, y) {
        return x, y;
    }
    

    因為前者會被挖出來之後,就一直在電腦裡面,等待其他人的呼叫。而使用後者將變數接上匿名函數,則像是當個基礎的運算。
    執行到 var add = function(x, y) {...} 這一行,才開始把這個函數的形體給準備好,丟給 add 這個變數。

    也因為這個特性,除了省空間,也多了彈性:

    var op = function(x, y) {
        return x + y;
    };
    op(1, 2); // 會回傳 3
    
    
    var op = function(x, y) {
        reutrn x - y;
    };
    op(5, 1); // 會回傳 4
    

    用變數這種方法去接上匿名函數,可以很彈性的切換 function 。
    接著匿名函式還有最經典,但是常常會弄得讓人沒有頭緒的樣貌。

    (function(x, y) {
         return x + y;
    })(1, 2);
    

    這答案一樣會回傳 1 + 2 的加法結果 3 ,那這個樣貌的匿名函式要如何看起呢?

    呼叫 function 的一般樣貌:
    函數(參數1, 參數2);
    

    這是我們一般呼叫 js 函數的方法,也就是說,對這個例子來說,那個匿名函式就是我們的主體。
    那為什麼需要前面的小括弧呢?如果沒有那個小括號,他就像是最一開始的宣告函數的樣子,直譯器期待你宣告一個函數的名稱。
    而小括號對 js 來講,算是個強迫運算,像我們平時運算 (2 + 3) * 5 會用小括號先強迫把 2 + 3 算出來。

    對這個小括弧而言,則是強迫生出個匿名函數並且呼叫

    具有名稱時我們是如此呼叫:
    add(1, 2);
    
    
    現在是匿名函數:
    (匿名函數)(1, 2);
    

    其實形式上還是一樣,單純只是變形的呼叫。這時使用匿名的最大大大優點就是「船過水無痕」。 任何你在裡頭做的事情,所佔的任何空間,到最後會馬上釋放。
    在小型 js 看不出來,但是再大型一點的程式,大家一股腦的宣告了一堆全域函數跟變數,如果瀏覽器還沒離開這個頁面,這些空間就一直被佔著,無法釋放。

    如果有同好有去看一下 jQuery 的原始碼,在前幾行就會看到這神奇的樣式了。

  • 總結

    其他說要導入 lambda 的語言,幾乎都會將 lambda 這個 keyword 抓進來,而對 js 而言則是完全不留痕跡的將這個 lambda 的特性導入,除了 lambda 這特性之外,就連物件導向也是利用函數式語言的方式去做到,並且完全不強求括弧海,完全是用大家最熟悉的 ALGOL 系語言去達成。

    總而言之,js 事實上是個優美的變形蟲,需要深入去了解才能體會他的美。未來等我研究更仔細的物件導向會在跟大家分享 js 的 OO。