前言
作用域链(Scope Chain)和调用栈(Call Stack)是两个与 JavaScript 中作用域和函数执行密切相关的概念,在开始今天的内容之前,我们先需要了解一下预编译这个这个概念,在之前的文章中我们有聊过,小伙伴们可以先去看看,配合起来食用效果更佳~
点击这里跳转:预编译
调用栈
我们先来举一个例子:
js
function foo() {
console.log('hello');
foo()
}
foo()
当我们调用foo()这个函数时,会一直打印'hello',我们在浏览器控制台里运行一下

可以发现,当打印了11192个'hello'时,控制台报错了,并提示 MAximum call stack size exceed,这句话的意思是已经超出了栈的最大容量,我们来思考一下,为什么会这样呢?
JavaScript 使用调用栈来跟踪函数的调用 ,每次函数调用都会将函数的上下文压入栈中 ,然后在函数返回时将其弹出。由于这个函数没有终止条件,调用栈会不断增长,直到达到最大深度,最终触发栈溢出错误。
在我们这个例子中,每执行一次都会继续调用foo(),每执行一次就会将它压入调用栈中,而因为这个函数并不会终止,调用栈并不会将每一次执行的函数弹出,所以一直往栈里面堆积,直到超出了栈的最大深度,就会报错,我们可以用下图来解释一下。

我们可以将蓝框看成一个栈,当foo()的调用达到栈的最大深度时,在调用一次,就会超出栈,然后报错!
相信大家现在已经对调用栈有些理解了吧,接下来我们来看看调用栈的概念。
JavaScript调用栈(JavaScript Call Stack)是一个用于跟踪函数调用的数据结构,它遵循后进先出(Last-In-First-Out,LIFO)的原则。JavaScript引擎使用调用栈来管理函数的执行顺序。当你在JavaScript程序中调用一个函数,引擎将该函数的调用添加到调用栈的顶部,并在函数执行完成后从栈中弹出。 我们来看一看这个例子:
js
function foo() {
console.log('foo');
bar();
}
function bar() {
console.log('bar');
}
foo();
在这个示例中,foo
函数被调用,然后 bar
函数被调用。调用栈会按照以下顺序记录函数的执行:
foo
进入调用栈。foo
调用bar
,bar
进入调用栈。bar
执行完成,从调用栈中弹出。foo
执行完成,从调用栈中弹出。
这样我们就很好理解调用栈了,接下来我们再来看一道题,进阶一下,结合全局预编译和函数预编译,我们来想想这个例子输出什么
js
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
}
console.log(addAll(3, 6));
- 每当一个函数被调用时,都会创建一个新的函数执行上下文(AO对象)。这个执行上下文包括函数的参数、局部变量和内部函数等信息。函数执行上下文可以嵌套,当一个函数内部调用另一个函数时,会在执行栈中创建一个新的执行上下文,形成执行上下文的栈结构。
- 全局执行上下文(Global Execution Context):全局执行上下文(GO对象) 是JavaScript代码的最顶层执行上下文,它在整个程序执行期间一直存在。在浏览器环境中,全局执行上下文通常代表的是全局作用域,包括全局对象(如
window
对象)和全局变量。全局执行上下文是执行栈的最底部,它首先被创建。
在代码开始执行时,先会开始全局预编译,创建GO对象,然后在函数执行之前,创建AO对象,GO对象和AO对象,这里在之前的文章有讲过,首先先把全局执行上下文压入栈区,当函数执行的那一刻前,讲函数执行上下文压入栈区,这里我们放一张图来直观的看一下:
首先,全局执行上下文先入栈,其次,因为先执行的addAll函数,所以它第二入栈
执行addAll函数时,调用了add函数,它最后入栈
执行add()函数完时,add()函数将b+c的值返回给result
,函数执行完成,出栈,result 的值为9
addAll函数执行到return a + result + d
时,发现内部并没有a
,于是从他的外部去找,在全局执行上下文中找到了a
,执行完毕,将值 10 + 9 + 2 = 21
返回给console.log输出,addAll函数出栈
最后打印输出完成时,全局执行上下文出栈
作用域链
我们先放出一道题,小伙伴们先来思考一下,最后会输出什么呢?
js
function bar() {
console.log(myName);
}
function foo() {
var myName = '菌菌'
bar()
}
var myName = '来颗奇趣蛋'
foo()
小伙伴们看了我们上面的解释,是不是会毫不犹豫的说,输入'菌菌'呢,但其实不是,这里输出的是 '来颗奇趣蛋'。

现在小伙伴们有疑问了,你不是说当内部没有查到变量,要从外面查找吗,这里在外面找到了myName,它的值为'菌菌',这里应该输出'菌菌'啊,为什么这里会去全局里找变量,输入'来颗奇趣蛋'呢?
其实从内往外找这个说法并不明确,只是为了我们更好的理解前面讲的知识而简洁的解释一下,具体而言应该是从外层作用域查找,查找变量由内而外的这种链状关系,叫做作用域链,为了更好的理解,我们要先知道词法作用域是什么
词法作用域:
一个函数或变量'出生'在哪里,那么他们的词法作用域就在哪里,下面我们举个例子
js
var global = 1
function foo() {
var x = 10;
function bar() {
console.log(x); // 可以访问外部作用域中的变量 x
}
bar();
}
foo();
- foo()声明在全局区,也可以理解为它'出生'在全局区,那么它的词法作用域就在全局区
- bar()声明在foo()函数的内部,它的词法作用域在foo()函数体内。
- global声明在全局区,它的词法作用域也在全局区.
了解了词法作用域后,我们来看看刚刚那个例子:

由内而外访问变量应该说的是如果内部没有找到变量,应该从它的外层作用域(也可以说词法作用域)中去寻找变量 ,例如图中我们标记的outer,bar()函数的外层作用域应该是全局区 ,所以我们得从全局区去寻找变量,而我们在全局区中寻找到了myName,它的值为'来颗奇趣蛋',所以我们应该输出 '来颗奇趣蛋' ,而我们这样的查找关系称为作用域链。
作用域链:通过词法作用域来确定作用域的 外层作用域,查找变量由内而外的这种链状关系,叫做作用域链
总结
调用栈
-
用来管理函数调用关系的一种数据结构
-
当一个函数执行完毕后,它的执行上下文就会出栈
作用域链
通过词法作用域来确定作用域的 外层作用域,查找变量由内而外的这种链状关系,叫做作用域链
今天的内容就到这啦,如果你觉得小编写的还不错的话,或者对你有所启发,请给小编一个辛苦的赞吧