执行上下文(Execution Context)简介

一、JavaScript代码运行的地方都存在执行上下文,它是一个概念,一种机制,用来完成JavaScript运行时作用域、生存期等方面的处理。

  • 执行上下文包括变量对象(Variable Object)、变量实例化(Variable Instatiation)、作用域/作用域链(Scope/Scope Chain)等概念,在不同的场景/执行环境下,处理上存在一些差异。
  • 执行上下文是一个内部数据结构,它包含有关函数执行时的详细细节:当前控制流所在的位置,当前的变量,this的值,以及其它的一些内部细节。

二、函数对象分为用户自定义函数对象和系统内置函数对象,对于用户自定义函数对象将按照下面描述的机制进行处理,但内置函数对象与具体实现相关,ECMA规范对它们执行上下文的处理没有要求,即它们基本不适合本节描述的内容。
三、所有的javascript代码都在执行上下文中执行。执行的JavaScript代码分三种类型

  1. Global Code,全局代码,即全局的、不在任何函数里面的代码,例如一个js文件、嵌入在HTML页面中的js代码等.
  2. Eval Code,即使用eval()函数动态执行的JS代码。
  3. Function Code,即用户自定义函数中的函数体JS代码。

    基本原理

    一、在用户自定义函数中,可以传入参数、在函数中定义局部变量,函数体代码可以使用这些入参、局部变量。背后的机制是什么样呢?
  • 当JS执行流进入函数时,JavaScript引擎在内部创建一个对象,叫做Variable Object。
  • 对应函数的每一个参数,在Variable Object上添加一个属性,属性的名字、值与参数的名字、值相同
  • 函数中每声明一个变量,也会在Variable Object上添加一个属性,名字就是变量名,因此为变量赋值就是给Variable Object对应的属性赋值。
  • 在函数中访问参数或者局部变量时,就是在variable Object上搜索相应的属性,返回其值。

二、一般情况下Variable Object是一个内部对象,JS代码中无法直接访问。规范中对其实现方式也不做要求,因此它可能只是引擎内部的一种数据结构。
三、大致处理方式就这样,但作用域的概念不只这么简单,例如函数体中可以使用全局变量、函数嵌套定义时情况更复杂点。这些情况下怎样处理?

  1. JavaScript引擎将不同执行位置上的Variable Object按照规则构建一个链表,在访问一个变量时,先在链表的第一个Variable Object上查找,如果没有找到则继续在第二个Variable Object上查找,直到搜索结束。这就是Scope/Scope Chain的大致概念。

    Global Object / 全局对象

    一、JavaScript的运行环境都必须存在一个唯一的全局对象-Global Object,例如HTML中的window对象。
    二、Global Object是一个宿主对象,除了作为JavaScript运行时的全局容器应具备的职责外,ECMA规范对它没有额外要求。
    三、它包含Math、String、Date、parseInt等JavaScript中内置的全局对象、函数(都作为Global Object的属性),还可以包含其它宿主环境需要的一些属性。

    Variable Object / 变量对象

    上面简述了Variable Object的基本概念。创建变量对象,将参数、局部变量设置为变量对象属性的处理过程叫做Variable Instatiation-变量实例化,后面结合作用域链再进行详细说明。

    Global Code / 全局代码

    一、变量对象就是全局对象,这是变量对象唯一特殊的地方(指它是内部的无法访问的对象而言)。

| 【示例1】```javascript var globalVariable = “WWW”; document.write(window.globalVariable); //result: WWW

  1. 上面代码在全局代码方式下运行,根据对变量对象的处理,定义变量-全局变量时就会在全局对象(即window)对象上添加这个属性,所以输出是WWW这个值。 |
  2. | --- |
  3. <a name="wNmLS"></a>
  4. ### Function Code / 函数代码
  5. 一、Variable Object也叫做Activation Object / 活动对象(因为有一些差异存在,所以规范中重新取一个名字以示区别,Global Code/Eval Code中叫Variable ObjectFunction Code中就叫做Activation Object)。<br />二、每次进入函数执行
  6. 1. 创建一个新的活动对象
  7. 1. 然后创建一个arguments对象并设置为活动对象的属性
  8. 1. 进行变量实例化处理。
  9. 1. 在退出函数时,活动对象会被丢弃(并不是内存释放,只是可以被垃圾回收了)。
  10. <a name="x0232"></a>
  11. #### arguments对象的属性
  12. 一、arguments对象的属性:lengthcallee、参数列表
  13. - **length: 为实际传入参数的个数**
  14. 一、注意,参考函数对象创建过程,函数对象上的length为函数定义时要求的参数个数;
  15. - **callee: 为执行的函数对象本身**
  16. 一、目的是使函数对象能够引用自己,例如需要递归调用的地方。<br />1function fnName(...) { ... }这样定义函数,它的递归调用可以在函数体内使用fnName完成。<br />2var fn=function(...) { ... }这样定义匿名函数,在函数体内无法使用名字引用自己,通过arguments.callee就可以引用自己而实现递归调用。
  17. - **参数列表: 调用者实际传入的参数列表**
  18. 一、这个参数列表提供一个使用索引访问实际参数的方法。<br />1、变量实例化处理时会在活动对象上添加属性,前提是函数声明时有指定参数列表。<br />2、如果函数声明中不给出参数列表,或者实际调用参数个数与声明时的不一样,可以通过arguments访问各个参数。<br />二、arguments中的参数列表与活动对象上的参数属性引用的是相同的参数对象(如果修改,在两处都会反映出来)。<br />三、规范并不要求arguments是一个数组对象
  19. | 【示例】```javascript
  20. // Passed in FF2.0, IE7, Opera9.25, Safari3.0.4
  21. var argumentsLike = { 0: "aaa", 1: 222, 2: "WWW", length: 3, callee: function() { } };
  22. document.write(argumentsLike[2] + "<br />"); // WWW
  23. document.write(argumentsLike[1] + "<br />"); // 222
  24. //convert the argumentsLike to an Array object, just as we can do this for the arguments property
  25. var arr = [].slice.apply(argumentsLike);
  26. document.write(arr instanceof Array); // true
  27. document.write("<br />");
  28. document.write(arr.reverse().join("|")); // WWW|222|aaa

| | —- |

Eval Code

一、Variable Object就是调用eval时当前执行上下文中的Variable Object。

  • 在Global Code中调用eval函数,它的Variable Object就是Global Object;
  • 在函数中调用eval,它的Variable Object就是函数的Activation Object。 | 【示例】javascript // Passed in FF2.0, IE7, Opera9.25, Safari3.0.4 function fn(arg){ var innerVar = "variable in function"; eval(' \ var evalVar = "variable in eval"; \ document.write(arg + "<br />"); \ document.write(innerVar + "<br />"); \ '); document.write(evalVar); } fn("arguments for function"); 输出结果是:
    arguments for function
    variable in function
    variable in eval
    说明: eval调用中可以访问函数fn的参数、局部变量;在eval中定义的局部变量在函数fn中也可以访问,因为它们的Varible Object是同一个对象。 | | —- |

Scope/Scope Chain / 作用域链

一、首先作用域链是一个类似链表/堆栈的结构,里面每个元素基本都是Variable Object/Activation Object。
二、其次存在执行上下文的地方都有当前作用域链,可以理解为作用域链就是执行上下文的具体表现形式。

Global Code

一、作用域链只包含一个对象,即Global Object。在开始JavaScript代码的执行之前,引擎会创建好这个作用域链结构。

Function Code

一、函数对象在内部都有一个[[Scope]]属性,用来记录该函数所处位置的作用域链。

  1. 创建函数对象时,引擎会将当前执行环境的作用域链传给Function的[[Construct]]方法。[[Construct]]会创建一个新的作用域链,内容与传入的作用域链完全一样,并赋给被创建函数的内部[[Scope]]属性。
  2. 进入函数调用时,也会创建一个新的作用域链,包括同一个函数的递归调用
    1. 新建的作用域链第一个对象是活动对象,接下来的内容与内部[[Scope]]上存储的作用域链内容完全一样。
  3. 退出函数时这个作用域链被丢弃。

    Eval Code

    一、进入Eval Code执行时会创建一个新的作用域链,内容与当前执行上下文的作用域链完全一样。

| 【示例】必须结合JS代码的执行、变量实例化的细节处理,才能理解上面这些如何产生作用,下面用一个简单的场景来综合说明。假设下面是一段JavaScript的Global Code:```javascript var outerVar1=”variable in global code”; function fn1(arg1, arg2){ var innerVar1=”variable in function code”; function fn2() { return outerVar1+” - “+innerVar1+” - “+” - “+(arg1 + arg2); } return fn2(); } var outerVar2=fn1(10, 20);

  1. 一、执行处理过程大致如下:<br />1. 初始化Global Objectwindow对象,Variable Objectwindow对象本身。创建Scope Chain对象,假设为scope_1,其中只包含window对象。<br />1. 扫描JS源代码(读入源代码、可能有词法语法分析过程),从结果中可以得到定义的变量名、函数对象。按照扫描顺序: <br /> 1. 发现变量outerVar1,在window对象上添加outerVar1属性,值为undefined;<br /> 1. 发现函数fn1的定义,使用这个定义创建函数对象,传给创建过程的Scope Chainscope_1。将结果添加到window的属性中,名字为fn1,值为返回的函数对象。注意fn1的内部[[Scope]]就是scope_1。另外注意,创建过程并不会对函数体中的JS代码做特殊处理,可以理解为只是将函数体JS代码的扫描结果保存在函数对象的内部属性上,在函数执行时再做进一步处理。这对理解Function Code,尤其是嵌套函数定义中的Variable Instantiation很关键;<br /> 1. 发现变量outerVar2,在window对象上添加outerVar2属性,值为undefined;<br />3. 执行outerVar1赋值语句,赋值为"variable in global code"。<br />3. 执行函数fn1,得到返回值:<br /> 1. 创建Activation Object,假设为activation_1;创建一个新的Scope Chain,假设为scope_2scope_2中第一个对象为activation_1,第二个对象为window对象(取自fn1的[[Scope]],即scope_1中的内容);<br /> 1. 处理参数列表。在activation_1上设置属性arg1arg2,值分别为1020。创建arguments对象并进行设置,将arguments设置为activation_1的属性;<br /> 1. fn1的函数体执行类似步骤2的处理过程:<br /> 1. 发现变量innerVar1,在activation_1对象上添加innerVar1属性,值为undefine;<br /> 1. 发现函数fn2的定义,使用这个定义创建函数对象,传给创建过程的Scope Chainscope_2(函数fn1Scope Chain为当前执行上下文的内容)。将结果添加到activation_1的属性中,名字为fn2,值为返回的函数对象。注意fn2的内部[[Scope]]就是scope_2;<br /> 4. 执行innerVar1赋值语句,赋值为"variable in function code"。<br /> 4. 执行fn2:<br /> 1. 创建Activation Object,假设为activation_2;创建一个新的Scope Chain,假设为scope_3scope_3中第一个对象为activation_2,接下来的对象依次为activation_1window对象(取自fn2的[[Scope]],即scope_2);<br /> 1. 处理参数列表。因为fn2没有参数,所以只用创建arguments对象并设置为activation_2的属性。<br /> 1. fn2的函数体执行类似步骤2的处理过程,没有发现变量定义和函数声明。<br /> 1. 执行函数体。对任何一个变量引用,从scope_3上进行搜索,这个示例中,outerVar1将在window上找到;innerVar1arg1arg2将在activation_1上找到。<br /> 1. 丢弃scope_3activation_2(指它们可以被垃圾回收了)。<br /> 1. 返回fn2的返回值。<br /> 6. 丢弃activation_1scope_2。<br /> 6. 返回结果。<br /> 6. 将结果赋值给outerVar2。<br />
  2. 二、其它情况下Scope ChainVariable Instantiation处理类似上面的过程进行分析就行了。 |
  3. | --- |
  4. | 【示例】解释下面这个测试代码的结果:```javascript
  5. //Passed in FF2.0, IE7, Opera9.25, Safari3.0.4
  6. function fn(obj){
  7. return {
  8. //test whether exists a local variable "outerVar" on obj
  9. exists: Object.prototype.hasOwnProperty.call(obj, "outerVar"),
  10. //test the value of the variable "outerVar"
  11. value: obj.outerVar
  12. };
  13. }
  14. var result1 = fn(window);
  15. var outerVar = "WWW";
  16. var result2 = fn(window);
  17. document.write(result1.exists + " " + result1.value); //result: true undefined
  18. document.write("<br />");
  19. document.write(result2.exists + " " + result2.value); //result: true WWW

1、result1调用的地方,outerVar声明和赋值的语句还没有被执行,但是测试结果window对象已经拥有一个本地属性outerVar,其值为undefined。result2的地方outerVar已经赋值,所以window.outerVar的值已经有了。
2、实际使用中不要出现这种先使用,后定义的情况,否则某些情况下会有问题,因为会涉及到一些规范中没有提及,不同厂商实现方式上不一致的地方。 | | —- |

一些特殊处理

一、 with(obj) { … }这个语法的实现方式,是在当前的作用域链最前面位置插入obj这个对象,这样就会先在obj上搜索是否有相应名字的属性存在。其它类似的还有catch语句。
二、前面对arguments对象的详细说明中,提到了对函数递归调用的支持问题,了解到了匿名函数使用arguments.callee来实现引用自己,而命名函数可以在函数体内引用自己,根据上面作用域链的工作原理我们还无法解释这个现象,因为这里有个特殊处理。
1、任何时候创建一个命名函数对象时,JavaScript引擎会在当前执行上下文作用域链的最前面插入一个对象,这个对象使用new Object()方式创建,并将这个作用域链传给Function的构造函数[[Construct]],最终创建出来的函数对象内部[[Scope]]上将包含这个object对象。
2、创建过程返回之后,JavaScript引擎在object上添加一个属性,名字为函数名,值为返回的函数对象,然后从当前执行上下文的作用域链中移除它。
3、这样函数对象的作用域链中第一个对象就是对自己的引用,而移除操作则确保了对函数对象创建处作用域链的恢复。

this关键字处理

一、执行上下文包含的另一个概念是this关键字。
见this:https://www.yuque.com/tqpuuk/yrrefz/gcb2wp