关于JavaScript作用域的一些理解

这其实是《You Don’t Know JS》的读书笔记,加上一点个人的理解。

首先需要明确的是,JavaScript里的作用域是词法作用域,也是函数作用域

在解释词法作用域的概念之前,首先需要明确的是,JavaScript虽然实际上依赖于解释执行,但是会有一个编译的阶段,这一点与Java是非常接近的(编译+解释执行,只不过Java生成可以在多平台移植的.class文件,而JavaScript由于平台限制,一般是编译后立即解释执行)。而编译的过程,可以大致分为三个阶段:

  1. 词法分析:将语句拆成一个个对编程语言来说有意义的小单元(即词法单元)。比如,对于下面这个语句:
var a = 2;

就会被拆成“var,a,=,2,;”五个单元,空格是不是语法单元取决于语言本身的要求。

  1. 语法分析:将词法单元的集合转换成程序的语法结构。
  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。

猜你喜欢

转载自blog.csdn.net/HermitSun/article/details/86615290