理解执行上下文

310 阅读5分钟

js的代码执行顺序,很多人都会觉得是顺序执行。但是执行的过程中我们会遇到很多坑,比如下面的代码:

function foo() {
  console.log('foo')
}

foo()  //foo1

function foo() {
  console.log('foo1');
}

foo() //foo1

说好的顺序执行呢?其实是因为函数是会提升的,提升过后,相同函数中,后面的函数会覆盖掉之前的,所以两次执行都会输出foo1

其实,上面的代码说明了js引擎对代码的分析其实是一段一段的,当执行一段代码的时候,就会进行一个准备工作

可执行代码

上面说的一段一段的分析是什么呢,这里我们要说到以客概念,就是可执行代码

可执行代码中包含:

  • 全局代码
  • 函数代码
  • eval代码

准备工作其实就是我们说的执行上下文

执行上下文栈

当函数增多的时候,就会很难管理,这时候,我们就需要使用一个执行上下文栈来进行管理

当执行JavaScript代码的时候,我们最先肯定是执行全局代码,这时候我们将全局上下文压入栈,因为栈的概念是后进先出,所以只有当代码全局完成的时候,ECstack才会被清空。

ECstack [
    函数上下文
    全局上下文
]

这么说说可能不好理解,我将开始的那段代码进行了一个图形的解析

这里我们将全局上下文用globalContext表示,我们可以看到当进栈的时候,第二个fooContext会覆盖掉之前的fooContext,所以在执行foo()操作的时候,出栈的就是第二个fooContext。

执行上下文属性

执行上下文中其实有三个重要属性,应该也是我们经常听说的:

  • 变量对象
  • 作用域链
  • this

变量对象

变量对象(VO)就是存储了上下文中的定义的变量和函数声明,那其中又有全局上下文中的全局对象和函数上下文中的活动对象(AO)。

全局对象

全局上下文中的变量对象其实就是全局对象,其实就是一个window对象,也是由Object构造函数实例化的一个对象

this instanceOf Object //全局中为true

其实它也是作为一个全局变量的宿主

var a = 1
console.log(this.a)

其实它还有很多属性,我就在这不多说了。

活动对象

函数上下文中用活动对象(AO)来表示变量对象。前面我们所说的全局中的变量对象是可以在任何地方访问,它是在执行任何上下文之前就先创建的对象。但是函数环境的局部变量对象是在函数执行之前创建的,VO是不能直接访问的,所以我们需要与VO相对应的AO来访问函数内部对象。

AO也就是VO激活后的对象,它通过函数的arguments进行初始化,arguments属性值是Arguments对象。Arguments中的属性有:

  • callee:可以调用函数自身
  • length:实参的长度
  • properties-indexes: 属性的值就是函数参数的值

执行过程

执行上下文代码的时候回分为两个阶段进行处理:进入执行上下文执行代码

  • 进入执行上下文,变量对象会包含:

    • 函数所有的形参

      • 按名称和对应值的形式创建变量对象的属性
      • 如果没有传递对应参数,则为undefined
    • 所有的函数说明

      • 按名称和对应值(函数对象function-object)的形式创建变量对象的属性
      • 如果已经存在相同名字的变量(变量没有进行赋值),就进行替代
    • 所有的变量声明

      • 按名称和对应值(undefined)的形式创建变量对象的属性
      • 如果已经存在相同名字(形参或者函数)的属性,不进行替代

举个栗子

function foo(x) {
   var y = 'friends';
   function z() {}
   y = 'world';
}
foo('hello');

这个时候所形成的VO为:

    VO = {
        arguments: {
            0:'hello'
            length: 1
        },
        x: 'hello',
        y: undefined,
        z: reference to function z(){},
    }
  • 代码执行

    • 在代码执行前,很多VO/AO都有了自己的属性,但是大部分属性都是体统的默认值undefined
    • 所以在代码执行的时候,会相应的填充属性中的值来计算结果

之前的栗子中的VO就可以为:

 VO = {
    arguments: {
        0:'hello'
        length: 1
    },
    x: 'hello',
    y: 'world',
    z: reference to function z(){},
    }

作用域链

简单来说,作用域链就是查找变量对象的过程,从当前的上下文开始查找,如果没有的话就会从父级执行上下文的变量对象中查找,就这样找呀找呀~~ 直到找到全局上下文的变量对象,也就是全局对象,再没有的话就说明你没有定义啦~~就会报错了。

函数的创建和变化过程

  • 函数创建

    每个函数的内部其实都有一个[[scope]]的属性,当函数创建的时候,就会保存所有的父级变量对象到其中,[[scope]]本质上就是一个数组,(我认为就是像栈一样的一个数组),但是它还不是一个完整的作用域链。

栗子栗子又来了

function father() {
    function son() {
        ...
    }
}

函数创建的时候,son的[[scope]]

son.[[scope]]: [
    fatherContext.AO,
    globalContext.VO
]
  • 函数激活

    当函数激活后,就会进入到函数上下文中,创建VO/AO后,就会将活动对象添加到作用域的最前端,那[AO]和之前的[[scope]]组合就形成了作用域链啦!!

Scope = [AO].concat([[scope]])

this

这一块的原理还在学习中...

其实我现在是用一句话行走于this中的: this永远指向最后调用它的那个对象

var a = 1
var obj = {
    a: 2,
    foo: function() {
        console.log(this.a) //2
    }
}

obj.foo() 

在这里foo是对象obj调用的,所以指向obj,那么值自然就为2了。

这里就先举一个小栗子,关于this原理会涉及到reference等的规范类型概念,我还没有看透,等我菜鸡归来~~

这里面很多都是看@冴羽的文章,大家可以去看他写的文章,应该更加深入~~

注:此文为本人学习过程中的笔记记录,如果有错误或者不准确的地方请大佬多多指教~