调用栈
调用栈是一个用来管理函数调用关系的数据结构。在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
在声明的时候就调用了自己外部的变量test1
和myName
,这个时候就会形成一个闭包,这两个值仍然会被保存在内存当中能够正常访问和修改。
最后总结一下,闭包的形成主要有以下两个关键条件:
- 必须有函数嵌套,即在一个函数内部定义了另一个函数。
- 内部函数引用外部变量: 内部函数引用了外部函数的变量。这个引用可以是直接的变量引用,也可以是通过参数传递、对象属性等方式。
闭包的优缺点
闭包主要有两个优点:
- 实现私有化变量: 通过闭包,我们可以创建私有变量,防止其被外部访问和修改,从而实现一定程度的封装和数据保护。
- 保持变量状态: 闭包可以用于保持某些状态,例如计数器、缓存等,这些状态不会受到外部环境的干扰。
闭包的缺点:
-
闭包最主要的缺点是可能导致内存泄漏。由于闭包会保持对外部作用域变量的引用,如果不适当管理,这些变量可能会一直存在于内存中,占用空间,导致内存泄漏的问题。
尾声
我们在以后的工作中使用闭包一定要谨慎。注意避免内存泄漏,在以后那样大体量的工作中出现内存泄漏是很严重的,对应用程序的性能和稳定性产生不良影响。我们需要注意在不再需要闭包时,及时释放对外部变量的引用。例如,解绑事件监听器、清空定时器等操作都是需要注意的。