声明
本系列文章内容全部梳理自以下几个来源:
- 《JavaScript权威指南》
- MDN web docs
- Github:smyhvae/web
- Github:goddyZhao/Translation/JavaScript
作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。
PS:梳理的内容以《JavaScript权威指南》这本书中的内容为主,因此接下去跟 JavaScript 语法相关的系列文章基本只介绍 ES5 标准规范的内容、ES6 等这系列梳理完再单独来讲讲。
正文-执行上下文EC和变量对象VO
EC:Execution Context,中文翻译执行上下文,也有翻译成执行环境的。
VO:Variable object,中文翻译变量对象。
这两个概念很重要,涉及到作用域以及作用域链的原理。
执行上下文 EC
先说说Android中的上下文:
- Android
Android 中也有上下文:Context,四大组件都是上下文,还有一个全局的 Application上下文。在 Android 中基本是以四大组件为界限,每创建一个四大组件,都会产生一个上下文,比如每个 Activity 都是独立的上下文。
在 Android 中,上下文 Context 的作用大体上用于标识各种资源的所属,要加载一张图片、创建一个 View、弹一个 Dialog 等等,你需要告诉系统,这些是谁发出的指令,要挂载到哪个上下文,这些资源依赖于上下文的生命周期。
所以才会出现,有时弹 Dialog 或者更新某个 View 时抛异常说 Context 已销毁,因为它需要挂载的上下文已经销毁了,那么就没有上下文来统筹管理这些资源了,自然会抛异常。
- JavaScript
在 JavaScript 中,上下文是指执行上下文,通俗点理解,代码执行的上下文,所以也有翻译成执行环境,可以通俗的把它理解成一个对象,对象名 EC,表示代码的执行上下文。
既然理解成一个对象,那么就有它创建的时机,在 JavaScript 中,每当要执行不同类型的代码时,就会创建一个执行上下文 EC。
而代码的类型分三种:
- 全局代码
- 函数代码
- eval()执行的代码
最后一种不讨论,全局代码就是指写在函数外的代码,在前端里,当 HTML 加载一个 js 文件时,全局代码就会被执行,那么在全局代码执行前就会先创建一个全局的执行上下文 EC,之后每调用一次函数,要执行函数内的代码时,会再创建一个函数执行上下文 EC。
也就是说,不讨论 eval 的话,那么在 JavaScript 有两种执行上下文,一种是全局执行上下文,一种是函数执行上下文。
而每次创建一个执行上下文时,都会将其放入一个栈结构,这个栈就称为执行上下文栈(ECS),也有翻译成执行环境栈。
所以执行 js 文件代码期间,这个栈底一直是全局执行上下文,直到 js 文件代码执行结束。全局代码执行过程中,每调用一次函数,新创建一个函数执行上下文,就放入栈内。
因此,栈顶就表示当前执行的代码,如果栈顶是全局执行上下文,表示正在执行全局代码;如果栈顶是函数执行上下文,表示正在执行函数内的代码。当函数执行结束时,这个函数执行上下文就从栈中移出。
那么执行上下文(EC)有什么用呢?
用途可多了,跟 Android 不一样,Android 里由于是各种资源的组合使用,但在 JavaScript 中更多的是嵌套函数的变量使用。所以,用途之一就是保存各个变量。
将 EC 理解成一个对象的话,它有两个属性,一个是变量对象(VO),另一个是作用域链(Scope Chain)。对于全局执行上下文,当 HTML 加载一个 js 文件时,就会创建一个全局 EC,此时会创建它的两个属性:变量对象和作用域链。之后,每调用一次函数,创建这次函数执行的上下文,函数内部的变量的使用就依赖于这个函数执行上下文中的变量对象和作用域链。
也就是说,内部函数之所以可以使用外部函数内的变量,之所以可以使用全局变量,都是依赖于当前这个内部函数的执行上下文。
而且,变量之所以会提前声明也是因为执行上下文的因素。这些当讲解了执行上下文 EC 的创建过程就清楚了。
变量对象 VO
变量对象只是一个抽象的概念,可以通俗的理解成保存当前上下文所有变量的对象。
在不同的执行上下文中,它有不同的具体表现。
在全局执行上下文中,变量对象 VO 的具体表现就是全局对象,因为所有的全局变量其实都是全局对象的属性,而变量对象 VO 的作用是要保存当前上下文中的所有变量,所以此时的变量对象 VO 实际上是指向的全局对象。
尤其在前端中,全局对象就是 window,所以全局执行上下文的变量对象 VO = window。
在函数执行上下文中,因为变量对象 VO 是要保存当前上下文中所有的变量,一个函数内的变量包括:形参变量、局部变量、自身函数对象引用变量、arguments、this。
针对函数执行上下文,为了保存这些变量,特意创建了一个对象,称它为活动对象 AO,函数内所需的变量就都保存在 AO 中,所以在函数执行上下文中,变量对象 VO 的具体表现也就是 AO。
小结:变量对象 VO 是一个抽象概念,用于保存当前执行上下文中所有的变量。所以在全局执行上下文中,因为全局对象已经保存着当前上下文所有的变量,所以 VO 在这里的具体实现就是全局对象。在函数执行上下文中,由于要保存函数形参、局部变量、自身函数对象引用变量、arguments、this,所以新创建了一个叫活动对象 AO 来保存,此时 VO 的具体实现就是 AO。
作用域链
每次函数调用时,都会创建一个函数执行上下文 EC,但其中的变量对象 VO 只保存着当前上下文中的变量而已,那么函数内如果需要使用到外部函数的变量,甚至是使用全局的变量时,此时就需要依赖于执行上下文的另一个属性:作用域链。
作用域链本质上,其实是将有嵌套层次关系的执行上下文的 VO 拼接起来。
所以大部分场景作用域链只有两个节点:当前函数执行上下文的 VO –> 全局执行上下文的 VO。所以函数内才可以根据作用域链访问全局内的变量。
当出现函数内再嵌套函数时,此时作用域链就会比较长:
内层函数执行上下文的 VO –> 外层函数执行上下文的 VO –> 全局执行上下文 VO。
至于作用域链是如何将有嵌套层次的执行上下文的 VO 拼接起来,需要借助函数对象的内部属性 [[Scope]],[[]]表示执行引擎为对象创建的内部属性,我们访问不了,也操作不了。具体原理在作用域链一节中讲解。
大家好,我是 dasu,欢迎关注我的公众号(dasuAndroidTv),公众号中有我的联系方式,欢迎有事没事来唠嗑一下,如果你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~