词法作用域
作用域有两种主要工作模式。第一种最为普遍,是被大多数编程语言所采用的词法作用域。另一种叫做动态作用域,仍有少数语言在使用,如Bash脚本语言、Perl中的一些模式等。
词法作用域就是定义在词法阶段的作用域。貌似是句有用的废话。更通俗的说,词法作用域是根据你写代码时的变量或方法等的位置决定的。当然,这不是绝对的,后面还会提到两种破坏词法作用域规则的方式,只是不被广泛接受了,在严格模式下,也是明令禁止的。我们应该要遵循词法作用域的规则来编程。
考虑如下代码:
function foo (a) {
var b = a * 2
function bar (c) {
console.log(a, b, c)
}
bar(b*3)
}
foo(100)
在上述代码中,存在3块作用域:
- 全局作用域:包含foo函数
- 函数foo内部作用域:包含变量a、变量b和bar函数
- 函数bar内部作用域:包含变量c
这3块作用域完全是根据编写代码的顺序或嵌套关系所决定的。
查找
在函数bar内部,console.log输出有3个变量,但在当前作用域内,只有变量c,而变量a和变量b都不存在,这时候会往上一个作用域bar函数内部查找,这时候是存在的。
如果在foo函数内部作用域或全局作用域内都存在变量c,其实log函数内取的变量c的值还是bar内部作用域所声明,这就叫做"遮蔽效应",就是说bar函数内作用域的变量值c会遮蔽掉foo函数内部作用域或全局作用域的相同变量值,说的更浅显一点就是近水楼台先得月。
词法作用域是在编译阶段就确定了的,接下来我们会说eval和with函数,它们骗过了编译阶段的正常词法作用域规则。
块作用域
考虑以下代码:
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i) // 10
}, 300)
}
console.log('i=' + i) // i=10
上述代码,输出了10次数字10,而不是1,2,3...10。如果去掉setTimeout延迟函数,那结果就符合预期。之所以出现这种情况,是因为for循环内变量i被声明到了全局作用域。更通俗的说,就是变量i仅存在一份,又由于是延迟输出,所以打印出的都是最后的结果10.
如何解决?貌似面试中偶尔会出现这种问题。
一种比较简单的方式就是将var换成let
let
let变量声明是ES6新增的,可以将变量绑定到所在的任意作用域中。并且其声明的变量隐式地劫持了所在的块作用域。
代码块在写代码之初就一直存在,只是之前没有这种作用域策略。用var声明无法形成块作用域。
块作用域是一个用来对之前的最小授权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。从ES6开始,作用域除了全局作用域和函数作用域,正式新增了块作用域的概念。
我们继续解释上面代码的问题:
之所以用let关键字声明就能得到符合预期的结果,是因为for循环头部的let不仅将i绑定到了for循环的块中,而且它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。也就是说,每一次for循环迭代都有其单独的变量i,循环间不相互影响,唯一的影响就是下一次迭代会基于上一次迭代的值进行操作,在其基础上累加1。
const
除了let关键字之外,ES6还新增了const关键字,同样可以创建块级作用域变量,只是const声明的一般是常量,不允许被再次修改。
函数作用域
考虑如下代码:
function foo (a) {
console.log(a,b,c) // 1,undefined,undefined
var b = 2
function bar () {
var d = 4
console.log(a) // 1
}
var c = 3
console.log(d) // ReferenceError
console.log(a,b,c) // 1,2,3
bar()
}
foo(1)
变量访问规则
我们都知道,每个函数都有其单独的作用域,在foo函数作用域气泡中,包含有变量a、b、c和bar函数;在嵌套的bar函数也有其独立的作用域,在bar函数作用域中,有变量d。
在内部函数bar内,可以访问到外部作用域foo的变量a;相反,在外部作用域foo内则无法获取内部函数变量d,打印出来报错ReferenceError。
之所以把作用域比作气泡,也是有考虑的,因为气泡会不断往上扩散,对比作用域内变量的查找规则也是如此,当前作用域内没有,可以继续往上找。也就是说,内部可以访问外部变量,而外部是无法访问内部变量的,这个规则非常重要。
变量提升
在上面代码中,有两处console.log(a,b,c),但输出的结果完全不同。为什么会这样?表面上很简单,因为在最上面变量b和c没有声明赋值呗。但仔细想想,未声明也未赋值,不应该是ReferenceError吗?怎么undefined了呢。
说明在函数作用域内部,不管你声明或赋值在哪个位置,在代码编译阶段,都会把当前作用域内的变量在顶部进行声明,这就被称作变量提升。从这个角度来说,是不是也是一种破坏词法作用域规则的行为,因为词法作用域只与编写的位置有关,而这改变了这个规则。
所以,变量提升不能算是一个好的规范,因为这是反直觉的,明明没有声明和赋值,却已经存在了。这个在ES6中得到了一定程度的改善,通过let或const声明的变量不会变量提升。