JavaScript函数闭包理解(面试篇)
前言:闭包可能是JavaScript中的一个特殊存在,在与后端和移动端同学沟通时他们都不知道闭包这个东西,而闭包也是我在面试过程中遇到很多的问题,在面试官问到我闭包是什么的时候我以前是这样回答的:闭包
就是js中的函数嵌套函数,内部函数将外部函数作用域中的变量保存使其在外部能访问到。
也就是一句话说闭包:闭包是指有权访问另一个函数作用域中的变量的函数。
我觉得闭包就是js执行环境以及作用域产生的一种特殊的环境。
最近通过文章《我从来不理解JavaScript闭包,直到有人这样向我解释它》再看闭包有了更深的理解,特此记录一下。
作用域与执行上下文
在这之前再看几个小例子:
a = 2;
var a;
console.log(a)
此时输出应该为什么?
console.log(a);
var a = 2;
那这个呢?
var a = 2;
function a (){}
console.log(a);
这个输出呢?
再来一个
var a = 2;
a = function(){}
console.log(a);
这个输出为什么?
这些问题的正确答案也是你看下去的基础。最好知道正确答案,或者去查阅知道正确答案。
要理解闭包就得先理解两个东西 一个是作用域
,一个是执行上下文
;
作用域
是什么?可以理解成我们可以有效访问变量或函数的区域。它是一个规则,规定了执行的程序中变量的存储和访问。也可以把它理解成一个“地盘”,我的地盘的东西只有我能使用你的地盘的东西我不能使用。
在javascript中又分全局作用域和局部作用域,这里就只说全局作用域
与函数作用域
;
来看代码:
1: var a = 1;
2: function func(){
3: var b = a+1
4: console.log(a+b); // 3
5: }
6: console.log(a); //1
7: func();
8: console.log(b); //Uncaught ReferenceError: b is not defined
这段代码有一个全局作用域,上面挂载了变量a,和函数fun;
还有一个函数fun的作用域,上面挂在了变量b;
关于这段代码我们细细品一下:(js引擎执行顺序)
当代码在解析执行的时候从上到下
- 第1行生成变量a,并赋值为10;
- 第2行到第5行是连在一起的,声明变量fun,并且是一个函数;(函数未执行时,内部东西先不用管)
- 第6行输出a;
- 第7行函数func()执行,此时回过头来看2-5行,第3行函数作用域fun中声明一个变量b,并赋值为a+1,此时在函数fun作用域中寻找a,未果,去函数外也就是父级作用域中寻找a,找到变量a=10,第4行输出a+b;函数执行完成。
- 第8行输出b
其实在这个分析过程就是js的执行上下文
。
执行上下文是当前正在执行的“代码环境”。执行上下文有两个阶段:编译和执行。
编译-在此阶段,JS 引擎获取所有函数声明并将其提升到其作用域的顶部,以便我们稍后可以引用它们并获取所有变量声明(使用var关键字进行声明),还会为它们提供默认值:undefined。
执行——在这个阶段中,它将值赋给之前提升的变量,并执行或调用函数(对象中的方法)。
注意:只有使用var声明的变量,或者函数声明才会被提升,相反,函数表达式或箭头函数,let和const声明的变量,这些都不会被提升。
执行全局代码时,会产生一个执行上下文环境
,每次调用函数都又会产生新的执行上下文环境
。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,因为处于活动状态的执行上下文环境只有一个。
了解了概念之后再看我们的执行过程
第1行 var a = undefined; a = 1;
第6行 输出a a = 1
第7行 函数func执行回到2-5行,var b = undefined; b = a+1;在函数func作用域中查找a没找到向上级找,在父级作用域也就是创建函数func的作用域中找到a = 1;此时这种向上查找的过程被称为作用域链
;函数执行完成后上下文环境以及其中的变量全都被销毁掉。处于活动状态的执行上下文环境只有一个;
第8行 输出变量b在当前作用域中查找不到,因此报错b is not defined
;
再从变量变化的角度清晰的分析一下这个代码执行过程:
(1)代码执行之前首先创建全局上下文环境
a: undefined
func: undefined
this: window
(2)接着执行代码到第5行之前,上下文环境中的变量都在执行过程中被赋值。
a: 1
func: function
this: window
(3)当执行到第7行的时候调用函数func时。生成新的执行上下文环境。
b: undefined
this: window
(4)赋值
b:2,
this.window
当func函数执行完毕之后,调用func函数生成的func上下文环境销毁
,里面的变量也被销毁,也就导致了在外面输出b变量会报错。
上面可能有些啰嗦,但我只是想从不同的角度去理解这些概念,不管怎么样,到这里执行上下文
与作用域
大概就稍微清晰一点了。
闭包
有了作用域以及执行上下文概念的辅助,理解闭包会更清晰一点。
javascript函数返回值类型可以是任何类型,如果没有返回值默认为undefind;
一. 先看一个返回函数的函数例子
1: var num = 1;
2: function creater(){
3: let add = function(a,b){
4: let ret = a+b;
5: return ret;
6: }
7: return add;
8: }
9: let func1 = creater();
10: let result = func1(num,2);
11: console.log(result);
- 第1行,声明变量
num
并赋值为1; - 第2-8行,声明变量
creater
并赋值一个函数,暂时不管它内部构造; - 第9行,全局执行上下文中声明变量
func1
,暂时,值为undefined
; - 第9行,看到括号
()
,此时需要执行调用一个函数,在全局作用域中查找找到变量creater
,然后调用它。 - 调用函数
creater
,走到第2行,此时创建一个新的creater
执行上下文,此时的活动对象就是creater
所在执行上下文的活动对象。 - 3-6行,声明变量
add
,并赋值为一个函数,此时add
只在creater
的执行上下文中。 - 第7行,返回变量
add
的内容,js引擎在当前作用域中查找名为add
的变量,好的在第3行找到,我们返回add
的定义,第4-5行括号之间的内容构成add
函数定义。 - 返回时,
creater
执行上下文被销毁,add
变量同时不复存在,但是,变量add
函数定义仍然存在,以为他返回并赋值给了func1
变量。 - 接着走第10行,在全局执行上下文声明一个变量
result
,这里拆解一下,先赋值为undefined
。 - 接着,需要执行一个函数,名为
func1
变量中定义的函数,全局查找,它找到有两个参数。 - 查找这两个参数,第一个参数为第1步的
num
,表示数字1,第二个数字是2。 - 现在需要执行这个函数,函数定义在3-5行,这时创建了一个
add
函数执行上下文,在add
执行上下文中创建两个变量a
和b
。他们分别被赋值为1和2。 - 第4行,在
add
执行上下文忠声明一个名为ret
的变量,并将变量a
,和变量b
相加的内容3赋值给了ret
。 - 第5行,
ret
变量从add
函数中返回,add
函数执行上下文被销毁,变量a
,b
,ret
不再存在。 - 返回值被分配到第10行的
result
变量中。 - 打印输出
result
的值。
整个流程大致走下来需要明白几点:
- 函数可以存储在变量中。
- 函数定义在程序调用之前是不可见的。
- 函数的返回值可以使任何类型,包括函数。
- 每次调用函数时,都会(临时)创建一个执行上下文,当函数完成时,执行上下文销毁。
- 函数在遇到return或者 } 时执行完成。
二. 看一个闭包
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
接着走一遍流程:
- 第1-8行,创建一个全局变量
createCounter
,并指定函数定义。 - 第9行,创建一个全局变量,
increment
; - 第9行,需要调用名为
createCounter
的函数。 - 回到1-8行,调用执行函数
createCounter
,创建一个名为createCounter
的新的执行上下文。 - 第2行,在
createCounter
执行上下文中声明一个变量counter
,并赋值为0; - 第3行,在
createCounter
执行上下文中声明一个变量myFunction
,并定义为一个函数,函数定义在4-5行。 - 第7行,返回
myFunction
函数定义,销毁createCounter
执行上下文,createCounter
上下文中声明的变量也全部销毁。 - 第10行,在全局上下文声明一个变量
c1
。 - 第10行,在全局查找
increment
变量,找到并调用它。它的函数定义在3-6行。 - 回到3-6行,创建一个名为
myFunction
的执行上下文。 - 第4行,
counter = counter + 1
,在myFunction
的执行上下文中查找counter
变量,没有找到,在全局执行上下文中查找counter
变量,也未找到。未找到会怎么样呢?会报错counter is not defined
。 - 这里会有个疑惑,为什么不去第2行去找这个
counter
变量?按照我们的理解createCounter
在此时是已经销毁的,所以createCounter
执行上下文里面的变量也是跟着销毁掉的,所以访问不到的。 - 到这里按照之前的分析逻辑似乎卡住了,貌似这段程序是没法执行的,但当你去运行这段程序的时候却发现它跑的很欢快。所以,这里是有猫腻的,什么猫腻呢?就是
闭包
。 - 我们可以这样理解闭包。
它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变量。当函数中返回一个函数的时候此时不仅仅返回的函数的定义,还连带他的背包也一并返回了,而这个背包中就存储了当前执行上下文的所有定义的变量
。 - 接下来再回到第7步,第7行返回
myFunction
函数定义,销毁createCounter
执行上下文,createCounter
上下文中声明的变量也全部销毁。但是此时不仅返回了myFunction
函数定义,还一并返回了它的背包。背包里存储了当前执行上下文的所有变量。 - 接下来直接到第11步,第4行,
counter = counter + 1
,在myFunction
执行上下文和全局执行上下文之前,先检查一下背包,背包里含有一个名为counter
的变量其值为0,在第4行表达之后它的值被设置为1,并且再次被存储在背包里;背包现在包含值为1的counter
。 - 第5行,
counter
值被返回出来,myFunction
执行上下文被销毁。 - 回到第10行。返回值1被赋给变量
c1
。 - 第11行,重复第9-18步,这一次在背包中变量
counter
的值是1,它是在第16步被设置,被递增为2并存储在当前执行上下文的函数背包里,c2
被赋值为2。 - 第12行,重复第9-18步,
c3
被赋值为3。 - 第13行,打印输出变量
c1,c2,c3
的值,分别为1,2,3
。
我们上面所说的背包也就是闭包。理解的话就把它理解成函数默认存在的一种机制,是否所有的函数都会有闭包,是的。全局范围创建的函数也具有闭包,但是由于全局环境变量都会访问到,所以全局范围的函数的闭包就显得不那么重要。但是当函数作为返回值被另外函数返回的时候,闭包就显得尤为重要,因为,被返回的函数可以访问到不属于全局作用域的变量,就是闭包中的变量。
最后总结一下,函数在被当做返回值返回的时候,该函数会携带自己的闭包,闭包是该函数声明时的作用域内部所有变量。
我们已经知道上面代码输出为1,2,3;
那么看一下下面这段代码c1,c2,c3分别输出多少呢?
function createCounter() {
let counter = 0
const myFunction = function(counter) {
counter = counter + 1
return counter
}
return myFunction(counter)
}
const increment = createCounter()
const c1 = increment
const c2 = increment
const c3 = increment
console.log('example increment', c1, c2, c3)
这是一种避免闭包的方式,闭包容易导致内存泄漏,造成性能问题。因此在写程序的时候尽量避免使用闭包。
这样将变量以参数的形式传递给被返回函数,这样就可以避免闭包。
觉得对你有用的,点个赞吧!!!