1. 程式人生 > >JavaScript閉包詳解

JavaScript閉包詳解

關於閉包:

ECMAScript中給閉包的定義: 閉包,指的是詞法表示包括不被計算的變數的函式,也就是說函式可以使用函式之外定義的變數。

閉包 是指有權訪問另一個函式作用域中的變數的函式,建立閉包的最常見的方式就是在一個函式內建立另一個函式,通過另一個函式訪問這個函式的區域性變數。
閉包的缺點就是常駐記憶體,會增大記憶體使用量,使用不當很容易造成記憶體洩露
閉包是javascript語言的一大特點,主要應用閉包場合主要是為了:設計私有的方法和變數。
一般函式執行完畢後,區域性活動物件就被銷燬,記憶體中僅僅儲存全域性作用域。但閉包的情況不同!

閉包的三個特點:

  1. 閉包是一個函式(函式巢狀函式)
  2. 閉包可以使用在它外面定義的變數(函式內部可以引用外部的引數和變數)
  3. 閉包存在定義該變數的作用域中(引數和變數不會被垃圾回收機制回收)

使用閉包的好處:

  1. 希望一個變數長期駐紮在記憶體中
  2. 避免全域性變數的汙染
  3. 私有成員變數的存在

變數作用域:

變數可分為全域性變數和區域性變數。全域性變數的作用域就是全域性性的,在 js 的任何地方都可以使用全域性變數。在函式中使用 var 關鍵字宣告變數,這時的變數即是區域性變數,它的作用域只在宣告該變數的函式內,在函式外面是訪問不到該變數的。

var func = function(){
var a = 'linxin';
console.log(a); // linxin
}
func();
console.log(a); // Uncaught ReferenceError: a is not defined

變數生存週期

全域性變數,生命週期是永久的。區域性變數,當定義該變數的函式呼叫結束時,該變數就會被垃圾回收機制回收而銷燬。再次呼叫該函式時又會重新定義了一個新變數。

var func = function(){
    var a = 'linxin';
    console.log(a);
}
func();

a 為區域性變數,在 func 呼叫完之後,a 就會被銷燬了。

var func = function(){
    var a = 'linxin';
    var func1 = function(){
        a += ' a';
        console.log(a);
    }
    return func1;
}
var func2 = func();
func2();                    // linxin a
func2();                    // linxin a a
func2();                    // linxin a a a

可以看出,在第一次呼叫完 func2 之後,func 中的變數 a 變成 ‘linxin a’,而沒有被銷燬。因為此時 func1 形成了一個閉包,導致了 a 的生命週期延續了。
這下子閉包就比較明朗了。

  1. 閉包是一個函式,比如上面的 func1 函式
  2. 閉包使用其他函式定義的變數,使其不被銷燬。比如上面 func1 呼叫了變數 a
  3. 閉包存在定義該變數的作用域中,變數 a 存在 func 的作用域中,那麼 func1 也必然存在這個作用域中。

現在可以說,滿足這三個條件的就是閉包了。

下面我們通過一個簡單而又經典的例子來進一步熟悉閉包。

for (var i = 0; i < 4; i++) {
        setTimeout(function () {
            console.log(i)
        }, 0)
    }

我們可能會簡單的以為控制檯會打印出 0 1 2 3,可事實卻打印出了 4 4 4 4,這又是為什麼呢?我們發現,setTimeout 函式時非同步的,等到函式執行時,for迴圈已經結束了,此時的 i 的值為 4,所以 function() { console.log(i) } 去找變數 i,只能拿到 4。

我們想起上一個例子中,閉包使 a 變數的值被儲存起來了,那麼這裡我們也可以用閉包把 0 1 2 3 儲存起來。

for (var i = 0; i < 4; i++) {
        (function (i) {
                setTimeout(function () {
                    console.log(i)
                }, 0)
        })(i)
}

當 i=0 時,把 0 作為引數傳進匿名函式中,此時 function(i){} 此匿名函式中的 i 的值為 0,等到 setTimeout 執行時順著外層去找 i,這時就能拿到 0。如此迴圈,就能拿到想要的 0 1 2 3。

記憶體管理

在閉包中呼叫區域性變數,會導致這個區域性變數無法及時被銷燬,相當於全域性變數一樣會一直佔用著記憶體。如果需要回收這些變數佔用的記憶體,可以手動將變數設定為null。
然而在使用閉包的過程中,比較容易形成 JavaScript 物件和 DOM 物件的迴圈引用,就有可能造成記憶體洩露。這是因為瀏覽器的垃圾回收機制中,如果兩個物件之間形成了迴圈引用,那麼它們都無法被回收。


function func() {
        var test = document.getElementById('test');
            test.onclick = function () {
                console.log('hello world');
        }
}

在上面例子中,func 函式中用匿名函式建立了一個閉包。變數 test 是 JavaScript 物件,引用了 id 為 test 的 DOM 物件,DOM 物件的 onclick 屬性又引用了閉包,而閉包又可以呼叫 test ,因而形成了迴圈引用,導致兩個物件都無法被回收。要解決這個問題,只需要把迴圈引用中的變數設為 null 即可。

function func() {
     var test = document.getElementById('test');
            test.onclick = function () {
            console.log('hello world');
    }
            test = null;
}

如果在 func 函式中不使用匿名函式建立閉包,而是通過引用一個外部函式,也不會出現迴圈引用的問題。

function func() {
    var test = document.getElementById('test');
    test.onclick = funcTest;
}
    function funcTest(){
    console.log('hello world');
}

參考