原文: 搞懂闭包 | 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前端工程师(社招)