深入理解 JavaScript 中的调用栈、作用域链和闭包

调用栈

调用栈是一个用来管理函数调用关系的数据结构。在JavaScript中,函数的调用是通过栈的方式进行的。每当一个函数被调用,它就会被推入调用栈的顶部,当函数执行完成后,就会从调用栈中弹出。这个过程符合"先进后出"的原则。

调用栈的主要作用是跟踪程序运行的位置,确保在执行函数时能够回到正确的上下文,例如

js 复制代码
var a=1;
function foo(){
    console.log(a);
}

function bar(){
    var a=2;
    foo()
}
bar()

在这个例子中,首先bar被压入调用栈中,而bar内部调用了foo,于是foo也被压入调用栈中,而调用栈先执行顶部的函数,于是foo被执行。foo执行完毕后被弹出接着再执行bar,bar执行完毕后也比被弹出调用栈。调用栈的维护确保了程序执行的顺序和上下文的正确切换。

作用域链

作用域链简单来说通过词法环境来确定某作用域的外层作用域,查找变量由内而外的链状关系,叫做作用域链。关于作用域的内容可以看我之前的一篇文章(什么是作用域 - 掘金 (juejin.cn))。

js 复制代码
var a=1;
function foo(){
   console.log(a);
}

function bar(){
   var a=2;
   foo()
}
bar()// 输出1

依旧是这个例子,foo和bar各自生成自己的词法作用域,而foo执行时会先在自己的词法作用域中寻找,找不到则去外层作用域也就是全局作用域中寻找,所以输出的a为1,各自的外层作用域是在函数声明时就被决定好了的。

闭包

了解完作用域和调用栈我们就可以来试着翻越一下闭包这座大山了。闭包是JavaScript中一个强大而复杂的概念。它产生于词法作用域的规则,即内部函数总是能够访问外部函数的变量。当通过调用一个外部函数返回一个内部函数后,即使外部函数已经执行完毕,但是内部函数引用了外部函数中的变量,这些变量依然会保存在内存中,形成了闭包。 或许看到这你仍然有些疑惑,接下来用几个例子跟大家详细说说。

js 复制代码
function foo(){
    var myName='Hu'
    let test1 = 1
    const test2 = 2
    var innerBar={
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName=newName
        }
    }
    return innerBar
}
var bar=foo()//foo执行完在调用栈被销毁,同时在相同位置生成一个闭包
bar.setName('Yang')
console.log(bar.getName());
// 输出1
// Yang

请大家仔细看,我们都知道函数在调用的时候会被压入调用栈。根据这段代码的逻辑,我们声明了一个bar会等于foo()的返回的结果也就是innerBar这个对象,但是根据调用栈的逻辑,foo()调用完之后,应该被调用栈弹出,并且相应的销毁他的词法作用域。但是!此时我们很惊讶的发现,我们的bar仍然能够正常调用innerBar的方法并且还能正常的访问和修改存在foo()词法作用域中的test1以及myName的值。这就是因为闭包。


我们原本的调用栈的知识并没有错,是的,foo()在执行完毕的时候确实在调用栈被销毁了,但是在v8引擎的预编译的阶段,他就发现innerBar在声明的时候就调用了自己外部的变量test1myName,这个时候就会形成一个闭包,这两个值仍然会被保存在内存当中能够正常访问和修改。
最后总结一下,闭包的形成主要有以下两个关键条件:

  1. 必须有函数嵌套,即在一个函数内部定义了另一个函数。
  2. 内部函数引用外部变量: 内部函数引用了外部函数的变量。这个引用可以是直接的变量引用,也可以是通过参数传递、对象属性等方式。

闭包的优缺点

闭包主要有两个优点:

  1. 实现私有化变量: 通过闭包,我们可以创建私有变量,防止其被外部访问和修改,从而实现一定程度的封装和数据保护。
  2. 保持变量状态: 闭包可以用于保持某些状态,例如计数器、缓存等,这些状态不会受到外部环境的干扰。

闭包的缺点:

  1. 闭包最主要的缺点是可能导致内存泄漏。由于闭包会保持对外部作用域变量的引用,如果不适当管理,这些变量可能会一直存在于内存中,占用空间,导致内存泄漏的问题。


尾声

我们在以后的工作中使用闭包一定要谨慎。注意避免内存泄漏,在以后那样大体量的工作中出现内存泄漏是很严重的,对应用程序的性能和稳定性产生不良影响。我们需要注意在不再需要闭包时,及时释放对外部变量的引用。例如,解绑事件监听器、清空定时器等操作都是需要注意的。

相关推荐
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
man20171 小时前
【2024最新】基于springboot+vue的闲一品交易平台lw+ppt
vue.js·spring boot·后端
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
hlsd#1 小时前
关于 SpringBoot 时间处理的总结
java·spring boot·后端
路在脚下@1 小时前
Spring Boot 的核心原理和工作机制
java·spring boot·后端
吖秧吖1 小时前
three.js 杂记
开发语言·前端·javascript