执行环境是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有序访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。PS:无法通过js代码去访问这个对象。
函数执行时,具体发生了什么?
每个函数都有自己的执行环境。当函数被执行时,函数的环境会被推入执行栈中。
1。创建活动对象,活动对象包含arguments、函数体内显示声明的变量和函数。在函数上下文中,变量对象(Variable Object)被表示为活动对象(Activation Object);
2。创建与之关联的作用域链。作用域链的用途是保证对执行环境有权访问所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。作用域链的下一个变量对象是来自包含(外部)环境,以此类推,一直延续到全局执行环境。(在创建函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的 [[ Scope ]]属性中)
完成以上步骤后开始执行函数。在函数执行完成之后,执行栈将环境弹出,把控制器返回给之前的执行环境。ECMAScript程序中的执行流正是由这个方便的机制控制着。
例子:
var nm = 'window';
function foo(){
console.log(nm);//'window'; 找到全局中的nm变量
var nm = 'foo';
console.log(nm);//'foo'; 局部变量已经定义过nm变量;直接读取局部变量nm的值
}
词法作用域
词法作用域完全由写代码期间函数所声明的位置来定义;
例子
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1;
执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是value 等于 1,所以结果会打印 1。
假设JavaScript采用动态作用域执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,结果就会打印 2。
然而JavaScript采用的是静态作用域,所以这个例子的结果是 1
“欺骗”词法作用域
eval()
JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
在执行 eval(..) 之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改。
function foo(str) {
eval( str );
console.log( b );
}
var b = 2;
foo( "var b = 3;" ); // 输出 3
由于eval(str)中又声明了一个新的变量 b;所以输出了3
默认情况下,如果 eval(..) 中所执行的代码包含有一个或多个声明(无论是变量还是函数),就会对 eval(..) 所处的词法作用域进行修改。在严格模式的程序中, eval(..) 在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。
with()
with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
var obj = {
a: 1,
b: 2
};
// 重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
在 with 块内部,我们写的代码看起来只是对变量 a 进行简单的词法引用,实际上就是一个LHS 引用。LHS在查找目标变量时,如果找到则返回目标变量,如果没有就在全局环境中定义一个(非严格模式);存在如下隐患
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——a 被泄漏到全局作用域上了!
不推荐使用 eval(..) 和 with 的原因是会被严格模式所影响;
所带来的性能影响:
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。在代码解析时 遇到eval(..) 或 with完全不做任何优化。因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码。