阅读 2190

【v8引擎】描述一下 V8 执行一段JS代码的过程?(预编译篇)

src=http___pic.vjshi.com_2017-02-16_5ec38e7d3c2a23caec36a13107521665_00004.jpg_x-oss-process=style_watermark&refer=http___pic.vjshi.jfif

前言

相信学习前端知识的小伙伴们,一旦去考究JS底层的知识,都被一些变量声明提升、函数声明提升搞得醍醐灌顶。到最后,再去想一段JS代码究竟是什么是怎样的运行顺序?仍然是模糊不清的。接下来,我将带领大家用简单好理解的、具体的步骤去推敲和理解JS代码的运行顺序、每个阶段都干了什么?真正地打开JS世界的大门!

一起来思考

二话不说,先上代码。

var global = 100
 function fn() {
   console.log(global);
}
fn()
复制代码

我们来假想一下,如果面试官问你:这段代码最后会输出什么?你大概率会觉得他在侮辱你,这不就是100吗;但如果面试官再问你,这个100怎么来的?你能回答的出来吗?

其实,面试官问你这个问题就是看你知不知道JS代码在运行时都发生了什么? 这个时候,很多小伙伴是不是已经开始想声明提升的问题了。用声明提升去思考代码也就是这样的顺序

var global //变量声明提升
function fn(){ //函数声明提升
console.log(global)
}
global = 100 //变量赋值
fn() //函数执行
复制代码

看到这,很多小伙伴肯定会说不过如此。那么保持这个想法,看下面的代码

 function fn(a) {
      console.log(a); // function() {}
      var a = 123;
      console.log(a); // 123
      function a() {}
      console.log(a); // 123
      var b = function() {}
      console.log(b); // function() {}
      function d() {}
      var d = a
      console.log(d); // 123
    }
    fn(1)
复制代码

请问:这段代码是怎么的运行顺序?开始声明提升?现在,大家应该就知道我要说什么,通过声明提升、作用域去思考一段代码的运行顺序,如果代码简单还好说,一旦代码的声明操作、赋值操作一大堆,就会浪费大把的时间,而且出错率极高。

干货

函数体内

先以上面的代码为例,在一个函数体内的代码运行。其实JS运行可以分为编译阶段和执行阶段。那么这两个阶段分别发生了什么呢?

上述代码中

  • 编译阶段
    • 先创建一个AO(activation object)对象
    • 然后去找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
    • 再者,将实参和形参统一
    • 最后,找函数声明,将函数名作为AO对象的属性名,值赋予函数体

带着这些,我们再来看这段代码

function fn(a) { //二、形参是a,值为undefined
      console.log(a); // function() {}
      var a = 123; //二、a变量声明,AO里已经有了,覆盖后还是一样的
      console.log(a); // 123
      function a() {}// 四、a 申明为一个函数
      console.log(a); // 123
      var b = function() {} //二、变量b声明,值为undefined
      console.log(b); // function() {}
      function d() {} // 四、d声明为一个函数
      var d = a //二、变量d声明,值为undefined
      console.log(d); // 123
    }
    //第一步在这、函数在执行前进行编译,创建AO对象
    AO:{
        //第二步,开始找形参和变量声明,并写入AO
        a:undefined 1 function (){}
        b:undefined 
        d:undefined function(){}
        //第三步,就相当于把实参传给形参,所以a的值现在就变成了1
        //第四步,找函数声明,写入AO
    }
    fn(1)
复制代码

到最后编译阶段结束,AO对象中的属性和值是:

AO:{
    a:function(){}
    b:undefined
    d:function(){}
}
复制代码

现在,我们来通过看AO对象中,属性对应的值来执行整个函数。

  • 从fn(1)开始执行函数
  • 第一个log要a,去AO中找a,是function(){}。然后a被赋值为123,AO对象中的a随之更新
  • 第二个log又要a,去AO中找a,现在输出的就是123了。然后变量b被赋值为一个函数,AO中也随之更新
  • 第三个log要b,去AO中找b,输出的就是function(){},然后a的值被赋给了d,AO中a的值是123,所以d也变成123,AO中随之更新
  • 第四个log要d,不就是123嘛

到这,这段代码就执行完了,不妨回过去看看,嗯?就这么简单?为了证明我不是在耍流氓,大家可以去跑一跑,对照一下。

看到这,聪明的朋友们马上会有疑问,这不过是函数体内的,那在全局上又是怎么样的。我只能说:更简单,接着看。。。

在全局下

在函数体中,整个编译阶段我总结成四部曲。在全局下,三步足以,多一步算我输。

  • 第一步、创建一个GO(global object)对象
  • 第二步、找变量声明,将变量声明作为GO的属性名,值为undefined
  • 第三步、找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体

三步,结束了,老规矩,拿代码来说话:

GO:{
    fn:function(){}
}
global = 100
function fn() {
   console.log(global); // undefined
   global = 200
   console.log(global); // 200
   var global = 300
}
AO:{
    global:undefined
}
fn()
复制代码
  • 编译阶段 来,三步走起!
    • 第一步、创建一个GO对象
    • 第二步、找变量声明,并没有,全局中只有一个赋值语句和函数声明
    • 第三步、找函数声明,所以GO对象中编译阶段只有fn
    编译到这,全局就编译完了,接下来就是函数体内的编译了
    • 第一步、创建一个AO对象
    • 第二步、找形参和变量声明,这里没有形参,变量global声明,值为undefined。写入AO
    • 第三步、传参,实参形参都没有
    • 第四步、找函数声明,也没有
    到这,整个编译就结束了,可以开始执行了。
  • 执行阶段
    • 先执行赋值语句,因为global在全局中没有定义,这里会强制声明一个global并赋值100,写入GO中
    • 然后到fn()执行函数体。
    • 第一个log要global,先去自己的AO找,找到了,值为undefined。所以输出undefined
    • 进行赋值,将AO中的global值更新为200
    • 第二个log还要global,这里已经变成200了,所以输出200
    • 最后AO中global被赋值为300

看到这里,再回想一下,是不是感觉很通透了。下面稍微总结下上面说的方法

总结

  • 函数体内的编译四步曲,发生在函数执行之前
    1. 创建AO对象 (activation object)
    2. 找形参和变量声明,将形参和变量声明作为AO对象的属性名,值为undefined
    3. 将实参和形参统一
    4. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体
  • 全局下的编译三步曲,发生在代码最前面
    1. 创建GO对象
    2. 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
    3. 找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体
    看到这里,小伙伴们应该通透了许多,但回顾整个文章,又会感觉怎么说得这么简单,会不会不够深奥?其实,这里才是重头戏。大家不妨试想,既然已经能够用这套方法去理解JS代码运行都了些什么,那么,结合这个知识,再去思考那些深奥的执行上下文、作用域、声明提升,这个时候,JS的大门就真正地为你打开了。你才会明白何为通透。

(觉得不错的小伙伴们,请把通透打在评论区!)

  • 文章中如果存在问题,或者大佬们有更好的见解,可以在评论去讨论,笔者会进行回复并修改,共同进步!
文章分类
前端
文章标签