这其实是《You Don’t Know JS》的读书笔记,加上一点个人的理解。
首先需要明确的是,JavaScript里的作用域是词法作用域,也是函数作用域。
在解释词法作用域的概念之前,首先需要明确的是,JavaScript虽然实际上依赖于解释执行,但是会有一个编译的阶段,这一点与Java是非常接近的(编译+解释执行,只不过Java生成可以在多平台移植的.class文件,而JavaScript由于平台限制,一般是编译后立即解释执行)。而编译的过程,可以大致分为三个阶段:
- 词法分析:将语句拆成一个个对编程语言来说有意义的小单元(即词法单元)。比如,对于下面这个语句:
var a = 2;
就会被拆成“var,a,=,2,;”五个单元,空格是不是语法单元取决于语言本身的要求。
- 语法分析:将词法单元的集合转换成程序的语法结构。
- 代码生成
所以,何为词法作用域?词法作用域是定义在词法阶段的作用域,也就是作用域由一开始写在哪里来决定。这种作用域关注的是函数在何处声明。之所以又是函数作用域,是因为在函数内声明的所有变量在函数体内始终是可见的。
由此,就引出了下面这个例子(当然这个例子中用到的东西不值得学习,因为在严格模式下with会被禁用):
function bar() {
var a = 0;
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
}
var a = 1;
bar();
console.log(a);//1
看明白了吗?如果没有完全明白,我来给出一个解释。不过,在解释这个之前,可能还需要对JavaScript的LHS、RHS查询有所了解;这两种查询方式会直接影响到赋值的行为。
LHS(Left-hand Side)查询和RHS(Right-hand Side)查询,通常是指变量出现在赋值操作的左侧或者右侧时进行的查询。当然,“赋值操作的左侧和右侧”并不一定就是”=”的左侧和右侧,因为赋值操作还有其他几种形式,因此在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。
我个人觉得,赋值操作可能还存在的形式有:函数调用(会隐式地给实参赋值)、声明具名函数(会给函数名赋值),等等。我觉得具名函数的声明过程其实是把一个匿名函数赋值给一个变量,也就是说,这两种表达方式是等价的(因为声明同名的变量和具名函数会出现重复声明):
function foo() {
//doSomething
}
var foo = function() {
//doSomething
}
看几个例子好了:
console.log(a);
这里对a的引用是一个RHS引用,因为这里a并没有赋予任何值,我们只是想查找并取得a的值,然后将它打印出来。
a = 2;
这里对a的引用是一个LHS引用,因为我们并不关心当前的值是什么,只是想要为赋值操作找到目标。
function foo(a) {
console.log(a);
}
foo(2);
console的举动是很显然的RHS查询,但这里有一个隐式的LHS查询:a=2。因为在调用函数的时候,2会被分配给参数a。同时有一个细节要强调一下,词法作用域的查找只会查找一级标识符,比如在对foo.bar.abc的调用过程中,只会查找foo,而后续的访问会由对象接管。
除此之外,LHS和RHS在当前作用域中无法查询到某个变量时,会在外层作用域中查找,直到找到目标或者抵达全局作用域为止。这叫做作用域链。这个嵌套关系如下图,1,2,3分别是三层作用域:
要特别注意的是,每一层的作用域包含该函数的形参。
还有一个很有意思的地方,就是异常:
不成功的RHS查询会导致抛出 ReferenceError
,而不成功的LHS查询则会导致自动隐式地创建一个全局变量,来源是作用域链(非严格模式下),该变量使用LHS查询的目标作为标识符;或者抛出 ReferenceError
(严格模式下)。同时,如果RHS查询成功了,但对变量进行的不存在的操作,则会抛出 TypeError
。
而且,结合之前的词法作用域问题,还有一点需要明确:无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。 比如,
function foo() {
console.log(a); //2
}
function bar() {
var a = 3;
}
var a = 2;
foo();
回到之前的问题上吧。
首先,全局定义的a会提升到前面,在函数bar()内部定义的a则会遮蔽外部的a,调用foo()给o1赋值的时候,因为o1有a这个属性,所以会直接赋值;而给o2赋值的时候,因为o2没有a这个属性,所以输出undefined,同时依据作用域链,会向外层作用域寻找a,并试图通过LHS给a赋值。然后,在bar()的作用域里找到了a,就把bar()里的a赋值为2。而全局的a并没有受到影响,所以还是1。