什么是作用域

几乎是所有编程语言的最基本的功能之一,就是能够存储变量的值,并且后续能够对这个值进行访问和修改,这种能力被称之为状态

如果没有状态这个概念,程序的使用会受到很大的限制,虽然没有状态也能执行一些简单的任务。

变量的引入会有几个有意思的问题:

  1. 变量存储在哪里?
  2. 程序如果找到它们?

解决这些问题的方式就是设计一套良好的规则来存储变量,并且之后也可以很方便的找到这些变量,这套规则被称之为作用域

编译原理

JavaScript在运行的时候,如何创建一个变量,如何获取一个变量的值,都是通过作用域来实现的,可以将作用域理解为一个工具,往里面存东西,从里面拿东西等一系列操作。

js的完整运行有三个模块:

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

js的编译原理我们也需要了解一下,这有利于我们后续的学习。

一段js代码在执行前会被浏览器进行编译,有三个步骤:

一、分词/词法分析(Tokenizing/Lexing)

将字符分解成有意义的代码块,这些代码块被称为词法单元,比如:var a = 2;会被拆解成:var、a、=、2 、;;空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。

二、解析/语法分析(Parsing)

这个过程就是将词法单元组成一个程序语法树结构,也就是 抽象语法树(AST);不好理解可以想一下vue的虚拟dom结构树。

三、代码生成

这里的代码生成指的是将AST转为可执行的代码,也就是机器语言。


var a = 2;的真实编译处理,并不是我们日常认为的:为a变量分配一个内存空间,然后将2存储到这个空间里

事实上编译器会分为2步进行处理:

  1. 编译器先询问作用域,在同一个作用域集合中,是否已经存在了一个名为a的变量,如果存在,编译器将忽略var a;如果不存在,会要求作用域在当前作用域集合中声明一个新的变量,并命名为a。
  2. 然后编译器会为引擎生成运行时所需要的代码,这些代码被用来处理a = 2这个赋值操作。引擎会询问作用域,当前作用域集合中是否存在一个叫a的变量,如果是,引擎就会使用这个变量,如果没有,引擎会通过通过作用域链往上查找,如果找到了,则进行赋值操作,如果最终也没找到,那么就会抛出一个ReferenceError异常报错。

我们可以发现,变量的赋值其实是有两个执行动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

作用域的查询方式

其实我们会发现,大部分都在对作用域进行查询操作,这个查询是非常重点的一个东西,查询方式被分为2种方式。

  1. LHS查询
  2. RHS查询

当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧的时候进行RHS查询。

RHS换句话说就是当你的右侧是一个变量时,会使用RHS查询源值。RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。

console.log( a );

这段代码中对a的查询就是一个RHS的操作。

作用域嵌套

作为一个新手期的程序员,也应该是知道作用域是可以嵌套的,而且作用域可以分为:块级作用域、函数作用域、全局作用域。

块级和函数作用域可以再全局作用域下相互嵌套。

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

异常

上面我们讲了作用域查询的两种方式,为什么要区分这两种方式?因为这两种查询行为的不一致会有不同的结果产生。

function foo(a) {
    console.log(a + b);
    b = a;
}
foo(2);

在foo函数中,第一次对b的查询是RHS方式,由于在作用域集合中无法查找到变量b,RHS最终会抛出异常:

ReferenceError: b is not defined

代码不会继续执行。

如果我们注释掉console.log(a + b);,此时对b的查询是LHS的方式,这种方式要区分两种模式:严格模式、正常模式。

在正常模式下,LHS如果在作用域集合中查询不到该变量,会在全局给我们创建一个该变量。

如果是在严格模式下,将会和RHS一样抛出ReferenceError的错误。

这种说法也只有在没有使用var关键词的时候。

而TypeError则表示作用域查询到了,但是对结果的操作是非法或者不合理的,比如一个非函数的变量当做函数来调用。

课外知识:隐式创建全局变量

function fn() {
    var a = b = c = 0; //a是局部变量,b、c是全局变量
    var e = 0;
    f = 0;
    g = 0; //e是局部变量,f、g是全局变量
    var x = 0,
        y = 0,
        z = 0; //x、y、z都是局部变量
}

为什么可以隐式创建,因为LHS的查询方式导致的。

小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。

JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像var a = 2这样的声明会被分解成两个独立的步骤:

  1. 首先,var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
  2. 接下来,a = 2 会查询(LHS 查询)变量 a 并对其进行赋值

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者出 ReferenceError 异常(严格模式下)。

分类: 你不知道的JavaScript 标签: 作用域LHSRHS

评论

暂无评论数据

暂无评论数据

目录