先记住这个答案
默认参数表达式在每次调用函数时才会求值,而不是定义时,并且只有对应实参为 undefined 时才触发。多个默认参数按从左到右的顺序初始化,因此后面的默认值可以引用前面的参数,例如 b = a * 2。参数列表形成一个独立的作用域,它是函数体作用域的父级,所以默认值里引用函数体内声明的变量或函数会抛出 ReferenceError,即便该变量用 var 声明也一样。
- 默认参数在调用时求值,非定义时
- 默认值从左到右,可引用前面参数
- 默认值看不到函数体内的变量
求值时机与参数作用域的机制
默认参数表达式不会写在函数体里执行,而是在每次调用时、实参与形参绑定的阶段求值。触发条件是实参为 undefined:不传、显式传 undefined 都会走默认值,但 null、0、空字符串等其它假值不会触发。因为每次调用都重新求值,像 items = [] 这样的默认值每次都会创建新数组,不会出现 Python 那种默认对象被多次调用共享的坑。
规范为参数列表单独建了一个作用域,它是函数体作用域的父级。初始化按从左到右进行:先绑定 a,再算 b 的默认值,所以 b = a + 1 合法;反过来 a = b 而 b 在后面定义,此时 b 尚未初始化,会落入暂时性死区报 ReferenceError。函数体里用 var、let、function 声明的东西都活在子作用域,默认值朝父级方向查找,自然看不到它们。
默认值里可以放任意同步表达式,包括函数调用、模板字符串、解构默认值,甚至 this 和 arguments 也可访问。但不能使用 await 或 yield 暂停求值,参数初始化必须同步完成;在 async 函数里写 a = await p 会直接是语法错误,需要把等待逻辑挪到函数体内。
function demo(a, b = a * 2, c = b + 1) {
return [a, b, c];
}
console.log(JSON.stringify(demo(3)));
console.log(JSON.stringify(demo(3, 10)));
let calls = 0;
function freshArray(value, list = []) {
calls += 1;
list.push(value);
return list;
}
console.log(JSON.stringify(freshArray(1)));
console.log(JSON.stringify(freshArray(2)));
console.log(calls);
function outer(x, y = () => x) {
var x = 99;
return y();
}
console.log(outer(5));查看输出与解释
[3,6,7]
[3,10,11]
[1]
[2]
2
5演示从左到右求值、每次调用新建数组,以及函数体内的 var x 不会遮蔽参数 x,因为两者分属子作用域与父作用域,代码在标准 JavaScript 环境直接运行。
构造配置对象时串联默认值
场景:写一个创建请求配置的函数 buildConfig(url, method = "GET", headers = method === "GET" ? {} : { "Content-Type": "application/json" })。约束是调用方经常只传 url,且 POST 请求要自动带 JSON 头。决策是利用"后参数可引用前参数"的规则,让 headers 依赖 method 的已初始化值。结果:buildConfig("/api") 得到空 headers,buildConfig("/api", "POST") 自动带上 JSON 头,调用方无需手写条件逻辑。
这样处理的原因是把派生逻辑收敛到签名处,调用方一眼能看到参数的依赖关系,函数体里不再需要 if (headers === undefined) 这类补丁式赋值。代价是签名变长、默认表达式不宜塞太复杂的逻辑;一旦派生规则超过两三步,应该在函数体内用显式语句计算,否则默认值求值失败时堆栈定位不如函数体内直观,可读性也会下降。
容易踩错的失效条件
第一类失效是顺序搞反:a = b 而 b 在 a 之后声明,调用时触发暂时性死区报 ReferenceError,与 let 先使用后声明的表现一致。第二类是试图在默认值里读函数体内的变量,例如 f(a = helper()) 而 helper 是函数体内部声明的函数,调用时抛 ReferenceError,即使 helper 是会被提升的函数声明也不例外,因为参数作用域根本看不到子作用域。
第三类误区是把默认值当成定义时求值一次,例如把 Date.now() 或递增计数器放进默认值,然后惊讶于每次调用结果不同。对应处理:需要共享或延迟计算的引用类型,在函数体内用惰性初始化;需要引用函数体逻辑的,把默认值降级为 undefined 哨兵,再在函数体第一行显式判断。代价是多写几行代码,但语义明确,且不依赖参数作用域这种容易被忽略的机制。
容易答错的地方
- 默认参数在函数定义时只求值一次
- 错误。默认表达式在每次调用时重新求值,且仅当实参为 undefined 时触发。写 items = [] 每次都会得到新数组,写计数函数调用也会每次执行,这与 Python 的默认参数共享行为完全不同。
- 默认值可以读到函数体内的 var 变量
- 错误。参数列表处于独立作用域,是函数体作用域的父级。函数体内的 var 虽然提升到函数体顶部,但只提升到子作用域,默认值向父级查找时看不到它,常见表现是拿到外层同名变量或抛 ReferenceError。
面试官还会怎么问?
传 null 会触发默认值吗?
不会。只有 undefined 触发默认值,null、0、false、空字符串都会原样绑定给形参。若需要 null 也兜底,应在函数体内用 nullish 合并运算符 ?? 处理。
默认参数对函数 length 属性有什么影响?
从第一个带默认值的参数开始,其自身及之后所有参数都不计入 length。例如 f(a, b = 1, c) 的 length 是 1,统计的是第一个默认参数之前的形参个数。
默认值里能写 await 吗?
不能。参数初始化必须同步完成,即使函数本身是 async 函数,a = await promise 也是语法错误。需要异步兜底时,把参数留空,在函数体内判断后再 await。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。