本篇文章給大家?guī)砹岁P(guān)于JavaScript的相關(guān)知識,其中主要介紹了關(guān)于JavaScript閉包的相關(guān)問題,閉包的概念有很多版本,不同的地方對閉包的說法不一,下面一起來看一下,希望對大家有幫助。
什么是閉包?
閉包的概念是有很多版本,不同的地方對閉包的說法不一
維基百科:在計算機科學(xué)中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures),是在支持頭等函數(shù)的編程語言中實現(xiàn)詞法綁定的一種技術(shù)。
MDN: 閉包(closure)是一個函數(shù)以及其捆綁的周邊環(huán)境狀態(tài)(lexical environment,詞法環(huán)境)的引用的組合。
個人理解:
- 閉包是一個函數(shù)(返回一個函數(shù))
- 返回的函數(shù)保存了對外變量引用
一個簡單的示例
function fn() { let num = 1; return function (n) { return n + num } }let rFn = fn()let newN = rFn(3) // 4
num 變量作用域在 fn 函數(shù)中, rFn 函數(shù)卻能訪問 num 變量,這就是閉包函數(shù)能訪問外部函數(shù)變量。
從瀏覽器調(diào)試和 VSCode Nodejs 調(diào)試看閉包
- 瀏覽器
- VS Code 配合 Node.js
看到 Closure 中 fn 是閉包函數(shù),其中保存 num 變量。
一個經(jīng)典的閉包:單線程事件機制+循環(huán)問題,以及解決辦法
for (var i = 1; i <= 5; i++) { setTimeout(() => { console.log(i); }, i * 1000); }
輸出的結(jié)果都是 6,為什么?
- for 循環(huán)是同步任務(wù)
- setTimeout 異步任務(wù)
for 循環(huán)一次,就會將 setTimeout 異步任務(wù)加入到瀏覽器的異步任務(wù)隊列中,同步任務(wù)完成之后,再從異步任務(wù)中拿新任務(wù)在線程中執(zhí)行。由于 setTimeout 能夠訪問外部變量 i, 當(dāng)同步任務(wù)完成之后,i 已經(jīng)變成了6, setTimeout 中能夠訪問變量 i 都是 6。
解決辦法1:使用 let 聲明
for (var i = 1; i <= 5; i++) { setTimeout(() => { console.log(i); }, i * 1000); }
解決辦法2:自執(zhí)行函數(shù) + 閉包
for (var i = 1; i <= 5; i++) { (function(i){ setTimeout(() => { console.log(i); }, i * 1000) })(i) }
解決辦法3:setTimeout 傳遞第三參數(shù)
第三個參數(shù)意思:附加參數(shù),一旦定時器到期,它們會作為參數(shù)傳遞給要執(zhí)行的函數(shù)
for (var i = 1; i <= 5; i++) { setTimeout((j) => { console.log(j); }, 1000 * i, i); }
閉包與函數(shù)科里化
function add(num) { return function (y) { return num + y; }; };let incOneFn = add(1); let n = incOneFn(1); // 2let decOneFn = add(-1); let m = decOneFn(1); // 0
add 函數(shù)的參數(shù)
保存了閉包函數(shù)變量。
實際作用
在函數(shù)式編程閉包有非常重要的作用,lodash 等早期工具函數(shù)彌補 javascript 缺陷的工具函數(shù),有大量的閉包的使用場景。
使用場景
- 創(chuàng)建私有變量
- 延長變量生命周期
節(jié)流函數(shù)
防止?jié)L動行為,過度執(zhí)行函數(shù),必須要節(jié)流, 節(jié)流函數(shù)接受 函數(shù)
+ 時間
作為參數(shù),都是閉包中變量,以下是一個簡單 setTimeout 版本:
function throttle(fn, time=300){ var t = null; return function(){ if(t) return; t = setTimeout(() => { fn.call(this); t = null; }, time); } }
防抖函數(shù)
一個簡單的基于 setTimeout 防抖的函數(shù)的實現(xiàn)
function debounce(fn,wait){ var timer = null; return function(){ if(timer !== null){ clearTimeout(timer); } timer = setTimeout(fn,wait); } }
React.useCallback 閉包陷阱問題
問題說明:父/子
組件關(guān)系, 父子組件都能使用 click 事件同時修改 state 數(shù)據(jù), 并且子組件拿到傳遞下的 props 事件屬性,是經(jīng)過 useCallback
優(yōu)化過的。也就是這個被優(yōu)化過的函數(shù),存在閉包陷阱,(保存一直是初始 state 值)
import { useState, useCallback, memo } from "react";const ChildWithMemo = memo((props: any) => { return ( <div> <button onClick={props.handleClick}>Child click</button> </div> ); });const Parent = () => { const [count, setCount] = useState(1); const handleClickWithUseCallback = useCallback(() => { console.log(count); }, []); // 注意這里是不能監(jiān)聽 count, 因為每次變化都會重新綁定,造成造成子組件重新渲染 return ( <div> <div>parent count : {count}</div> <button onClick={() => setCount(count + 1)}>click</button> <ChildWithMemo handleClick={handleClickWithUseCallback} /> </div> ); };export default Parent
- ChildWithMemo 使用 memo 進行優(yōu)化,
- handleClickWithUseCallback 使用 useCallback 優(yōu)化
問題是點擊子組件時候,輸出的 count 是初始值(被閉包了)。
解決辦法就是使用 useRef 保存操作變量函數(shù):
import { useState, useCallback, memo, useRef } from "react";const ChildWithMemo = memo((props: any) => { console.log("rendered children") return ( <div> <button onClick={() => props.countRef.current()}>Child click</button> </div> ); });const Parent = () => { const [count, setCount] = useState(1); const countRef = useRef<any>(null) countRef.current = () => { console.log(count); } return ( <div> <div>parent count : {count}</div> <button onClick={() => setCount(count + 1)}>click</button> <ChildWithMemo countRef={countRef} /> </div> ); };export default Parent
針對這個問題,React 曾經(jīng)認可過社區(qū)提出的增加 useEvent 方案,但是后面 useEvent 語義問題被廢棄了,對于渲染優(yōu)化 React 采用了編譯優(yōu)化的方案。其實類似的問題也會發(fā)生在 useEffect 中,使用時要注意閉包陷阱。
性能問題
- 閉包不要隨意定義,定義了一定找到合適的位置進行銷毀。因為閉包的變量保存在內(nèi)存中,不會被銷毀,占用較高的內(nèi)存。
使用 chrome 面板功能 timeline + profiles 面板
- 打開開發(fā)者工具,選擇 Timeline 面板
- 在頂部的
Capture
字段里面勾選 Memory- 點擊左上角的錄制按鈕。
- 在頁面上進行各種操作,模擬用戶的使用情況。
- 一段時間后,點擊對話框的 stop 按鈕,面板上就會顯示這段時間的內(nèi)存占用情況。
【