函數表達式

JavaScript中有兩種定義函數的方式:函數聲明和函數表達式。使用函數表達式無須對函數命名,從而實現動態編程,也即匿名函數。有了匿名函數,JavaScript函數有了更強大的用處。

遞歸

遞歸是一種很常見的算法,經典例子就是階乘。也不扯其他的,直接說遞歸的最佳實踐,上代碼:

// 最佳實踐,函數表達式
var factorial = (function f(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1);
    }
});

// 缺點:
// factorial存在被修改的可能
// 導致 return num * factorial(num - 1) 報錯
function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}

// 缺點:
// arguments.callee,規範已經不推薦使用
function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}

遞歸就是這樣,好多人還在使用arguments.callee的方式,改回函數表達式的方式吧,這纔是最佳實踐。

囉嗦一句,好多人覺得遞歸難寫,其實你將其分爲兩個步驟就會清晰很多了。

邊界條件,通常是if-else。遞歸調用。
按這個模式,找幾個經典的遞歸練練手,就熟悉了。

閉包

很多人經常覺得閉包很複雜,很容易掉到坑裏,其實不然。

那麼閉包是什麼呢?如果一個函數可以訪問另一個函數作用域中的變量,那麼前者就是閉包。由於JavaScript函數可以返回函數,自然,創建閉包的常用方式就是在一個函數內部創建另一個函數!

這並沒有什麼神奇的,在父函數中定義子函數就可以創建閉包,而子函數可以訪問父函數的作用域。

我們通常是因爲被閉包坑了,纔會被閉包嚇到,尤其是面試題裏一堆閉包。

閉包的定義前面提了,如何創建閉包也說了,那麼我們說說閉包的缺陷以及如何解決?

/* 我們通過subFuncs返回函數數組,然後分別調用執行 */

// 返回函數的數組subFuncs,而這些函數對superFunc的變量有引用
// 這就是一個典型的閉包
// 那麼有什麼問題呢?
// 當我們回頭執行subFuncs中的函數的時候,我們得到的i其實一直都是10,爲什麼?
// 因爲當我們返回subFuncs之後,superFunc中的i=10
// 所以當執行subFuncs中的函數的時候,輸出i都爲10。
// 
// 以上,就是閉包最大的坑,一句話理解就是:
// 子函數對父函數變量的引用,是父函數運行結束之後的變量的狀態
function superFunc() {
    var subFuncs = new Array();
    for (var i = 0; i < 10; i++) {
        subFuncs[i] = function() {
            return i;
        };
    }

    return subFuncs;
}

// 那麼,如何解決上訴的閉包坑呢?
// 其實原理很簡單,既然閉包坑的本質是:子函數對父函數變量的引用,是父函數運行結束之後的變量的狀態
// 那麼我們解決這個問題的方式就是:子函數對父函數變量的引用,使用運行時的狀態
// 如何做呢?
// 在函數表達式的基礎上,加上自執行即可。
function superFunc() {
    var subFuncs = new Array();
    for (var i = 0; i < 10; i++) {
        subFuncs[i] = function(num) {
            return function() {
                return num;
            };
        }(i);
    }
    return subFuncs;
}

綜上,閉包本身不是什麼複雜的機制,就是子函數可以訪問父函數的作用域。

而由於JavaScript函數的特殊性,我們可以返回函數,如果我們將作爲閉包的函數返回,那麼該函數引用的父函數變量是父函數運行結束之後的狀態,而不是運行時的狀態,這便是閉包最大的坑。而爲了解決這個坑,我們常用的方式就是讓函數表達式自執行。

此外,由於閉包引用了祖先函數的作用域,所以濫用閉包會有內存問題。

好像把閉包說得一無是處,那麼閉包有什麼用處呢?
主要是封裝吧...

封裝

閉包可以封裝私有變量或者封裝塊級作用域。

➙ 封裝塊級作用域

JavaScript並沒有塊級作用域的概念,只有全局作用域和函數作用域,那麼如果想要創建塊級作用域的話,我們可以通過閉包來模擬。

創建並立即調用一個函數,就可以封裝一個塊級作用域。該函數可以立即執行其中的代碼,內部變量執行結束就會被立即銷燬。

function outputNumbers(count) {
    // 在函數作用域下,利用閉包封裝塊級作用域
    // 這樣的話,i在外部不可用,便有了類似塊級作用域
    (function() {
        for (var i = 0; i < count; i++) {
            alert(i);
        }
    })();

    alert(i); //導致一個錯誤! 
}

// 在全局作用域下,利用閉包封裝塊級作用域
// 這樣的話,代碼塊不會對全局作用域造成污染
(function() {
    var now = new Date();

    if (now.getMonth() == 0 && now.getDate() == 1) {
        alert("Happy new year!");
    }
})();

// 是的,封裝塊級作用域的核心就是這個:函數表達式 + 自執行!
(function() {
    //這裏是塊級作用域
})();

➙ 封裝私有變量

JavaScript也沒有私有變量的概念,我們也可以使用閉包來實現公有方法,通過隱藏變量暴露方法的方式來實現封裝私有變量。

(function() {
    //私有變量和私有函數
    var privateVariable = 10;

    function privateFunction() {
        return false;
    }

    //構造函數
    MyObject = function() {};
    //公有/特權方法
    MyObject.prototype.publicMethod = function() {
        privateVariable++;

        return privateFunction();
    };
})();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章