一. 前言
对于一门编程语言来说,变量的存储和访问,是最基本的功能之一。今天,将向大家深入介绍一下,JS 程序在运行的时候,如何找到变量,这需要一套良好的规范来规范,这套规则,就成为了作用域
二. V8 JS 代码运行的步骤
在介绍作用域的知识点之前,还需要先介绍一下,V8 JS 代码的运行流程。网上有较多的 V8 工作运行流程图,举例如下:
其中后面的几步骤,像 Ignition 的字节码、TurboFan 优化编译器,主要是 V8 为了优化性能而做的,在本文不做过多展开。本文主要专注于前面几步骤,可以看到,V8 在拿到 JS 的 source code 后,会首先解析生成 AST 抽象语法树,然后后面再经过若干编译步骤,生成机器码并被运行。其中的作用域规则确定,是在解析成 AST 这一步骤的时候,就已经被确定了
下面以 V8 源码内的 hello-world 程序为例,对流程进行讲解:
在介绍上图的代码流程之前,先介绍一些概念:
isolate:
- 一个 Isolate 是一个独立的虚拟机。对应一个或多个线程。但同一时刻 只能被一个线程进入。
- 所有的 Isolate 彼此之间是完全隔离的, 它们不能够有任何共享的资源。
- 如果不显式创建 Isolate, 会自动创建一个默认的 Isolate
handleScope:
- 表示JS对象的生命周期的范围。
- 在 V8 中,内存分配都是在 V8 的 Heap 中进行分配的,JavaScript 的值和对象也都存放在 V8 的 Heap 中。而 Handle 即是对 Heap 中对象的引用。V8 为了对内存分配进行管理,GC 需要对 V8 中的所有对象进行跟踪。HandleScope 就是用来管理 Handle 的
- Handle 分为 Local 和 Persistent 两种。Local 是局部的,创建一个指向 JS 对象的本地引用,它同时被 HandleScope 进行管理。 persistent,创建一个指向 JS 变量的持久引用,类似与全局的,不受 HandleScope 的影响,其作用域可以延伸到不同的函数。
context:
- 可以理解为「执行上下文」或者「 执行环境」
- 每当程序的执行流进入到一个可执行的代码时,就进入到了一个执行环境中
三者关系的示意图如下:
回到上述一开始的源码截图里,大概做了以下几步工作:
- 定义了一个 isolate
- 在 isolate 下,定义了一个handle_scope。handle_sope 的生命周期,决定了下面所有 v8::Local 的声明周期的有效性
- 定义了一个 context,并切换进入
- 编译 JS 源码成字节码
- 在当前 context 中,运行上一步编译出的字节码
三. 作用域与执行上下文相关
在上面第二节中,简单介绍了 V8 执行 JS 代码的流程。再简单概括一下,JavaScript属于解释型语言,JavaScript 的执行分为 「解释 」和 「执行 」两个阶段,如下:
解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
执行阶段:
- 创建执行上下文 context
- 执行函数代码
- 垃圾回收
-
两者区别
很多同学容易混淆,「作用域 」和 「执行上下文」 这两个概念,的确他们两有一定的相关性,但又有区别:
-
可以把作用域抽象理解成,是根据名称查找变量的一套规则,这套规则用来管理 js 引擎根据标识符名称如何查找变量。而一系列的嵌套作用域就形成了作用域链(你不知道的 JavaScript 中定义)
-
而执行上下文,如上一节中描述,是在函数运行之前,V8 创建的函数运行环境
-
作用域在 AST 解析阶段就确定,不会改变;而执行上下文,是在执行阶段才确定,可能发生改变。举个例子:
var a = 10; function fn() { var b = 20; function bar() { console.log(this.b); // 200 console.log(a + b); // 30 } return bar; } var x = fn(), b = 200; x();
上面的打印的结果为 200、30,是因为对于 this.b 来说,this 的指向,就是执行上下文中确定的;而 bar 函数中的 b 值,是在 AST 解析 bar 函数定义时,就已经明确 bar 函数的作用域链,为 bar -> fn -> 全局,所以 b 变量会沿着作用域链寻找,找到 fn 中的定义,值为 20
- 一个作用域下,可能包含若干个上下文环境。因为在一个函数作用域里,每次在调用别的函数前,都要先创建调用函数所需的执行上下文。但是调用函数的次数是不定的,需要在运行时才能确定
-
函数执行流程介绍
为了更深入的介绍作用域与执行上下文的原理,我们以一个函数的执行为例子,进行详细描述,并对其中一些概念再进行介绍:
var scope = "global scope";
function checkscope() {
var scope = "local scope";
function f() {
return scope;
}
return f();
}
checkscope();
-
执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈
ECStack = [ globalContext ];
-
全局上下文初始化(初始化全局环境的变量对象 VO,确定全局环境的 Scope,绑定全局环境的 this)
globalContext = { VO: { global: window, scope: undefined, checkscope: reference to function checkscope }, Scope: [globalContext.VO], this: globalContext.VO }
-
checkScope 函数执行前阶段。初始化的同时,checkscope 函数被创建,保存全局环境的作用域链,到函数 checkscope 的内部属性 [[scope]] 中
checkscope.[[scope]] = [ globalContext.VO ];
globalContext = { VO: { global: window, scope: "global scope", checkscope: reference to function checkscope }, Scope: [globalContext.VO], this: globalContext.VO }
-
执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数的执行上下文,被压入执行上下文栈
ECStack = [ checkscopeContext, globalContext ];
-
初始化 checkscope 函数执行上下文。会有以下几步:
- 用 arguments 创建活动对象 checkscopeContext.AO
- 利用 checkscopeContext.AO 与 checkscope.[[scope]],形成checkscope 函数执行环境的作用域链 checkscopeContext.Scope
- 绑定 this 到 undefined(非严格模式下会绑定到全局对象)
checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined }
-
f 函数执行前阶段。更新 f.[[scope]], checkscopeContext.AO.scope 等赋值
f.[[scope]] = [ checkscopeContext.AO, globalContext.VO ];
checkscopeContext = { AO: { arguments: { length: 0 }, scope: "local scope", f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined }
-
执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈
ECStack = [ fContext, checkscopeContext, globalContext ];
-
f 函数执行环境初始化(参考第 e 步)
fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined }
-
f 函数中代码执行。需要对 scope 进行 RHS 查找。查找从作用域链中当前活动对象,开始沿着作用域链向上查找
-
f 函数执行完毕,返回"local scope"。f 函数上下文从执行上下文栈中弹出
ECStack = [ checkscopeContext, globalContext ];
-
checkscope 函数在执行完 f 处,获取 f 执行的返回值 "local scope",函数继续向下执行
-
checkScope 执行完毕,返回获取到的返回值 "local scope"。checkScope 函数上下文,从执行上下文栈中弹出
ECStack = [ globalContext ];
-
代码执行流回到全局执行环境中调用 checoscope 处,拿到 checkScope 返回值并继续向下执行
-
直到程序终止,或者页面关闭。全局上下文出栈并销毁
作者:陆瀚陶
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!