本文已参与「新人创作礼」活动,一起开启掘金创作之路。
作用域、作用域链、变量提升、预编译和闭包
闭包
什么情况下会形成闭包
函数内部定义的函数,被返回了出去并在外部调用时会产生闭包
function a(){
function b(){
var bbb = 234
console.log(aaa);//123 闭包
}
var aaa = 123
return b//b定义在a里面,但是被保存出去了
}
var demo = a()
demo()
函数b是定义在函数a内部的,函数a执行时将函数b返回了出来,赋值给变量demo并调用。这种情况就产生了闭包
什么是闭包
闭包定义:在js中根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回的一个内部函数后,即使该外部函数已经执行结束,但是内部函数引用外部函数的变量依然保存在内存中,我们把这些变量的集合称为闭包
首先我们要知道,在一个执行上下文里面包括变量环境,词法环境和一个outer,简单的理解,就是var和function的声明会存储在变量环境中,而let,const,try-catch等声明会存储在词法环境当中,词法环境仍然保持一个栈的存储结构,而outer是指向当前执行上下文的上一级(父级)执行上下文。
接着我们用一个实例,让我们对闭包的认识不再那么抽象:
function foo() {
var myName = 'aaa'
let test1 = 1
let test2 = 2
var innerBar = {
getName: function () {
console.log(test1);
return myName
},
setName: function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName('bbb')
bar.getName()
console.log(bar.getName());
全局下定义了foo函数和bar变量,将全局上下文压入调用栈,
接着,是foo函数的执行,所以将foo的执行上下文压入调用栈,并返回其内部的innerBar,将innerBar赋值给bar变量,
在foo执行结束后,foo的执行上下文应该被销毁,但是由于后面调用了getName函数和setName函数,且这两个函数里有使用到foo中定义的myName和test1,所以,foo的执行上下文(AO对象)并没有被销毁,而是变成了这样
没错,foo(closure)这个变量的集合就是闭包,它里面只有被用到的myName和test1,这个闭包就像一个小背包一样,函数getName和setName无论在哪里被调用,都会带着这个小背包。
相信,通过上述的例子,大家对闭包的认识更具象化了,那么闭包到底有什么用呢,他肯定是有优缺点的。
闭包的优点(作用)
- 实现公有化变量(企业的模块开发)
- 模块化开发,防止污染全局变量
假设现在有一个变量count = 0,要实现一个函数,使得每调用一次函数count的值都加一,如下
var count = 0
function test(){
count++
console.log(count);
}
test()//1
test()//2
test()//3
test()//4
那么如果是这样写,就会造成全局变量污染,因为count是定义在全局的,但是闭包能够解决这个问题,
function add(){
var num = 0
function a(){
console.log(++num);
}
return a
}
var res = add()
res()
res()
res()
res()
使用闭包的方法,这样既能保证模块化开发,又能放止污染全局变量
- 做缓存
function fruit(){
var food = 'apple'
var obj = {
eatFood:function(){
if(food!==''){
console.log('I am eating ' + food);
food = ''
}else{
console.log('There is nothing');
}
},
pushFood:function(myFood){
food = myFood
}
}
return obj
}
var person = fruit()
person.eatFood()
person.eatFood()
person.pushFood('banana')
person.eatFood()
这段代码运行的结果如下
我们会发现,像这样,我们可以使得两个或多个函数,去连续的修改一个变量(此处的food),这就叫做缓存。
- 实现属性的私有化
闭包的缺点
- 闭包会导致原有的作用域链不释放,造成内存泄漏,导致调用栈的空间原来越少,而调用栈其实是有固定大小的,所以会导致栈溢出。闭包虽然有这个缺点,但是它利大于弊,我们要注意的是不要滥用闭包就好。
拓展
变量的查找路径
前面我们知道了,执行上下文中,有变量环境,词法环境,和outer,当我们查找变量的时候,我们是从词法环境开始找,如果没有,则进到变量环境找。如图:
那么如果还没有,就会去到outer指向的执行上下文里继续找,就这样,直到找到为止,如果找到底了还是没有,就会报错。前文说过outer是指向当前执行上下文的上一级(父级)执行上下文,那么这个上一级的执行上下文到底是哪个呢?我们看下面这个例子:
function bar(){
console.log(myName);
}
function foo () {
var myName = 'aaa'
bar()
}
var myName = 'bbb'
foo()
bar里没有myName,他要去父级执行上下文中去找,这个打印结果一般来说,大家都会觉得会进入foo的执行上下文去找(也就是说认为outer指向的是foo,foo是bar的父级),所以最终打印的应该是aaa,实则不然:
打印的结果是bbb,因为,bar的父级执行上下文其实是Window全局,所以他找到了全局下的myName,打印bbb,这是因为,父级执行上下文不是看这个函数在哪里被调用的,而是看它在哪里被定义的。 bar是在全局下定义的,所以全局的执行上下文才是它的父级执行上下文,bar执行上下文中的outer指向全局执行上下文(GO对象)。
一道难题(面试题)
for(var i = 0;i<6;i++){
setTimeout(()=>{
console.log(i);
})
}
可以看到,上述代码的执行结果是打印了六次6,这是因为setTimeout是异步执行的,简而言之就是会放到最后一起执行,我们如何让他照常打印出0,1,2,3,4,5呢?
最根本的思路:找个变量将i存起来。
第一种方法就是用let
将代码中for循环的var i 改成let i
for(let i = 0;i<6;i++){
setTimeout(()=>{
console.log(i);
})
}
其原理如下
let i;
for(i=0;i<10;i++){
let j = i
setTimeout(()=>{
console.log(j)
})
}
这其实就是相当于在for循环里面定义了一个j,去保存i的值,而let 声明不会变量提升,var的变量声明会提升,所以将var改成let即可,这是最简单的方法,那么其实还有另外两种。
第二种就是今天聊到的闭包
将代码改写成闭包的样子:
for(var i = 0;i<6;i++){
(function(j){
setTimeout(()=>{
console.log(j);
})
})(i)
}
这是一个自执行函数形成闭包,因为let声明不会变量提升,var会,但是在函数中var声明提升到函数体内的最前面,不会提升到函数外面,换句话说,var的变量提升会穿过到{}(花括号)块级作用域外面,但是不会穿过到函数作用域外面,这就相当于,在自执行函数定义了一个形参j,然后将i传进去,用j去保存i的值,而这里定义j变量提升不会提到外面去,所以生效,这就是闭包的好处。打印结果如下:
第三种方法就是用setTimeout的第三个参数
将代码中setTimeout后面传入第三个参数
for(var i = 0;i<6;i++){
setTimeout((j)=>{
console.log(j);
},1000,i)
}
这第三个参数,就是将i传进去,然后setTimeout中的箭头函数定义一个形参j去保存i,最后的效果如下:
这三种方法在最基本的思路都是用另一个变量去保存i的值,其中有一种是闭包的方法解决问题,就拉出来讲一下。