深刻理解JS执行流程

前言

想要成为一名 JavaScript 开发者,那么你必须知道 JavaScript 程序内部的执行机制。执行上下文和执行栈、词法做用域、this、内存空间、变量对象等都是JavaScript中关键点,同时也是JavaScript难点。前端

1、内存空间

由于JavaScript具备自动垃圾回收机制,因此对于前端开发来讲,内存空间并非一个常常被说起的概念,很容易被你们忽视。segmentfault

在很长一段时间里认为内存空间的概念在JS的学习中并非那么重要。但是后我当我回过头来从新整理JS基础时,发现因为对它们的模糊认知,致使了不少东西我都理解得并不明白。好比最基本的引用数据类型和引用传递究竟是怎么回事儿?好比浅复制与深复制有什么不一样?还有闭包,原型等等。数组

一、堆和栈

JavaScript中并无严格意义上区分栈内存与堆内存。所以咱们能够粗浅的理解为JavaScript的全部数据都保存在堆内存中。可是在某些场景,咱们仍然须要基于堆栈数据结构的思路进行处理,好比JavaScript的执行上下文。浏览器

经过下图类比如下咱们的栈: bash

堆存取数据的方式,则与书架与书很是类似,只要知道书的名字,咱们就能够很方便的取出咱们想要的书,而不用像从乒乓球盒子里取乒乓同样,非得将上面的全部乒乓球拿出来才能取到中间的某一个乒乓球。数据结构

二、变量对象与基础数据类型

js的执行上下文建立以后会生成一个变量对象,咱们的基本数据类型基本都存储在了这里面。闭包

严格意义上来讲,变量对象也是存放于堆内存中,可是因为变量对象的特殊职能,咱们在理解时仍然须要将其于堆内存区分开来。函数

咱们的代码执行是在一个一个的执行上下文中进行的。咱们声明的一些变量都存放在了相应的变量对象里~性能

基础数据类型都是一些简单的数据段,JavaScript中有5中基础数据类型,分别是Undefined、Null、Boolean、Number、String。基础数据类型都是按值访问,由于咱们能够直接操做保存在变量中的实际的值。学习

三、引用数据类型

JS的引用数据类型,好比数组Array,它们值的大小是不固定的。引用数据类型的值是保存在堆内存中的对象。JavaScript不容许直接访问堆内存中的位置,所以咱们不能直接操做对象的堆内存空间。在操做对象时,其实是在操做对象的引用而不是实际的对象。所以,引用类型的值都是按引用访问的。这里的引用,咱们能够粗浅地理解为保存在变量对象中的一个地址,该地址与堆内存的实际值相关联。

例如:

var a1 = 0;   // 变量对象
var a2 = 'this is string'; // 变量对象
var a3 = null; // 变量对象
var b = { m: 20 }; // 变量b存在于变量对象中,{m: 20} 做为对象存在于堆内存中
var c = [1, 2, 3]; // 变量c存在于变量对象中,[1, 2, 3] 做为对象存在于堆内存中
复制代码

a1 a2 a3 b c 变量都被放入了变量对象,b c存储的是实际引用对象的堆内存地址,访问时属于引用访问。

四、案例分析

// demo01.js
var a = 20;
var b = a;
b = 30;
// 这时a的值是多少?
复制代码

// demo02.js
var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
复制代码

五、内存管理

虽然js有本身的自动垃圾回收机制,咱们能够不用太多的管理,js会在CPU空闲时刻按期去清理内存空间。可是咱们了解js的内存管理,可让咱们更好的理解js的执行流程,帮组咱们写出性能更好的代码。

var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100);  // 使用内存
a = null; // 使用完毕以后,释放内存空间
复制代码

上面就是一个简单的内存释放案例,js的垃圾回收机制其实就是去查找那些再也不被引用或者再也不被使用的内存空间,而后释放掉。最经常使用的就是标记清除的方式,js会从根部也就是全局开始对各变量进行标记,层层标记下去,知道全部变脸都被标记,这些标记代表了变量的使用状态,垃圾回收机制会根据这些标记去高效的清除那些再也不被使用的变量的地址空间,以及那些互相引用,可是不能被根访问到的变量空间(函数内部)。

在局部做用域中,当函数执行完毕,局部变量也就没有存在的必要了,所以垃圾收集器很容易作出判断并回收。可是全局变量何时须要自动释放内存空间则很难判断,所以在咱们的开发中,须要尽可能避免使用全局变量,以确保性能问题。

2、变量对象

在js中咱们确定须要知道咱们的变量和函数在哪里声明的,咱们怎么找到并访问?因此咱们还得靠执行上下文来帮忙,咱们知道代码的执行是在一个一个的执行上下文中进行的,最外层的全局上下文、函数的函数执行上下文。

执行上下文的组成为:变量对象做用域链this。 执行上下文的生命周期能够分为两个阶段:建立阶段执行阶段

  • 建立阶段: 在这个阶段,执行上下文会建立变量对象、绑定this、建立做用域链
  • 执行阶段: 这个阶段,会根据this绑定、做用域链来完成对变量对象的赋值、使用以及函数的调用和执行其余代码~

变量对象(Variable Object)是一个与执行上下文相关的数据做用域,存储了在上下文中定义的变量和函数声明,先来看一段代码示例:

一、全局执行上下文的变量对象

全局而言,全局对象是window,全局上下文有一个特殊之处就是他的变量对象就是window。this指向也是window.咱们在全局声明的变量和函数都会存储在window中。

// 以浏览器中为例,全局对象为window
// 全局上下文建立阶段
// VO 为变量对象(Variable Object)的缩写
windowEC = {
    VO: Window,
    scopeChain: {},
    this: Window
}
复制代码

二、函数执行上下文的变量对象

变量对象存储了执行上下文中的变量和函数声明,但在函数上下文中,还多了一个arguments(函数参数列表), 一个伪数组对象。而且这里的VO是经过arguments来初始化的。

一、建立arguments对象。检查当前上下文中的参数,创建该对象下的属性与属性值。

二、检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名创建一个属性,属性值为指向该函数所在内存地址的引用。若是变量对象已经存在相同名称的属性,则彻底替换这个属性(函数是第一公民)。

三、检查当前上下文中的变量声明(var 声明的变量),默认为 undefined;若是变量名称跟已经声明的形式参数或函数相同,为了防止同名的函数被修改成undefined,则会直接跳过变量声明,原属性值不会被修改。

上诉是VO变量对象的建立过程~~咱们平时说的变量的提高、函数的提高其实就是在说这里,好比:

console.log(foo);
foo();//能够执行,此时foo是函数
var foo=10; // foo被从新赋值为10
foo();//foo已经被赋值为一个变量,没法执行foo为函数,会报错
console.log(foo);  // 10
function foo(){
  var a;
  console.log(a);
  a=12;
  console.log(a);
}
console.log(foo); // 10
复制代码
// 建立变量对象以下:
VO = {
    arguments: {
        length: 0
    },
    foo: function(),
}
复制代码

在看一个例子:

alert(a);//输出:function a(){ alert('我是函数') }
function a(){ alert('我是函数') }//
var a = '我是变量';
alert(a);   //输出:'我是变量'
复制代码

有个细节必须注意:当遇到函数和变量同名且都会被提高的状况,函数声明优先级比较高,所以变量声明会被函数声明所覆盖,可是能够从新赋值。

固然还须要注意的是,函数未进入执行阶段以前,变量对象中的属性都不能访问!可是进入执行阶段以后,变量对象(VO)转变为了活动对象(AO),而后开始进行执行阶段的操做。

3、执行上下文

一、什么是执行上下文

执行上下文就是咱们当前代码执行所处的环境的一个抽象概念,咱们的代码执行是在一个一个的执行上下文中进行的。

为了方便管理咱们的执行上下文,以及如何调配处理执行上下文的执行顺序,咱们引入了执行上下文栈,用于方便咱们存放执行上下文和调用执行上下文。

先来一个直观的例子:

var a = 1;
function foo() {
    var b = 2;
    function bar() {

        console.log(b)

    }
    bar()
    console.log(a);
}
foo()
复制代码

1.执行这段代码,首先会建立全局上下文globleEC,并推入执行上下文栈中;

2.当调用foo()时便会建立foo的上下文fooEC,并推入执行上下文栈中;

3.当调用bar()时便会建立bar的上下文barEC,并推入执行上下文栈中;

4.当bar函数执行完,barEC便会从执行上下文栈中弹出;

5.当foo函数执行完,fooEC便会从执行上下文栈中弹出;

6.在浏览器窗口关闭后,全局上下文globleEC便会从执行上下文栈中弹出;

二、执行上下文的类型

  • 全局执行上下文: 1. 建立一个全局对象,在浏览器中这个全局对象就是 window 对象。2. 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
  • 函数执行上下文:每次调用函数时,都会为该函数建立一个新的执行上下文。每一个函数都拥有本身的执行上下文,可是只有在函数被调用的时候才会被建立。一个程序中能够存在任意数量的函数执行上下文。
  • eval执行上下文:运行在 eval 函数中的代码也得到了本身的执行上下文,但因为 Javascript 开发人员不经常使用 eval 函数,因此在这里再也不讨论。

三、执行上下文的声明周期

执行上下文的生命周期包括三个阶段:建立阶段→执行阶段→回收阶段,重点是建立阶段。

建立阶段其实就是建立变量对象、建立做用域涟、绑定this指向。

四、执行上下文栈

JavaScript 引擎建立了执行上下文栈来管理执行上下文。能够把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。

深刻理解JavaScript执行上下文和执行栈

4、做用域链与闭包

相关文章
相关标签/搜索