[JavaScript閉包]Javascript閉包的判別,作用和示例

禿桔子發表於2021-11-09

閉包是JavaScript最重要的特性之一,也是全棧/前端/JS面試的考點。
那閉包究竟該如何理解呢?
如果不愛看文字,喜歡看視訊。那本文配套講解視訊已傳送到B站上供大家參考學習。
如果覺得有所收穫,可以給點個贊支援一下!
地址在這:
javascript閉包講解視訊

閉包函式的判斷和作用

閉包(closure)是Javascript語言的一個難點,也是它的特色,很多高階應用都要依靠閉包實現。
那如何判斷函式是一個閉包呢?接下來我會配合一些具體的例子來對閉包問題做講解。
首先問下大家,這個G函式是否是一個閉包呢?

const F = function A(){
    return function B(){
        return function C(){
            return function D(){  
                var a = 1;  
                return a++
            }
        }
    }
}
const G = F()()();
for(var i=0;i<10;i++){
    console.log(G())
}

一看就是不是對吧,在這裡面的G函式一看就是D函式,只不過長得比較怪而已。
如果是閉包函式那應該長成這樣

const F = function A(){
    var a = 1;  
    return function B(){
        return function C(){
            return function D(){  
                return a++
            }
        }
    }
}
const G = F()()();
for(var i=0;i<10;i++){
    console.log(G())
}

執行效果如下:

主要區別是這個變數a的宣告位置。如果a是在A中宣告的,那G就構成了閉包。也就是在G的作用域內,會形成一個名為closure作用域的子域。

那接下來第二個問題來了,這個a存在記憶體中的哪個位置呢?

在MDN中對JavaScript的定義是這樣的

一個函式和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函式被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函式中訪問到其外層函式的作用域。在 JavaScript 中,每當建立一個函式,閉包就會在函式建立的同時被建立出來。

好傢伙,看起來就很迷。

當定義形式難以理解的時候,我們需要語義,這也說明了一件事,我們需要偵錯程式!
進入偵錯程式後,一切就都明朗了起來。

我們清楚地看到,當指令碼執行到 D的內部時,這個Scope也就是作用域裡面包含了,Local作用域,Closure作用域和Script以及Global作用域。
Local不用說了,肯定就是函式外的物件,在這裡應該是window物件。
那Closure自然就是閉包作用域了。

我們依次執行時,可以清晰地看到,closure作用域內的a在不斷增加。

那第三個問題來了。

const F = function A(){
    var a = 1;  
    return function B(){
        return function C(){
            return function D(){  
                var a = 2;
                return a++
            }
        }
    }
}
const G = F()()();
for(var i=0;i<10;i++){
    console.log(G())
}

這裡的G是閉包函式嗎?

答案肯定不是,因為G已經能在D中找到 a變數了,那就不需要A再提供給他了,因此我們在偵錯程式中也看不到Closure了。

我們在這裡可以看到,根本沒有了之前的Closure了。

現在第四個問題來了,這個程式的執行結果是什麼?

const F = function A(){
    var a = 1;  
    return function B(){
        return function C(){
            var a = 2;
            return function D(){  
                return a++
            }
        }
    }
}
const G = F()()();
for(var i=0;i<10;i++){
    console.log(G())
}

這個是從2開始列印的,而非從1開始列印。
看到這,大家應該對閉包的優先順序有認識,閉包也是離得越近優先順序越高。

現在第五個問題來了,這個程式中,G的scope作用域裡存在幾個閉包?


const F = function A(){
    var b = 1;  
    return function B(){
      var c = 3;
        return function C(){
            var a = 2;
            return function D(){  
                b,c
                return a++
            }
        }
    }
}
const G = F()()();
for(var i=0;i<10;i++){
    console.log(G())
}

答案是3個,為什麼?這裡有兩個角度可以解釋

  1. bca在D中都沒有定義,之鞥能從A,B,C中找到abc,所以這裡存在三個閉包。
  2. 直接看偵錯程式就知道啦


在偵錯程式中我們能清楚地看到,這裡有三個閉包。不解釋!

閉包函式的示例

1.計數功能

在閉包函式的應用中,有很多,這裡舉個最常見的計數器的例子。


<html>
<head></head>
<body>
<script>
var A = (function B(){
    return function C(){
        var b = 0;
        return function D(){
            debugger
            return ++b;
        }
    }
})()

var E = A();
var F = A();
</script>

<button onclick="console.log('E='+ E())">E++</button>
<button onclick="console.warn('F='+ F())">F++</button>
</body>

</html>

開啟後執行效果如下:

點選E++和F++後的效果

在上面的例子中我們發現,我可以用一個類似物件導向的方法,去實現計數功能。

2.setTimeout

原生的setTimeout傳遞的第一個函式不能帶引數,通過閉包可以實現傳參效果。

function func1(a) {
    function func2() {
        console.log(a);
    }
    return func2;
}
var fun = func(1);
setTimeout(fun,1000);//一秒之後列印出1

3.回撥

定義行為,然後把它關聯到某個使用者事件上(點選或者按鍵)。程式碼通常會作為一個回撥(事件觸發時呼叫的函式)繫結到事件。
比如下面這段程式碼:當點選數字時,字型也會變成相應的大小。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>測試</title>
</head>
<body>
    <a href="#" id="size-12">12</a>
    <a href="#" id="size-20">20</a>
    <a href="#" id="size-30">30</a>

    <script type="text/javascript">
        function changeSize(size){
            return function(){
                document.body.style.fontSize = size + 'px';
            };
        }

        var size12 = changeSize(12);
        var size14 = changeSize(20);
        var size16 = changeSize(30);

        document.getElementById('size-12').onclick = size12;
        document.getElementById('size-20').onclick = size14;
        document.getElementById('size-30').onclick = size16;

    </script>
</body>
</html>

4.函式防抖

在事件被觸發n秒後再執行回撥,如果在這n秒內又被觸發,則重新計時。
實現的關鍵就在於setTimeOut這個函式,由於還需要一個變數來儲存計時,考慮維護全域性純淨,可以藉助閉包來實現。
如下程式碼所示:

/*
* fn [function] 需要防抖的函式
* delay [number] 毫秒,防抖期限值
*/
function debounce(fn,delay){
    let timer = null    //藉助閉包
    return function() {
        if(timer){
            clearTimeout(timer) //進入該分支語句,說明當前正在一個計時過程中,並且又觸發了相同事件。所以要取消當前的計時,重新開始計時
            timer = setTimeOut(fn,delay) 
        }else{
            timer = setTimeOut(fn,delay) // 進入該分支說明當前並沒有在計時,那麼就開始一個計時
        }
    }
}

總之閉包的用處很多,而且很廣泛。
希望這篇文章可以對大家能有所幫助!

相關文章