首先,作用域是一套规则,让引擎可以在作用域和嵌套的子作用域中根据标示符查找变量。

作用域模型一般分为两种,词法作用域和动态作用域。javascript作用域使用的类型是词法作用域。

词法作用域就是在词法阶段的定义作用域的位置。即,在编写代码时变量和块作用域写在哪里,词法分析器在处理代码时会保持作用域在位置不变(大部分情况是这样的)。

变量查找会在使用的作用域中进行查找,如果未找到向上级作用域继续查找,直到最外作用域(全局作用域)停止。因此,在多级嵌套的作用域中定义多个同名标示符,会产生“遮蔽效应”(找到第一个标示符即停止)。如果要查找的同名标示符在全局作用域中,可以在内部直接通过

window.a

的方式绕过“遮蔽效应”,但同名标示符在非全局作用域中被遮蔽,则永远无法被访问。

函数的作用域,无论该函数在哪被调用或如何被调用,它的作用域的位置都由函数被声明时所在的位置决定。

词法作用域的查询,永远只查询一级标示符,如a,b,c。二级标示符如:foo.a, foo.b引擎只查找foo,找到foo后,会由属性访问规则访问.a , .b属性。

有哪些方法可以动态修改词法作用域呢?看下代码

function foo(a, b){
   eval(a);
   console.log(b , c);// 1, 3
}
var c = 2;
foo('var c = 3', 1);

eval(…)会将字符串转换成语句运行,所以在foo中引擎找到c即停止查询,遮蔽了全局变量c。

在严格模式中eval会有自己的词法作用域并不会修改原有作用域,如下:

function foo(a, b){
   "use strict";
   eval(a);
   console.log(b , c);//1, 2
}
var c = 2;
foo('var c = 3', 1);

类型eval的方法还有setTimeout和setInterval,第一个参数可以是字符串进行运行。还有new Function(……)方式,最后一个参数是字符串语句,这种方式比eval好一些。但改变作用域都会对性能造成影响,所以这些都需要避免使用,应该保持原有的词法作用域。

另一个方法是with,先看下with的作用。

var obj = {
   a:1,
   b:2,
   c:3
}
//重复引用obj
obj.a = 2;
obj.b = 3;
obj.c = 4;
//简单引用obj
with (obj){
   a = 3;
   b = 4;
   c = 5;
}

本质是方便引用,其实有副作用,如下代码:

function foo(obj){
   with (obj){
      a = 2;
   }
}
var obj1 = {
   a:3
}
var obj2 = {
   b:3
}
foo(obj1);
console.log(obj1.a);//2
foo(obj2);
console.log(obj2.a);//undefined
console.log(a);//2 a被泄露到全局作用域中了!

在调用foo(obj2)方法时obj2中没有属性a,之后进行了LHS查询,到全局作用域中仍未找到,根据LHS查询规则自动在全局作用域中创建标示符。

不推荐使用eval和with的另一个原因是,他们都受到严格模式的影响(with在严格模式下是被禁用的)。