闭包是JavaScript中最基本也是最重要的概念之一,很多开发者都对它了如指掌,可是闭包又绝对不是一个单一的概念,它涉及作用域,作用域链,执行上下文,内存管理等众多知识点,不管是新手还是“老司机”,经常会出现我觉得我弄懂了闭包,但是在一些情况下也会翻车。
闭包不是一个单一的概念,在知道闭包的概念之前,我们要先了解一下相关知识点。
作用域
作用域可以被理解为某种规则下的限定范围,任何语言都有作用域的概念,同一种语言在演进过程中也会不断完善其作用域规则。
函数作用域和全局作用域
在JavaScript中执行某个函数时,如果遇见变量且需要读取其值,就会就近先在函数内部查找该变量的声明或者赋值情况,如果在函数内无法找到该变量,就要跳出函数作用域到更上层作用域中查找这里的更上层作用域,可能也是一个函数作用域。另外,更上层作用域也可以顺着作用域范围向外扩散,一直到全局作用域。
块级作用域和暂时性死区
作用域概念不断演进,ES6中增加了通过let和const声明变量的块级作用域,使得JavaScript中的作用域内涵更加丰富,块级作用域是指作用域范围限制在代码块中,这个概念在其他语言中也非常普遍,当然,这些新特性的出现也增加了一定的复杂度,带来了新的概念,比如暂时性死区。
ES6新增的let、const关键字声明的变量会产生块级作用域,如果变量在当前作用域中被创建之前被创建出来,由于此时还未完成语法绑定,如果我们访问或使用该变量,就会产生暂时性死区的问题,由此我们可以得知,从变量的创建到语法绑定之间这一段空间,我们就可以理解为‘暂时性死区’。
执行上下文和调用栈
很多人都无法准确定义执行上下文和调用栈,其实从我们接触JavaScript开始,这两个概念常伴左右,我们写出的每一行代码,每一个函数,都与他们息息相关,但他们却是隐形的藏在代码背后,出现在JavaScript引擎中。执行上下文就是当前代码的执行环境或者作用域和前文介绍的作用域链相辅相成,但又完全是不同的概念,直观上来看,执行上下文包含了作用域链,同时它们又像是一条河的上下游,有了作用域链才会有执行上下文的一部分。
执行JavaScript代码主要分为两个阶段,一是代码预编译阶段,二是代码执行阶段。
预编译阶段是前置阶段,这一阶段会由编译器将代码编译成可执行代码,这里的预编译和传统的编译不同,传统的编译非常复杂,涉及分词,解析,代码生成等过程,这里的预编译是JavaScript中的独特概念,虽然JavaScript是解释型语言,编译一行,执行一行,但是在代码执行前,JavaScript引擎确实会做一些预先准备工作。
执行阶段的主要任务是执行代码逻辑,执行上下文,在这个阶段会全部创建完成,再通过语法分析确认语法无误之后,便会在预编译阶段对JavaScript代码中的变量的内存空间进行分配,我们熟悉的变量提升过程就是在此阶段完成的。
对于预编译过程的一些细节,我们应该注意三点,一是在预编译阶段进行变量声明,二是在预编译阶段对变量声明进行提升,其值为underfined,三是在预编译阶段,对所有非表达式的函数声明进行提升。
作用域在预编译阶段确定,但是作用域链是在执行上下文的创建阶段完全生成的,应为函数调用时才会创建对应的执行上下文,执行上下文包含变量,对象作用域链以及this的指向。
调用栈
函数调用栈就比较好理解了,在执行一个函数时,如果这个函数又调用了另外一个函数,而这另外一个函数又调了另外一个函数,这样就形成一系列的调用栈。
闭包
闭包并不是JavaScript中特有的概念,社区中对于闭包的定义并不完全相同,我理解中的闭包的定义就是函数嵌套,函数时内层,函数引用了外层函数作用,域下的变量,并且内层函数在全局变量下可以访问,进而形成了闭包,代码如下:
function numGrnerator(){
let num = 1
num++
return() => {
console.log(num)
}
}
var getNum = numGenerator()
getNum()
我们知道,在正常情况下,外界是无法访问函数内部变量的函数执行之后,上下文就被销毁,但是在函数外层中,如果我们返回了另一个函数,且这个返回的函数使用了函数外层内部的变量,那么外界便能够通过这个返回的函数获取原函数外层内部的变量值,这就是闭包的基本原理。