原文: 搞懂閉包 | AlloyTeam
做者:TAT.yaoyao
閉包這個概念是前端工程師必需要深入理解的,可是網上確實有一些文章會讓初學者以爲晦澀難懂,並且閉包的文章描述不一。前端
本文面向初級的程序員,聊一聊我對閉包的理解。固然若是你看到閉包聯想不到做用域鏈
與垃圾回收
也不妨看一眼。但願讀了它以後你再也不對閉包蒙圈。git
這裏有個需求,即寫一個計數器的函數,每調用一次計數器返回值加一:程序員
counter() // 1 counter() // 2 counter() // 3 ......
要想函數每次執行的返回不同,怎麼搞呢? 先簡單的寫一下:github
var index = 1; function counter() { return index ++; }
這樣作的確每次返回一個遞增的數。可是,它有如下三個問題:web
三個痛點,讓閉包來一次性優雅解決:前端工程師
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啊?閉包
鋪墊一些知識點,不展開講。app
函數每次執行,都會生成一個會建立一個稱爲執行上下文的內部對象(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) { /* 本身寫的代碼 */ })();
一些使用閉包的經典例子:
最後,若是你對閉包有更好的理解或者我文章裏寫的很差的地方,還請指教。
AlloyTeam 歡迎優秀的小夥伴加入。
簡歷投遞: alloyteam@qq.com
詳情可點擊 騰訊AlloyTeam招募Web前端工程師(社招)