小白请看!深入了解调用栈和作用域链

前言

作用域链(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 函数被调用。调用栈会按照以下顺序记录函数的执行:

  1. foo 进入调用栈。
  2. foo 调用 barbar 进入调用栈。
  3. bar 执行完成,从调用栈中弹出。
  4. 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,它的值为'来颗奇趣蛋',所以我们应该输出 '来颗奇趣蛋' ,而我们这样的查找关系称为作用域链

作用域链:通过词法作用域来确定作用域的 外层作用域,查找变量由内而外的这种链状关系,叫做作用域链

总结

调用栈

  • 用来管理函数调用关系的一种数据结构

  • 当一个函数执行完毕后,它的执行上下文就会出栈

作用域链

通过词法作用域来确定作用域的 外层作用域,查找变量由内而外的这种链状关系,叫做作用域链

今天的内容就到这啦,如果你觉得小编写的还不错的话,或者对你有所启发,请给小编一个辛苦的赞吧

相关推荐
csdn2015_5 分钟前
java 把对象转化为json字符串
java·前端·json
shughui8 分钟前
Fiddler(二):自动转发(AutoResponder)功能详解
前端·测试工具·fiddler
初见雨夜10 分钟前
OpenAI 官方出手:把 Codex 接进 Claude Code
前端·openai·ai编程
前端付豪11 分钟前
实现消息级操作栏
前端·人工智能·后端
GISer_Jing14 分钟前
Claude Code的「渐进式披露」——让AI Agent从“信息过载”到“精准高效”
前端·人工智能·aigc
apcipot_rain20 分钟前
HTML知识概述
前端·javascript·html
leiming628 分钟前
巧用 FreeRTOS 任务通知作“邮箱”:NeoPixel 灯环控制实战
java·前端·算法
茶底世界之下34 分钟前
Harbeth:高性能Metal图像处理库,让你的图片处理速度飞起来!
前端·github·swift
wangfpp37 分钟前
Pretext 如何颠覆前端文本布局
前端
从文处安40 分钟前
「前端何去何从」AI 把开发变快之后:Monorepo 与 Turborepo 如何接住被放大的工程复杂度
前端·人工智能