执行上下文

上下文的原意是 context, 做用域的原意是scope, 这两个不是一个东西。javascript

每个函数的调用(function invocation) 都有对应的scope 和context.java

scope 指的是 函数被调用的时候, 各个变量的做用区域
context 指的是 current scope and its enclosing scope. 就是当前scope 和包裹它外面的scope. 若是一个变量在当前scope没找到,那么它会自底向上继续找enclosing scope 直到找到为止很像javascript 的prototype那样的找法。常常在javascript中,函数被调用的时候, 查看this 指向哪一个object, 那么那个object 就是当前的 "上下文"浏览器

 

小明告诉小红:“你放心吧,他答应你的条件了。”函数

  在读者的眼中,“他”是谁根本无从知晓,由于这句话缺乏“上下文”;this

从小强家里出来后,小明告诉小红:“你放心吧,他答应你的条件了。”spa

   谁都知道,“他”指的是“小强”,由于有“上下文”。prototype

 

什么是执行上下文

Javascript中代码的运行环境分为如下三种:线程

  • 全局级别的代码 – 这个是默认的代码运行环境,一旦代码被载入,引擎最早进入的就是这个环境。
  • 函数级别的代码 – 当执行一个函数时,运行函数体中的代码。
  • Eval的代码 – 在Eval函数内运行的代码。

在网上能够找到不少阐述做用域的资源,为了使该文便于你们理解,咱们能够将“执行上下文”看作当前代码的运行环境或者做用域。下面咱们来看一个示例,其中包括了全局以及函数级别的执行上下文:3d

上图中,一共用4个执行上下文。紫色的表明全局的上下文;绿色表明person函数内的上下文;蓝色以及橙色表明person函数内的另外两个函数的上下文。注意,无论什么状况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也就是说,咱们能够在person的上下文中访问到全局上下文中的sayHello变量,固然在函数firstName或者lastName中一样能够访问到该变量。code

至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出一个函数上下文,换句话说,就是新建一个局部做用域,能够在该局部做用域中声明私有变量等,在外部的上下文中是没法直接访问到该局部做用域内的元素的。在上述例子的,内部的函数能够访问到外部上下文中的声明的变量,反之则行不通。那么,这究竟是什么缘由呢?引擎内部是如何处理的呢?

执行上下文堆栈

在浏览器中,javascript引擎的工做方式是单线程的。也就是说,某一时刻只有惟一的一个事件是被激活处理的,其它的事件被放入队列中,等待被处理。下面的示例图描述了这样的一个堆栈:

咱们已经知道,当javascript代码文件被浏览器载入后,默认最早进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数建立一个新的执行上下文,而且将其压入到执行上下文堆栈的顶部。浏览器老是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,而后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行而且弹出堆栈,直到回到全局的上下文。请看下面一个例子:

 (function foo(i) {
            if (i === 3) {
                return;
            }
            else {
                foo(++i);
            }
        }(0));

上述foo被声明后,经过()运算符强制直接运行了。函数代码就是调用了其自身3次,每次是局部变量i增长1。每次foo函数被自身调用时,就会有一个新的执行上下文被建立。每当一个上下文执行完毕,该上上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。真个过程抽象以下图:

因而可知 ,对于执行上下文这个抽象的概念,能够概括为如下几点:

  • 单线程
  • 同步执行
  • 惟一的一个全局上下文
  • 函数的执行上下文的个数没有限制
  • 每次某个函数被调用,就会有个新的执行上下文为其建立,即便是调用的自身函数,也是如此。

执行上下文的创建过程

咱们如今已经知道,每当调用一个函数时,一个新的执行上下文就会被建立出来。然而,在javascript引擎内部,这个上下文的建立过程具体分为两个阶段:

  1. 创建阶段(发生在当调用一个函数时,可是在执行函数体内的具体代码之前)
    • 创建变量,函数,arguments对象,参数
    • 创建做用域链
    • 肯定this的值
  2. 代码执行阶段:
    • 变量赋值,函数引用,执行其它代码

实际上,能够把执行上下文看作一个对象,其下包含了以上3个属性:

    
(executionContextObj = { variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ }, scopeChain: { /* variableObject 以及全部父执行上下文中的variableObject */ }, this: {} } 

创建阶段以及代码执行阶段的详细分析

确切地说,执行上下文对象(上述的executionContextObj)是在函数被调用时,可是在函数体被真正执行之前所建立的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 – 创建阶段。这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,而后基于这些信息创建执行上下文对象(executionContextObj)。在这个阶段,variableObject对象,做用域链,以及this所指向的对象都会被肯定。

上述第一个阶段的具体过程以下:

  1. 找到当前上下文中的调用函数的代码
  2. 在执行被调用的函数体中的代码之前,开始建立执行上下文
  3. 进入第一个阶段-创建阶段:

    • 创建variableObject对象:
      1. 创建arguments对象,检查当前上下文中的参数,创建该对象下的属性以及属性值
      2. 检查当前上下文中的函数声明:

        每找到一个函数声明,就在variableObject下面用函数名创建一个属性,属性值就是指向该函数在内存中的地址的一个引用

        若是上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。

    • 初始化做用域链
    • 肯定上下文中this的指向对象
  4. 代码执行阶段:

    执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。

下面来看个具体的代码示例:

    
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22); 

在调用foo(22)的时候,创建阶段以下:

    
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, scopeChain: { ... }, this: { ... } } 

因而可知,在创建阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述创建阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象以下:

    
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, scopeChain: { ... }, this: { ... } } 

咱们看到,只有在代码执行阶段,变量属性才会被赋予具体的值。

局部变量做用域提高的原因

在网上一直看到这样的总结: 在函数中声明的变量以及函数,其做用域提高到函数顶部,换句话说,就是一进入函数体,就能够访问到其中声明的变量以及函数。这是对的,可是知道其中的原因吗?相信你经过上述的解释应该也有所明白了。不过在这边再分析一下。看下面一段代码:

    
(function() { console.log(typeof foo); // function pointer console.log(typeof bar); // undefined var foo = 'hello', bar = function() { return 'world'; }; function foo() { return 'hello'; } }());​ 

上述代码定义了一个匿名函数,而且经过()运算符强制理解执行。那么咱们知道这个时候就会有个执行上下文被建立,咱们看到例子中立刻能够访问foo以及bar变量,而且经过typeof输出foo为一个函数引用,bar为undefined。

为何咱们能够在声明foo变量之前就能够访问到foo呢?

由于在上下文的创建阶段,先是处理arguments, 参数,接着是函数的声明,最后是变量的声明。那么,发现foo函数的声明后,就会在variableObject下面创建一个foo属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有var foo的声明,可是variableObject已经具备了foo属性,因此直接跳过。当进入代码执行阶段的时候,就能够经过访问到foo属性了,由于它已经就存在,而且是一个函数引用。

为何bar是undefined呢?

由于bar是变量的声明,在创建阶段的时候,被赋予的默认的值为undefined。因为它只要在代码执行阶段才会被赋予具体的值,因此,当调用typeof(bar)的时候输出的值为undefined。

这样差很少对执行上下文有所理解了吧,大佬们但愿能给出更好地建议

相关文章
相关标签/搜索