我不知道的js-作用域

184 阅读5分钟
原文链接: qxfuture.com

作用域

理解作用域

1.引擎 从头到尾负责整个 JavaScript 程序的编译及执行过程
2.编译器 引擎的好朋友之一,负责语法分析及代码生成等脏活累活
3.作用域 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查 询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

分析 var a = 2;

引擎会做些什么

引擎会把 var a = 2 分解

  1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。

如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个 异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如 果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。

编译器有话说

编译器在上面第二步中生成了代码,引擎执行它时,会通过查找变量 a 来判断它是否已声明过。
查找的过程由作用域进行协助,但引擎执行怎样的查找,会影响最终的查找结果
例子中,引擎会为变量 a 进行 LHS(Left-hand Side) 查询。另外一个查找的类型叫作 RHS(retrieve his source value,取到它的源值)。
当变量出现在赋值操作的左侧时进行 LHS 查询,出现在右侧时进行 RHS 查询
LHS 和 RHS 的含义是“赋值操作的左侧或右侧”并不一定意味着就是 “= 赋值操作符的左侧或右侧”
赋值操作还有其他几种形式,因此在概念上最 好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”
例子:

function foo(a) { 
    var b = a;
    return a + b; 
}
var c = foo( 2 );

  1. 找出所有的LHS查询(这里有3处!)
    c = ..;a = 2(隐式变量分配)、b = ..
  2. 找出所有的RHS查询(这里有4处!) foo(2..= a;a .... b

小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对
变量进行赋值,就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。
不成功的 RHS 引用会导致抛出ReferenceError异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError 异常(严格模式下)

作用域的两种模式

词法作用域

词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。

动态作用域

动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
如果 JavaScript 具有动态作用域,理论上,下面代码中的 foo() 在执行时将会输出 3。

function foo() {
    console.log( a ); // 3(不是 2 !)
}
function bar() { 
    var a = 3;
    foo(); 
}
var a = 2; bar();

小结

词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定 的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

欺骗(修改)词法作用域

eval

JavaScript 中的 eval(..) 函数可以接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码。

eval例子

function foo(str, a) { 
    eval( str ); // 欺骗! 
    console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

val(..) 调用中的 “var b = 3;” 这段代码会被当作本来就在那里一样来处理。由于那段代 码声明了一个新的变量 b,因此它对已经存在的 foo(..) 的词法作用域进行了修改。事实上,这段代码实际上在 foo(..) 内部创建了一个变量 b,并遮蔽 了外部(全局)作用域中的同名变量。

with作用域

什么是with

with 通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象 本身。

with怎么用

var obj = { a: 1,b: 2,c: 3 };
// 单调乏味的重复 "obj" 
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式 
with (obj) {
    a = 3;
    b = 4;
    c = 5;
}

with应该注意的地方

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 被泄漏到全局作用域上了!

当我们传递 o1 给 with 时,with 所声明的作用域是 o1,而这个作用域中含 有一个同 o1.a 属性相符的标识符。但当我们将 o2 作为作用域时,其中并没有 a 标识符, 因此进行了正常的 LHS标识符查找, o2 的 with 最后都没找到a这个标识符,当执行a = 2非严格模式下回自动创建这个全局变量

参考文献

你不知道的js