JS执行原理

目录:

JS 中的执行上下文

JS 是一种描述性脚本语言,不同于 C#,JAVA,它不需要编译成中间语言,而是由 JS 引擎动态解析和执行。执行上下文( Execution Context ),也便是常说的 执行环境
执行上下文 三属性: 变量对象作用域链this 指向

注意

这里我们需要注意的一点是, JS 引擎解析执行代码的过程是一个边执行边解析的过程,解析发生在执行一段可执行代码之前。举个例子,当执行到一个函数的时候,就会先对这个函数进行解析,然后再执行这个函数。

变量提升

某函数或者某变量 在作用域中前面被调用,但声明却在后面,按理来说应该调用失败,但实际上 JS 在解析(预处理)时,会根据 可执行代码 创建相对应的 执行上下文,在这个环境中,所有变量会被事先提出来(变量提升),解析完代码之后才开始执行。

可执行代码

在代码解析阶段会根据不同的可执行代码创建相应的执行上下文,可执行代码分类:

  • 全局执行代码,在执行所有代码前,解析创建全局执行上下文。
  • 函数执行代码,执行函数前,解析创建函数执行上下文。
  • eval 执行代码,运行于当前执行上下文中。

执行上下文的组成

执行上下文定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每一个执行上下文都由以下三个属性组成。

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

执行上下文栈

解析 可执行代码 会创建对应的 执行上下文,JS 引擎通过 执行上下文栈(Execution context stack,ECS) 来管理这些执行上下文。

var a = "global var";

function foo() {
  console.log(a);
}

function outerFunc() {
  var b = "var in outerFunc";
  console.log(b);

  function innerFunc() {
    var c = "var in innerFunc";
    console.log(c);
    foo();
  }

  innerFunc();
}

outerFunc();

代码首先进入 全局执行上下文 ,然后依次进入 outerFunc,innerFunc 和 foo 的执行上下文,执行上下文栈就可以表示为:
执行上下文栈

JavaScript 开始要解析执行代码的时候,最先遇到的就是全局代码,所以 JavaScript 引擎会先解析创建全局执行上下文,然后将全局执行上下文压栈。然后当执行流进入一个函数时,会先解析创建函数执行上下文,然后将它的执行上下文压栈。而在函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文。注意,全局执行上下文会一直留在栈底,直到整个应用结束


变量对象_皆是 var 声明

创建变量对象(VO)时就是将各种变量和函数声明进行提升的环节

定义:

变量对象(Variable object,VO)存储了在执行上下文中定义的所有变量和函数声明,保证代码执行时对变量和函数的正确访问。

//用下面代码为例子
console.log(a);
console.log(b);
console.log(c);
console.log(d);
var a = 100;
b = 10;
function c() {}
var d = function () {};

上述代码的变量对象:

//这里用VO表示变量对象
VO = {
    a = undefined; //有a,a使用var声明,值会被赋值为undefined
    //没有b,因为b没用var声明
    c = function c (){}  //有c,c是函数声明,并且c指向该函数
    d = undefined; //有d,d用var声明,值会被赋值为undefined
}

解说:执行上述代码的时候,会创建一个全局执行上下文,上下文中包含上面变量对象,创建完执行上下文后,这个执行上下文才会被压进执行栈中。开始执行后,因为 js 代码一步一步被执行,后面赋值的代码还没被执行到,所以使用 console.log 函数打印各个变量的值是变量对象中的值。

在运行到第二行时会报错(报错后就不再执行了),因为没有 b(b is no defined)。把第二行注释掉后,再执行各个结果就是 VO 里面的对应的值。

活动对象(active Object)

活动对象是在函数执行上下文里面的,其实也是变量对象,只是它需要在函数被调用时才被激活,而且初始化 arguments,激活后就是看做变量对象执行上面一样的步骤。

//例子
function fn(name){
    var age = 3;
    console.log(name);
}
fn('ry');

//当上面的函数fn被调用,就会创建一个执行上下文,同时活动对象被激活

//活动对象
AO = {
    arguments : {0:'ry'},  //arguments的值初始化为传入的参数
    name : ry,  //形参初始化为传进来的值
    age : undefined  //var 声明的age,赋值为undefined
}

活动对象其实也是变量对象,做着同样的工作。其实不管变量还是活动对象,这里都表明了,全局执行和函数执行时都有一个变量对象来储存着该上下文(环境内)定义的变量和函数。


作用域链

在创建执行上下文时还要创建一个重要的东西,就是作用域链。每个执行环境的作用域链由当前环境的变量对象父级环境的作用域链构成。(父级环境作用域链会继续向上套娃)

创建作用域链过程:

//以本段代码为例
function fn(a,b){
    var x = 'string',
}
fn(1,2);
1. 函数被调用前,先创建了 全局执行上下文,初始化 function fn,fn 有个私有属性[[scope]],它会被初始化为当前全局的作用域,fn.[[scope]="globalScope"。

2. 调用函数 fn(1,2),开始创建 fn 执行上下文,同时创建作用域链 fn.scopeChain = [fn.[[scope]]],此时作用域链中有全局作用域。

3. fn 活动对象 AO 被初始化后,把活动对象作为变量对象推到作用域链前端,此时 fn.scopeChain = [fn.AO,fn.[[scope]]],构建完成,此时作用域链中有两个值,一个当前活动对象,一个全局作用域。

fn 的作用域链构建完成,作用域链中有两个值,第一个是 fn 函数自身的活动对象,能访问自身的变量,还有一个是全局作用域,所以 fn 能访问外部的变量。这里就说明了为什么函数中能够访问函数外部的变量,因为有作用域链,在自身找不到就顺着作用域链往上找。


this

执行上下文 ,一个全局执行上下文,一个函数执行上下,下面分别说说这两种上下文的 this。

  1. 全局执行上下文的 this

    指向 window 全局对象

  2. 函数执行上下文的 this(主要讲函数的 this)

    在《JavaScript 权威指南》中有这么几句话:
    1.this 是关键字,不是变量,不是属性名,js 语法不允许给 this 赋值。

    2.关键字 this 没有作用域限制,嵌套的函数不会从调用它的函数中继承 this。

    3.如果嵌套函数作为方法调用,其 this 指向调用它的对象。

    4.如果嵌套函数作为函数调用,其 this 值是 window(非严格模式),或 undefined(严格模式下)。

解读: 上面说的概括了 this 两种值的情况:

  1. 函数直接作为某对象的方法被调用则函数的 this 指向该对象。
  2. 函数作为函数直接独立调用(不是某对象的方法),或是函数中的函数,其 this 指向 window。

例子 1:函数被独立运行时,其 this 的值指向 window 对象。

function a() {
  console.log(this);
}
//独立运行
a(); //window

例子 2:(函数中函数,这里嵌套了个外围函数)这里也是指向 window 对象,也相当于函数作为函数调用,就是独立运行。其实这个例子也说明闭包的 this 指向 Window。

//外围函数
function a() {
  //b函数在里面
  function b() {
    console.log(this);
  }
  //虽然在函数中,但b函数独立运行,不是那个对象的方法
  b();
}
a(); //window

例子 3:(再写复杂点的话)x 函数即使在对象里面,但它是函数中的函数,也是作为函数运行,不是 Object 的方法。getName 才是 objcet 的方法,所以 getName 的 this 指向 object(在下个栗子有)。

//一个对象
var object = {
  //getName是Object的方法
  getName: function () {
    //x是getName里面的函数,它是作为函数调用的,this就是window啦
    function x() {
      console.log(this); //this是window啦
    }
    x();
  },
};
object.getName(); //window

例子 4:函数作为某个对象的方法被调用。

//一个对象
var object = {
  name: "object",
  //getName 是 Object 的方法
  getName: function () {
    console.log(this === object);
  },
};
object.getName(); //true , 说明 this 指向了 object

这里的 getName 中的 this 是指向 objct 对象的,因为 getName 是 object 的一个方法,它作为对象方法被调用。

例子 5 :通过 call 改变 this

var name = "window";
var obj = {
  name: "obj",
};
function fn() {
  console.log(this.name);
}

//将fn通过call或bind或apply直接绑定给obj,从而成为obj的方法。
fn.call(obj); //obj

总结一下 this 的值

  • 全局执行上下文:this 的值是 window

  • 函数执行上下文:this 的值两种:

    1. 函数中 this 指向某对象,因为函数作为对象的方法:怎么看函数是对象的方法,一种是直接写在对象里面,另一种是通过 call 等方法直接绑定在对象中。

    2. 函数中 this 指向 window:函数独立运行,不是对象的方法,函数中的函数(闭包),其 this 指向 window。


总结整个 js 代码执行过程

js 代码执行分成了两部分:解析执行

解析创建执行上下文,有两种,一种是开始执行 js 代码就创建全局执行上下文,一种是当某个函数被调用时创建它自己的函数执行上下文。执行上下文包含 三个部分(变量对象,作用域链,this)

执行:在执行栈中执行,栈顶的执行上下文获得执行权,并按顺序执行当前上下文中的代码,执行完后弹栈销毁上下文,执行权交给下一个栈顶执行上下文。

posted @ 2020-06-05 18:24  彭尼玛  阅读(1235)  评论(0编辑  收藏  举报