最近写项目的间隙偶尔复习下JS的基础内容,遇到闭包突然懵了,很多东西都忘记了(或者说根本没掌握过?哭。。)。于是在看闭包相关内容的时候顺便把闭包涉及到的一些概念都梳理一下,方便后续忘了的时候再复习(肯定会再次忘记啊啊啊啊啊,受不了)。
篇幅较长,理解原理可以直接跳转 3 闭包实现?
目录
1. 闭包概念
1.1 定义
- MDN给出的定义:
闭包是一个函数及其捆绑的周边环境状态(词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在JavaScript中,闭包会随着函数的创建而被同时创建。
说实话,第一句话我感觉自己怎么读都读不懂-.-。我自己觉得更容易理解的说法是:
闭包是函数和声明该函数的词法环境的组合。
即内部函数引用外部函数中的变量,就形成了闭包。
1.2 作用 及 使用场景
作用:
①创建私有变量,避免全局污染
②延长变量的生命周期
使用场景(以下使用场景的核心思想就是利用了闭包的上述两点作用):
计数器、延时调用、回调....可参考:面试官:谈谈对JS闭包的理解及常见应用场景(闭包的作用)_LYFlied的博客-CSDN博客
1.3 缺点
- 使用不当会造成内存泄漏。(闭包本身并不会导致内存泄漏)
- 闭包在处理速度和内存消耗方面堆脚本性能有负面影响
1.4 大白话总结
用大白话来讲的话,我觉得可以简单总结为:函数套函数。复杂点来说,就是:
有一个函数F,函数内部定义了一个成员函数C,并且F返回值为C,这就使得成员函数C可以访问其父函数F作用域中变量。由于F返回值为C,所以当调用F时我们会拿到成员函数C,此时C让F中的变量生命周期延长到了和外部调用者同样的周期。
2. 其他概念
为什么使用闭包可以延长变量的生命周期?要理解这一原理就必须先理解作用域链、执行上下文、JavaScript垃圾回收机制的概念以及JavaScript代码执行的两个阶段。
2.1 作用域链
2.1.1 定义
当在JS中使用一个变量时,首先JS引擎会尝试在当前作用域寻找该变量,若没找到,则到上层作用域寻找,直至找到该变量或到达全局作用域,如果在全局作用域仍没找到,则会在全局范围内隐式声明该变量(非严格模式下)或直接报错。
2.1.2 何时确定??
作用域链在函数定义时就已经创建了(重要)!!!储存在内部[[scope]]属性中。
2.2 执行上下文
2.2.1 定义和分类
执行上下文是一种对JS代码执行环境的抽象概念,其类型可以分为以下三种:
①全局执行上下文:只有一个,浏览器中的全局对象,即window对象
②函数执行上下文:可以有无数个,在函数被调用时创建(重要!)
③eval函数执行上下文:执行在eval函数中的代码,很少用
2.2.2 执行上下文的生命周期
如下图所示,各单独部分的具体内容可见:【你不知道的JavaScript】(三)执行上下文及其生命周期 - 简书 (jianshu.com)
2.3 内存管理 与 垃圾回收
2.3.1 内存管理
在JavaScript编程中,内存的管理(生命周期)分三个步骤:
① 分配内存
- 简单数据类型内存保存在固定的栈空间中,可直接通过值进行访问
- 引用数据类型的值大小不固定,其引用地址保存在栈空间,引用所指向的值保存在堆空间中,需要通过引用进行访问
② 使用内存
③ 清理内存
- 栈内存中的基本数据类型,可以直接通过操作系统进行处理(当关闭网页或者刷新页面时,window栈被销毁,栈内存被销毁时,基本类型数据也被销毁)
- 堆内存中的引用数据类型的值大小不确定,需要JS的引擎通过垃圾回收机制进行处理(当堆内存没被任何变量或其他东西占用时,浏览器会在空闲的时候自主进行内存回收)
2.3.2 垃圾回收
垃圾回收指的是一种自动内存管理机制,垃圾收集器会定期(周期性)找出不再继续使用的变量、对象等,释放其占用的内存,从而避免内存泄漏。
内存泄漏:向系统申请了内存进行使用,但是使用完了以后没有归还,导致内存空间浪费。
通俗来说——占着茅坑不拉屎。
浏览器的发展历史上有两种清除垃圾的策略:标记清除法(常用)、引用计数法(不常用),两种方法的差别以及造成内存泄漏的常见原因可以参考此文章:Javascript的垃圾回收机制知多少? - 掘金 (juejin.cn)
2.3.4 JS中的变量回收原则:
- 全局变量不会被回收
- 局部变量会被回收,即当函数运行完后,函数内部的东西会被销毁
- 只要被另一个作用域引用就不会被回收
2.4 JavaScript的执行
JavaScript属于解释型语言,JavaScript的执行分为解释和执行两个阶段,这两个阶段所做的事并不一样:
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定(重要!!!)
执行阶段:
- 创建执行上下文(重要!!!)
- 执行函数代码
- 垃圾回收
可以看出作用域和执行上下文的最大区别是——执行上下文在函数执行时确定,随时可能改变,而作用域在函数被定义时就已经确定,并且不会改变。
3 闭包实现?
3.1 原理
了解了上述概念后,我们回到正题:闭包为什么可以延长变量的生命周期?
让我们从程序执行流程开始梳理(以下列代码为例):
let a = 1
function createCompareFun(propertyName){
return function closure (object1,object2){
console.log(closure.prototype)
let name1 = object1[propertyName]
let name2 = object2[propertyName]
return name1>name2?1:0
}
}
let compare = createCompareFun(name)
let result = compare({name:'Nicholas'},{name: 'Matt'})
①程序启动,创建全局执行上下文,压入执行栈
- 此时createCompareFun函数还未被调用,但是已经为其创建作用域链,预装载全局变量对象,存储在内部[[scope]]属性中。
②代码开始执行,变量被赋值,createCompareFun函数被调用,创建函数执行上下文,压入执行栈
- 函数被调用,通过复制[[scope]]属性中的对象,构建作用域链,并将自己的活动对象推向当前作用域链的顶端,从而形成完整的作用域链。
- 此时内部函数closure被定义,其作用域链被创建(预装载了全局变量对象和createCompareFun函数的活动对象)并存储在内部[[scope]]属性中,即此时变量propertyName已经在[[scope]]属性中被引用。
③createCompareFun函数执行完毕,正常情况下当函数执行完毕时,它所产生的所有变量都会被垃圾回收,但是由于此时变量propertyName被闭包函数引用,因此不会被回收。
④closure函数被调用,创建其函数执行上下文,压入执行栈
- 函数被调用,通过复制[[scope]]属性中的对象,构建作用域链,并将自己的活动对象推向当前作用域链的顶端,形成完成作用域链。
⑤closure函数执行完毕,其产生的变量object1和object2被垃圾回收。
3.2 闭包所占用的内存如何释放
看到这里自然会产生一个问题:那么propertyName何时会被回收从而释放内存呢????
答:由于外层函数受闭包函数的影响,其变量被闭包函数引用,因此这个变量必须等到闭包函数回收后才被回收。可参考此网站中的评论:javascript - 闭包占用的内存可以被释放吗? - SegmentFault 思否
在上述示例代码中,compare变量是对闭包函数的引用,由于它存在于全局作用域中,因此不会被回收,即闭包函数一直被引用,没有被回收,因此propertyName的引用也一直存在,不会被回收。
如果要回收propertyName释放内存,我们需要回收闭包函数,通过 compare = null 手动解除引用,此时闭包就可以被回收掉了!!意味着propertyName也可以被回收掉!!!
至此,闭包分析结束。其应用场景潦草带过了,后续有空再进行补充。