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

前言

作用域链(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,它的值为'来颗奇趣蛋',所以我们应该输出 '来颗奇趣蛋' ,而我们这样的查找关系称为作用域链

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

总结

调用栈

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

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

作用域链

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

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

相关推荐
MickeyCV19 分钟前
Nginx学习笔记:常用命令&端口占用报错解决&Nginx核心配置文件解读
前端·nginx
祈澈菇凉36 分钟前
webpack和grunt以及gulp有什么不同?
前端·webpack·gulp
十步杀一人_千里不留行39 分钟前
React Native 下拉选择组件首次点击失效问题的深入分析与解决
javascript·react native·react.js
zy01010143 分钟前
HTML列表,表格和表单
前端·html
初辰ge1 小时前
【p-camera-h5】 一款开箱即用的H5相机插件,支持拍照、录像、动态水印与样式高度定制化。
前端·相机
HugeYLH1 小时前
解决npm问题:错误的代理设置
前端·npm·node.js
三天不学习1 小时前
Redis面试宝典【刷题系列】
数据库·redis·面试
六个点2 小时前
DNS与获取页面白屏时间
前端·面试·dns
道不尽世间的沧桑2 小时前
第9篇:插槽(Slots)的使用
前端·javascript·vue.js
bin91532 小时前
DeepSeek 助力 Vue 开发:打造丝滑的滑块(Slider)
前端·javascript·vue.js·前端框架·ecmascript·deepseek