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