深入JavaScript:作用域和执行上下文

作用域

作用域从字面意思理解,就是「起作用的范围」。

《你不知道的JavaScript》上卷中,这样解释作用域:

但是将变量引入程序会引起几个很有意思的问题,也正是我们将要讨论的:这些变量住在哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?

这些问题说明需要一套设计良好的规则存储变量 ,并且之后可以方便地找到这些变量 。这套规则被称为作用域

根据这本书的解释:作用域是一套规则,定义了如何存储变量和查找变量。

MDN 中对作用域的解释:

The scope is the current context of execution in which values and expressions are "visible" or can be referenced. If a variable or expression is not in the current scope, it will not be available for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.

MDN 解释作用域是当前的可执行上下文,值或表达式在其中可见或可被访问。并且作用域能够嵌套形成层次结构,这样就有了子作用域和父作用域的概念。而且子作用域可以父作用域,反之则不行。

其本质也是说作用域定义了存储变量和查找变量的规则。(不过这里引出了一个可执行上下文的概念,作用域和可执行上下文是一回事吗?我们后面再解答。)

再看看《JavaScript高级程序设计》第4版的解释:

执行上下文(以下简称"上下文")的概念在JavaScript中是颇为重要的。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。

在《JavaScript高级程序设计》中,有一节的标题是《执行上下文与作用域》,但看完这一届,作者并没有明确给出「作用域」的定义,只在开篇说了以上这段话。

这段话我理解:上下文都包含一个变量对象,变量对象存储了所有的变量和函数。并且上下文决定了如何查找变量和函数。

如此看来,作用域和执行上下文好像差不多,都是定义了变量以及查找的行为。那它俩真的等价吗?后面讲执行上下文我们再来回答这个问题。

综上,我们大致知道「作用域决定了代码执行时可以访问到哪些变量,并且定义了查找这些变量的规则。作用域可以嵌套形成层次结构,在查找变量时,子作用域可以访问父作用域,反之不行。

执行上下文

在JavaScript中,我从字面意思理解「执行上下文」,就是指代码在运行过程中的环境信息。例如可以访问哪些变量和函数等信息。

但在JavaScript相关书籍中,我并没有找到关于「执行上下文」的明确定义,更多地都是对其特点进行描述。

例如《JavaScript高级程序设计》第4版中说:

1、变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。

2、全局上下文是最外层的上下文。

3、在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。

4、上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)

5、每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript程序的执行流就是通过这个上下文栈进行控制的。

6、上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)

7、全局上下文的变量对象始终是作用域链的最后一个变量对象。

通过以上描述,我们知道:

1、「执行上下文」主要分2种:全局执行上下文、函数执行上下文

ps: eval 函数执行时也有相应的上下文,这里不做探讨。

2、「执行上下文」包含:变量对象、作用域链

3、代码执行流进入函数,会形成上下文栈,全局上下文在栈底

这样看下来,作用域和执行上下文虽然无法等价,但是它们确实针对的是同样的东西:哪些变量访问,以及该如何查找它们。

我来理解的话:作用域是理论基础,执行上下文是基于这个理论基础的具体实现。

🌰例子

看完关于作用域和执行上下文的解释后,感觉还是很虚,接下来,结合具体代码的执行流程来理解这个过程,以如下代码为例:

javascript 复制代码
function bar() {
    console.log(myName)
}
function foo() {
    var myName = "呵呵"
    bar()
}
var myName = "哈哈"
foo()

我们看看这段代码执行时的上下文栈情况:

1、执行流进入 foo 函数时,会把当前的函数上下文推入到上下文栈,即 foo 函数的执行上下文

2、执行 foo 函数时,发现它又调用了 bar 函数,则会将 bar 函数的执行上下文 也推入栈中

因此,有了以上这个图。

我们知道每个执行上下文都有一个关联的「变量对象」,上下文中定义的变量、函数都存在这个对象上。

从上图,我们可以看到每个上下文的变量对象情况。

《JavaScript高级程序》中提到:「上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。」

这就意味着,每个执行上下文都有作用域链,要想形成链条,我们只需要关心每个上下文的父级作用域就可以了。

具体作用域链情况如上图所示,由此可以得出,执行 bar 函数中的 console.log(myName) 会输出"哈哈"。

我们简要分析一下:

1、编译到全局代码中的 var myName = "哈哈" 时,会在全局执行上下文的变量对象上新增一个 myName 变量信息

2、当编译 foo 函数时,遇到 var myName = "呵呵",重新声明变量 myName,在 foo 函数的执行上下文中也会添加一个变量 myName

3、在 foo 函数中调用 bar,bar 直接打印 myName 的值,因为 bar 中未定义变量 myName ,此时会直接找到全局执行上下文中定义的变量 myName。

分析到这里,我恍惚了一下,为什么 bar 的父级作用域不是指向 foo 的执行上下文?明明是在 foo 中调用的 bar。

这里需要引出一个另一个概念:词法作用域和动态作用域

动态作用域:函数的作用域是在函数调用的时候才决定的。

词法作用域:函数的作用域在函数定义时就决定了。

JavaScript 采用的词法作用域,无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。即函数的作用域基于函数的创建位置。

更多关于词法作用域和动态作用域的讲解可参考这里

我们来分析这段代码:

执行 bar 函数时,先从 bar 函数执行上下文中查找是否有 myName,如果没有,就根据书写的位置,查找上面一层的代码,也就是全局执行上下文。

总结

作用域和执行上下文我们经常听到,甚至也经常在讲,但是深入其背后原理才发现理解起来没那么容易。

欢迎大家一起讨论,如有不严谨或错误的地方,欢迎指正!

相关推荐
吕彬-前端6 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱9 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai18 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb