简述 js 闭包 来龙去脉

1,076 阅读6分钟

内容按个人学习经历、笔记总结,梳理对闭包的理解,可能有一些知识点理解有出入,希望大家多多指教。


闭包早期的使用场景

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对象 上设置同名keyvalue 默认为 undefined(var 声明提前的原因 , es6 let const有解决这个问题)

    • 初始化全局函数声明式:设置同名 keyvalue 初始化为 函数堆内存索引(这一点与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全局变量待遇一致,不做回收)

闭包优缺点总结

优势:

  • 变量、方法私有化(避免全局变量污染): 匿名函数自运行包裹业务代码

  • 访问权限:通过 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

关于闭包优化:

  1. 避开全局引用牵连: 在日常开发过程中,尽量避免把函数局部变量赋值给全局 window
  2. 主动释放局部变量: 对于已知体积较大的局部变量,必要时设置 null 清空其内存占用
(function(){      
    var responseData = [{}, ...] //例如这个变量存储了10万条数据
    var num = 0;    
                        
    setInterval(function(){ 
          console.log(++num)
          
          if(num === 8){//在确定业务代码完成任务,不再需要 responseDate 时
            responseData = null //解除内存占用
          }
    },500)

})()

《 第一次发文,如有收获,希望得到点赞!嘻嘻~ 》

主要参考资料:

  1. javaScript高级程序设计
  2. 前端大佬的相关文章(公众号:不知非攻)