什么是作用域
对于几乎所有编程语言,最基本的功能之一就是能够存储变量的值,并且能在之后对这个值进行访问和修改。这样就会带来几个问题,这些变量存储在哪里?程序在需要的时候又是如何找到它们的?要解决这些问题,就需要引入一套规则来存储变量和访问变量,这套规则就是作用域。
理解作用域
在刚开始接触 JavaScript 这门语言时,肯定会经常接触到 JavaScript 是动态语言, 是解释执行的,但事实上 JavaScript 是一门编译语言。只不过和 Java、C# 这些传统意义上的编译语言不同,JavaScript 的编译过程不是发生在构建之前的。大部分情况下,JavaScript 的编译过程发生在代码执行前的很短时间内。也就是说 JavaScript 代码在执行前都要进行编译。
为了更好地理解作用域,我们需要明确下面几个概念
- 引擎
从头到尾负责整个 JavaScript 程序的编译及执行过程
- 编译器
负责语法分析及代码生成等脏活累活
- 作用域
负责收集并维护由所有声明的标识符(变量) 组成的一系列查询, 并实施一套非常严格的规则, 确定当前执行的代码对这些标识符的访问权限。
下面我们从引擎、编译器和作用域的角度,分析 var a = 2
这条声明语句,看看它们是如何协同完成工作的
- 遇到 var a, 编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。 如果是, 编译器会忽略该声明, 继续进行编译; 否则它会要求作用域在当前作用域的集合中声明一个新的变量, 并命名为a。
- 接下来编译器会为引擎生成运行时所需的代码, 这些代码被用来处理 a = 2 这个赋值操作。 引擎运行时会首先询问作用域, 在当前的作用域集合中是否存在一个叫作 a 的变量。 如果是, 引擎就会使用这个变量; 如果否, 引擎会继续查找该变量。如果引擎最终找到了 a 变量, 就会将 2 赋值给它。 否则引擎就会举手示意并抛出一个异常!
简单来说,变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值,否则就会并抛出一个异常。
作用域嵌套
我们知道引擎查找变量的过程在作用域中进行的,而这个过程通常会涉及多个作用域。
当一个块或函数嵌套在另一个块或函数中时, 就发生了作用域的嵌套。 因此, 在当前作用域中无法找到某个变量时, 引擎就会在外层嵌套的作用域中继续查找, 直到找到该变量,或抵达最外层的作用域(也就是全局作用域) 为止。
为了便于理解,可以将作用域嵌套比喻成一栋高楼,我们从一楼(当前作用域)开始查找,如果没有找到,就会前往上一个楼层继续查找,以此类推。一旦到达顶层(全局作用域),可能找到,也可能没有找到,查找过程都必须停止。
LHS查询和RHS查询
继续上文的示例,引擎在执行编译器生成的代码时,会通过查找变量 a 来判断它是否已经声明过。查找的过程由作用域进行协助, 但是引擎执行怎样的查找, 会影响最终的查找结果。查找过程分为两类:LHS查询和RHS查询。其实很简单,当变量出现在赋值操作的左侧时进行 LHS 查询, 出现在右侧时进行 RHS 查询。
更准确一点的讲,** RHS 查询是查找某个变量的值,而 LHS 查询是查找变量的容器本身,从而可以对其赋值。**如下面的示例:
var a = 2; // a: LHS查询
var b = 3; // b: LHS查询
a = b; //a: LHS查询 b:RHS查询
为什么区分 LHS 和 RHS 是一件重要的事情?
因为在变量还没有声明(在任何作用域中都无法找到该变量) 的情况下, LHS 和 RHS两种查询的行为是不一样的。
1.当引擎执行 RHS 查询时,如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量, 引擎就会抛出 ReferenceError异常。
console.log(a); //ReferenceError: a is not defined
2.当引擎执行 LHS 查询时, 如果在顶层(全局作用域) 中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量, 并将其返还给引擎, 前提是程序运行在非“严格模式” 下。如果在“严格模式”下,引擎也会抛出 ReferenceError异常。
//非严格模式
var a =2;
b = a;
console.log(b); //2
//严格模式
'use strict';
var a =2;
b = a;
console.log(b); //ReferenceError: b is not defined
另外,如果 RHS 查询找到了一个变量, 但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用, 或着引用 null 或 undefined 类型的值中的属性, 那么引擎会抛出另外一种类型的异常, 叫作TypeError。
ReferenceError 代表作用域判别失败相关, 而 TypeError 则代表作用域判别成功了, 但是对结果的操作是非法或不合理的。
参考
《你不知道的JavaScript》