【JS深渊】一定要彻底弄懂javascript执行机制(一)

377 阅读10分钟

前言

【JS深渊】系列是我用来对前端原生javascript语言相关技术进行的深度探究或者笔记记录,我个人是一个很爱去挖层的这么一个人,但不幸的是觉悟的太晚,但我不会放弃。决定开始写博客目的有两个,一个是有仪式性的去持续提高自己的技术能力,另一个就是希望向上再向上。不过自己能力也是有限的,如果写的不好或者出现什么纰漏又或者错误的地方,脱裤式欢迎大家的建议、指正。没错看着我,对,盯着你的屏幕,别眨眼,请记住我的这句话:
您的反馈是就是我持续进步的动力
好了,收!biu~

正文

javascrip是一个弱类型解释性语言,依赖于浏览器JS引擎进行解析执行。里面的解析过程也十分复杂。但是稍微了解一下,对javascript语言的掌握有着很大的帮助,能够使我们在日常的开发过程中更加灵活运用js语言本身的特性。废话不多说了,开始:🤣🤣

lotoze我先来段风骚的代码

console.log("123")
console.log("456");

某某侠士:“喂,糟老头!你在逗我玩?“,哪里风骚了,不就是两句打印吗? 擦擦鼻血,整整发型。略显正经起身后:“这位侠士,别急别急,请听老头我细细道来“。先来个问题:虽然是两句简单的打印语句,但是我想问的是从在浏览器打开此html文件,针对于js代码打印输出到控制台结果,这之间发生了什么?
:上面以及下面的所涉及的js代码均在此html文件中测试执行,html代码为:

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>Test</title>
</head>
<body>
<script type="text/javascript">
    //js代码块...
</script>
</body>
</html>

过程一:语法分析(代码通读)

这一过程,专业一点叫做语法分析,每个计算机语言都会有这一步,而且计算机语言的区分的奥妙就在语法分析、词法分析、语义分析的差异。不过这里只说语法分析,可以理解为对加载好的代码的一次的初次检验。
首先,系统,不,确切的说是js引擎线程并不会立即执行文件中的js代码,而是会先通读一遍js代码,看看是否有低级的语法错误,如果有低级语法错误,那么就立即停止并抛出错误到控制台;如果没有低级的语法错误,就会为接下来js代码的真正执行做一些准备工作。但是这样说真的对吗?一定要注意理解! 我们来看一下代码:

console.log("123");
console.log"456");
console.log("789");

我们先来假设一下:按照刚才我们所说的js代码并不会立即执行,而是会先进行通读一遍代码,如果有低级错误就抛出,并立即停止后续的代码。那么按照这种理解,最后的结果就是:直接抛出低级错误,"123"并不会输出。但是你会惊奇的发现😱😱,"123"居然正常输出了,第二行报错,后边的不执行。最终结果为:

console.log("123"); //正常打印"123"
console.log"456"); //抛出低级错误,立即停止解析执行
console.log("789");

这说明什么?

说明: js引擎在通读代码做分析时,并不是读全部的js代码,而是一行一行的去读,如果这一行代码没有低级语法错误,那么是这一行代码进入js执行的下一准备阶段,如果这一行代码有低级语法错误则会在这一行抛出错误并立即停止。注意是 这一行。

这又是为什么?为什么要一行一行的读?

因为javascript语言是解释性的语言,是解释一行执行一行。那么这里请跟lotoze去发散思考一下,解释性语言是不是都是解释一行执行一行呢?答案是肯定的。如果是先等到全部的代码解释完成再同一执行,这性能得有多慢啊,黄花菜都凉了。值得一提的是,一有错误就会立即停止,这是单线程计算机语言的特性。

过程二:预编译(执行前的准备)

预编译是js代码执行前一刻的准备过程。为js的执行提供一些必要所需,在加载好的js代码经历过js引擎的语法分析检验之后会立即进入此阶段。
首先,想要了解这一阶段我们需要先明确几个重要的概念。

  • 隐式属性

    顾名思义,隐式属性即为系统内部调用,不能被我们看到或使用的属性。

  • 作用域

    作用域分为全局作用域和函数作用域,可以说这两类出现原因是因为函数的出现,函数有着自己专属的作用域。
    作用域有个比较专业一点的名字叫做执行期上下文。其实看着高大上的名字,本质就是一个对象,里面存储着js代码执行时所需要的必要“粮食”。

    //全局作用域
    ...
    function test() {
        //局部/函数作用域
        ...
    }
    
  • 作用域链

    作用域链专业一点的说法叫做,执行期上下文的集合按照一定的规律形成的链式结构叫做作用域链。 通俗一点讲,执行期上下文穿串了,然后串太长了,成了个链子la😂😂 不要冲动,大哥!
    需要注意的是,全局作用域只有一个,函数作用域每个函数并不是独一无二的(比如闭包),但每个函数都会有一个自己的作用域链。

  • 函数上的[[scopes]]隐性属性

    我们说,在js中的数据结构受Java的影响很大,在js中也是一切皆对象,函数也可以看做一个对象,对象可以拥有属性和方法,属性分为显式属性和隐式属性。在函数对象上都有着一个叫做[[scopes]]的隐式属性。这个属性的作用就是存储着执行期上下文的集合。换句话说,[[scopes]]就是作用域链。它是一个栈结构,可以理解为是一个数组,具有栈结构First in, last out的特性。

    var b = 20;
    function test(argument) {
    	var a = 10;
    }
    

    下面是在控制台打印出来的test函数体,可以看到[[scopes]]:

基本的概念已经了解了,但是我们进一步思考一下,我们说预编译是为js代码真正执行阶段准备过程,那么预编译到底准备了什么?准备的过程是怎样的?

*下面我来总结一下预编译的过程,预编译的执行过程分为两步:

  • 全局预编译过程

    1. 创建Global Object对象(后面简称GO)。
    2. 找全局变量声明,将全局变量名做为GO对象的属性名,值赋予undfined。
    3. 找全局函数声明,将函数名作为GO对象的属性名,值赋予函数体。
  • 函数预编译过程

    1. 创建Active Objct对象(后面简称AO)。
    2. 找局部变量或形参声明,将局部变量或形参声明作为AO对象的属性名,值赋予为undefined。
    3. 实参与形参相统一。
    4. 找局部函数声明,将函数名作为AO对象的属性名,值赋予函数体。

预编译是变量声明提升和函数声明提升的根本原因。经过预编译过程之后,代码将立即进入真正的执行阶段。

这里我们来一个的栗子,来进行理解一下预编译执行过程。

console.log(a);
console.log(b);
var a = 10;
var b = a;
function a(a) {
    var a = "aaa";
    console.log(a);
}
function foo(b) {
    var a = 100;
    var b = a;
    console.log(a);
}
foo();
a("lalala");
//请问上述代码打印什么?

这个栗子如果你只知道变量声明提升和函数声明提升,想要得出正确结果真的很麻烦。但是这个栗子实质上就是js代码的预编译过程。 我们来分析一下:
当js代码加载之后,经过语法分析后,进入程序运行的前一刻的预编译阶段。这个阶段分为全局预编译阶段和函数预编译阶段。 那么我们依据这个栗子去具体分析一下预编译过程:

  • 全局预编译的过程:
    1. 创建一个GO对象
      //Go
      {}
      
    2. 找全局变量声明,将全局变量名做为GO对象的属性名,值赋予undfined。
      //GO
      {
          a: undefined,
          b: undefined
      }
      
    3. 找全局函数声明,将函数名作为GO对象的属性名,值赋予函数体。.
      //GO
      {
          a: a(){}, //赋值为函数体
          b: undefined
      }
      
  • 函数预编译过程
    • a函数预编译过程
      1. 创建Active Objct对象(后面简称AO)。
        // aAO --> 表示a函数的AO对象
        {}
        
      2. 找局部变量或形参声明,将局部变量或形参声明作为AO对象的属性名,值赋予为undefined。
        //aAO --> 表示a函数的AO对象
        {
            a: undefined
        }
        
      3. 实参与形参相统一。
        //aAO --> 表示a函数的AO对象
        {
            a: "lalala"
        }
        
      4. 找局部函数声明,将函数名作为AO对象的属性名,值赋予函数体。
        //aAO --> 表示a函数的AO对象
        {
            a: a(){}
        }
        
    • foo函数预编译过程
      同上,最终的foo函数的Ao对象是:
         //aAO --> 表示foo函数的AO对象
         {
             a: undefined,
             b: undefined
         }
      

这时预编译过程就已经完成了,生成了这个一个GO和两个AO对象,每个预编译对象(虚构的,为了理解,包括GO和AO)生成完毕的同时都会立即压入[[scopes]]栈中,通常GO先入栈,然后是当前的AO对象入栈。下一步就是真正的执行了,调用栈则会按照先进后出的方式出栈执行。 最后的结果为:

console.log(a); //a(){}
console.log(b); //undefined
var a = 10;
var b = a;
function a(a) {
    var a = "aaa";
    console.log(a);
}
function foo(b) {
    var a = 100;
    var b = a;
    console.log(a);
}
foo(); //100
a("lalala"); //报错a is not a function

过程三:执行阶段(真正执行)

js的内部执行阶段,是一个非常庞杂的过程。老头我会去使用专门的篇幅来写。这里只做一个简单的介绍。
当加载的js代码经过语法分析以及预编译后,会进入js引擎线程会把js代码划分为的宏任务(包括同步任务、异步任务)、微任务。然后按照宏任务(同步任务)-->微任务-->宏任务(异步任务)的轮询中。

lotoze | 【原创】
着重说明:里面一些表情图片并非原创,只是为了读者读起来不是那么枯燥乏味。但如果原作者觉得有侵犯版权的意思,请使用下方联系方式与我联系,为了尊重原创作者的辛苦创作,我将及时处理!
当然,没事也可以联系啦😘😘欢迎交流!

求赞/求关注

写作不易,
如果您还觉得凑合,就给个赞!
如果觉得确实觉得: “老家伙,有你的啊!”就加个关注!
如果文章有任何的错误,脱裤式欢迎大家来进行批评指正!
每一个鼓励都是lotoze我持续抛头颅,撒鸡血的创作动力!
每一个批评反馈也都是lotoze我持续成长的台阶!