在上一篇文章✨从预编译到闭包(一)---预编译【学习笔记】✨ - 掘金 (juejin.cn)中我们学习了关于js预编译的概念,在这篇文章中我们将了解闭包的概念
在了解闭包之前我们先了解一几个小概念
函数声明整体提升与变量声明变量提升
js
WuHu()
function WuHu(){
console.log('芜湖');
}
console.log(fly);
var fly = '起飞'
按直觉来说,代码从上往下一行一行执行的话,这段代码中调用WuHu()
时和console.log(fly);
时是不能够打印出我们想要的结果的,但是需要了解的是,在js中,函数声明会整体提升 ,用var声明变量,声明提升
所以在js的执行引擎眼中,这段代码是这样子的:
js
function WuHu(){
console.log('芜湖');
}
var fly
WuHu()
console.log(fly);
var fly = '起飞'
可以看到函数的声明和var的声明都芜湖起飞了,它们会芜湖起飞到其所在的执行上下文的最顶端,这样一来,在执行函数调用WuHu()
时,便可以打印到芜湖了,在执行console.log(fly);
时,由于只能找到变量声明var fly
,但不能找到fly的值,所以此时打印为undefined
。需要注意的是,打印undefined
与报错中的ReferenceError: fly is not defined
是不同的,undefined是指能找到这个变量,但是找不到这个变量的值,而报错中的ReferenceError: fly is not defined是指压根找不到这个变量,连变量的声明都找不到。
调用栈
我们来执行这样一段代码
js
function foo(){
console.log('咕噜');
foo()
}
foo()
放在浏览器中执行之后:
可以看到在打印了10235个咕噜之后,浏览器终究是扛不住了,这里我们便可以引入一个调用栈的概念。 其中RangeError: Maximum call stack size exceeded
是指栈溢出了,那么为什么会栈溢出呢,我们先来了解调用栈是干什么的
调用栈
· 是用来管理函数调用关系的一种数据结构
· 当一个函数执行完毕后,它的执行上下文一定会出栈
js
var a = 2
function add(b,c){
return b + c
}
function addAll(b,c){
var d = 10
var result = add(b,c)
return a + result + d
}
addAll(3,6)
在这段代码中,add函数的执行发生在addAll函数中,首先,创建一个全局的执行上下文压入调用栈的栈底,然后在调用addAll函数时,addAll的执行上下文会入栈,在addAll执行过程中,调用了add函数,此时add的执行上下文也入栈
首先add函数会先执行完成,然后出栈,紧接着addAll函数执行完成,出栈,最后所有代码执行完成,全局执行上下文出栈。
可以看出来,调用栈是用来管理函数调用关系的一种数据结构,并且当一个函数执行完毕后,它的执行上下文一定会出栈。所以在刚刚的foo函数的执行中,调用栈中会被持续压入很多很多个执行上下文,当超过调用栈的内存时,就会爆栈
作用域链
通过词法作用域来确定某作用域的外层作用域,查找变量由内而外的这种链状关系,叫做作用域链
js
function bar(){
console.log(myName);
}
function foo(){
var myName = '龙龙'
bar()
}
var myName = '君君'
foo()
在这段代码中,全局声明了一个myName为君君,然后调用foo时,在foo中,myName被赋值为龙龙,然后紧接着调用bar,bar执行时会打印myName,那么此时打印结果会是什么呢,这里就要涉及到作用域链了,由于console.log(myName);的词法作用域是bar,所以其外层作用域为全局,所以在打印myName时,由于bar的执行上下文中找不到myName,那么便往外一层的作用域即全局作用域中找myName,而全局中有一个var myName = '君君',所以console.log(myName)打印的结果为君君。而这种通过词法作用域来确定某作用域的外层作用域,查找变量由内而外的这种链状关系,叫做作用域链其实在全局和函数的执行上下文中都会有一个outer来记录该执行上下文外层的作用域,全局由于找不到更外层的作用域了,所以outer为null,而在这个例子中,bar和foo的outer都指向全局执行上下文,所以这两个函数在查找自身没有的变量时,都会到自己outer所指的作用域去查找,即全局执行上下文中查找变量。
闭包
在js中根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量的, 当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的 变量,那么这些变量依旧会被保存在内存中,我们把这些变量的集合称为闭包
js
function a(){
function b(){
var bbb = 234
console.log(aaa);
}
var aaa= 123
return b
}
var c = a()
c() //打印结果:123
我们以这段代码来理解闭包,首先a函数的执行结果为一个函数b,并且函数b声明在a内,由于var c = a() 并且调用了c,这其实相当于把a函数内的b函数拿到全局来执行了一遍。那么按照直觉来说,由于b函数在全局被执行,按照作用域的知识,应该会查找不到在a函数内的aaa这个变量,但是当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存中,我们把这些变量的集合称为闭包
用调用栈来理解,首先全局执行上下文先入栈,然后执行a函数,a函数的执行上下文入栈,a函数执行完毕后,正常情况下a函数的执行上下文会出栈,但是在a函数内声明了一个b函数,并且b函数在全局中调用,并且需要用到a函数内的aaa变量,此时a函数的执行上下文出栈后会留下一个变量的集合,这个集合里包含了内部函数b引用外部函数a的变量aaa,所以在执行到b函数中的console.log(aaa);时,首先在b函数内没有找到aaa这个变量,便往外层查找,虽然此时a函数的执行上下文出栈了,但是a函数留下了一个闭包,所以可以在闭包中找到aaa = 123,所以执行结果为123
所以闭包其实就是内部函数会引用到外部函数里的变量,但是内部函数被返回到外部函数之外被调用,当外部函数执行完毕并且执行上下文出栈后,会留下一个变量的集合来装下内部函数会引用到外部函数里的变量,这个变量的集合就称之为闭包