Skip to content

作用域与作用域链

作用域用于规定“一个标识符在什么范围内可访问”。JavaScript 采用词法作用域,变量的可访问范围主要取决于代码写在什么位置,而不是函数在什么位置被调用。

什么是作用域

可以把作用域理解为一套查找规则:

  • 当前代码能访问哪些变量。
  • 变量名冲突时,优先使用哪一层。
  • 离开某个代码块或函数后,哪些变量不再可见。
js
const globalName = 'global'

function outer() {
  const outerName = 'outer'

  function inner() {
    const innerName = 'inner'
    console.log(globalName, outerName, innerName)
  }

  inner()
}

inner 内部可以访问 innerNameouterNameglobalName,这是因为它会沿着作用域链逐层向外查找。

常见作用域层级

全局作用域

最外层的作用域。浏览器里通过 script 顶层声明的变量通常会处于全局环境中。

js
const siteName = 'vfan docs'

函数作用域

函数内部声明的变量只在当前函数内可见。

js
function sum(a, b) {
  const total = a + b
  return total
}

console.log(sum(1, 2))
// console.log(total) // total is not defined

块级作用域

letconstclass 会形成块级作用域,常见于 ifforwhile、独立花括号代码块。

js
if (true) {
  const message = 'hello'
  console.log(message)
}

// console.log(message) // message is not defined

作用域链是什么

当读取一个变量时,JavaScript 会按如下顺序查找:

  1. 先找当前作用域。
  2. 当前没有,就找外层作用域。
  3. 一直找到全局作用域。
  4. 仍然找不到,才抛出错误。
js
const name = 'global'

function foo() {
  const name = 'foo'

  function bar() {
    console.log(name)
  }

  bar()
}

foo() // foo

bar 查找 name 时,先看自己内部,没有;然后去外层 foo 作用域找到,所以输出 foo

词法作用域

JavaScript 的作用域在代码编写阶段就已经确定了,和调用位置无关。

js
const name = 'global'

function showName() {
  console.log(name)
}

function run() {
  const name = 'local'
  showName()
}

run() // global

showName 的外层作用域是全局,不是 run,因此输出 global

暂时性死区

letconst 在块级作用域内会存在暂时性死区,声明前不能访问。

js
{
  // console.log(count) // Cannot access 'count' before initialization
  const count = 1
}

这不是“没声明”,而是“已经进入作用域,但还没完成初始化”。

常见误区

var 没有块级作用域

js
if (true) {
  var value = 1
}

console.log(value) // 1

for 循环中的 let 会为每次迭代创建新绑定

js
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i)
  }, 0)
}

// 0 1 2

和闭包的关系

闭包本质上就是函数保留了对其词法作用域的访问能力。理解作用域链之后,再看闭包会更自然。

基于 MIT 许可发布