最近更新: 2007-02-13

The practice of anonymous recursion function in JavaScript

匿名遞迴函數在 JavaScript 中之實踐途徑。所謂遞迴函數即函數在其內部調用自己的函數,為了能夠自我調用,我們通常會為函數命名以便以名稱調用之。然而在某些語言中,其語言特性足以實踐匿名函數之遞迴。

在 JavaScript 中欲實踐此技法,須具備三個 ECMAScript/JavaScript 知識基礎:一、理解什麼是 Function object 及匿名函數;二、理解 operator() 的用法;三、理解如何以 Function.call()/Function.apply() 改變 this 的指涉對象。

首先,我取《為部落格加上「加入xx分享書籤」的按鈕》中一段遞迴函數程式碼作為本文範例。

    var d = encodeURIComponent((function getInnerText(node) {
      if (!node) return '';
      var t=[];
      for (var n = node.firstChild; n; n = n.nextSibling) {
        if (n.nodeType == 3) t.push(n.nodeValue);
        else t.push(getInnerText(n));
      }
      return t.join('');
    })((function() {
      var ds = document.getElementsByTagName('div');
      for (var i = 0; i < ds.length; ++i) {
        if (ds[i].className == 'main')
          return ds[i];
      }
    })()).substring(0, 4000-u.length-t.length));

該程式碼係以遞迴函數取出網頁中指定節點的所有 TextNode 的字串內容。為便於說明,先移除不需要的動作,並拆開成結構分明的敘述形式。

function getElementsByClassName(tagName, className) {
    var ds = document.getElementsByTagName(tagName, className);
    for (var i = 0; i < ds.length; ++i) {
        if (ds[i].className == className) {
            return ds[i];
        }
    }
}

var divMain = getElementsByClassName('div', 'main');

function getInnerText(node) {
    if (!node) return '';
    var t=[];
    for (var n = node.firstChild; n; n = n.nextSibling) {
        if (n.nodeType == 3) t.push(n.nodeValue);
        else t.push(getInnerText(n));
    }
    return t.join('');
}

var d = getInnerText(divMain);

在 JavaScript 中,函數也是一個 object ,以關鍵字 function 就可定義一個 Function object 及其內容。關鍵字 function 之後的符號則是此函數的名稱。如果沒有指定名稱,那麼這個函數就是一個匿名函數 (anonymous function) 。匿名函數的使用時機通常用於:一、做為一個引數傳給另一個函數,而另一個函數就藉參數名稱調用該匿名函數;二、被指派給 object 的屬性,使該屬性成為一個函數成員 (function member, method) (See also: 掌握 JavaScript 的「封裝」特性)。到此為止是 Function object 及匿名函數的知識基礎。

JavaScript 定義函數之語法有一個隱而不顯的意義。所謂「定義」就是配置一個 Function object ,亦即 new Function(); 。這實際上是一個建構運算,故會「傳回一個函數」。即然傳回了一個函數,我們自然可以接著操作其他可用於函數的運算子,其中最常使用的就是 operator( ) ,也就是執行這個函數。據此,我們可以把上述的 getElementsByClassName() 改寫為定義函數後直接調用之形式。基於運算子的運算優先順序限制,必須用 ( ) 括起函數定義的內容,要求 JavaScript 先完成函數的配置並傳回此函數。接著再以傳回的函數做為運算元直接參與 operator( ) 之運算。

此外,本例中此函數只被調用這麼一次,故一併改寫成匿名函數。展示匿名函數的第三種使用時機:以程式區塊的形式包括住一段程式碼及區域變數,實踐局部性之資料隔離。

var divMain = (function(tagName, className) {
    var ds = document.getElementsByTagName(tagName);
    for (var i = 0; i < ds.length; ++i) {
        if (ds[i].className == className) {
            return ds[i];
        }
    }
})('div', 'main');

到此為止,我們應該可以掌握到匿名函數及 operator( ) 的使用方式。最後我們要解決的問題是匿名函數如何在其內部調用自己以實踐遞迴動作。

直覺上,我們想到是否可以用 this 調用自己?答案是不行。依 ECMAScript/JavaScript 之規範指出,當 Function object 不是作為一個函數成員被調用時 (包含 new ) ,this 將指涉 Global object 而不是函數自身。在其他場合中,我們唯有透過 Function.call()/Function.apply() 調用函數,才能改變 this 之指涉對象 (See also: Function.prototype.call() and Function.prototype.apply())。底下先示範一個簡單的範式。

(function() {
    this();
}).call(function() {
    alert(this);
});

本文至此已經展示了實踐匿名遞迴函數的三個知識基礎。上列範式中定義了兩個匿名函數,第一個匿名函數在定義時同時調用其 call() 行為;第二個匿名函數則做為一個引數傳給第一個匿名函數的 call() 行為。此時第一個匿名函數中的 this 就是指涉第二個匿名函數了。只需再把 this 作為 this.call()/this.apply() 的引數,就能實踐遞迴動作。下列即最終實作結果。

var d =
(function(node) {
    return this.apply(this, [node]);
}).apply(
    function(node) {
        if (!node) return '';
        var t=[];
        for (var n = node.firstChild; n; n = n.nextSibling) {
            if (n.nodeType == 3) t.push(n.nodeValue);
            else t.push(this.apply(this, [n]));
        }
        return t.join('');
    },
    [(function(tagName, className) {
        var ds = document.getElementsByTagName(tagName, className);
        for (var i = 0; i < ds.length; ++i) {
            if (ds[i].className == className) {
                return ds[i];
            }
        }
    })('div', 'main')]
);

容我提醒,這是一個詭技,是我對 ECMAScript/JavaScript 之語言特性所做的自我挑戰,實務上並不建議使用此一詭技。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/2718420.html