一文詳解JavaScript閉包典型應(yīng)用
1.應(yīng)用
以下這幾個方面是我們開發(fā)中最為常用到的,同時也是面試中回答比較穩(wěn)的幾個方面。
1.1 模擬私有變量
我們都知道JS是基于對象的語言,JS強(qiáng)調(diào)的是對象,而非類的概念,在ES6中,可以通過class關(guān)鍵字模擬類,生成對象實(shí)例。
通過class模擬出來的類,仍然無法實(shí)現(xiàn)傳統(tǒng)面向?qū)ο笳Z言中的一些能力 —— 比如私有變量的定義和使用。
我們通過看這樣一個User類來了解私有變量(偽代碼,不能直接運(yùn)行)
class User{ constructor(username,password){ // 用戶名 this.username = username // 密碼 this.password = password } login(){ // 使用axious進(jìn)行登錄請求 axios({ method: 'GET', url: 'http://127.0.0.1/server', params: { username, password }, }).then(response => { console.log(response); }); } }
在這個User類里,我們定義了一些屬性,和一個login方法,我們嘗試輸出password這個屬性。
let user = new User('小明',123456) user.password // 123465
我們發(fā)現(xiàn),登錄密碼這么關(guān)鍵敏感的信息,竟然可以通過一個簡單的屬性就可以拿到,這就意味著,后面人只有拿到user這個對象,就可以非常輕松的獲取,甚至改寫他的密碼。 在實(shí)際的業(yè)務(wù)開發(fā)中,這是一個非常危險的操作,我們需要從代碼的層面保護(hù)password。
像password這樣變量,我們希望它只在函數(shù)內(nèi)部,或者對象內(nèi)部方法訪問到,外部無法觸及。 這樣的變量,就是私有變量,私有變量一般使用 _ 或雙 _ 定義。
在類里聲明變量的私有性,我們可以借助閉包實(shí)現(xiàn),我們的思路就是把我們把私有變量放在最外層立即執(zhí)行函數(shù)中,并通過立即執(zhí)行User這個函數(shù),創(chuàng)造了一個閉包作用域的環(huán)境。
// 利用IIFE生成閉包,返回user類 const User = (function () { // 定義私有變量_password let _password class User { constructor(username, password) { // 初始化私有變量_password _password = password this.username = username } login() { console.log(this.username, _password) } } return User })() let user = new User('小明',123465) console.log(user.username); // 小明 console.log(user.password); // undefined console.log(user._password); //undefined user.login(); // 小明 undefined
在這段代碼中,私有變量_password被好好的保護(hù)在User這個立即執(zhí)行函數(shù)內(nèi)部,此時實(shí)例暴露的屬性已經(jīng)沒有_password,通過閉包,我們成功利用了自由變量模擬私有變量的效果。
1.2 柯里化
定義一個函數(shù),該函數(shù)返回一個函數(shù)。 柯里化是把接收 n個參數(shù)的1個函數(shù)改造為只接收1個參數(shù)的n個互相嵌套的函數(shù)的過程。也就是從fn(a,b,c)變成fn(a)(b)(c)。
我們通過以下案例進(jìn)行深入理解:以慕課網(wǎng)為例,我們使用site(站點(diǎn))、type(課程類型)、name(課程名稱)三個字符串拼接的方式為課程生成一個完整版名稱。對應(yīng)方法如下:
function generateName(site,type,name){ return site + type + name }
我們看到這個函數(shù)需要傳遞三個參數(shù),此時如果我是課程運(yùn)營負(fù)責(zé)人,如我只負(fù)責(zé)“體系課”的業(yè)務(wù),那么我每次生成課程時,都會固定傳參site,像這樣傳參:
generateName('體系課',type,name)
如果我是細(xì)分工種的前端助教,我僅僅負(fù)責(zé)“體系課”站點(diǎn)下的“前端”課程,那么我進(jìn)行傳參就是這樣:
generateName('體系課','前端',name)
我們不難發(fā)現(xiàn),調(diào)用generateName時,真正的變量只有一個,但是我每次不得不把前兩個參數(shù)手動傳一遍。此時,我們的柯里化就出現(xiàn)了,柯里化可以幫助我們在必要情況下,記住一部分參數(shù)。
function generateName(site){ // var site = '體系課' return function(type){ // var type = '前端' return function(name){ // var name = '零基礎(chǔ)就業(yè)班' return prefix + type + name } } } // 生成體系課專屬函數(shù) var salesName = generateName('體系課'); // “記住”site,生成體系課前端課程專屬函數(shù) var salesBabyName = salesName('前端') // 輸出 '體系課前端零基礎(chǔ)就業(yè)班' res = salesBabyName('零基礎(chǔ)就業(yè)班') console.log(res)
我們可以看到,在生成體系課專屬函數(shù)中,我們將site作為實(shí)參傳遞給generateName函數(shù)中,將site的值保留在generateName內(nèi)部作用域中。
在生成體系課前端課程函數(shù)中,將type的值保留在salesBabyName函數(shù)中,最終調(diào)用salesBabyName函數(shù),輸出。
這樣一來,原有的generateName (site, type, name)函數(shù)經(jīng)過柯里化變成了generateName(site)(type)(name)。通過后者這種形式,我們可以記住一部分形參,選擇性的傳遞參數(shù),從而編寫出更符合預(yù)期,復(fù)用性更高的函數(shù)。
function generateName(site){ // var site = '實(shí)戰(zhàn)課' return function(type){ // var type = 'Java' return function(name){ // var name = '零基礎(chǔ)' return site + type + name } } } // "記住“site和type,生成實(shí)戰(zhàn)課java專屬函數(shù) var shiZhanName = generateName('實(shí)戰(zhàn)課')('Java') console.log(shiZhanName); // 輸出 '實(shí)戰(zhàn)課java零基礎(chǔ)' var res = shiZhanName('零基礎(chǔ)') console.log(res) // 啥也不記,直接生成一個完整課程 var itemFullName = generateName('實(shí)戰(zhàn)課')('大數(shù)據(jù)')('零基礎(chǔ)') console.log(itemFullName);
1.3 偏函數(shù)
偏函數(shù)和柯里化類似,如果理解了柯里化,那么偏函數(shù)就小菜一碟了。
柯里化是將一個n個參數(shù)的函數(shù)轉(zhuǎn)化成n個單參數(shù)函數(shù),這里假如你有三個入?yún)?,你得嵌套三層函?shù),且每層函數(shù)只能有一個入?yún)???吕锘哪繕?biāo)是把函數(shù)拆解為精準(zhǔn)的n部分。
偏函數(shù)相比之下就比較隨意了,偏函數(shù)是固定函數(shù)中的某一個或幾個參數(shù),然后返回一個新的函數(shù)。假如你有三個入?yún)ⅲ憧梢灾还潭ㄒ粋€入?yún)?,然后返回另一個入?yún)⒑瘮?shù)。也就是說,偏函數(shù)應(yīng)用是不強(qiáng)調(diào) “單參數(shù)” 這個概念的。
仍然是上面的例子,原函數(shù)形式調(diào)用:
function generateName(site,type,name){ return site + type + name; } // 調(diào)用時傳入三個參數(shù) var itemFullName = generateName('體系課', '前端', '2022')
偏函數(shù)改造:
function generateName(site){ return function(type,name){ return site + type + name } } // 把3個參數(shù)分兩部分傳入 var itemFullName = generateName('體系課')('前端', '2022')
1.4 防抖
在瀏覽器的各種事件中,有一些容易頻繁觸發(fā)的事件,比如scroll、resize、鼠標(biāo)事件(比如 mousemove、mouseover)、鍵盤事件(keyup、keydown )等。頻繁觸發(fā)回調(diào)導(dǎo)致大量的計算會引發(fā)頁面抖動甚至卡頓,影響瀏覽器性能。防抖和節(jié)流就是控制事件觸發(fā)的頻率的兩種手段。
防抖的中心思想是:在某段時間內(nèi),不管你觸發(fā)了多少次回調(diào),我都只執(zhí)行最后一次。
// fn是我們需要包裝的事件回調(diào), delay是每次推遲執(zhí)行的等待時間 function debounce(fn, delay) { // 定時器 let timer = null // 將debounce處理結(jié)果當(dāng)作函數(shù)返回 return function () { // 保留調(diào)用時的this上下文 let context = this // 保留調(diào)用時傳入的參數(shù) let args = arguments // 每次事件被觸發(fā)時,都去清除之前的舊定時器 if(timer) { clearTimeout(timer) } // 設(shè)立新定時器 timer = setTimeout(function () { fn.apply(context, args) }, delay) } } // 用debounce來包裝scroll的回調(diào) const better_scroll = debounce(() => console.log('觸發(fā)了滾動事件'), 1000) document.addEventListener('scroll', better_scroll)
1.5 節(jié)流
節(jié)流的中心思想是:在某段時間內(nèi),不管你觸發(fā)了多少次回調(diào),我都只認(rèn)第一次,并在計時結(jié)束時給予響應(yīng),也就是隔一段時間執(zhí)行一次。
// fn是我們需要包裝的事件回調(diào), interval是時間間隔的閾值 function throttle(fn, interval) { // last為上一次觸發(fā)回調(diào)的時間 let last = 0 // 將throttle處理結(jié)果當(dāng)作函數(shù)返回 return function () { // 保留調(diào)用時的this上下文 let context = this // 保留調(diào)用時傳入的參數(shù) let args = arguments // 記錄本次觸發(fā)回調(diào)的時間 let now = +new Date() // 判斷上次觸發(fā)的時間和本次觸發(fā)的時間差是否小于時間間隔的閾值 if (now - last >= interval) { // 如果時間間隔大于我們設(shè)定的時間間隔閾值,則執(zhí)行回調(diào) last = now; fn.apply(context, args); } } } // 用throttle來包裝scroll的回調(diào) const better_scroll = throttle(() => console.log('觸發(fā)了滾動事件'), 1000) document.addEventListener('scroll', better_scroll)
2.性能問題
以上我們講解了閉包的常見應(yīng)用,可見閉包是一個非常強(qiáng)大的特性,但人們對其也有諸多誤解。一種聳人聽聞的說法是閉包會造成內(nèi)存泄露,所以要盡量減少閉包的使用。真的是這樣嗎?
2.1 內(nèi)存泄漏
該釋放的變量沒有釋放,依然占據(jù)著內(nèi)存空間,導(dǎo)致內(nèi)存占用不斷攀高,帶來性能惡化,系統(tǒng)崩潰等一系列問題,這種現(xiàn)象叫做內(nèi)存泄漏。
閉包里的變量是我們需要的變量本就不該釋放,而又怎么稱之為內(nèi)存泄漏呢?
所以有關(guān)內(nèi)存泄露問題,是謠言,是誤傳。
這個誤傳來源于IE,IE在我們使用完閉包之后,依然會受不了里面的變量,而這是IE的bug,不是閉包問題。
如果還不放心,我們來看以下例子:
function f1(){ var num = Math.randon(); function f2(){ return num } return f2 } var f = f1(); f();
上面這段代碼,f2函數(shù)中存在對變量num的引用,所以num變量并不會回收,我們可以,在函數(shù)調(diào)用后,可以把外部引用關(guān)系置空,如下:
function f1(){ var num = Math.randon(); function f2(){ return num } return f2 } var f = f1(); f(); f = null;
事實(shí)上,閉包導(dǎo)致的內(nèi)存泄漏是誤傳,閉包中引用的變量,其實(shí)也就相當(dāng)于一個全局變量,并不會構(gòu)成內(nèi)存泄漏問題,內(nèi)存泄漏大多原因是由于代碼不規(guī)范導(dǎo)致。
2.2 常見的內(nèi)存泄漏
2.21 不必要的全局變量
function f1() { name = '小明' }
在非嚴(yán)格模式下引用未聲明的變量,會在全局對象中創(chuàng)建一個新變量,在瀏覽器中,全局對象是window,這就意味著name這個變量將泄漏到全局。全局變量是在網(wǎng)頁關(guān)閉時才會釋放,這樣的變量一多,內(nèi)存壓力也會隨之增高。
2.22 遺忘清理的計時器
程序中我們經(jīng)常會用到計時器,也就是setInterval和setTimeout
var timeId = setInterval(function(){ // 函數(shù)體 },1000)
在計時器中,定時器內(nèi)部邏輯是是無窮無盡的,當(dāng)定時器囊括的函數(shù)邏輯不再被需要、而我們又忘記手動清除定時器時,它們就會永遠(yuǎn)保持對內(nèi)存的占用。因此當(dāng)我們使用定時器時,一定要明確計時器在何時會被清除,并使用 clearInterval(timeId)手動清除定時器。
2.23 遺忘清理的dom元素引用
var divObj = document.getElementById('mydiv') // dom刪除myDiv document.body.removeChild(divObj); console.log(divObj); // 能console出整個div 說明沒有被回收,引用存在 // 移出引用 divObj = null; console.log(divObj) // null
3.閉包與循環(huán)體
閉包和循環(huán)體的結(jié)合,是閉包最為經(jīng)典的一種考察方式。
3.1 這段代碼輸出啥
我們來看一個大家非常熟悉的題目,以上6行代碼輸出什么?
for(var i=0; i<5; i++){ setTimeout(function(){ console.log(i) },1000) } console.log(i)
如果你是剛?cè)腴T的新手,你可能會給出這樣的答案:
0 1 2 3 4 5
給出這樣答案的同學(xué),內(nèi)心一般都是這樣想:for循環(huán)輸出了0-4個i的值,最后一行打印5,setTimeout這個好像在哪見過,但具體咋回事印象不深了,干脆直接忽略好了。
對于基礎(chǔ)還不錯的同學(xué),對于setTimeout函數(shù)用法特性還有印象,很快就給出了“進(jìn)化版”答案:
5 0 1 2 3 4
這一部分的同學(xué)是這樣想的:for循環(huán)逐個輸出0-4的值,但是setTimeout把輸入延遲了1s,所以最后一行先執(zhí)行,先輸出5,然后過了1000ms,0-4會逐個輸出。
如果你對JS中的for循環(huán)、同步與異步區(qū)別、變量作用域、閉包有正確理解,就知道正確答案應(yīng)該是:
5 5 5 5 5 5
我們試著分析一下正確答案,seTimeout內(nèi)函數(shù)延遲1000ms后執(zhí)行,最后一行console先輸出,最后一行輸出5,所以第一個值是5。
for(var i =0;i<5;i++){ // 5<5? 不滿足 } console.log(i) // 5
for循環(huán)里setTimeout執(zhí)行了5次,函數(shù)延遲1000ms執(zhí)行,大家看這個函數(shù),它自身作用域壓根就沒有i這個變量,根據(jù)作用域鏈查找規(guī)則,要想輸出i,需要去上層查找。
setTimeout(function() { console.log(i); }, 1000);
但是,這個函數(shù)第一次被執(zhí)行也是1000ms以后的事情了,此時它試圖向上一層作用域(這里也就是全局作用域)去找一個叫i的變量,此時for循環(huán)已執(zhí)行完畢,i也進(jìn)入了最終狀態(tài)5。所以當(dāng)1000ms后,這個函數(shù)真正被執(zhí)行的時候,引用到的i值已經(jīng)是5了。 此時,這段代碼的作用域狀態(tài)示意如下:
對應(yīng)的作用域關(guān)系如下:
接下來的連續(xù)四次,都會有一個一模一樣的setTimeout回調(diào)被執(zhí)行,它輸出的也是同一個全局的i,所以說每一次輸出都是5。
3.2 改造方法
循環(huán)了五次,每次卻輸出一個值,這種輸出效果顯然不好。如果我們希望讓i從0-4依次被輸出,我們改如何改造呢?
方案一:利用setTimeout中第三個參數(shù)
開頭我們先復(fù)習(xí)一下setTimeout參數(shù)用法:
setTimeout(function(arg1,arg2){ console.log(arg1); console.log(arg2); },delay,arg1,arg2)
- function(必須):調(diào)用函數(shù)執(zhí)行的代碼塊
- delay(可選):函數(shù)調(diào)用延遲的毫秒值,默認(rèn)是0,意味著馬上執(zhí)行
- arg1,...arg2(可選):附加參數(shù),當(dāng)計時器啟動時,會作為參數(shù)傳遞給function
我們來看例子:
setTimeout(function(a,b){ console.log(a); // 1 console.log(b); // 2 },1000,1,2)
需要注意的一點(diǎn)是,附加參數(shù)只支持在ie9及以上瀏覽器,如要兼容,需要引入一段MDN提供的兼容舊IE代碼。
利用setTimeout的第三個參數(shù),i作為形參傳遞給setTimeout的j,由于每次傳入的參數(shù)是從for循環(huán)里面取到的值,所以會依次輸出0~4:
for(var i=0; i<5; i++){ setTimeout(function(j){ console.log(j) // 0 1 2 3 4 },1000,i) }
方案二:使用閉包
使用閉包,我們往往會用到匿名函數(shù)。匿名函數(shù)也叫一次性函數(shù),在函數(shù)定義時執(zhí)行,且只執(zhí)行一次。我們在setTimeout外面套一個匿名函數(shù),利用匿名函數(shù)的實(shí)參來緩存每一個循環(huán)的i值。
for(var i= 0; i<5; i++){ (function(j){ setTimeout(function(){ console.log(j) },1000) })(i) }
當(dāng)輸出j時,引用的是外部函數(shù)傳遞的變量i,這個i是根據(jù)循環(huán)來的,執(zhí)行setTimeout時已經(jīng)確定了里面i的值,進(jìn)而確定了j的值。
方案三:使用let
for(let i= 0; i<5; i++){ setTimeout(function(){ console.log(i) },1000) }
for循環(huán)每次循環(huán)產(chǎn)生一個新的塊級作用域,每個塊級作用域的變量是不同的。函數(shù)輸出的是自己的上一級(循環(huán)產(chǎn)生的塊級作用域)下i的值。
4.總結(jié)
- 作用:模擬私有變量、柯里化、偏函數(shù)、防抖、節(jié)流、實(shí)現(xiàn)緩存。
- 模擬私有變量:將私有變量放在外在的立即執(zhí)行函數(shù)中,并通過立即執(zhí)行U這個函數(shù),創(chuàng)造一個閉包環(huán)境(私有變量:只允許函數(shù)內(nèi)部,或?qū)ο蠓椒ㄔL問的變量)。
- 柯里化:把接受n個參數(shù)的一個函數(shù)轉(zhuǎn)化成只接受一個參數(shù)n個函數(shù)互相嵌套的函數(shù)過程,目標(biāo)是把函數(shù)拆解為精準(zhǔn)的n部分,也就是將fn(a,b,c)轉(zhuǎn)化成fn(a)(b)(c)的過程。
- 偏函數(shù):固定函數(shù)中的某一個或幾個參數(shù),然后返回一個新的函數(shù),不強(qiáng)調(diào)但函數(shù)。
- 防抖:只執(zhí)行最后一次。
- 節(jié)流:隔一段時間執(zhí)行一次。
- 緩存變量:計時器打印問題。
- 閉包造成內(nèi)存泄露問題是誤傳,誤傳來源于IE瀏覽器bug,可以放心大膽使用。
以上就是一文詳解JavaScript閉包典型應(yīng)用的詳細(xì)內(nèi)容,更多關(guān)于JavaScript閉包典型應(yīng)用的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript學(xué)習(xí)筆記之DOM基礎(chǔ)操作實(shí)例小結(jié)
這篇文章主要介紹了JavaScript學(xué)習(xí)筆記之DOM基礎(chǔ)操作,結(jié)合實(shí)例形式總結(jié)分析了javascript針對dom元素節(jié)點(diǎn)、屬性的相關(guān)獲取、設(shè)置等操作技巧,需要的朋友可以參考下2019-01-01一文徹底理解js原生語法prototype,__proto__和constructor
作為一名前端工程師,必須搞懂JS中的prototype、__proto__與constructor屬性,相信很多初學(xué)者對這些屬性存在許多困惑,容易把它們混淆,下面這篇文章主要給大家介紹了關(guān)于js原生語法prototype,__proto__和constructor的相關(guān)資料,需要的朋友可以參考下2021-10-10Bootstrap 3 按鈕標(biāo)簽實(shí)例代碼
這篇文章主要介紹了Bootstrap 3 按鈕標(biāo)簽實(shí)例代碼,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-02-02JavaScript實(shí)現(xiàn)圖片懶加載的三種常用方法總結(jié)
懶加載是一種對網(wǎng)頁性能優(yōu)化的方式,也是我們經(jīng)常會用到的技術(shù),這篇文章為大家整理了JavaScript實(shí)現(xiàn)圖片懶加載的三種常用方法,希望對大家有所幫助2023-06-06