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