Skip to content

JavaScript 进阶 一:词法作用域

约 1492 字大约 5 分钟

javascript

2020-02-01

在JavaScript的世界中,词法作用域是一个既基础又核心的概念,它决定了变量的可访问范围和生命周期。理解词法作用域不仅有助于写出更健壮的代码,更是掌握闭包、模块化等高级特性的前提。

什么是作用域?

作用域可以理解为变量的可见范围和生命周期。JavaScript中有三种主要的作用域类型:

1. 全局作用域

// 全局变量,在任何地方都可以访问
const globalVar = '我在全局作用域'

function testGlobal() {
  console.log(globalVar) // 可以访问
}

testGlobal()
console.log(globalVar) // 可以访问

全局作用域的陷阱

过度使用全局变量会导致命名冲突变量污染,应尽量避免。

2. 函数作用域

function outerFunction() {
  const outerVar = '我在外部函数作用域'

  function innerFunction() {
    const innerVar = '我在内部函数作用域'
    console.log(outerVar) // 可以访问外部变量
  }

  innerFunction()
  // console.log(innerVar); // 错误:无法访问内部变量
}

outerFunction()

3. 块级作用域(ES6+)

{
  let blockVar = '我在块级作用域'
  const constVar = '我也是块级作用域'
  console.log(blockVar) // 可以访问
}

// console.log(blockVar); // 错误:无法访问块级变量

什么是词法作用域?

词法作用域(Lexical Scope)也称为静态作用域,指的是变量在代码书写阶段就已经确定的作用域,而不是在运行时确定。

关键理解

词法作用域由代码书写的位置决定,与函数调用位置无关。

词法作用域示例

const globalName = '全局变量'

function outer() {
  const outerName = '外部函数变量'

  function inner() {
    console.log(outerName) // 可以访问外部变量
    console.log(globalName) // 可以访问全局变量
  }

  return inner
}

const innerFunc = outer()
innerFunc() // 输出:"外部函数变量" 和 "全局变量"

在这个例子中,inner函数在定义时就确定了它能访问哪些变量,这就是词法作用域的体现。

作用域链的运作机制

当访问一个变量时,JavaScript引擎会沿着作用域链逐级查找:

  • 第一步:在当前作用域查找变量
  • 第二步:如果没找到,向上一级作用域查找
  • 第三步:重复第二步,直到全局作用域
  • 第四步:如果全局作用域也没找到,抛出ReferenceError

作用域链示例

const globalNum = 10

function level1() {
  const num1 = 20

  function level2() {
    const num2 = 30

    function level3() {
      console.log(globalNum) // 10
      console.log(num1) // 20
      console.log(num2) // 30
    }

    level3()
  }

  level2()
}

level1()

作用域链关系:

level3 → level2 → level1 → 全局作用域

词法作用域 vs 动态作用域

为了更好地理解词法作用域,让我们对比一下动态作用域:

const name = '全局名称'

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

function wrapper() {
  const name = '局部名称'
  showName() // 输出什么?
}

wrapper()

思考题

在词法作用域下输出"全局名称",在动态作用域下会输出"局部名称"。 JavaScript采用词法作用域,所以这里输出"全局名称"。

变量查找:LHS vs RHS

理解变量查找的两种方式有助于调试作用域问题:

LHS(Left-Hand Side)查询

let x = 10 // LHS:为变量赋值
x = 20 // LHS:修改变量值

LHS查询关注的是变量的存储位置

RHS(Right-Hand Side)查询

console.log(x) // RHS:读取变量值
const y = x + 5 // RHS:读取x的值

RHS查询关注的是变量的值

闭包与词法作用域

闭包是词法作用域的直接应用:

function createCounter() {
  let count = 0 // 词法作用域内的变量

  return function () {
    count++ // 闭包记住了count变量
    return count
  }
}

const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2
console.log(counter()) // 3

闭包的本质

闭包就是函数能够记住并访问其词法作用域,即使函数在其词法作用域之外执行。

实际应用场景

1. 数据封装

function createUser(name) {
  let privateData = {
    loginCount: 0,
    lastLogin: null
  }

  return {
    getName: () => name,
    login: () => {
      privateData.loginCount++
      privateData.lastLogin = new Date()
      console.log(`${name} 登录成功`)
    },
    getStats: () => ({ ...privateData })
  }
}

const user = createUser('张三')
user.login()
console.log(user.getStats())

2. 模块模式

const MyModule = (function () {
  let privateVar = '私有变量'

  function privateMethod() {
    console.log('私有方法')
  }

  return {
    publicMethod() {
      privateMethod()
      return privateVar
    }
  }
})()

console.log(MyModule.publicMethod())

3. 事件处理

function setupButtons() {
  const buttons = document.querySelectorAll('.btn')

  for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', () => {
      console.log(`按钮 ${i} 被点击`)
    })
  }
}

常见陷阱与最佳实践

陷阱1:循环中的闭包

// ❌ 错误写法
// eslint-disable-next-line vars-on-top, no-var
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i) // 总是输出 3
  }, 100)
}

// ✅ 正确写法
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i) // 输出 0, 1, 2
  }, 100)
}

陷阱2:内存泄漏

// ❌ 可能造成内存泄漏
function createHeavyClosure() {
  const largeData = Array.from({ length: 1000000 }).fill('data')

  return function () {
    // 即使不需要largeData,它仍然被保留在内存中
    console.log('操作完成')
  }
}

// ✅ 及时释放引用
function createOptimizedClosure() {
  let largeData = Array.from({ length: 1000000 }).fill('data')

  const result = function () {
    console.log('操作完成')
  }

  // 使用完后释放大对象
  largeData = null

  return result
}

调试技巧

调试作用域问题

  • 使用浏览器开发者工具的Scope面板
  • 添加console.log检查变量状态
  • 使用debugger语句设置断点
function debugScope() {
  const localVar = '局部变量'
  debugger // 在这里暂停,查看作用域
  console.log(localVar)
}

debugScope()

总结

词法作用域是JavaScript的基础支柱,它:

  • 在代码书写时确定作用域关系
  • 支持作用域链的变量查找机制
  • 实现闭包功能,允许函数"记住"其创建时的环境
  • 促进模块化和代码封装

掌握词法作用域不仅有助于理解JavaScript的运行机制,更能帮助你在实际开发中写出更安全、更高效的代码。

记住:作用域在书写时确定,闭包让作用域"活"得更久

深入学习建议

  • 阅读ECMAScript规范中的作用域相关章节
  • 实践闭包在各种场景下的应用
  • 学习函数式编程中的相关概念