躲不掉的函數和閉包

定義函數javascript

在 JS 中定義函數的方式有兩種:函數聲明函數表達式java

函數聲明 的語法爲:數組

function functionName(arg0, arg1, arg2) {
    // 函數體;
}
複製代碼

函數聲明的一個重要特徵就是 函數聲明提高 ,即在執行代碼以前會先讀取函數聲明,這意味着能夠把函數聲明放在調用它的語句後面:瀏覽器

sayHi(); // 'hi';
sayHi() {
    alert('hi');
}
複製代碼

第二種建立函數的形式是 函數表達式 。函數表達式有建立幾種不一樣的語法形式,下面是最多見的一種:閉包

var functionName = function(arg0, arg1, arg2) {
    // 函數體;
}
複製代碼

這看來好像是常規的變量複製語句,即建立一個函數並將它賦值給變量 functionName,在這種狀況下建立的函數叫作 匿名函數 ( 也稱爲 Lambda 拉姆達函數 ),在使用前必須先賦值。以下面的代碼會報錯:app

sayHi(); // 錯誤,函數還不存在;
var sayHi = function(){
    alert('hi');
}
複製代碼

理解函數提高的關鍵,就是理解函數聲明與函數表達式之間的區別。例如,執行如下代碼的結果可能會讓人意想不到:函數

if(condition) {
    function sayHi() {
        alert('hi');
    }
} else {
    function sayHi() {
        alert('Yo~');
    }
}
複製代碼

表面上看,上述代碼會在 condition 爲 true 時使用一個 sayHi() 的定義,不然就使用另外一個定義。實際上,這在 ECMAScript 中屬於無效語法,JavaScript 引擎會嘗試修正錯誤,將其轉換爲合理的狀態,大多數瀏覽器會返回第二個聲明,忽略 condition。ui

不過,若是是使用函數表達式,那就沒什麼問題:this

var sayHi;
if(condition) {
    sayHi = function() {
        alert('hi');
    }
} else {
    sayHi = function() {
        alert('Yo~');
    }
}
複製代碼

這樣不一樣的函數將根據不一樣的 condition 被賦值給 sayHi。spa

遞歸

遞歸函數是在一個函數經過名字調用自身的狀況下構成的:

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}
複製代碼

這是一個經典的遞歸階乘函數。雖然這個函數表面上看起來沒有問題,但下面的代碼卻可能致使它出錯:

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); // error!
複製代碼

在調用 anotherFactorial() 時,因爲必須執行 factorial(),而此時 factorial 已再也不是函數,因此會致使錯誤。

在這種狀況下,可使用 arguments.callee 解決問題。

  • arguments 是函數內部對象,它是一個類數組對象,包含着傳入函數中的全部參數。arguments 對象有一個 callee 屬性,該屬性是一個指針,指向 arguments 所在的函數。

所以能夠用它來實現對函數的遞歸調用:

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}
複製代碼

經過使用 arguments.callee 代替函數名,能夠確保不管怎樣調用函數都不會出問題。所以,在編寫遞歸函數時,使用 arguments.callee 總比使用函數名更保險。

不過在嚴格模式下,不能經過腳本訪問 arguments.callee,訪問這個屬性會致使錯誤。but~ 咱們可使用命名一個函數表達式來完成一樣的效果:

var factorial = (function f(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1);
    }
});
複製代碼

這樣遞歸調用仍能正常完成。

閉包

很多童鞋老是會混淆 匿名函數閉包 這兩個概念。 匿名函數 是沒有實際名字的函數;而 閉包 是指有權訪問另外一個函數做用域中的變量的函數。

而建立閉包的常見方式,就是在一個函數內部建立另外一個函數:

function compare(name) {
    return function(obj1, obj2) {
        var value1 = obj1[name]; // 能夠訪問到外部函數中的變量 name;
        var value2 = obj2[name];
        
        if (value1 < value2) {
            return -1;
        } else {
            return 1
        }
    };
}
複製代碼

之因此還可以訪問這個變量,是由於這個內部函數的做用域鏈中包含 compare() 的做用域。

而瞭解做用域的細節,對完全理解閉包相當重要:

當某個函數被調用時,會建立一個執行環境及相應的做用域鏈。而後,使用 arguments 和其它命名參數的值來初始化函數的活動對象。在做用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,......直至做爲做用域鏈終點的全局執行環境。

在函數執行過程當中,爲讀取和寫入變量的值,就須要在做用域鏈中查找變量,來看下面的例子:

function compare(value1, value2) {
    if (value1 < value2) {
        return -1;
    } else {
        return 1;
    }
}

var result = compare(5, 10);
複製代碼

上述代碼首先定義了 compare() 函數。而後又在全局做用域中調用了它。

當第一次調用 compare() 時,會建立一個包含 this、arguments、value1 和 value2 的活動對象。全局執行環境的變量對象(包含 this、result 和 compare)在 compare() 執行環境的做用域中則處於第二位。

下圖展現了包含上述關係的 compare() 函數執行時的做用域鏈:

後臺的每一個執行環境都有一個表示變量的對象 —— 變量對象。

全局環境的變量對象始終存在,而像 compare() 函數這樣的局部環境的變量對象,則只在函數執行的過程當中存在。

顯然,做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

不管何時在函數中訪問一個變量時,都會從做用域鏈中搜索具備相應名字的變量。通常來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域。

但,閉包的狀況又有所不一樣:

function compare(name) {
    return function(obj1, obj2) {
        var value1 = obj1[name]; // 能夠訪問到外部函數中的變量 name;
        var value2 = obj2[name];
        
        if (value1 < value2) {
            return -1;
        } else {
            return 1
        }
    };
}
複製代碼

在匿名函數從 compare() 中被返回後,它的做用域鏈被初始化爲包含 compare() 函數的活動對象和全局對象。

更爲重要的是,compare() 函數在執行完畢後,其活動對象也不會被銷燬,由於匿名函數的做用域鏈仍在引用這個活動對象,直到匿名函數被銷燬後,compare() 的活動對象纔會被銷燬。

// 建立函數;
var compareNames = compare('name');

// 調用函數;
var result = compareNames({name: 'Fly_001'}, { name: 'juejin' });

// 解除對匿名函數的引用,以便釋放內存;
compareNmaes = null;
複製代碼

Tips: 因爲閉包會攜帶包含它的函數的做用域,所以會比其它函數佔用更多的內存,因此過分使用閉包可能會致使內存佔用過多。

閉包與變量

做用域鏈的這種配置機制引出了一個值得注意的反作用,即閉包只能取得包含函數中任何變量的最後一個值。

別忘了閉包保存的是整個變量對象,而不是某個特殊的變量。

下面是一個經典的例子:

function createFunctions() {
    var result = [];
    
    for (var i = 0; i < 10; i ++) {
        result[i] = function() {
            return i;
        };
    }
    
    return result;
}
複製代碼

表面上看,彷佛每一個函數都應該返回本身的索引值,但實際上,每一個函數都返回 10。

由於每一個函數的做用域鏈中都保存着 createFunctions() 函數的活動對象,因此它們引用的都是同一個變量 i。

當 createFunctions() 函數返回時,變量 i 的值是 10,此時每一個函數都引用着變量 i 的同一個變量對象,因此在每一個函數內部 i 的值都是 10。

不過,咱們能夠經過建立另外一個匿名函數強制讓閉包的行爲符合預期:

function createFunctions() {
    var result = [];
    
    for (var i = 0; i < 10; i ++) {
        result[i] = function(num) {
            return function() {
                return num;
            };
        }(i);
    }
    
    return result;
}
複製代碼

在這個版本中,咱們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,並將當即執行該匿名函數的結果賦給數組。

這裏的匿名函數有一個參數 num,也就是最終要返回的值。

在調用每一個匿名函數時,咱們傳入了變量 i,並會將變量 i 的當前值複製給參數 num,而在這個匿名函數內部,又建立並返回了一個訪問 num 的閉包。

因此 result 數組中的每一個函數都有本身 num 變量的一個副本,所以就能夠返回各自不一樣的數值了。

另外,咱們如今能夠用 ES6 中的 let 命令實現上述效果:

function createFunctions() {
    var result = [];
    
    for (let i = 0; i < 10; i ++) {
        result[i] = function() {
            return i;
        };
    }
    
    return result;
}
複製代碼

Tips: 由於 let 聲明的變量只在所在的塊級做用域有效,因此每一次循環的變量 i 都是一個新的變量。

閉包中的 this 對象

咱們知道,this 對象是在運行時基於函數的執行環境綁定的:在全局函數中,this 等於 window;而當函數被做爲某個對象的方法調用時,this 等於那個對象。

不過, 匿名函數的執行環境具備全局性, 所以其 this 對象一般指向 window (在經過 call() 或 apply() 改變函數執行環境的狀況下,this 就會指向其它對象)。

但有時因爲編寫閉包的方式不一樣,這一點可能不會那麼明顯:

var name = 'The Window';

var object = {
    name: 'My Object',
    
    getName: function() {
        return function() {
            return this.name;
        };
    }
};

alert(object.getName()()); // 'The Window', ( 在非嚴格模式下 )
複製代碼
  • 因爲 getName() 會返回一個函數,因此調用 object.getName()() 就會當即調用它返回的函數。

不過,把外部做用域中的 this 對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了:

var name = 'The Window';

var object = {
    name: 'My Object',
    
    getName: function() {
        var that = this;
        return function() {
            return that.name;
        };
    }
};

alert(object.getName()()); // 'My Object';
複製代碼

在定義匿名函數以前,咱們把 this 對象賦值給 that 變量,且閉包也能夠訪問這個變量,即便在函數返回後,that 也仍然引用着 object,因此調用 object.getName()() 就返回了 'My Object'。

JavaScript 中的函數表達式和閉包都是極其有用的特性,利用它們能夠實現不少功能。

不過,由於建立閉包必須維護額外的做用域,過分使用它們可能會佔用大量內存,因此不要爲了閉包而閉包~

關於函數和閉包的淺薄知識就先講到這裏,若有不正確的地方,歡迎各位指正。【比心】

相關文章
相關標籤/搜索