Skip to content

作用域与作用域链

首先说明:

JavaScript同绝大多数语言一样,使用的是词法作用域,即静态作用域。

词法作用域的特点是,变量的作用域在静态编译阶段就完成了,而不是在运行期间。

作用域

js
function func() {
  var a = 1
  console.log(a)
}

func()

作用域一般指代码中使用大括号 {} 括起来的区域,如上代码中,有两个作用域:全局作用域和 func 函数作用域,我们以 func 函数作用域为例:

在这个函数作用域中,定义了变量 a ,这个定义在编译期间就确定了。

func 函数执行时,会从 func 的函数作用域中查找变量 a ,如果 a 在函数中定义了,就直接使用,如果没有找到,就到父作用域中继续查找。

作用域链

js
function func() {
  var a = 1
  function funcInner() {
    console.log(a)
  }
  funcInner()
}

func()

在这个例子中,funcInner 函数作用域中没有定义任何变量,当执行 console.log(a) 时,由于没有查找到变量 a ,会继续在父级作用域中查找,因此找到了 func 函数作用域中,该函数作用域找到了变量 a ,因此查找成功。

从子作用域一直往父级作用域查找所形成的链,称为作用域链。直到查找到全局作用域时,走到链的末端,查找过程结束。

作用域的类型

在JavaScript中,有四种类型的作用域:

  • 全局作用域:根作用域,代码一般是从全局作用域开始执行的
  • 函数作用域:函数区块所包含的区域
  • 块级作用域:在ES6以后,由于 let/const 提升的范围变窄了(以往 var 声明会提升至函数作用域的顶部),变成了提升至 {} 的顶部,从而多出了块级作用域
  • eval作用域:使用 eval 运行的代码会在一个特殊的作用域内,该作用域是动态创建的

重学作用域

之前,我们讨论了三个话题:

  • JavaScript作用域是词法作用域
  • 作用域链的形成及变量查找
  • 作用域的种类

这些讨论默认了作用域的存在,而忽略了一个问题:作用域是什么?为什么需要作用域?

简而言之,作用域是用来 组织代码 的,通过作用域可以将代码隔离开来,保证互不干扰。其中最有意义的一件事就是:

命名的隔离

想象没有作用域的场景:

js
var name = '张三'
console.log(name)

var name2 = 'Model3'
console.log(name2)

两段类似的代码,由于没有作用域,命名很容易冲突,而使得开发者不得不手动区分不同的命名。

但是有了作用域后,这件事情就变得简单了许多:

js
function printPersonName() {
  var name = '张三'
  console.log(name)
}
function printCarName() {
  var name = 'Model3'
  console.log(name)
}
printPersonName()
printCarName()

闭包

闭包是由函数作用域嵌套产生的一种特性。

举个最简单的例子:

js
function createFunc() {
  var a = 0
  return function() {
    a++
    console.log(a)
  }
}

var func = createFunc()
func() // 1
func() // 2

在这个例子中,函数 createFunc 中嵌套了返回值中的一个匿名函数,从而导致在这个匿名函数执行时,仍然能获取到其父级函数作用域中的变量 a

在其他语言中(如C、Java),一个函数执行完成后,函数内部定义的变量将会被释放。

而由于JavaScript是函数式编程语言,其允许函数的嵌套,如果父级函数作用域内的变量被释放,而子级函数仍然被持有引用的话,就会发生子级作用域获取不到父级作用域中已经释放的变量这种情况,因此,才需要闭包的支持。

常见问题