引言
在前面的两篇文章中,我们聊到了调用栈、作用域链。不知你发现了没有,我们都是用的var来声明的变量。如果你真的关注我的文章,那你肯定要问,为什么不用let、const声明变量。使用let、const声明变量,我们必先要了解一下词法环境。再补充一下作用域链,最后深入了解什么是闭包
词法环境
简单地说,就是用来存储let、const声明的变量。废话不多说,我们来看一份代码。
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()
请问输出的是什么?直接上图解
首先,创建了全局执行上下文和函数foo的执行上下文,我们可以看到用let声明的b被放在了词法环境中,并用了一个框框,这是因为词法环境也是一个小的栈。我们接下来看这个块级作用域的代码如何编译。
在这里,我们可以看到,var声明的c被放在了函数foo执行上下文的变量环境里,这是因为var不会和{}形成块级作用域。并且,let声明的b和d被放在了同一个框框中,这是因为在当前作用域里用let声明的b和d一起形成了这个块级作用域。
此时的代码已经编译完成,就应该要开始执行代码,那么从哪里开始执行呢?是第2行还是第5行?答案是第5行,这是因为在块级作用域里要先执行当前作用域的代码,所以,接下来,进行赋值操作,再进行打印输出。
此时,先执行第8、9行的打印输出,输出1、3,根据规则,当一个作用域内的所有代码都执行完毕后,就要出栈。所以此时,调用栈长这样。
我们接下来再执行11、12、13行的打印输出,需要说明的是,变量查找先从词法环境中再到变量环境中,所以完整的打印输出结果如下:
作用域链
同样的,我们还是先来看一份代码。请问输出的是什么?
function fn1() {
console.log(myname);
}
function fn2() {
var myname = "zs";
fn1();
}
var myname = "ls";
fn2();
直接上图解,所以,请你告诉我,输出的结果是什么?是zs吗?
答案是ls,如果你答错了,且听我狡辩。
如果你一直看我的文章,按照我们原来的思维,那输出结果还真会认为是zs,那么为什么输出的是ls呢?这是因为指针
我们第一天聊过作用域查找规则,是这么说的:先从当前作用域开始查找,如果没有找到,就去上一级作用域查找,直到找到为止,如果一直到全局作用域都没有找到,就会报错。每一个局部作用域压入栈时,都有一个指针指向上一级作用域,这才是压入栈的顺序。所以,请你告诉我,函数fn1的上一级作用域是在哪?很明显,在全局作用域。为什么?fn1不就是声明在全局作用域嘛!!所以,按照作用域查找规则,应该是这样的:
回归正题:闭包
同样的,先看一份代码,请问输出的是什么?zs 还是 ReferenceError: myname is not defined
function fn() {
var myname = "zs";
var age = 18;
return function fn1() {
console.log(myname);
}
}
var f = fn()
f()
直接上图解。
抓住重点:
1、fn1声明在fn里面,但fn里面并没有执行fn1方法体的代码,只是声明。
2、根据规则,当一个作用域里的代码都执行完毕后,就要出栈。
所以,我再问你,输出的是什么??
输出的是zs,如果你答错了,就再听我狡辩一回吧。
接下来讲解的便是闭包的核心。
你肯定要问我咯,边上的里面放着myname=zs的框框是什么东西。那我得先告诉你,这就是闭包。
你肯定又要问我??为什么会有这种设定?
我们熟记的规则:
1、根据作用域链的查找规则,内部函数一定有权力访问外部函数的变量
2、一个函数执行完毕后,它的执行上下文一定会被销毁。
闭包的出现就是为了保证上述两个条件都能够正常执行!
所以,什么是闭包?
定义:
根据作用域链的查找规则,内部函数一定有权力访问外部函数的变量
一个函数执行完毕后,它的执行上下文一定会被销毁。
那么,函数A内部声明了一个函数B,而函数B被拿到A的外部执行时(比如:return),为了保证以上两个条件都能正常执行,A函数在执行完毕后会将B需要访问的变量保存在一个集合中,并保留在调用栈当中,这个集合就是闭包。
经典面试题
请问输出的是什么?
var arr = []
for (var i = 0; i <= 5; i++) {
arr.push(function () {
console.log(i)
})
}
for (var j = 0; j < arr.length; j++) {
arr[j]()
}
输出结果:
解释:
1、变量i声明的全局作用域,匿名函数也是声明在全局作用域,数组arr只是保存了每一次for循环声明的匿名函数,但不会执行。真正开始执行是第9行代码,此时,已经结束了2-6行的for循环,但是变量i并没有被销毁,此时i=6,所以会循环输出6个6。
如果你得出正确答案,面试官就要接着往下问了:你能不能告诉我,怎么让它依次输出1 2 3 4 5
解法一:用let声明变量i,形成块级作用域。
// 1、使用let
var arr = []
for (let i = 0; i <= 5; i++) {
arr.push(function () {
console.log(i)
})
}
for (var j = 0; j < arr.length; j++) {
arr[j]()
}
解法二:形成闭包
如果你已经认真看了前面的讲解,那现在这种解法就很好理解了。
var arr = []
for (var i = 0; i <= 5; i++) {
function fn(j) {
arr.push(function () {
console.log(i)
})
}
}
for (var j = 0; j < arr.length; j++) {
arr[j]()
}
解法三:我愿称之为《暗度陈仓》
解释:利用var能重复声明的特性,输出打印i时,重复声明一个i来控制循环。
var arr = []
for (var i = 0; i <= 5; i++) {
arr.push(function () {
console.log(i)
})
}
for (var i = 0; i < arr.length; i++) {
arr[i]()
}
你还有其他的解法吗?欢迎评论区讨论
好了,第三更完结。
感谢阅读,期末周写作不易,明示求赞。