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


閉包是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) // 進入該分支說明當前並沒有在計時,那么就開始一個計時
        }
    }
}

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM