欢迎评论和 star
天气渐渐转暖了, 树渐渐露出了枝芽, 小河也欢快的向前流着, 感觉大地充满了生命力, 好开心
小伙伴们, 我们也出来活动活动筋骨, 迎接我们 2018 年的春天.
今天我们说说 JS 执行流程, 现在我们先暂且不考虑异步的情况.
如果你把下面的内容都吃透, 那你就会发现 JS 内部是多么精彩的一个世界.
还等什么, go...
编译阶段
词法分析(Lexing)
这个过程会将由字符组成的字符串分解成 (对编程语言来说) 有意义的代码块, 这些代 码块被称为词法单元(token).
简单举个例子: c = b - a 转换为
- NAME "c"
- EQUALS
- NAME "a"
- MINUS
- NAME "b"
- SEMICOLON
语法分析(Parsing)
这个过程是将词法单元流 (数组) 转换成一个由元素逐级嵌套所组成的代表了程序语法 结构的树. 这个树被称为 "抽象语法树"(Abstract Syntax Tree,AST).
AST 大概是下面的样子:
https://user-images.githubusercontent.com/22290721/37942239-c3fd329c-31a4-11e8-81e8-6d18d8f13181.jpg
生成可执行代码
将 AST 转换为可执行代码的过程称被称为代码生成.
执行阶段
接下来, 我们以一个简单例子进行分析.
- var a = 2;
- function bar() {
- var b = 2;
- function foo() {
- var c = 2;
- }
- foo();
- }
- bar();
1. JS 引擎创建一个全局对象(Global Object)
这个对象全局只存在一份, 它的属性在任何地方都可以访问, 它的存在伴随着应用程序的整个生命周期. 全局对象在创建时, 将 Math,String,Date,document 等常用的 JS 对象作为其属性. 由于这个全局对象不能通过名字直接访问, 因此还有另外一个属性 window, 并将 window 指向了自身, 这样就可以通过 window 访问这个全局对象了. 用伪代码模拟全局对象的大体结构如下:
- // 创建一个全局对象
- var globalObject = {
- Math:{},
- String:{},
- Date:{},
- document:{}, //DOM 操作
- ...
- window:this // 让 window 属性指向了自身
- }
2. JS 引擎会创建一个执行环境栈(Execution Context Stack)
栈
提到栈, 小伙伴们都知道, 栈是一种类似羽毛球筒存储羽毛球的数据结构, 采用先进后出, 后进先出的特点.
https://user-images.githubusercontent.com/22290721/37942549-3538e1ee-31a6-11e8-8d58-718fac1e0851.jpg
上图中的羽毛球 1 一定是先放入栈中, 然后是羽毛球 2, 以此类推, 而出栈时, 一定是羽毛球 5 先拿出来, 然后是羽毛球 4, 以此类推, 这种方式和栈存取数据的方式如出一辙.
堆
堆数据类型类似与书架. 书虽然也整齐的存放在书架上, 但是我们只要知道书的名字, 我们就可以很方便的取出我们想要的书.
好了好了, 扯远了. 我们接着往下说, 在这只需知道执行环境栈是怎样存取数据的就行.
3. 创建全局执行上下文(Execution Context)
到这你可能会问, 上下文是个啥玩意?
是啊, 上下文是个什么鬼啊?
上下文不是玩意, 也不是什么鬼.
执行上下文可以理解为当前代码的执行环境. JS 所有代码都会在自己的上下文环境下运行.
说到上下文, 你可能会有这样的疑惑: 上下文不就是作用域吗?
老铁, 我肯定的告诉你, 上下文不是作用域. 的确, 在 JS 里, 这还真是个很难区分的东东. 不过现在我还不能马上道出他们的区别, 因为作用域的知识, 我们还没有涉及, 彻底搞懂 JavaScript 作用域 https://github.com/prettyEcho/deep-js/issues/2 , 通过这篇文章, 你将彻彻底底了解关于作用域的一切.
那在 JS 中会有几种执行环境呢?
大概有 3 种:
全局环境: JavaScript 代码运行起来会首先进入该环境
函数环境: 当函数被调用执行时, 会进入当前函数中执行代码
eval,with(不建议使用, 可忽略)
因此在一个 JavaScript 程序中, 必定会产生多个执行上下文.
go on...
4. 全局上下文推入执行环境栈底
5. 代码开始从上往下执行, 这里我们暂且不谈标识符处理, 当代码执行到 bar(), 生成 bar 执行上下文, 推入栈中
6. 代码执行到 foo(), 生成 foo 执行上下文, 推入栈中
7. foo()执行完, foo 执行上下文出栈
8. bar()执行完, bar 执行上下文出栈
9. 全局上下文执行上下文出栈
我们用图走一下 js 执行流程, 是这样的:
https://user-images.githubusercontent.com/22290721/37942685-f02620fc-31a6-11e8-9b53-1e87f6cb3d26.jpg
小伙伴们, 现在是不是对 JS 执行流程有了一个整体认识, 下面我们来说点更有意思的.
上下文执行细节
我们先看整体了解下
https://user-images.githubusercontent.com/22290721/37942763-4f4e5b94-31a7-11e8-8a58-e53fa3b193ef.png
创建阶段
1. 创建变量对象(Variable Object)
创建变量对象, 依次经历了以下几个步骤
建立 arguments 对象. 检测当前上下文参数, 建立该对对象下的属性及属性值.(这里提一下, 函数的参数是按值传递, 我知道你是知道的)
检测关键词 function 函数声明. 检测当前上下文中的函数声明, 并挂载到变量对象上, 其值是函数对象的引用.
检测 var 变量声明. 检测当前上下文中的 var 声明, 并赋值为 undefined; 如遇到同名 var 声明的变量, 则会默认覆盖; 如遇到同名函数声明, 则默认忽略, 这也就体现了函数声明的优先级要高于 var 声明. 谁的大哥还是得分清的, 哈哈...
变量提升
看到这, 我觉得你对变量提升具体是什么以及如何实现的应该了解的一清二楚了.
是不是呢?
我们来一道题测试下
- function foo() {
- console.log(a);
- console.log(baz);
- var a = 'inner';
- var baz = 1;
- function baz() {}
- }
- foo();
第一处是 undefined, 第二处是[Function: baz], 是不是很简单?
下面我用代码简单模拟下上面的过程
- function foo() {
- function baz() {}
- var a = undefined;
- console.log(a);
- a = 'inner';
- console.log(baz);
- }
- foo();
变量对象大概是这样的
- VO(foo) = {
- arguments: {},
- baz: <foo reference>, // 表示 foo 的地址引用
- a: undefined
- }
2. 确定作用域链
作用域链是由当前作用域与上层一系列父级作用域组成, 作用域的头部永远是当前作用域, 尾部永远是全局作用域. 作用域链保证了当前上下文对其有权访问的变量的有序访问.
我们先简单了解下, 详细的我们会在彻底搞懂 JavaScript 作用域 https://github.com/prettyEcho/deep-js/issues/2 中谈到.
- var a = 1;
- function foo() {
- function baz() {
- console.log( a );
- }
- baz();
- }
- foo(); // 1
上面的对于我们来说很简单, 是吧? 没错这就是作用域链的应用.
我们简单模拟下
- EC(foo) = {
- VO(foo): {...}, // 省略
- ScopeChain: [VO(foo), window],
- this:
- }
- EC(baz) = {
- VO(baz): {...}, // 省略
- ScopeChain: [VO(baz), VO(foo), window],
- this:
- }
3. 确定 this 指向
谈到 this, 大家是不是感到很兴奋, 平时写代码时, 被这家伙整的晕头转向的, 这回我们终于可以揭开 this 的神秘面纱了, 搞清楚它在 JS 到底是怎样的存在, 不过客官别着急, 我们这里先不介绍 this, 因为关于 this 的内容太多了, 我们得慢慢去品味它, 这里先记住, this 是在执行上下文创建阶段确定的.
this 传送门
this 真是一个淘气鬼 https://github.com/prettyEcho/deep-js/issues/4
全局上下文
全局上下文有些特殊, 其变量对象永远是 window,this 永远指向 window(在浏览器中, Node 中不是).
即
- EC(global) = {
- VO: window,
- ScopeChain: {},
- this: window
- }
执行阶段
在执行阶段变量对象 (Variable Object) 变为活动对象(Active Object). VO => AO
这样, 如果再面试的时候被问到变量对象和活动对象有什么区别, 就又可以自如的应答了, 他们其实都是同一个对象, 只是处于执行上下文的不同生命周期. 不过只有处于函数调用栈栈顶的执行上下文中的变量对象, 才会变成活动对象.
执行阶段 JS 引擎会进行变量赋值, 函数引用, 执行其他代码, 执行顺序取决于代码的位置.
来源: https://juejin.im/entry/5ac23454f265da23923684a8