文本中将由南潮首席架构师周爱民为大家介绍JavaScript语言在引擎级别的执行过程,其中包括JavaScript语言中的环境的准备,作用域及环境的区别,可执行上下文的构建及执行原理,过程中的控制和执行结果的返回。在最后,周爱民展开语法的概念,解释...x如何构成可执行组件。
嘉宾:周爱民,南潮首席架构师,曾担任支付宝业务架构师,盛大网络平台架构师。著有《大道至简——软件工程实践者的思想》、《大道至易——实践者的思想》、《Delphi源代码分析》、《JavaScript语言精髓与编程实践》等专著。
本次分享将主要围绕以下五个方面展开:
一、环境的准备
二、执行上下文
三、过程控制
四、结果返回
五、展开语法
一、环境的准备
1.作用域Scope
代码当中经常出...x的一段代码,表明可迭代对象的展开。而事实上,直接执行这段代码并不正确。必须将其放在一段表达式内才有可能被执行。如下图中的console.log(...x)。但是如果仔细推敲,此表达式依然无法执行,再往上追溯到第二层,可以将此表达式放到if语句中,但此时如果if语句没有指明其作用域,则x依然无法在if语句中进行查找。此时需要为if加作用域,使其变成有块级作用域的if语句。但是下图中的if语句仍然无法找到x,可以再上到第三层,在function函数作用域内,x仍然没有被找到。直到全局的范围内才可以找到x的信息,代码才可以被成功执行。整个过程中会涉及到作用域的概念,下图中蓝色框的即表明作用域。
传统的作用域的概念在JavaScript之父BrendanEich在github上的narcissns项目中有所介绍。作用域本身有两个成员,object和parent,作用域中包含对象及属性。Object是属性列表,其中做变量定义以及函数定义等声明,变量的名字可以被映射为JavaScript中的名称列表。因此作用域主要有两项功能,首先是查找名字,如果没有,查找parent上一层。
2.环境Environment
作用域scope的概念在ECMAScript5(ES5)之后被替代为Environment环境,Environment取代了作用域的价值和作用。下图中展示了环境在ES5以后的规范。首先是词法环境规范,依然包含两个成员,环境记录和outer。环境记录可以映射为作用域中的object,outer映射为作用域中的parent。此时,词法环境规范与作用域的内容完全一致,但不同点在于环境记录成员是由下图中右侧的五种环境记录规范所实现。可以发现,五种环境记录规范中都有一个共同的方法HasBinding(N),这个方法本身只是细化了查找名字功能。
3.属性标识符
ES5中较为重要的规范是属性描述符和属性标识符规范。所有的环境记录通过环境对外只有一个有意义的Interface,即标识符引用GetIdentifierReference。
无论哪种环境记录都通过标识符引用取到...x,都会将其转化为同一种格式,如下图。其中有base,name,strict等信息,其中name都是一致的。标识符引用的作用代替了作用域中的查找名字功能。而ES5中引入标识符引用的方式的目的是统一和规范下一步的操作,即执行上下文。
当代码功能简化到查找名字功能时,才开始涉及到执行。下图中最外层是全局环境,里面一层是函数环境,再里面一层是词法作用域。将代码分为这几种环境之后,每个环境对外public的功能就是查找名字。
二、可执行上下文Executive Context
在此基础上,执行上下文添加了两个成员,词法环境和变量环境。理论上词法环境和变量环境只需要有一个就可以查找名字。但JavaScript中变量环境解决var声明,词法环境解决一般变量声明,两种声明在JavaScript中不兼容。
任务队列RunJobs:任务队列以先前先出的规则处理任务。最早放到任务队列中的job是脚本执行job(ScriptsEvaluationJob)以及顶层模块job (TopModuleJob),之后开始run。
执行栈Execution context stack:在此基础上,又加了可执行组件执行栈。当执行栈为空时,自动取RunJobs中的最顶上的job,开始执行。
1.代码层面如何run
下图中左侧的代码块放到JavaScript执行引擎,此时处于代码还未正式执行,但引擎以准备好了前期工作。执行栈中有三个任务,最底层任务虚化的是初始化操作,第0个任务是newContext for job,是为任务队列中的脚本执行job或者顶层模块job执行的新的上下文。此时可以执行这个job,然后再创建一个scriptContext执行上下文。这里需要注意scriptContext执行上下文和第0个任务newContext for job的执行上下文稍微不同。第0个任务的上下文是内核引擎所需要的执行上下文,而scriptContext执行上下文是JavaScript代码可执行的上下文。此外,scriptContext执行上下文中有变量环境和词法环境,可以访问代码。而第0个任务newContext for job的执行上下文没有这两个环境。
ScriptContext执行上下文:ScriptContext执行上下文具体还可分为四种可执行的上下文,全局初始化、模块初始化环境、实例化函数环境、实例化Eval环境等。
只有全局上下文的准备,代码中的console.log(...x)依然无法执行,还需准备函数环境上下文call f(),代码才可以被执行。
三、过程的控制
函数环境在ES6之后变得非常重要,几乎所有job都变成函数的调用。
1.生成器
下图中从tor中获取到对象,其中包含GeneratorContext和GeneratorState,即生成器的上下文。在生成器中,JavaScript通过函数将执行中的上下文调度交到了用户手上。在tor.next()中即直接将生成器上下文放到执行栈中,yield可以将执行上下文从执行栈中弹出。
2.Promise
在Promise中,JavaScript通过函数将任务队列中的任务管理交到了用户手中。下图中Promise需要先new,再p.then(..)。而p.then(..)的功能只是将所接到的函数放到对象p中的Reaction列表中。此时任务队列有两种,resolve时的任务队列和reject时的任务队列。如果执行resolve,对应的任务队列会被执行,即总会有一个任务队列不会被执行。但此时发现下图中的函数没有执行语句。原因是JavaScript创建Promise时一共创建了三个对象,首先是Promise对象自己,第二个是函数resolve,第三个是函数reject,三个同时被创建。后两个函数有同样的内部属性,promise内部槽,指向对象p。用户可指定执行resolve还是reject。但如果执行栈中的全局初始化没有完成,任务队列仍然不会被执行。
四、结果的返回
函数调用后会有结果的返回,意味着ES6之后执行相关的特性都需要有值的返回。下图中第一行代码会返回true,第二行代码返回false。这两行代码代表了JavaScript两种代码的核心执行逻辑,执行表达式和执行语句。两种执行逻辑返回的结果值是不一样的。
1.执行表达式
如下图右侧,执行表达式返回的结果包括原始值,对象,引用规范类型。
2.执行语句
执行语句返回的结果是完整规范类型,表示语句是否被完整执行,是否中断,返回值不包含引用。
五、展开语法
执行函数是执行表达式的一种,而执行表达式只能返回一个值,语句不能返回引用。可以发现...x并不满足上述任何的返回值。当...x放在“[ ]”中,则变成数值的展开,表示一堆element的填充,放到“()”时,则变成参数的展开。只有在这两种场景中才可以使用...x,[...x]/(...x)既不是语句也不是表达式,而是展开语法,是目前为数不多的可称为语法的可执行组件。展开语法不是以表达式或语句的方式执行的,而是直接在执行位置插入代码,即当解析数据声明时,遇到...x,于是将其放到数组列表中。