JavaScript高级编程中关于执行环境与作用域的问题在第四章有过提及,但是交代的不是很明确,因此查阅了网上各种资料,对于执行环境以及作用域有了一个初步的认识。

一、什么是执行环境(execution context)
执行环境在书中是这样定义的:执行环境定义了变量或函数有权访问的其他数据,决定了他们各自的行为。
对于execution context的翻译有两种,一种是执行环境,一种是执行上下文。此处使用执行环境的概念。
JavaScript中有以下三种运行环境:
1.全局级的运行环境:默认的代码运行环境,当代码被载入的时候,JavaScript引擎默认进入该环境;
2.函数级的运行环境:执行流进入函数的时候,运行函数中的代码;
3.Eval函数的运行环境:执行Eval()函数内的代码

二、什么是作用域
JavaScript中的作用域分为全局作用域和函数作用域,目前不支持块状作用域。JavaScript的作用是静态作用域也叫词法作用域,意思就是作用域是静态的确定的。有一些说法经常将作用域与执行环境等同起来,个人的理解是这是两个不同层次的概念,虽然两者在某些方面的描述着实有些相似,但是并不能等同起来讲。作用域不光是针对JavaScript这门语言来说的,在其他语言中也有这种概念。而执行环境是JavaScript解释器在解释执行的过程中产生的这种概念。因此个人认为不能等同来讲。

三、什么是执行环境栈
浏览器的JavaScript引擎是单线程,因此在某一个时刻只能有一个事件被激活处理,因此便产生了这样一个执行环境栈。JavaScript的代码被载入到后,JavaScript引擎默认进入全局执行环境,当在全局执行环境中调用一个函数的时候,程序的执行流会进入该函数,同时创建该函数的执行环境,然后将该函数的执行环境压入执行环境栈的顶部。浏览器引擎在执行的时候总是在执行环境栈顶部的执行环境中执行。当顶部执行环境中的代码被执行完毕后,该执行环境被从环境栈顶部弹出,然后在其下的执行环境中执行。这样执行环境经历一个不断压栈,弹栈的过程,直到最底部的全局执行环境。

四、执行环境的创建过程
由上面的描述我们知道,执行环境是一个动态的概念,每当调用一个函数的时候,就会该函数创建一个执行环境。在JavaScript引擎内部这个过程分为两步:

  1. 创建阶段
    建立变量、函数、arguments对象、参数
    建立作用域链
    确定this值

  2. 执行阶段
    变量赋值、函数引用、执行其他代码

可以把执行环境想象成一个对象,里面包含了变量对象(函数中的arguments对象,参数,内部的变量以及函数声明),作用域链(当前执行环境的变量对象加上父执行环境的变量对象)以及this的值。

五、执行环境对象创建过程详解
根据上面的描述,执行环境对象的创建发生在函数被调用后,真正的函数体被执行以前。在这个过程中JavaScript引擎会检查函数的参数,变量的声明以及内部的函数,根据获得这些信息创建执行环境对象。这个过程中变量对象(variable object)、作用域链(scope chain)以及this所指定的对象都会被确定。创建过程的具体步骤如下:

  • 找到当前执行环境中被调用函数的代码
  • 被调用函数的函数体执行以前,创建被调用函数的执行环境
  • 进入建立阶段

    创建执行环境对象:

    建立arguments对象、检查当前执行环境中的参数、建立该对象的属性及属性值

    检查当前执行环境中的函数声明:

    每找到一个函数声明,就在该执行环境对象下用该函数名建立一个属性,属性值就是一个执行该函数在内存中的一个地址的引用。(这就是一个变量提升的过程)如果上述属性值已经在该执行环境对象中存在,就用新的引用值覆盖之前的值(这就是为什么在JavaScript中不存在函数重载的原因)

    检查当前执行环境中的变量声明:

    每找到一个变量声明,就在该执行环境对象下用该变量名建立一个属性,属性值就是undefined。如果该属性值已经存在的话,直接跳过(这就是为什么同名的变量无法覆盖同名的函数声明的原因),原属性值不会被修改

    初始化作用域链

    确定执行环境中this的指向

  • 代码执行阶段

    执行函数体中的代码,给执行环境对象中的属性赋值。

六、实例解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script type="text/javascript">
(function(){
console.log('execution context test');
console.log(typeof foo);//function
console.log(typeof a);//undefined
console.log(typeof b);//undefined
console.log(typeof c);//undefined
function foo(){
console.log('foo function');
}
var foo='test';
var a='a';
var b='b';
var c=function(){
console.log('c function');
};

})();

</script>

如上面程序实例所示,创建了一个自执行函数,代码被加载后且代码未被执行前自动创建一个执行环境的对象,并且分析执行环境的参数,然后查找对应的函数声明以及变量声明。这就是为什么在打印foo的类型的时候是function的原因,解析变量声明会被自动赋值为undefind,c为一个函数表达式,因此也被解析为变量,赋值为undefind。在有foo函数声明的前提下,代码中又声明了一个foo的变量,这种情况下在解析变量声明的过程中,会发现在执行环境对象中有了一个foo的属性,因此直接跳过。


参考文章:深入理解Javascript之执行上下文(Execution Context)
JavaScript高级编程第四章