Javascript执行顺序和执行环境

写这篇文纠结了很久,也查阅了很多资料,但总是似是而非,这一篇我只说自己的理解和一些困惑,有错误的地方欢迎指正。本文的宿主环境就是特指浏览器。

javascript解释器概述

《初步了解浏览器》中,我们介绍了浏览器的组成部分,javascript解释器就是其中的一个组成部分:
在这里插入图片描述
interpreter有翻译者、翻译器的意思,在 javascrip中一般叫解释器、解析器、引擎。从中文的字面意思来理解,前端程序猿写了一大堆莫名其妙的代码,只有放到浏览器中,在js解释器翻译之后才具有一定的意义,解释器按照程序猿的规定去完成一个个任务,所有任务可以分成两种,同步任务(synchronous)和异步任务(asynchronous),这一节我们只讨论同步任务。

javascript代码本质上是一段文本,在浏览器宿主环境中需要被script标签包裹才能被正确的执行,这很好理解。JS引擎并不是编译器,不会将代码编译成机器码,JS引擎本身也是一种程序(可能是C、C++、Java编写的程序),它通过算法能识别javascript代码并执行,简单的说JS引擎其实就是能读懂javascript程序的程序。

内存堆和调用栈

以谷歌 V8 引擎为例,v8引擎包括两个主要组件:

  • 内存堆 — 这是发生内存分配的地方
  • 调用栈 — 这是代码执行的地方
    V8 引擎主要是通过这两个组件执行代码的。
    在这里插入图片描述

javascript解释器执行过程

关于Javascript执行我们要牢记,浏览器是多线程的,而javascript是单线程的。
1、浏览器内部是多线程的。I/O操作、定时器的计时和事件监听(click, keydown…)等都是由浏览器提供的其他线程来完成的。
2、JS是单线程语言,浏览器只分配给js一个主线程,用来执行任务。原则上js引擎从上到下顺序执行代码,当一个任务在执行的时候,其他未执行的任务都在排队。

为什么我要说原则上js解释器从上到下顺序执行代码,这是因为JS解释器执行代码不是按照书写代码的顺序执行的,而是有自己的执行规则,见下图:
在这里插入图片描述

一、语法检查

js的代码块加载完毕之后,会首先进入到语法分析阶段,该阶段的主要作用:
分析该js脚本代码块的语法是否正确,如果出现不正确会向外抛出一个语法错误,停止该js代码块的执行,然后继续查找并加载下一个代码块;如果语法正确,则进入到预编译阶段。

例1:通过下面的代码我们发现语法错误b();只阻塞了代码块1的执行
在这里插入图片描述
例2:看这段代码,会报错吗?

    var a=1;
    function b(){
        console.log(c);
    }
    console.log(a);

执行的结果是控制台打印了1,没有报错。函数b中明明有语法错误为什么不报错呢?这是因为JS引擎不会编译代码,而是一边解析,一边执行的,JS引擎没有执行b函数所以不会报错。
我们把代码改一下:
在这里插入图片描述
当调用了b函数,控制台报错了。这个因为在执行b函数的时候,开始检查b函数的语法,发现在b函数作用域中没有定义c,继续在全局作用域中寻找c,还是没找到,JS引擎就会抛出错误。

通过上面的代码,我们验证了一件事,就是JS解释器不会编译代码,而是边解析边执行的,对于语法错误,如果没有被执行到就不会报错。

二、预解析

语法检查检查之后,进入预解析阶段,js解释器在预解析阶段会在内存中创建执行上下文(Execution Context),我个人认为执行上下文这个说法比较不好理解,这里姑且叫它执行环境,执行环境是什么呢?就是正在执行的某行或者某段代码的所处的环境。

执行环境

客观实际中的环境是相对的概念,对于一个人来说,他的环境可以是一个城市、山川河流、食物、能源、建筑等等、甚至是整个地球,也可以是日月星辰,甚至是我们所处的宇宙,如果一个地球人离开地球去星际旅行,他就离开了地球环境进入了宇宙环境,如果我们把宇宙认定为最大的环境,那么人永远处于这个大环境中。在浏览器中,JS的执行环境有3种,全局环境、函数环境、和eval环境,本文不考虑eval环境,其实JS解释器在执行代码时,线程就是在全局环境和函数环境之间来回穿梭的,要说明的一点是函数环境是一个相对于全局环境的概念,调用任何一个函数一个新的执行环境就会被创建出来。

执行环境是在预解析阶段被创建、代码执行阶段被重新赋值,代码执行完毕即出栈、等待被回收,这是执行上下文的生命周期。
在这里插入图片描述

执行环境可以用一个对象(并不是说执行环境就是一个对象,执行环境是一个抽象的概念)表示,该包括三个属性:变量对象、作用域链、this的值
在这里插入图片描述

创建执行环境

就像先有宇宙再有地球一样,js代码中一定是先创建全局环境,在预解析阶段创建全局环境,JS解释器会先做变量和函数的声明,这在变量提升和函数提升中已经讲过了,作用域链是个数组,在全局环境中很简单,作用域链只有全局作用域Global,this指向window。全局环境不会被自动回收,所以在定义全局变量一定要小心。

如果在全局环境中调用函数,则对函数代码进行预解析,创建该函数的函数环境,执行函数,执行结束后返回全局环境(函数环境等待回收)。我们主要对函数环境进行解释。
函数环境与调用栈息息相关,我们代码中可能定义了很多函数,具体是那个函数的环境在创建和使用呢,我们用谷歌浏览器的Sources面板打断点看一下:

"use strict";
function f(x) {
  var a=1;
  function ff() {
    console.log(1);
  }
  ff();
}
f(1);

在这里插入图片描述
处于调用栈的栈顶的函数就是当前执行的函数,该函数的执行环境就是当前环境。

  • 预解析阶段的变量对象(用VO表示)三类属性:参数、变量声明、函数声明,在执行阶段之前参数的属性名是形参,属性值是实参(如果没有传参则是undefined)、变量声明的属性值是undefined,函数的属性值是函数的定义。
  • 作用域链:作用域链,是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。可以用一个数组来表示作用域链,数组的第一项总是位于执行栈栈顶函数的上下文中的变量对象local,而数组的最后一项总是全局变量对象global。
  • this指向:this指向*是执行上下文被创建时确定的。在一个函数环境中,this由调用者决定,怎么理解这句话呢?
    函数可以作为某个对象的方法调用,也可以独立调用,作为对象的方法调用时this指向这个对象,独立调用时this指向undefined,但在非严格模式中,它会被自动指向全局对象。

三、解释执行

在这里插入图片描述
执行代码阶段,VO/AO就会重新赋予真实的值,“预解析”阶段赋予的undefined值会被覆盖。

此阶段才是程序真正进入执行阶段,Javascript引擎会一行一行的读取并运行代码。此时那些变量都会重新赋值。

假如变量是定义在函数内的,而函数从头到尾都没被调用的话,则变量值永远都是undefined值。
一旦函数的执行结束(会有返回值),该函数就会从执行栈弹出,执行栈中的下一个函数(如果没有则是全局代码)会继续执行,直到执行栈为空。

猜你喜欢

转载自blog.csdn.net/dreamingbaobei3/article/details/88910508