接着上一篇文章JavaScript闭包之自执行函数和匿名函数,今天我们继续来说说闭包的前置知识–作用域和词法作用域。


作用域

作用域是编程语言中的一个重要概念,目前大部分编程语言都有作用域这个概念。JavaScript当然也不例外。那么什么是作用域呢?

下面我们就先来说说JavaScript的作用域
简单的来说,作用域就是定义的一组用来查找变量的规则,那么JavaScript是如何来查找变量的呢?
我们先来看个例子:

1
2
3
4
5
function foo () {
var a = 1;
console.log(a) // 1
}
foo()

在执行foo()的时候,JavaScript引擎去查找定义a的地方,诶,在上方就是的,于是不继续找了直接打印。
再来看一个例子:

1
2
3
4
5
var a = 1;
function foo () {
console.log(a) // 1
}
foo()

在执行foo()的时候,JavaScript引擎试图在函数内部查找a的值,但是遗憾的是没有找到,JavaScript引擎这个时候就很聪明了,在里面找不到我就不能去外面找嘛,诶,还真让我找到了,在函数外面的第一层里面就有一个a值,找到了我就不继续找了,直接去打印吧。
注意上面的两段代码,它们之间一个共同点就是查找变量,第一段代码在函数内部直接就查找到了,第二段代码是在函数外部查找到的。
把上面这段话正式一点的说就是:第一段代码是在函数作用域查找到了变量a,第二段代码是在全局作用域查找到了变量a。所以作用域就是查找变量的地方。
至于第二段代码查找变量的过程,有一个由里向外的过程,好像是顺着一条链条从下到上查找,这条链条我们就称之为作用域链,关于作用域链我们后面会详细讲解。

静态作用域和动态作用域

先来看看维基百科给静态作用域和动态作用域的定义:

静态作用域又叫做词法作用域,采用词法作用域的变量叫词法变量。函数的作用域在函数定义的时候就决定了。
采用动态作用域的变量叫做动态变量。函数的作用域是在函数调用的时候才决定的。

让我们来看个例子就能明白它们两个的区别了。

1
2
3
4
5
6
7
8
9
10
11
var value = 1;
function foo () {
console.log(value)
}

function bar () {
var value = 2;
foo()
}

bar()

我们用假设法来分析一下这个函数的执行,假设JavaScript采用的是静态作用域,那么函数的作用域在函数定义的时候就已经确定了,当执行foo()函数的时候,先在foo()函数内部查找是否有变量value,如果没有就到全局作用域中查找,那么此时打印出来的就应该是1。
而如果JavaScript采用的是动态作用域,函数的作用域是在函数调用的时候决定的,那么当执行函数foo()的时候,先从foo()函数内部查找是否有变量value,如果没有就去函数调用的上一级作用域中查找,一级级的查找,直到全局作用域,此时函数就应该打印2。
让我们执行函数

通过函数的执行结果,和我们分析的第一段结果一样,所以JavaScript采用的是静态作用域,也就是词法作用域。

变量的查找类型

在JavaScript中,变量的查找有两种类型–LHS和RHS。
LHS,RHS 这两个术语就是出现在引擎对变量进行查询的时候。在《你不知道的Javascript(上)》也有很清楚的描述。在这里,我想引用freecodecamp 上面的回答来解释:

LHS = 变量赋值或写入内存。想象为将文本文件保存到硬盘中。
RHS = 变量查找或从内存中读取。想象为从硬盘打开文本文件。

两者的特性:

  • 都会在所有作用域中查询
  • 严格模式下,找不到所需的变量时,引擎都会抛出ReferenceError异常。
  • 非严格模式下,LHR稍微比较特殊: 会自动创建一个全局变量。
  • 查询成功时,如果对变量的值进行不合理的操作,比如:对一个非函数类型的值进行函数调用,引擎会抛出TypeError异常。

我们来看一个简单的例子:

1
2
3
4
5
6
7
function foo () {
console.log(a); // 1
console.log(b); // undefined
}
var a = 1;
foo();
var b = 1;

这段代码在预编译的时候存在一个变量提升的过程,这个以后会详细讲解,这里并不妨碍我们理解LHS和RHS。
预编译后:

1
2
3
4
5
6
7
8
9
var a;
var b;
function foo () {
console.log(a); // 1
console.log(b); // undefined
}
a = 1;
foo();
b = 1;

我们直接来看查找过程,在查找变量a和变量b时用的是RHS(读取内存),而查找a的时候a已经被赋值a = 1,这步操作也就是LHS(写入内存),而b此时只是被定义了还没有被赋值,所以查找到的只是undefined。


本文介绍了JavaScript的词法作用域以及两种变量查找方式,下一篇文章我们将重点介绍下JavaScript中的提升。

本文完