閉包?什麼是閉包?--JavaScript前端

語言: CN / TW / HK

閉包的背景

由於js中只有兩種作用域,全域性作用域和函式作用域(模組作用域和塊級作用域的原理也是匿名函式作用域實現的),而在開發場景下,將變數暴露在全域性作用域下的時候,是一件非常危險的事情,特別是在團隊協同開發的時候,變數的值會被無意篡改,並且極難除錯分析。這樣的情況下,閉包將變數封裝在區域性的函式作用域中,是一種非常合適的做法,這樣規避掉了被其他程式碼干擾的情況。

閉包的使用

下面是一種最簡單直接的閉包示例 javascript //媽媽本體 function mother(){ //口袋裡的總錢數 let money = 100 //消費行為 return function (pay){ //返回剩餘錢數 return money - pay } } //為兒子消費 let payForSon = mother() //列印最後的剩餘錢數 console.log(payForSon(5)) 為了便於理解,我們將外部函式比喻為媽媽本體,裡面儲存著總錢數這個變數和消費這個行為,通過建立為兒子消費的這個行為物件,然後執行這個行為花費5元,返回剩餘的95元。

這個就是為了將變數money儲存在mother本體內而避免暴露在外部的全域性環境作用域中,只能通過mother()建立消費行為來影響money這個變數。

由此可以歸納總結使用閉包的三個步驟 1. 用外層函式包裹變數,函式; 2. 外層函式返回內層函式; 3. 外部用變數儲存外部函式返回的內層函式

目的是為了形成一個專屬的變數,只在專屬的作用域中操作。

上述的閉包程式碼示例中,有一個缺陷的場景是,在後續不需要money變數的情況下,沒有釋放該變數,造成記憶體洩露。原因是payForSon這個函式的作用域鏈引用著money物件,解決的辦法是將payForSon = null就可以釋放方法作用域,進而解除對money的引用,最後釋放money變數。

閉包的擴充套件

函式柯里化

在開發的場景中,有時需要通過閉包來實現函式的柯里化呼叫。呼叫示例如下 alert(add(1)(2)(3)) 這種連續的傳參呼叫函式,叫做函式柯里化。

通過閉包的實現方式如下 function add(a){ //儲存第一個引數 let sum = a function tmp(b){ //從第二個函式開始遞加 sum = sum + b //返回tmp,讓後續可以繼續傳參執行 return tmp } tmp.toString = function(){ return sum } //返回加法函式 return tmp } alert(add(1)(2)(3)) 下面我們來一步步分析, 1. add(1)執行時,儲存第一個引數到sum變數中,返回tmp函式 2. add(1)(2)執行等於tmp(2),將2的值加到了變數sum上,返回tmp函式本身 3. add(1)(2)(3)執行等同於上述步驟的加到比變數sum上,返回tmp函式本身 4. alert(add(1)(2)(3))執行時,alert需要將值轉為string顯示,最後的tmp函式執行tmp.toString,返回sum的值。

矩陣點選應用

該例子的demo程式碼在我的github上,可以自行取閱

需求:在一個4*4的矩陣方塊中,實現點選每個按鈕時記錄下各自的點選次數,相互之間互不干擾。

思路:在按鈕事件中使用閉包,建立獨立的儲存變數空間。

注意:下列的方案1到方案3是逐次演進的優化方案,需要按照方案標號的次序逐層理解,更有利於理解最終的優化方案

方案1

```

... let container = document.getElementById('container') for (let r = 0; r < arr.length; r++) { for (let c = 0; c < arr[r].length; c++) { let cell = document.createElement('div') cell.innerHTML = (${r},${c}) container.append(cell) cell.onclick = (function () { let n = 0 return function () { n++ cell.innerHTML = 點${n} } })() } } ``` 在每個按鈕上通過onclick繫結閉包方法,儲存操作獨立的n變數,這樣就可以單獨記錄每個按鈕的點選次數

缺點:這樣做有一個不足的地方是,外部無法獲取內部的n變數,不能實現與外部的互動,比如按鈕間的相互影響。

方案2

為了改善方案1的缺點,我們引入外部資料arr來操作管控按鈕點選數。 程式碼示例如下: let arr = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], ] let container = document.getElementById('container') for (let r = 0; r < arr.length; r++) { for (let c = 0; c < arr[r].length; c++) { let cell = document.createElement('div') cell.innerHTML = `(${r},${c})` container.append(cell) cell.onclick = (function (r, c) { return function () { arr[r][c]++ cell.innerHTML = `點${arr[r][c]}` } })(r, c) } } 參照方案1 ,改動點包含兩個 * 新增arr二維陣列來記錄點選數,這樣可以達到與外部互動的目的 * onclick繫結的事件新增r,c兩個引數,並且執行時傳參進入,這樣就可以把行列引數傳遞到方法內部(onclick的執行環境作用域與r,c所在的環境不一致,所以無法直接使用)

這樣改進完以後,外部可以通過操作arr來與每個按鈕的點選次數進行互動。

缺點:這樣會將arr暴露在全域性作用域下(可以在console控制檯訪問到),很容易被其他人或者模組誤操作,也不利於封裝

方案3

基於方案2的改進實現為,用一個立即執行的函式包裹住整個執行程式碼,這樣就構建了一個函式作用域來封裝arr變數為私有。程式碼如下: (function () { let arr = [ [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], ] let container = document.getElementById('container') for (let r = 0; r < arr.length; r++) { for (let c = 0; c < arr[r].length; c++) { let cell = document.createElement('div') cell.innerHTML = `(${r},${c})` container.append(cell) cell.onclick = (function (r, c) { return function () { arr[r][c]++ cell.innerHTML = `點${arr[r][c]}` } })(r, c) } } })() 這樣一個相對完整的按鈕點選次數的方案就完成了。

使用call實現bind

這個需要有call和bind的使用知識的前提,可以自行百度哈

廢話不多說,直接上程式碼 ``` Function.prototype.bind = function(obj){ console.log('呼叫自定義bind函式'); //儲存當前函式物件 let fun = this //去除第一個obj引數,並且轉換為js陣列 let outerArg = Array.prototype.slice.call(arguments,1) return function(){ //將arguments轉為js陣列 let innerArg = Array.prototype.slice.call(arguments) //彙總所有引數 let totalArg = outerArg.concat(innerArg) //呼叫外部儲存的函式,並且傳參 fun.call(obj,...totalArg) } }

//呼叫示例 let zhangsan = {name:'wawawa'} function total(s1,s2){ console.log(this.name + s1 + s2); } let bindTotal = total.bind(zhangsan,100) bindTotal(200) ``` 重寫函式類的bind函式, 1. 先將函式物件(也就是下面示例中的total函式)儲存在fun變數中,等於閉包外層儲存了fun,obj以及其他繫結的引數(由於arguments是類陣列物件,需要轉換為陣列,且去除第一個函式obj); 2. 然後返回匿名函式,在匿名函式中,將外部和內部的引數進行轉換和拼接; 3. 最後通過fun.call(obj,...totalArg),呼叫儲存的函式物件fun,並且通過call來實現傳遞繫結的作用域obj,和其他引數totalArg

注意: * arguments是類陣列物件,不能直接使用陣列方法,需要轉化為陣列操作 * 外層函式arguments轉化時,需要剔除掉obj,因為下面的fun.call需要單獨傳遞obj作為函式作用域 * totalArg傳遞給call函式時,需要通過...語法糖攤開陣列