内容按个人学习经历、笔记总结,梳理对闭包的理解,可能有一些知识点理解有出入,希望大家多多指教。
闭包早期的使用场景
JS模块化规范还未成熟前,前端开发过程中为了避免全局变量污染,比较流行的一种匿名函数自运行的写法
(function(){
var name = '123' //局部变量
function hobby(){ //局部方法
console.log(`my name is ${name}`)
}
window.module_A = { //对外 暴露对象 module_A
hobby
}
})()
看起来像不像把业务代码通过一个函数包裹起来?
可以这样理解:
-
闭包是前端模块化实现过程中的
一种js编写技巧:把业务代码放入一个函数中 -
变量私有化:把变量、方法进行私有化(声明在一个全局函数内,抽象为一个私有模块) -
变量访问权限:该私有模块可以指定暴露内容,控制外部对内部变量、方法的访问权限。
对闭包更细节的理解:
闭包的实现涉及js作用域、变量查找规则等知识,这也是这篇文章重点梳理的地方,因为光是抛出这两个知识点,不足以完整理解闭包的原理和特性。
所以接下来的内容,会做简要梳理,致力这些知识点串联起来,理解js内部的运行过程, function 的创建、执行、结束所发生的事情,才能更好的理解闭包及闭包的一些应用场景。
浏览器执行 index.js 文件的过程
-
语法检测: 例如使用变量未声明、花括号、引号格式不对等等,这个阶段会直接抛错,index.js 不做执行。
-
初始化全局执行环境:简单理解就是在内存中
创建window对象。-
初始化内置js对象:
document、location、history等 -
初始化全局变量:例如
var声明,在 window对象 上设置同名key,value默认为undefined(var 声明提前的原因 , es6 let const有解决这个问题) -
初始化全局函数声明式:设置同名
key,value初始化为函数堆内存索引(这一点与var声明不同,面试经常考:变量声明提前,函数整体提升的原因)
-
-
逐行执行代码:
- 赋值操作:
var a = 1, 把1同步到window.a = 1,供后续代码取值使用 - 代码取值:默认在
window[attr]取值 - function 执行: js函数调用栈推入该函数,函数初始化局部环境,执行:
语法检测,局部变量创建、逐行执行代码、内存回收(详情见下)
- 赋值操作:
-
代码执行结束: js内存回收机制不工作(与函数执行不同,全局变量不回收内存)
三、Function 执行的过程:
-
语法检测:同上
-
初始化局部执行环境: 创建局部变量对象,这个变量对象同全局变量对象
window类似 ,但有所不同-
this: 函数执行时,确认其调用者 -
arguments: 形参数组,存放传入的参数(实参) -
作用域链:变量对象(全局 or 局部)根据从内往外的顺序,形成类数组 -
局部变量和函数声明式的初始化:同全局相同function a (name,age){ var myname = name function b(){} //函数声明式 } window.a('tom',18) //a的局部变量 var a_scope = { this: window, arguments: ['tom',18], scopes: [a_scope, window], //作用域链 myname: undefined, b: 'xxx0102xx'//函数内存索引 ... }
-
-
逐行执行代码:同全局相同
-
代码执行结束:
return(对理解闭包缺点很重要)- return 函数执行结束:
js内存回收机制,会检测所有局部变量、函数声明式的引用次数(这里拿引用计数举例,不同浏览器回收算法不同) - 若都为 0 : 说明其他作用域的代码执行与a函数的局部变量对象
无牵连 无引用关系,回收a函数的局部执行环境,删除a函数的局部变量对象 - 若有局部变量 or 函数声明式被其他作用域引用: 其他作用域的代码执行可能依赖a函数的局部变量对象,
有牵连 有引用关系,保留a函数的局部变量对象(同window全局变量待遇一致,不做回收)
- return 函数执行结束:
闭包优缺点总结
优势:
-
变量、方法私有化(避免全局变量污染): 匿名函数自运行包裹业务代码 -
访问权限:通过 window 暴露模块接口
缺点:
-
局部变量跨作用域引用,导致自身
内存驻留:所占用的内存无法正常被js回收内存function parent(){ var name1 = 'parent' return function son(){ var name2 = name1 +'-son' //这里 子函数 引用 父函数的局部变量,导致 name1 被引用1次 window.sonFn = son //这里 son 与全局变量sonFn关联,产生引用牵连 } } parent()() //执行son //引用牵连: 1.sonFn 是全局变量,不能被回收,所以它指向的 闭包函数son 也不能被回收 2.闭包函数son 存储在 parent的局部变量对象 中,所以 parent的局部变量也不会被回收 //执行结果: 1.parent 执行结束后,局部变量对象被缓存在内存中,同window全局变量一样,等待页面关闭后回收 2.son 执行结束后,局部变量对象被释放,因为 name2 没有被其他作用域引用
五、常见闭包场景:
对于一些闭包写法,合理梳理引用关系,是否会导内存无法回收的现象 ?
判断公式:这个局部变量,是否直接 or 间接被 window 对象引用(牵连)?
案例分析:
1.定时器回调函数
(function(){
var num = 0;
setInterval(function(){ //注意setInterval的回调函数,是注册在window下的
console.log(++num)
},500)
})()
//1.setInterval的回调函数 被 window 引用,所以不能回收
//2.回调函数执行引用匿名函数的num,所以匿名函数的局部变量对象,不能回收
2.事件处理函数
(function(){
var num = 0;
document.onclick = function(){ //注意事件处理函数,注册在 window 下
console.log(++num)
}
})()
//num会同上述情况一样,无法被回收
3.利用闭包写法保存 变量 i
var arr1 = []
var arr2 = []
for(var i=0;i<10;i++){
//不使用不闭包
arr1[i] = ()=>{
console.log(i) //这里的 i,在函数执行时,访问的是 10(循环条件i<10不满足,最后的保留值)
}
//使用闭包
arr2[i] = ((i)=>{ //这里的 i 保存在 匿名函数的局部变量对象中
return ()=>{
console.log(i) //访问 局部变量对象 保存的 i
}
})(i)
}
arr1.forEach((f)=>{console.log(f())}) // 都是 10 ,因为访问的是全局的i,都指向一个值
arr2.forEach((f)=>{console.log(f())}) // 0~9 ,每次执行,访问的是自身所处作用域 专属的 i
关于闭包优化:
避开全局引用牵连: 在日常开发过程中,尽量避免把函数局部变量赋值给全局 window主动释放局部变量: 对于已知体积较大的局部变量,必要时设置 null 清空其内存占用
(function(){
var responseData = [{}, ...] //例如这个变量存储了10万条数据
var num = 0;
setInterval(function(){
console.log(++num)
if(num === 8){//在确定业务代码完成任务,不再需要 responseDate 时
responseData = null //解除内存占用
}
},500)
})()
《 第一次发文,如有收获,希望得到点赞!嘻嘻~ 》
主要参考资料:
- javaScript高级程序设计
- 前端大佬的相关文章(公众号:不知非攻)