摘要:閉包利用的,其實(shí)就是作用域嵌套情況下,內(nèi)部作用域可以訪問(wèn)外部作用域這一特性。之所以要將閉包和垃圾回收策略聯(lián)系在一起,是因?yàn)檫@涉及到閉包的一些重要特性,如變量暫時(shí)存儲(chǔ)和內(nèi)存泄漏。因?yàn)槟涿瘮?shù)回調(diào)的閉包實(shí)際引用的是變量,而非變量的值。
本文旨在總結(jié)在js學(xué)習(xí)過(guò)程中,對(duì)閉包的思考,理解,以及一些反思,如有不足,還請(qǐng)大家指教
閉包---closure閉包是js中比較特殊的一個(gè)概念,其特殊之處在于,本身有些晦澀難懂,是js中的一種高級(jí)用法,但其實(shí)閉包在整個(gè)js中無(wú)處不在,甚至很多情況下都是在不知情的情況下用到了閉包。
下面就從原理上分析一下閉包。
要談閉包,就必須先說(shuō)一下詞法作用域,因?yàn)殚]包是基于詞法作用域書寫代碼時(shí)所產(chǎn)生的自然結(jié)果
詞法作用域由于本文章不是詳細(xì)介紹詞法作用域的。所以就只把閉包中利用到的部分做一個(gè)簡(jiǎn)要的說(shuō)明。
閉包利用的,其實(shí)就是作用域嵌套情況下,內(nèi)部作用域可以訪問(wèn)外部作用域這一特性。
function foo(a) { let b = a * 2; function bar(c) { console.log(a, b, c); } bar(b * 3); }
對(duì)于foo所創(chuàng)建的作用域,其中有三個(gè)標(biāo)識(shí)符:a, bar和b
bar所創(chuàng)建的作用域,其中有一個(gè)標(biāo)識(shí)符: c
但是由于bar完全處于foo的作用域內(nèi)部,所以在bar中查詢變量a時(shí),由于bar中不包含a,就會(huì)根據(jù)詞法作用域的規(guī)則,到其父作用域中尋找變量a
所以bar的作用域中實(shí)際可以使用的變量是a,b,c,bar。
這一原理正式閉包的基礎(chǔ)
閉包首先說(shuō)一下筆者比較認(rèn)同的閉包的定義:
如果函數(shù)可以訪問(wèn)自身作用域以外的變量,并在詞法作用域以外的地方執(zhí)行,那么就可以認(rèn)為創(chuàng)建了一個(gè)閉包
對(duì)于閉包的定義其實(shí)筆者遇到過(guò)一種說(shuō)法
眼鏡鑒別法-----()()
這是在學(xué)校中學(xué)習(xí)時(shí)聽到的一套理論,表示的是立即執(zhí)行函數(shù)IIFE(immediately invoked function expression)通常都會(huì)是一個(gè)閉包
let a = 1; (function IIFE() { console.log(2); })();
但是這種說(shuō)法并不準(zhǔn)確,因?yàn)镮IFE函數(shù)雖然訪問(wèn)了自身作用域外的變量,但是是在定義他的作用域內(nèi)運(yùn)行的(此處為window),所以這并不能算是一個(gè)典型的閉包
下面寫一個(gè)真正符合定義的閉包
let fn; function foo () { let a = 2; function baz() { console.log(a); } fn = baz; } function bar() { fn(); //此處為閉包 } foo(); bar(); // 2
之所以fn可以稱為閉包,是因?yàn)楸A袅薭az的引用,而根據(jù)詞法作用域的規(guī)則,baz是可以訪問(wèn)外部變量a的,于是fn就可以訪問(wèn)foo內(nèi)的變量a了,并且可以在任意一個(gè)地方進(jìn)行調(diào)用。
寫到這個(gè)時(shí)候,閉包的概念就介紹完了,下面淺談一下垃圾回收機(jī)制GC(garbage collection)。
垃圾回收機(jī)制(garbage collection)一般能見到的垃圾回收機(jī)制有兩種
標(biāo)記清除當(dāng)一個(gè)變量進(jìn)入環(huán)境時(shí),會(huì)被標(biāo)記為‘進(jìn)入環(huán)境’,并被分配內(nèi)存。
當(dāng)變量被標(biāo)記為‘進(jìn)入環(huán)境’時(shí),其占用的內(nèi)存是永遠(yuǎn)不會(huì)被清空的。
而當(dāng)變量離開環(huán)境時(shí),則將其標(biāo)記為‘離開環(huán)境’,并在下一次垃圾回收的輪詢中,將其清除,
這個(gè)策略不太常見。。甚至已經(jīng)絕種。。。
其原理可以理解為,對(duì)于一個(gè)變量,有一個(gè)統(tǒng)計(jì)其引用次數(shù)的統(tǒng)計(jì)器,當(dāng)引用次數(shù)變?yōu)?時(shí),就會(huì)被清除。
但因?yàn)楫?dāng)存在循環(huán)引用時(shí),這種GC策略可能會(huì)導(dǎo)致一些bug,所以漸漸用這種策略的就不多了。
之所以要將閉包和垃圾回收策略聯(lián)系在一起,是因?yàn)檫@涉及到閉包的一些重要特性,如變量暫時(shí)存儲(chǔ)和內(nèi)存泄漏。
在一般的函數(shù)執(zhí)行過(guò)程中
function foo (a) { console.log(a); } foo(1); //1
foo函數(shù)被調(diào)用完成后,變量a就會(huì)離開環(huán)境(或者說(shuō)其引用數(shù)為0),那a所占用的內(nèi)存就會(huì)被清除
但對(duì)于閉包
function foo() { let a = 2; return function() { console.log(a); } } let bar = foo(); bar(); // 2
由于bar中保留了對(duì)a的引用,使a無(wú)法被打上‘離開環(huán)境’的標(biāo)簽,所以變量a會(huì)一直占用內(nèi)存,由此引申出一些高級(jí)功能,如函數(shù)調(diào)用次數(shù)統(tǒng)計(jì),函數(shù)柯里化等,隨之而來(lái)的,因?yàn)殚]包會(huì)持續(xù)占用內(nèi)存,當(dāng)系統(tǒng)中存在很多閉包時(shí),必然會(huì)影響性能,甚至導(dǎo)致內(nèi)存泄漏。
所以當(dāng)使用完閉包后,應(yīng)該嘗試手動(dòng)清除內(nèi)存
bar = null;閉包的用途
下面介紹幾種筆者遇到的閉包的實(shí)例,可能經(jīng)驗(yàn)優(yōu)先,歡迎大家補(bǔ)充
異步for循環(huán)問(wèn)題for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 1000); }
這例子,不能再眼熟,不能再經(jīng)典的一個(gè)坑
這個(gè)坑,歸根結(jié)底,就是因?yàn)殚]包。
但是因?yàn)樯婕暗揭恍﹋s異步策略的問(wèn)題,所以就簡(jiǎn)單分析下
js代碼會(huì)分為同步和異步兩種方式運(yùn)行,在一個(gè)任務(wù)隊(duì)列中,會(huì)先執(zhí)行同步操作,如遇到異步操作(回調(diào)),會(huì)將異步操作先掛起,待本隊(duì)列中的所有同步操作都跑完后,再去跑異步操作。
所以for循環(huán)中的代碼邏輯,大概可以解釋為
for - start
var i = 0;
setTimeout----異步回調(diào)掛起
i = 1;
setTimeout----異步回調(diào)掛起
...
...
...
i = 9;
setTimeout----異步回調(diào)掛起
i = 10;
for - end
// 開始執(zhí)行異步的回調(diào)
function() { console.log(i); // 10 }
到此大家應(yīng)該就可以理解為什么會(huì)打出10個(gè)10了。
因?yàn)槟涿瘮?shù)回調(diào)的閉包實(shí)際引用的是變量i,而非變量i的值。
所以對(duì)于這個(gè)坑,解決方法就是將變量引用變?yōu)橹狄?/p>
for (var i = 0;i < 10; i++) { (function (j) { setTimeout(function() { console.log(j); }); })(i); }
由于函數(shù)參數(shù)是采用值傳遞的方式,所以解決了這個(gè)問(wèn)題
ES6中提供了一種更簡(jiǎn)單的做法
for(let i = 0; i++; i < 10) { setTimeout(() => { console.log(i); }); }
由于let聲明在for循環(huán)中有不同的表現(xiàn)----既每次循環(huán)并非只是previous value進(jìn)行了++操作,而是加++后的值賦值給了一個(gè)新的變量,所以解決了閉包只會(huì)引用變量最新值的這個(gè)問(wèn)題
函數(shù)調(diào)用統(tǒng)計(jì)如果想統(tǒng)計(jì)一個(gè)函數(shù)的調(diào)用次數(shù),那就應(yīng)該存在一個(gè)計(jì)數(shù)器變量用以統(tǒng)計(jì),但如果將這個(gè)變量放在全局環(huán)境中,就會(huì)存在變量污染問(wèn)題。
但如果將其放在閉包中,就可以保證在不污染全局的前提下,進(jìn)行變量保存
function foo() { let i = 0; return function target() { ...... i++; } } let bar = foo(); bar() // i = 1 bar() // i = 2函數(shù)柯里化
柯里化的理論知識(shí)可以參考張?chǎng)涡翊罄械膫€(gè)人網(wǎng)站
柯里化的定義和代碼實(shí)例
這里簡(jiǎn)單說(shuō)下柯里化,柯里化有很多很多的用途,但下文主要介紹的是利用柯里化設(shè)置函數(shù)默認(rèn)參數(shù)的問(wèn)題
首先我們先看下es5環(huán)境下的張?chǎng)涡翊罄械目吕锘a
var currying = function(fn) { // fn 指官員消化老婆的手段 var args = [].slice.call(arguments, 1); // args 指的是那個(gè)合法老婆 return function() { // 已經(jīng)有的老婆和新搞定的老婆們合成一體,方便控制 var newArgs = args.concat([].slice.call(arguments)); // 這些老婆們用 fn 這個(gè)手段消化利用,完成韋小寶前輩的壯舉并返回 return fn.apply(null, newArgs); }; }; // 下為官員如何搞定7個(gè)老婆的測(cè)試 // 獲得合法老婆 var getWife = currying(function() { var allWife = [].slice.call(arguments); // allwife 就是所有的老婆的,包括暗渡陳倉(cāng)進(jìn)來(lái)的老婆 console.log(allWife.join(";")); }, "合法老婆"); getWife("大老婆","小老婆","俏老婆"); // 合法老婆;大老婆;小老婆;俏老婆 getWife("超越韋小寶的老婆"); // 合法老婆;超越韋小寶的老婆
其原理其實(shí)就是通過(guò)閉包的方法將默認(rèn)參數(shù)存儲(chǔ)在外層函數(shù)的作用域中,其實(shí)現(xiàn)方法與函數(shù)調(diào)用計(jì)數(shù)器是相似的。
下面貼出個(gè)人在es6環(huán)境下對(duì)這段代碼的優(yōu)化
function currying(fn, ...default_args) { return function (...args) { return fn(...default_args, ...args); }; } const getWife = currying(function () { console.log([...arguments].join(";")); }, "a"); getWife("b", "c", "d"); // a;b;c;d
主要的優(yōu)化點(diǎn)是利用拓展運(yùn)算符對(duì)arguments的展開作用,取代繁瑣的Array.slice.call()和Array.concat.call()
模塊化因?yàn)閑s6中已經(jīng)有了十分完備的模塊機(jī)制,所以我們只是利用閉包來(lái)做一個(gè)模塊化思想的實(shí)現(xiàn),該實(shí)現(xiàn)包括了模塊存儲(chǔ),依賴關(guān)系處理
let MyModules = (function Manager() { let modules = {}; function define(name, deps, impl) { deps = deps.map(val => { return modules[val]; } modules[name] = impl.apply(impl, deps); } function get(name) { return modules[name]; } return { define: define, get: get } })();
對(duì)閉包的總結(jié)差不多就到這了,感覺(jué)一番總結(jié)之下,還是有所收獲的,也希望能給看到這篇文章的各位,提供一些幫助。
不足之處,歡迎各位一起來(lái)交流
以上內(nèi)容參考
javascript高級(jí)程序設(shè)計(jì)(第三版)(Nicolas C.Zakas)
你不知道的javascript 上卷(Kyle Simpson)
張?chǎng)涡翊罄械膫€(gè)人主頁(yè)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/90795.html
摘要:前端日?qǐng)?bào)精選免費(fèi)的計(jì)算機(jī)編程類中文書籍英文技術(shù)文檔看不懂看印記中文就夠了的內(nèi)部工作原理美團(tuán)點(diǎn)評(píng)點(diǎn)餐前后端分離實(shí)踐讓你的動(dòng)畫坐上時(shí)光機(jī)中文譯有多棒簡(jiǎn)書譯別再使用圖片輪播了掘金譯如何在中使用掘金個(gè)讓增長(zhǎng)成億美元公司的獨(dú)特方法眾成翻 2017-08-23 前端日?qǐng)?bào) 精選 FPB 2.0:免費(fèi)的計(jì)算機(jī)編程類中文書籍 2.0英文技術(shù)文檔看不懂?看印記中文就夠了!Virtual DOM 的內(nèi)部工作...
摘要:前端日?qǐng)?bào)精選中的垃圾收集,圖文指南十個(gè)免費(fèi)的前端開發(fā)工具專題之遞歸如何在鏈中共享變量基于的爬蟲框架中文譯十六進(jìn)制顏色揭秘掘金掘金小書基本環(huán)境安裝小書教程中間件對(duì)閉包的一個(gè)巧妙使用簡(jiǎn)書源碼分析掘金組件開發(fā)練習(xí)焦點(diǎn)圖切換前端學(xué) 2017-09-13 前端日?qǐng)?bào) 精選 V8 中的垃圾收集(GC),圖文指南十個(gè)免費(fèi)的web前端開發(fā)工具JavaScript專題之遞歸 · Issue #49 · m...
摘要:投射劇中人物對(duì)車禍妻子偷情肇事者死亡的真相聽而不聞視而不見閉嘴不言。想方設(shè)法把自己培養(yǎng)成工程師而不是最后成為了碼農(nóng)查看更多列表回顧九月份第一周為什么你的前端工作經(jīng)驗(yàn)不值錢回顧九月份第二周前端你該知道的事兒回顧九月份第三周最近的資訊集合 原鏈接:http://bluezhan.me/weekly/#/9-3 1、web前端 JavaScript實(shí)現(xiàn)H5游戲斷線自動(dòng)重連的技術(shù) 前端日?qǐng)?bào):...
摘要:投射劇中人物對(duì)車禍妻子偷情肇事者死亡的真相聽而不聞視而不見閉嘴不言。想方設(shè)法把自己培養(yǎng)成工程師而不是最后成為了碼農(nóng)查看更多列表回顧九月份第一周為什么你的前端工作經(jīng)驗(yàn)不值錢回顧九月份第二周前端你該知道的事兒回顧九月份第三周最近的資訊集合 原鏈接:http://bluezhan.me/weekly/#/9-3 1、web前端 JavaScript實(shí)現(xiàn)H5游戲斷線自動(dòng)重連的技術(shù) 前端日?qǐng)?bào):...
閱讀 3171·2021-11-19 09:40
閱讀 3663·2021-11-16 11:52
閱讀 2988·2021-11-11 16:55
閱讀 3186·2019-08-30 15:55
閱讀 1191·2019-08-30 13:08
閱讀 1663·2019-08-29 17:03
閱讀 3020·2019-08-29 16:19
閱讀 2587·2019-08-29 13:43