1. 程式人生 > 其它 >JS知識點梳理之作用域、作用域鏈、柯里化、閉包

JS知識點梳理之作用域、作用域鏈、柯里化、閉包

一、作用域與作用域鏈

作用域是指 js 變數使用時所存在的一個區域,分為全域性作用域(window)和區域性作用域(function、setTimeout...等都會產生區域性作用域)。當局部作用域變數名與全域性作用域變數名重複時,區域性變數會覆蓋全域性變數。

在區域性作用域使用變數時,如果在自己作用域找不到對應變數,則會往上一級作用域查詢,直到全域性作用域,如果全域性作用域無此變數則會報 undefined。相反,全域性作用域中無法使用區域性作用域中的變數。

window.a = 1
function(){
  // 輸出 1,雖然區域性沒有 a 變數,但是 全域性中有。
  console.log(a)
  var b = 2
}
// 報錯,全域性中無法使用區域性變數。
console.log(b)

上面這種一層層向外查詢變數的過程叫做查詢作用域鏈。而這種一層層區域性作用域直到全域性作用域的結構被稱為作用域鏈。

// 全域性作用域,聲明瞭一個全域性變數 a
var a = 100

// 函式會生成區域性作用域
function acs(){
  // 在此區域性作用域中宣告一個區域性變數 b
    var b = 50
  // 輸出:100, 50
  console.log(a, b) // 執行過程:在此作用域查詢變數 a,
                                      // 找不到-->往上一級作用域找-->在全域性找到,使用全域性作用域中的a
                                      // 在此作用域查詢變數 b,查詢到了,使用此區域性變數的 b
}()

// 輸出:b is not defined
console.log(a, b)

二、閉包(Closure)

1. 閉包是什麼?

閉包是指在函式外部呼叫函式內部的區域性變數,且在呼叫後區域性變數不會被瀏覽器立即回收,會一直存在的一種私有變數。再簡單點說就是函式返回函式。

紅寶書中的描寫:閉包是指有權訪問另一個函式作用域中的變數的函式。

其實閉包就是返回一個函式,且這個函式對區域性變數存在引用形成的包含關係就是閉包。

其實就是建立一個不會被 GC 回收的區域性變數。也正因如此,閉包才會有記憶體洩漏的風險,需要在每次使用完後立刻清除。

閉包的形成:當前環境中存在指向父級作用域的引用。

2. 閉包的寫法

// 使用自執行函式形成閉包
var add = function(){
    let sum = 0
  return function operation(){
      return sum = sum ? sum + 1 : 1
  }
}()

// 輸出:1
add()
// 輸出:2
add()
// 輸出:3
add()
// 輸出:4
add()

// 清除閉包,刪除私有變數
add = null
// 輸出:null
console.log(add)
// 輸出:add is not function
add()

3. 閉包的作用

使用閉包的目的――隱藏變數,間接訪問一個變數,在定義函式的詞法作用域外,呼叫函式。

閉包通常在回撥函式、私有屬性、函式柯里化中使用。

4. 使用閉包實現多個圖片點贊功能

使用閉包完成,多圖點贊單獨點贊功能,且每個 input 的點贊數量互不干擾。在這個例子中利用閉包聲明瞭 5 個新的獨立詞法作用域。

<!-- * @Description: 閉包實現多圖點贊 * @Author: CY小塵s * @Date: 2021-07-28 18:39:33 * @LastEditTime: 2021-11-08 17:19:37 * @LastEditors: Please set LastEditors-->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>閉包實現多圖點贊</title>
  </head>
  <style>
    ul li {      list-style: none;      float: left;      margin: 0px 20px 20px 0px;    }    ul li img {      width: 200px;      height: 200px;    }  </style>
  <body>
    <ul>
      <li>
        <img
          class="img"
          src="https://www.keaidian.com/uploads/allimg/181206/co1Q206121F4-0-0.jpg"
          alt="你好"
        />
        <input type="button" class="add" value="當前點贊數量(1)" />
      </li>
      <li>
        <img
          class="img"
          src="https://www.keaidian.com/uploads/allimg/181206/co1Q206121F4-0-1.jpg"
          alt="你好"
        />
        <input type="button" class="add" value="當前點贊數量(1)" />
      </li>
      <li>
        <img
          class="img"
          src="https://www.keaidian.com/uploads/allimg/181206/co1Q206121F4-0-2.jpg"
          alt="你好"
        />
        <input type="button" class="add" value="當前點贊數量(1)" />
      </li>
      <li>
        <img
          class="img"
          src="https://www.keaidian.com/uploads/allimg/181206/co1Q206121F4-0-3.jpg"
          alt="你好"
        />
        <input type="button" class="add" value="當前點贊數量(1)" />
      </li>
      <li>
        <img
          class="img"
          src="https://www.keaidian.com/uploads/allimg/181206/co1Q206121F4-0-6.jpg"
          alt="你好"
        />
        <input type="button" class="add" value="當前點贊數量(1)" />
      </li>
      <li>
        <img
          class="img"
          src="https://www.keaidian.com/uploads/allimg/181206/co1Q206121F4-0-7.jpg"
          alt="你好"
        />
        <input type="button" class="add" value="當前點贊數量(1)" />
      </li>
    </ul>

    <script>
      window.onload = function () {        // 獲取 ul
        let add = document.querySelectorAll("ul li input");        // 迴圈取出每個 input 新增上閉包
        for (let i = 0; i < add.length; i++) {          // 給每個 input 新增點選事件
          add[i].onclick = (function () {            let sum = 2;            // 返回函式,完成私有變數的建立,形成閉包
            return function () {              this.value = "當前點贊數量(" + sum++ + ")";            };          })();        }      };    </script>
  </body>
</html>

5. 使用閉包保護私有屬性

建立一個計數器函式,在裡面定義一個私有屬性,這裡通過閉包保護它不會被直接修改。

可以看見在這個例子中我們並沒有直接操作 privatelyCounter,而是通過 makeCounter 主動暴露的方法來操作計數器中的 privateCounter。

// 建立一個計數器
const makeCounter = function(){
    // 建立私有變數
    var privatelyCounter = 0
    // 輸出私有變數
    function console(){
        return privatelyCounter
    }
    // 更改計數器方法
    function change(num){
        privatelyCounter += num
    }
    // 暴露公有方法
    return {
        CounterAdd(num){
            change(num)
        },
        CounterSub(num){
            change(num)
        },
        CounterLog(){
            return console()
        }
    }
}
// 宣告兩計數器
const counter1 = makeCounter()
const counter2 = makeCounter()
counter1.CounterAdd(1)
counter1.CounterAdd(1)
counter2.CounterSub(-1)
counter2.CounterSub(-1)
// 輸出:2
console.log(counter1.CounterLog())
// 輸出:-2
console.log(counter2.CounterLog())

參考 前端面試題詳細解答

三、使用閉包實現函式柯里化

所謂函式柯里化就是將一個多參函式轉為單參函式。

// 正常求自增方法
function numAdd(x, y){
    return x + y
}
console.log(numAdd(1, 2))

// 使用閉包實現柯里化
function numAddCurry(x){
    return function(y){
        return x + y
    }
}
// 先宣告一個變數拿到自增方法
const curry = numAddCurry(1)
// 在呼叫這個變數進行自增,輸出:3
console.log(curry(2))
// 亦或者直接呼叫自增方法傳入兩個引數,輸出也是:3
console.log(numAddCurry(1)(2))