恍然大悟之JS闭包快速理解

269 阅读4分钟

前言

闭包是JS的一个难点也是它的一个特色,理解闭包非常重要,我们必须掌握。那么什么是闭包呢?它又有什么作用呢?下面我们一起来寻找答案!

问题1:什么是调用栈

在理解闭包之前,我们先了解下什么是调用栈

所谓的调用栈,是用来管理函数调用关系的一种数据结构。当一个函数执行完毕后,该函数的执行上下文就会被销毁(出栈)。

请看下面一个案例:

var a=2
function add(){
    var b=10
    return a+b;
}
add()

我们知道V8引擎会维护一个调用栈,代码执行前进行编译,该案例的调用栈如下所示:

image.png

解释一下:变量环境和词法环境并没有本质的区别,只是为了方便区分var和let、const。 变量环境: 通过var声明或者function(){}声明变量存放在这里,用来提升;词法环境: 通过let、const创建的变量存放在这,不会提升。

问题2:什么是闭包

  • 定义: 当通过调用外部函数A返回的内部函数B后,即使外部函数已经执行结束了,但是 内部函数引用了外部函数的变量依然会保存在内存中,我们把这些变量的集合,称为闭包

  • 形成: 当函数A将内部函数B返回出来调用时

案例1:

function a(){
    function b(){
        var bbb=234
        console.log(aaa); //123
    }
    var aaa=123
    return b
}
var demo=a() //函数a的调用返回得到函数b     //闭包 
demo()  //调用函数b

此调用栈为:

image.png

a的执行带来了b的创建,b函数在a函数里面,所以b函数可以访问a函数的执行上下文。从上到下进行调用。

可以看到函数a返回的是函数b,在全局作用域下调用了函数a,并把返回的值给了变量demo,然后进行调用,也就是函数b的调用,在函数b内用到了外部函数a的变量aaa,在函数a调用结束后该函数执行上下文会销毁,但会保留一部分留在内存中供函数b使用,这就形成了闭包。

案例2:

function foo(){
    var myName ='好看'

    let test1=1
    const test2=2 //let const 词法环境里的

    var innerBar={
        getName: function(){
            console.log(test1);
            return myName
        },
        setName:function(newName){
            myName=newName
        }
    }
    return innerBar
}
var bar=foo() //innerBar foo执行完后销毁,但有小部分保存在内部
bar.setName('计划')
console.log(bar.getName());

此调用栈为:

image.png image.png

var bar=foo() foo执行完后销毁,但有小部分保存在内部。本来foo函数执行完后应该被销毁,但是因为形成了闭包,还有其他内部函数对其变量的访问(setName、getName),所以强制导致foo执行上下文无法彻底销毁,在内存中会有小部分closure(闭包)——其中存放的就是被用到的变量myName、test1

image.png

closure(闭包)就像一个setName、getName的专属背包,只有它们可以访问,其他方法都不能访问,所以我们把这个背包称为是foo函数的闭包。

问题3:闭包有什么作用

  • 模块化开发(实现公有变量)

案例1:

function add(){
    let count=0
    count++
    console.log(count);
}
 add()
 add() 
 add()
 add()

会发现打印输出了4次1,但是我们想要实现的是累加效果,也不想用全局变量,那么我们怎么实现这个效果呢?结果就是用到闭包!

function add(){
    let count=0
    // count++
    // console.log(count);
    function a(){
        count++
        console.log(count);
    }
    return a
}
var res=add() //把函数a赋值给res变量
// add()
// add()
// add()
res() //1 调用了函数a
res() //2
res() //3
res() //4
  • 做缓存
  • 封装私有化属性
  • 防止全局变量污染

问题4:闭包有什么缺点

  • 一旦形成闭包,只有在页面关闭后闭包占用的内存才会被收回,所以造成了内存泄漏。

思考题:

function test(){
    var arr=[]    //arr[0]=function(){},arr[1]=function(){},arr[2]=function(){}...  
    for(var i=0;i<10;i++){
        arr[i]=function(){
            console.log(i);
        }
    }
    return arr
}
var myArr=test() //调用函数test,返回的是arr数组
for(var j=0;j<myArr.length;j++){
    myArr[j]();
}

打印出来的是10次10,如果我们想要打印出来0-9的连续数,那么怎么实现呢?怎么利用闭包的方法实现?

思路: 可以利用自执行函数并形成闭包。for循环里的变量i是在test函数里不是在内部function内,相当于一个公有的变量i,最后的值都是循环结束后i的值。所以这样的闭包形成的是公用的i。

解决办法:arr[i]=function(){ console.log(i); }外再加一层闭包——可以用一个自执行函数把它包起来

function test(){
    var arr=[]
    for(var i=0;i<10;i++){
        (function(j){
            // var j=0
            arr[j]=function(){
            console.log(j);
            }
        })(i)
        
    }
    return arr
}
var myArr=test()
for(var j=0;j<myArr.length;j++){
    myArr[j]();
}
image.png

结语

本篇文章就到此为止啦,由于本人经验水平有限,难免会有纰漏,对此欢迎指正。如觉得本文对你有帮助的话,欢迎点赞收藏❤❤❤,写作不易,持续输出的背后是无数个日夜的积累,您的点赞是持续写作的动力,感谢支持。