摘要:此時閉包函數的作用域鏈得以保存,不會被垃圾回收機制所回收。執行執行完畢,返回總結閉包的原理,就是把閉包函數的作用域鏈保存了下來。
原文:搞懂閉包 | AlloyTeam
作者:TAT.yaoyao
閉包這個概念是前端工程師必須要深刻理解的,但是網上確實有一些文章會讓初學者覺得晦澀難懂,而且閉包的文章描述不一。
本文面向初級的程序員,聊一聊我對閉包的理解。當然如果你看到閉包聯想不到作用域鏈與垃圾回收也不妨看一眼。希望讀了它之后你不再對閉包蒙圈。
先體驗一下閉包這里有個需求,即寫一個計數器的函數,每調用一次計數器返回值加一:
counter() // 1 counter() // 2 counter() // 3 ......
要想函數每次執行的返回不一樣,怎么搞呢? 先簡單的寫一下:
var index = 1; function counter() { return index ++; }
這樣做的確每次返回一個遞增的數。但是,它有以下三個問題:
這個index放在全局,其他代碼可能會對他進行修改
如果我需要同時用兩個計數器,但這種寫法只能滿足一個使用,另一個還想用的話就要再寫個counter2函數,再定義一個index2的全局變量。
計數器是一個功能,我只希望我的代碼里有個 counter函數就好,其他的最好不要出現。這是稍微有點代碼潔癖的都會覺得不爽的。
三個痛點,讓閉包來一次性優雅解決:
function counterCreator() { var index = 1; function counter() { return index ++; } return counter; } // test var counterA = counterCreator(); var counterB = counterCreator(); counterA(); // 1 counterA(); // 2 counterB(); // 1 counterB(); // 2
我的counterCreator函數只是把上面的幾行代碼包起來,然后返回了里面的 counter 函數而已。卻能同時解決這么多問題,這就是閉包的魅力! 6不6啊?
鋪墊知識鋪墊一些知識點,不展開講。
執行上下文函數每次執行,都會生成一個會創建一個稱為執行上下文的內部對象(AO對象,可理解為函數作用域),這個AO對象會保存這個函數中所有的變量值和該函數內部定義的函數的引用。函數每次執行時對應的執行上下文都是獨一無二的,正常情況下函數執行完畢執行上下文就會被銷毀。
作用域鏈在函數定義的時候,他還獲得[[scope]]。這個是里面包含該函數的作用域鏈,初始值為引用著上一層作用域鏈里面所有的作用域,后面執行的時候還會將AO對象添加進去 。作用域鏈就是執行上下文對象的集合,這個集合是鏈條狀的。
function a () { // (1)創建 a函數的AO對象:{ x: undfind, b: function(){...} , 作用域鏈上層:window的AO對象} var x = 1; function b () { // (3)創建 b函數的AO對象:{ y: undfind , 作用域鏈上層:a函數AO對象} var y = 2; // (4)b函數的AO對象:{ y: 3 , 作用域鏈上層:a函數AO對象} console.log(x, y); // 在 b函數的AO對象中沒有找到x, 會到a函數AO對象中查找 } //(2)此時 a函數的AO對象:{ x: 1, b: function(){...} , 作用域鏈上層:window的AO對象} b(); } a();
正常情況函數每次執行后AO對象都被銷毀,且每次執行時都是生成新的AO對象。我們得出這個結論: 只要是這個函數每次調用的結果不一樣,那么這個函數內部一定是使用了函數外部的變量。
垃圾回收如何確定哪些內存需要回收,哪些內存不需要回收,這依賴于活對象這個概念。我們可以這樣假定:一個對象為活對象當且僅當它被一個根對象 或另一個活對象指向。根對象永遠是活對象。
function a () { var x = 1; function b () { var y = 2; // b函數執行完了,b函數AO被銷毀,y 被回收 } b(); //a 函數執行完了,a函數AO被銷毀, x 和 b 都被回收 } a(); // 這里是在全局下,window中的 a 直到頁面關閉才被回收。分析閉包結構
// 生成閉包的函數 function counterCreator() { // 被返回函數所依賴的變量 var index = 1; // 被返回的函數 function counter() { return index ++; } return counter; } // 被賦值為閉包函數 var counterA = counterCreator(); // 使用 counterA();
閉包的創造函數必定包含兩部分:
一些閉包函數執行時依賴的變量,每次執行閉包函數時都能訪問和修改
返回的函數,這個函數中必定使用到上面所說的那些變量
// 被賦值的閉包函數 var counterA = counterCreator(); var counterB = counterCreator();
而上面這兩句代碼很重要,它其實是把閉包函數賦值給了一個變量,這個變量是一個活對象,這活對象引用了閉包函數,閉包函數又引用了AO對象,所以這個時候AO對象也是一個活對象。此時閉包函數的作用域鏈得以保存,不會被垃圾回收機制所回收。
當我們想重新創建一個新的計數器時,只需要重新再調用一次 counterCreator, 他會新生成了一個新的執行期上下文,所以counterB與counterA是互不干擾的。
counterCreator 執行
counterCreator 執行完畢,返回counter
閉包的原理,就是把閉包函數的作用域鏈保存了下來。
使用閉包帶你手寫一個簡單的防抖函數,趁熱打鐵。
第一步,先把閉包的架子搭起來,因為我們已經分析了閉包生成函數內部一定有的兩部分內容。
function debunce(func, timeout) { // 閉包函數執行時依賴的變量,每次執行閉包函數時都能訪問和修改 return function() { // 這個函數最終會被賦值給一個變量 } }
第二步: 把閉包第一次執行的情況寫出來
function debunce(func, timeout) { timeout = timeout || 300; return function(...args) { var _this = this; setTimeout(function () { func.apply(_this, args); }, timeout); } }
第三步: 加上一些判斷條件。就像我們最開始寫計數器的index一樣,不過這一次你不是把變量寫在全局下,而是寫在閉包生成器的內部。
function debunce(func, timeout) { timeout = timeout || 300; var timer = null; // 被閉包函數使用 return function(...args) { var _this = this; clearTimeout(timer); // 做一些邏輯讓每次執行效果可不一致 timer = setTimeout(function () { func.apply(_this, args); }, timeout); } } // 測試: function log(...args) { console.log("log: ", args); } var d_log = debunce(log, 1000); d_log(1); // 預期:不輸出 d_log(2); // 預期:1s后輸出 setTimeout( function () { d_log(3); // 預期:不輸出 d_log(4); // 預期:1s后輸出 }, 1500)閉包運用
閉包用到的真的是太多了,再舉幾個例子再來鞏固一下:
模塊化例NodeJS模塊化原理:
NodeJS 會給每個文件包上這樣一層函數,引入模塊使用require,導出使用exports,而那些文件中定義的變量也將留在這個閉包中,不會污染到其他地方。
(funciton(exports, require, module, __filename, __dirname) { /* 自己寫的代碼 */ })();高階函數
一些使用閉包的經典例子:
節流函數
柯里化(Currying)
組合(Composing)
bind的實現
最后,如果你對閉包有更好的理解或者我文章里寫的不好的地方,還請指教。
AlloyTeam 歡迎優秀的小伙伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105833.html
摘要:同步一次執行一件事,同步引擎一次只執行一行,是同步的。調用函數將其推入堆棧并從函數返回將其彈出堆棧。執行上下文當函數放入到調用堆棧時由創建的環境。執行結果它會立即被推到回調隊列,但它仍然會等待調用堆棧為空才會執行。 為了保證可讀性,本文采用意譯而非直譯。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 一些名詞 JS引擎 — 一個讀取代碼并運行的引擎,沒有單一的J...
摘要:曾幾何時,閉包好像就是一個十分難以捉摸透的東西,看了很多文章,對閉包都各有說法,以致讓我十分暈,什么內部變量外部變量的,而且大多數都只描述一個過程,沒有給閉包的定義,最后,舉幾個例子,告訴你這就是閉包。 曾幾何時,閉包好像就是一個十分難以捉摸透的東西,看了很多文章,對閉包都各有說法,以致讓我十分暈,什么內部變量、外部變量的,而且大多數都只描述一個過程,沒有給閉包的定義,最后,舉幾個例子...
摘要:雙向數據綁定的核心和基礎是其內部真正參與數據雙向綁定流程的主要有和基于和發布者訂閱者模式,最終實現數據的雙向綁定。在這里把雙向數據綁定分為兩個流程收集依賴流程依賴收集會經過以上流程,最終數組中存放列表,數組中存放列表。 Vue雙向數據綁定的核心和基礎api是Object.defineProperty,其內部真正參與數據雙向綁定流程的主要有Obderver、Dep和Watcher,基于d...
摘要:引擎的運行原理引擎也是程序,是屬于瀏覽器的一部分,由瀏覽器廠商自行開發。為了提高運行速度,現代瀏覽器一般采用即時編譯即字節碼只在運行時編譯,用到哪一行就編譯哪一行,并且把編譯結果緩存這樣整個程序的運行速度能得到顯著提升。 相信大家在面試的過程中經常遇到查看執行順序的問題,如setTimeout,promise,async await等等,各種組合,是不是感覺頭都要暈掉了,其實這些問題最...
閱讀 2942·2021-10-28 09:32
閱讀 2967·2021-10-11 10:57
閱讀 3114·2021-10-08 10:05
閱讀 2588·2021-09-28 09:36
閱讀 2213·2019-08-30 15:55
閱讀 2270·2019-08-30 15:44
閱讀 2394·2019-08-30 14:02
閱讀 3075·2019-08-29 17:16