【JS】”编程中的魔法:深入理解闭包”

前言

假如您也和我一样,在准备春招。欢迎加我微信lyhGetup,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

闭包是许多程序员头疼的一座山,本文将用通俗易懂的句子带领大家翻越这座山,看完这篇文章,定会让你对闭包有一个更深的理解。

那么要全面了解闭包之前呢,我们必须先了解三个概念,一个是执行上下文,一个是调用栈,还有一个就是作用域链了。如果你已经对这些很熟悉了,可以跳过。

正文

1.首先什么是执行上下文

只要有作用域的地方就有执行上下文,包括变量环境、词法环境。执行上下文中存储着当前执行代码所需的所有信息,包括变量、函数、作用域链、this 指向等。

V8 的执行上下文可以分为二种类型:

  1. 全局执行上下文:在代码执行之初就会创建的执行上下文,代表着整个 JavaScript 程序的运行环境。全局执行上下文只有一个,存储着全局变量、全局函数等信息。
  2. 函数执行上下文:每当调用一个函数时,都会创建一个函数执行上下文。函数执行上下文中包含了函数的参数、局部变量以及函数内部的作用域链等信息。当函数执行完毕后,其执行上下文会被销毁。

用图示来看会更清楚些(基于下面的代码):
编译代码时创建全局执行上下文,然后去执行全局代码。然后去编译add函数内部代码,创建函数执行上下文,然后去执行函数内部代码

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

【JS】"编程中的魔法:深入理解闭包"

那么执行到return a+b时如何访问a变量呢?就涉及到了下一个知识点:调用栈。

2.什么是调用栈

在JavaScript中,调用栈是一个用于存储函数调用的栈结构。当执行JavaScript代码时,每次函数调用都会在调用栈中创建一个新的栈帧,用于存储该函数的执行上下文(execution context)

当一个函数被调用时,它的栈帧被推入调用栈的顶部,然后执行函数体内的代码。如果在函数体内又调用了其他函数,那么这些新函数的栈帧也会被依次推入栈顶。当一个函数执行完毕后,它的栈帧会被弹出调用栈,控制权回到调用该函数的地方,并继续执行。

继续使用一段代码来举例:

var a =2 
function add(b,c){
    return b+c
}
function addAll(b,c){
    var d = 10
    result =add(b,c)
    return a + result + d
}
// add是一个执行上下文对象
addAll(3,6)    

先依次创建全局执行上下文、addAll的执行上下文、add的执行上下文,并被推入调用栈,然后当add、addAll、全局代码依次执行完毕后,依次被弹出调用栈。

【JS】"编程中的魔法:深入理解闭包"

3.什么是作用域链

简单来说就是通过词法作用域来确定某作用域的外层作用域,查找变量由内而外的这种链状关系叫作用域链,当代码在一个作用域中查找变量或函数时,它会先从当前作用域开始查找,如果没有找到,就会沿着作用域链向上查找,直到找到为止。

来个例子加强下理解:

function outer()
{ 
    var x = 10;
    function inner() { 
        var y = 20;
        console.log(y); // 20 
        console.log(x); // 10 
        console.log(outer()); // undefined,因为 outer 函数没有返回值 
    } 
    inner(); 
 } 
 
 outer();

在上述示例中,函数 inner 的外部作用域是函数 outer,全局作用域的外部作用域为 null。通过作用域链,我们可以在 inner 函数内部访问到外部作用域中的变量 x,以及外部作用域中的函数 outer

闭包

—— 那么什么是闭包?

在js中根据词法作用域的规则,內部函数总是可以访问其外部函数中声明的变量的,当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,那么这些变量依旧会被保存在内存 (调用栈) 中。那么我们把这些变量的集合(比喻为小背包)称为 “闭包”。

相信大家对这段话依然很懵,别急,我将用一个实例给你讲明白:

//  闭包实例
function foo(){
    var myName = '旭旭'
    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('明明')
console.log(bar.getName());

打印结果为: 1、明明

那么我们来认真分析一下。首先这里有一个函数foo1. foo内部还有两个函数:getName和setName。 那么很显然这里的getNamesetName就是内部函数,foo就是外部函数。且2. getName中访问了foo中声明的myName和test1变量,setName访问了foo中声明的myName。 ,最后3.getName和setName在foo外部被调用。 所以聪明的你肯定能发现myNametest1的集合就是一个闭包。所以就能很轻松的解释,为何foo的执行上下文被销毁了,还能访问其中声明的变量。这也是闭包的核心所在。

总结来说闭包的核心就是:外部和内部函数、内部函数访问外部函数中声明的变量、内部函数被返回到外部函数之外。

优点

闭包是 JavaScript 中一种非常强大和灵活的特性,它提供了许多功能和优势,包括封装变量、保持状态、实现模块化、解决异步问题以及创建函数工厂等。通过合理地使用闭包,可以使代码更加优雅、简洁和可维护。

缺点

闭包的缺点也很明显,最简单的就是内存泄漏:闭包会导致额外的内存消耗。由于闭包会捕获外部函数的变量和作用域链,这些变量和作用域链会一直存在于内存中,即使外部函数已经执行完毕。如果闭包被滥用或不正确使用,可能会导致内存泄漏或占用过多的内存。

面试

闭包在面试中也是经常被提及,今天给大家模拟一道闭包的面试题:

如何成功打印出数字从0-9

var arr = []
for(var i = 0;i < 10;i++){ 
    arr[i] = function(){ 
        console.log(j); 
     } 
} 
for(var j = 0;j < arr.length;j++){ 
    arr[j]() 
}

第一种:

直接把 var i = 0 改成 let i = 0 即可。

第二种:

用闭包。

var arr = []
for(var i=0; i<10;i++)
{
    (function a(j){
        arr[i]=function(){
            console.log(j);
        }
    })(i)
}
for(var j = 0;j < arr.length;j++){ 
    arr[j]() 
}

注意(func a(j){...})(i)是一个自执行函数。a是外部函数、arr[i]是内部函数。内部函数拿到外部函数外调用,且i被当作实参传入a函数中,即形成i的闭包,i被保存下来。 这样,我们就实现了按顺序输出0123456789的效果。

最后

本篇比较干,我力求以小白的视角为大家深入剖析,真诚期待每一位读者都能对包装类有更深刻的认知。

有任何想法和建议欢迎大家在评论区留言哦~

点个免费的赞鼓励支持一下吧!

假如您也和我一样,在准备春招。欢迎加我微信lyhGetup,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!

原文链接:https://juejin.cn/post/7337205293775028264 作者:UrGend

(0)
上一篇 2024年2月20日 下午5:05
下一篇 2024年2月20日 下午5:15

相关推荐

发表回复

登录后才能评论