从ECMAScript规范深度分析JavaScript(一):执行期上下文

版权声明:转载需注明出处 https://blog.csdn.net/zf2014122891/article/details/85861486

本文译自Dmitry Soshnikov的《ECMA-262-3 in detail》系列教程。其中会加入一些个人见解以及配图举例等等,来帮助读者更好的理解JavaScript。

前言

谈到javascript不得不说执行期上下文——Execution context,执行期上下文是学习javascript必须要理解的一项内容,我们在这个系列的开始将首先来理解ECMAScript的执行期上下文以及它和可执行代码的区分。

执行期上下文

我们知道代码执行需要有控制权,每次当控制器转到ECMAScript可执行代码的时候,就会进入到一个执行上下文,其中Dmitry Soshnikov提到“可执行代码的概念与抽象的执行上下文的概念是相对的。在某些时刻,可执行代码与执行上下文是等价的。”,我不认同这句话,可执行代码就是代码,永远不可能和执行上下文等价,我们很容易从语言层面上将这两个概念区分开来。

ECMAScript使用Execution context(EC)就是为了和可执行代码(executable code)的概念区分开来。刚才我们也讲到了ECMAScript只是一个标准,他并没有从技术实现的角度去规定EC的具体结构和类型,这是ECMAScript引擎实现ECMA标准时需要考虑的问题。

从逻辑上讲,活动的执行期上下文会形成一个先进后出的栈结构,这个栈的底部总是全局上下文(global context),栈顶部是当前执行期上下文。栈在各个执行期上下文进出栈的情况下被修改。

理解执行期上下文栈

这里我们效仿Dmitry Soshnikov,可以定义一个数组来模拟执行上下文堆栈:
伪代码示例:

ECStack = [];

每次进入函数 (即使函数被递归或作为构造函数调用) 的时候或者内置的eval函数工作的时候,这个堆栈都会被推入。我们接下来详解各种情况:

1、全局代码

这类代码在程序级别处理,比如加载外部的js文件或者本地的标签中的内部代码,全局代码不包括任何函数体内的部分。
在初始化(也就是程序开始)时,ECStack是这个样子的:

ECStack = [
	globalContext
];

2、函数代码

当进入函数代码(所有类型的函数)时,ECStack会被推入新元素。要注意的是,具体的函数代码不包括内部函数(inner functions)代码。
如下所示,我们使函数自己调自己的方式递归一次:

(function foo(flag) {
  if (flag) {
    return;
  }
  foo(true);
})(false);

然后,ECStack的变化如下(ES6的情况有所不同,涉及尾调用的概念,我们本系列不涉及ES6的范围):

// 第一次foo执行, ECStack.push(<foo> functionContext)
ECStack = [
  <foo> functionContext
  globalContext
];
  
// 递归调用foo时,ECStack.push(<foo> functionContext – recursively)
ECStack = [
  <foo> functionContext – recursively 
  <foo> functionContext
  globalContext
];

函数的每个返回都退出当前执行上下文,ECStack相应地弹出——连续地、倒过来地弹出——这是堆栈的很自然的实现。完成此代码的工作后,ECStack再次只包含globalContext——直到程序结束,ECStack变化如下:

// 递归调用返回时,ECStack.pop()
ECStack = [
  <foo> functionContext
  globalContext
];
// 立即执行函数返回时,ECStack.pop()
ECStack = [
  globalContext
];

其中要注意,抛出但未捕获的异常也可以退出一个或多个执行上下文:

(function foo() {
  (function bar() {
    throw 'Exit from bar and foo contexts';
  })();
})();

具体的ECStack的状态和上面一样,我就不再赘述一遍了。

3、eval代码

eval代码有些比较有意思的东西,在这里有一个调用上下文(calling context)的概念——调用eval代函数的上下文。
eval的行为会影响调用上下文,比如变量或者函数的定义:

// 这里会影响全局上下文
eval('var x = 10');
 
(function foo() {
  // 这里,变量y是foo函数局部创建的变量
  // 影响的是foo函数的上下文
  eval('var y = 20');
})();
 
console.log(x); // 10
console.log(y); // "y" is not defined

相应的ECStack的变化:

ECStack = [
  globalContext
];
  
// eval('var x = 10');
ECStack.push({
  context: evalContext,
  callingContext: globalContext
});
 
// eval返回
ECStack.pop();
 
// 调用foo函数
ECStack.push(<foo> functionContext);
 
// eval('var y = 20');
ECStack.push({
  context: evalContext,
  callingContext: <foo> functionContext
});
 
// eval调用返回
ECStack.pop();
 
// foo函数返回
ECStack.pop();

你会发现这是一个很普通的逻辑栈调用。

注意:在ES5的严格模式(strict-code)中,eval已经不再影响调用它的上下文了,而是在局部沙箱( local sandbox)中计算执行代码,在这里我们对此先不作讨论。

这里有个更有趣的事情(大家不需要关注这个点,已经作为bug被修复了):
在老版本(1.7.0以下)的SpiderMonkey(Firefox的引擎)的实现中,可以把调用上下文作为第二个参数传递给eval。那么,如果这个上下文存在,就有可能影响“私有”变量。

function foo() {
  var x = 1;
  return function () { alert(x); };
};
var bar = foo();
 
bar(); // 1
eval('x = 2', bar); // pass context, influence internal var "x"
bar(); // 2

然而,由于安全性问题,这在现代的引擎中已经被修复,不再具有意义了。

总结

这里的理论是对将来对执行期上下文做详细分析的基础,比如变量对象(variable object)和作用域链,所以希望大家能深入了解这里的内容。

希望此文能够解决大家工作和学习中的一些疑问,避免不必要的时间浪费,有不严谨的地方,也请大家批评指正,共同进步!
转载请注明出处,谢谢!

交流方式:QQ1670765991

猜你喜欢

转载自blog.csdn.net/zf2014122891/article/details/85861486