前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
首页程序员面试题库JavaScript 词法作用域与作用域链
JavaScript函数与作用域

什么是词法作用域?作用域链是怎么形成的?

函数里用到一个变量,引擎先在函数自己的作用域里找,找不到就去定义这个函数的外层作用域找,再往外,一直到全局,这条查找路径就是作用域链。它由代码写在哪里决定,函数在哪里被调用都不会改变它,这就叫词法作用域。

前端进阶之旅 · 一题精讲更新于 2026.09.29
JavaScript#函数与作用域
先看核心答案读代码示例
理解线索

一次变量查找经过的三站

  1. 当前作用域先查函数的形参、函数体里的声明、所在块里的 let 和 const
  2. 外层作用域找不到就去定义这个函数的那一层找,一层一层往外
  3. 全局作用域最外一层,还找不到就抛 ReferenceError

函数被当成回调传到别处执行,它的作用域链也不会变。调用它的函数里有同名变量,也读不到。

核心回答

先记住这个答案

JavaScript 用的是词法作用域,也叫静态作用域:一个函数能访问哪些变量,在写代码的时候就定了,取决于它写在哪个函数里面,和它运行时在哪里被调用没关系。函数里读一个变量时,先在自己的作用域里找,找不到就去外层函数找,再往外,直到全局作用域,这一串就是作用域链。找到就停下,全局也没有就抛 ReferenceError。

  • 变量查找看函数写在哪,不看在哪调用
  • 从内往外逐层查找,找到就停
  • 内层同名变量会遮蔽外层变量
  • 闭包就是函数带着它的作用域链离开了原地

调用位置不会改变查找结果

下面这段代码是这道题最直接的证明:三个地方都有一个叫 label 的变量,看看函数最后读到的是哪个。

同名变量出现在三层JavaScript
const label = 'global'

function makeReader() {
  const label = 'outer'
  return function read() {
    return label // read 写在 makeReader 里面
  }
}

function run(callback) {
  const label = 'caller'
  return callback() // read 在这里被调用
}

console.log(run(makeReader()))
查看输出与解释
outer

read 在 run 里面被调用,run 里有一个 label 等于 'caller',但 read 读到的是 'outer'。因为 read 是写在 makeReader 里面的,它的外层就是 makeReader,查找时根本不会经过 run。 如果 JavaScript 是动态作用域,按调用关系查找,这里就会打印 'caller'。Bash 脚本的变量就是这种规则,所以面试官有时会拿两种作用域做对比。

作用域链是怎么搭起来的

可以把每个作用域想象成一个记录变量的小本子,每个本子上都写着「找不到就去看哪个本子」。函数创建时会记住自己定义所在的那个本子,每次调用时,引擎新建一个本子放这次调用的局部变量,并让它指向函数记住的那个外层本子。这样一层套一层,就是作用域链。

因为「外层是谁」在函数创建时就记下了,所以不管函数后来被传到哪里、被谁调用,查找路径都不变。闭包也是这么来的:makeReader 执行完了,但 read 还记着它的本子,所以 'outer' 一直能读到。

遮蔽:内层同名变量挡住外层
查找是「找到就停」,所以内层有同名变量时,外层的就被挡住了,这叫遮蔽。要注意的是,只要内层作用域里声明了同名的 let 或 const,哪怕声明写在后面,从进入这个作用域开始外层变量就读不到了。在声明之前读取会抛 ReferenceError,而不是退回去读外层的值。
块也是一层作用域
if、for 和单独的一对花括号,里面用 let、const、class 声明的变量只在块内可见,块也是作用域链上的一环。var 不认块,只认函数,所以用 var 声明的变量会直接落到外层函数的作用域里。
回答前,多想一步

容易答错的地方

以为函数在谁里面调用,就能读谁的变量
调用关系组成的是调用栈,不是作用域链。回调函数被传进别的函数执行,也读不到那个函数的局部变量,要用就只能通过参数传进去。
把作用域链和原型链混为一谈
作用域链用来查变量,比如直接写 count。原型链用来查对象属性,比如 obj.count。一个找不到抛 ReferenceError,一个找不到返回 undefined,是两套独立的机制。
试着用自己的话回答

面试官还会怎么问?

JavaScript 里有没有能破坏词法作用域的东西?

有两个:with 和直接调用的 eval。with (obj) 会把一个对象临时插到作用域链上,eval 可以在运行时往当前作用域里加变量,两者都让查找结果没法在写代码时确定,引擎也没法优化。严格模式下 with 直接禁用,eval 声明的变量也不再泄漏到外层。

this 也是按词法作用域查找的吗?

普通函数不是,它的 this 由调用方式决定,这正好是 this 让人困惑的地方。箭头函数没有自己的 this,它会像普通变量一样沿作用域链往外找,所以箭头函数的 this 是词法的。

作用域链越长,性能越差吗?

理论上查找层数越多越慢,但现代引擎在编译时就能确定大多数变量在哪一层,访问开销很小,日常开发不用为此刻意减少嵌套。真正该注意的是闭包长时间持有大对象带来的内存占用。

从一道题,走向一组知识

把知识连起来

函数与作用域

匿名函数和箭头函数的 name 是怎么来的?

具名函数表达式的名字只在函数体内可见,是作用域规则的一个特例,适合结合作用域链一起理解。

函数与作用域

new.target 是什么?在箭头函数里读它会得到什么?

箭头函数里的 new.target 和 this 一样,按词法作用域从外层函数取值。

参考资料

  • JavaScript:函数与作用域参考资料

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 调用位置不会改变查找结果
  3. 作用域链是怎么搭起来的
  4. 容易答错的地方
  5. 面试官还会怎么问
  6. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑