JavaScript作用域

130 阅读5分钟

词法作用域

作用域有两种主要工作模式。第一种最为普遍,是被大多数编程语言所采用的词法作用域。另一种叫做动态作用域,仍有少数语言在使用,如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函数内部作用域或全局作用域的相同变量值,说的更浅显一点就是近水楼台先得月。

词法作用域是在编译阶段就确定了的,接下来我们会说evalwith函数,它们骗过了编译阶段的正常词法作用域规则。

块作用域

考虑以下代码:

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中得到了一定程度的改善,通过letconst声明的变量不会变量提升