前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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函数与作用域

函数默认参数里写 x = x,为什么会报错?

默认参数有一个独立于函数体的作用域,参数从左到右逐个初始化。形参名会遮蔽外层同名变量,写 x = x 时右边读的是还没初始化的形参 x,不是外层变量,直接触发 TDZ 报 ReferenceError。

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

参数作用域的初始化顺序

  1. 遮蔽形参绑定一旦存在,同名外层变量就查不到了
  2. 左到右先初始化的参数可以被后面引用,反向触发 TDZ
  3. 独立作用域参数环境和函数体分开,默认表达式看不到函数体里的声明

把被依赖的参数放在左边,依赖别人的放在右边,就不会踩坑。

核心回答

先记住这个答案

默认参数有自己的作用域,和函数体是分开的。进入函数调用时,引擎先给所有形参创建绑定,再从左到右执行默认表达式来初始化。形参名一旦存在就会遮蔽外层同名变量,即使这个形参还没初始化完。所以 function f(x = x) 里右边的 x 找到的是形参 x,但它还在暂时性死区里,读取直接抛 ReferenceError。

  • x = x 读的是未初始化的形参,不会回退到外层变量
  • 后面的参数可以引用前面的,反过来会触发 TDZ
  • 默认表达式里读不到函数体内声明的变量
  • 传了实参就不走默认表达式,也不会踩 TDZ

三种典型的报错场景

默认参数的 TDZ 问题有三种常见变体:同名遮蔽自己、前参引用后参、以及引用函数体内部变量。面试里随便拿一种都能考。

对比正常写法和两种报错JavaScript
const value = 10
function ok(first = 2, second = first * 3) { return second }
function self(value = value) { return value }
function forward(first = second, second = 4) { return first }

console.log(ok())
for (const fn of [self, forward]) {
  try { fn() } catch (e) { console.log(e.constructor.name) }
}
查看输出与解释
6
ReferenceError
ReferenceError

ok 能正常返回 6,因为 second 引用的 first 已经初始化完了,从左到右没有问题。self 报错是因为形参 value 遮蔽了外层的 const value = 10,右边读到的是还没初始化的形参自己。forward 报错是因为 first 的默认表达式去读 second,而 second 排在后面,还没轮到它初始化。 这三个例子说明一条规则:默认表达式只能读已经初始化完的东西,也就是外层变量和排在左边的参数。

为什么形参会遮蔽外层变量

这个行为和块级作用域里的 let 一模一样。在一个块里用 let 声明了变量,即使声明写在后面,整个块从一开始就有了这个绑定,声明之前的区域就是 TDZ。

块级作用域的 TDZ 类比JavaScript
const x = 1
{
  // let x 的绑定在这一刻就已存在,外层 x = 1 被遮蔽了
  console.log(x) // ReferenceError
  let x = 2
}

函数默认参数的道理完全相同。进入调用时,引擎会先在参数作用域里给每个形参都创建一个绑定。绑定是一开始就全部创建的,初始化才是从左到右逐个进行的。所以 function f(value = value) 里,形参 value 的绑定在执行 = value 之前就已经存在了,名称查找先命中这个绑定,发现它还没初始化,直接报错。外层那个同名变量被完全遮蔽了,查找链根本走不到那里。 要想用外层变量做默认值,最简单的办法是让形参名和外层变量不同名。比如 function f(val = value) 就不会有遮蔽问题,val 和 value 是两个名字,value 会正常从外层找到。

参数作用域和函数体是分开的

当函数有默认参数时,参数列表和函数体各自拥有独立的作用域。默认表达式的查找链是「参数环境 → 外层环境」,不会向内深入到函数体。这样设计是为了避免函数体还没开始执行就被提前读取。

默认表达式读不到函数体里的变量JavaScript
function test(a = inner) {
  var inner = 'hello'
  return a
}
try { test() } catch (e) { console.log(e.constructor.name) }
查看输出与解释
ReferenceError

inner 在函数体里用 var 声明,但默认表达式在参数作用域里执行,参数作用域建立在函数体之前,查找链不会向内看,所以找不到 inner。 反过来是可以的:函数体能正常读到参数。比如 function f(a = 1) { return a } 里 a 在函数体里完全可用,因为函数体的作用域链会经过参数环境。 没有默认参数的函数不存在这个问题。没有默认参数时,引擎会把参数环境和函数体环境合并成一个,var 声明和形参共享同一个作用域。只有出现了默认参数,两个作用域才会分开。

回答前,多想一步

容易答错的地方

以为 x = x 会读到外层的 x
很多人看到外层有个同名变量就以为默认表达式会用它。实际上形参绑定的优先级更高,同名形参一旦存在,外层变量就被遮蔽了。要引用外层变量,形参名不能和它重名。
把参数依赖关系写反了
(a = b, b = 1) 会报错,(b = 1, a = b) 才对。初始化从左到右,后面的参数在前面的默认表达式执行时还在 TDZ 里,不能被引用。
试着用自己的话回答

面试官还会怎么问?

传了实参还会触发 TDZ 吗?

不会。只要传的不是 undefined,默认表达式根本不执行。比如调 self(42) 完全正常,value 直接用实参 42 初始化,不走 = value 这个表达式,TDZ 根本不会被触碰。

默认表达式里能调用外层函数吗?

可以。参数作用域的外层就是函数定义所在的作用域,里面能访问到的函数和变量在默认表达式里都能用。但要注意调用发生在进入函数体之前,如果函数有副作用,执行时机可能和你预期的不同。比如默认表达式里调了一个会修改全局状态的函数,这个修改在函数体第一行之前就已经发生了。

解构参数也有 TDZ 问题吗?

有。解构参数的默认值走的是同一套机制。比如 function f({ a = b, b } = {}) 里 a 的默认值引用 b,但解构也是从左到右逐个初始化的,b 还没轮到,会报 ReferenceError。解构只是语法上看起来不同,底层的初始化顺序规则没变。

从一道题,走向一组知识

把知识连起来

函数与作用域

class 里的方法传出去当回调,this 为什么就丢了?

class 方法里 this 丢失也和作用域、绑定机制有关,同属「声明位置决定行为」这个主题。

函数与作用域

var、let、const 的作用域有什么不同?

理解了默认参数的 TDZ,再看 let 和 const 在块级作用域的 TDZ 就很清楚了,原理完全一样。

参考资料

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

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

本题目录
  1. 先记住这个答案
  2. 三种典型的报错场景
  3. 为什么形参会遮蔽外层变量
  4. 参数作用域和函数体是分开的
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

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

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