js中的函式巢狀和閉包

飛鷹3995發表於2022-03-22

小編已經有一段時間沒有更新文章了,最近一直在考慮接下來要更新什麼內容。接下來,小編會圍繞以下三個方面更新文章。實際專案中遇到的問題和解決方案、Vue原始碼解析、程式碼重構、關於資料視覺化。小編也會按照這個順序,逐步的去更新。期待著一起進步。

今天就先和大家一起聊一聊我理解的閉包。在聊這個問題之前,先了解一下變數的定義域。

在js中,變數定義域有全域性作用域和區域性作用域之說。es6中新出現的變數宣告關鍵字,就是為了解決部分變數作用域混亂引入的。全域性作用域在這就不談了。主要說說函式的作用域。

一、作用域

簡單一點說,函式的作用域,就是函式的花括號內部,先看兩個例子,或許能對這個概念理解的更好一點

function f1(){
  let n = 999
  console.log(n)
}
f1() // 999

function f2(){
  let n = 999
}
alert(n); // 報錯

二、函式的返回值

要說閉包之前,我得先說一下函式返回值。關於函式的返回值,小編也是年初才有了一個更深層次的理解。沒有返回值的函式,執行之後會返回undefined,有返回值的函式,執行之後就變成了對應的返回值。就像這樣

// 沒有返回值的函式
function f1(){
  alert(666)
}
console.log(f1()) // 出現彈窗之後,在控制檯輸出undefind

// 存在返回值
function f2(){
  alert(666)
  return 'over'
}
console.log(f2()) // 出現彈窗之後,在控制檯輸出over。當然,可以返回字串,也可以返回Bealon,還可以返回函式。

三、函式巢狀
在《重構——改善既有程式碼的設計》中,提出了js語法是允許函式內部巢狀函式的,但並不是所有的程式語言都可以的,所謂程式碼巢狀,就是在函式內部又有函式宣告,就像這樣:

function outer(){
  let name = 'lilei'
  function inner(){
    console.log(name)
  }
} 

四、閉包
前面說明了在js中的區域性變數作用域的問題,在實際專案中,就是需要在函式外部,訪問函式內部的變數,這個時候,按照區域性變數作用域的問題。似乎是不可能的,閉包的出現,解決了這個問題。

function outer(){
  let name = 'lilei'
  function inner(){
    return name
  }
  return inner
}

上面是一個典型的閉包函式,在使用這個閉包函式的時候,我們可以這樣:

let g = outer()
console.log(g()) // lilei

至此,已經解決了在全域性訪問函式內的區域性變數。但是小編在回家的路上在想,為了實現這個功能,是不是不用這個麻煩,我通過這樣的函式,也是可以滿足需求的。

function outer(){
  let name = 'lilei'
  return name
}

console.log(outer()) // lilei  

確實上面的程式碼和通過閉包最終在控制檯輸出的內容是一樣的,那為什麼還要引入閉包呢?小編也是想了接近一週才明白的,這就好比變數->函式->類,每層往上都是逐步提升的過程,通過函式可以實現更多的邏輯,比如對資料進行處理,僅僅靠變數是不能實現的。

五、閉包的實際應用
上面小編介紹了閉包,那麼在實際專案中有什麼應用呢?先看下面程式碼:

1、隱藏內部變數名稱和函式執行暫停

function outer() {
    let name = 1
    function inner() {
        return name ++
    }
    return inner
}
let g = outer()
console.log(g()) // 2
console.log(g()) // 3
console.log(g()) // 4
console.log(g()) // 5

2、setTimeout函式傳遞引數

預設的setTimeout是這樣的

 

 

 

小編也曾經這樣試驗過

function f1(p) {
    console.log(p)
}
setTimeout(f1(666),3000) // 並沒有延時,直接輸出666

要想通過延時來實現對函式傳遞引數,這時候,閉包的作用就顯現出來了。

function f1(a) {
    function f2() {
        console.log(a);
    }
    return f2;
}
var fun = f1(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) // 進入該分支說明當前並沒有在計時,那麼就開始一個計時
        }
    }
}

六、使用類實現類似閉包中隱藏內部變數功能

上面是一個關於閉包的實際應用,小編在晚上睡不著覺的時候,想起同樣的需求,是不是也可以通過類來實現呢?最後經過一頓折騰,答案是肯定的,就像這樣:

class Adder{
    constructor(c){
        this._c = c
    }
    increace(){
        this._c ++ 
    }
    decreace(){
        this._c --
    }
    get finalNum(){
        return this._c
    }
}
let c = new Adder(1)
c.increace()
console.log(c.finalNum) // 2
c.increace()
console.log(c.finalNum) // 3
c.increace()
console.log(c.finalNum) // 4
c.decreace()
console.log(c.finalNum) // 3

參考文章:https://www.cnblogs.com/gg-qq/p/11399152.html                    

    https://www.cnblogs.com/pikachuworld/p/5325868.html
            https://developer.mozilla.org/zh-CN/docs/Web/API/setTimeout

相關文章