先记住这个答案
默认参数是在每次调用函数时才计算的,不是定义函数时算好存起来。只有对应的实参没传,或者传的是 undefined,默认值表达式才会执行,传 null、0、空字符串都不会触发。参数按从左到右的顺序初始化,所以写 function f(a, b = a * 2) 没问题,右边的参数可以用左边的。反过来 function f(a = b, b) 会报 ReferenceError,因为初始化 a 时 b 还没初始化。
- 每次调用都重新计算,不是定义时算一次
- 只有
undefined触发默认值,null不会 - 从左到右初始化,后面的参数能用前面的
- 默认值表达式里读不到函数体里声明的变量
用一个计数器看清求值时机
把默认值写成一个会计数的函数调用,就能直接看出它什么时候执行、执行了几次。
let calls = 0
const nextId = () => ++calls
function create(id = nextId(), label = `item-${id}`) {
return label
}
console.log(create()) // id 没传,执行 nextId()
console.log(create()) // 再执行一次,不是复用上次的值
console.log(create(99)) // 传了 id,nextId 不执行
console.log(create(null)) // null 不触发默认值
console.log(calls)查看输出与解释
item-1
item-2
item-99
item-null
2前两次调用没传 id,所以 nextId() 各执行一次,分别得到 1 和 2。第三次传了 99,默认值表达式根本不执行。第四次传了 null,null 不是 undefined,也不执行,于是 id 就是 null,模板字符串里显示成 item-null。最后 calls 是 2,说明默认值只在需要时才算。
label 的默认值用到了 id,这就是「后面的参数引用前面的参数」。执行到 label 时,id 已经初始化完了,所以可以读。
默认值表达式能看到哪些变量
有默认参数的函数,参数列表会有一个自己的作用域,夹在外层作用域和函数体中间。默认值表达式能看到的,是这个参数作用域和外层。
- 读不到函数体里的变量
- 比如
function f(a = x) { const x = 1 },调用f()时,默认值里的x找的不是函数体里那个x。函数体的变量要等参数全部初始化完才创建,默认值表达式看不到它。如果外层也没有x,就会报ReferenceError。 - 引用后面的参数为什么报错
- 所有参数的名字在进入函数时就已经登记了,但要按顺序初始化。初始化
a时去读b,b虽然存在,却处于还没初始化的状态,这段时间叫暂时性死区,读它就会报错。同理function f(x = x)也会报错,右边的x读的是正在初始化的参数x自己,而不是外层的x。
容易答错的地方
- 以为传
null也会用默认值 - 只有
undefined触发默认值。后端接口经常返回null,直接传给带默认值的函数,拿到的就是null。如果想把null也当作没传,要在函数体里写value ?? defaultValue。 - 以为默认值只计算一次
- 有人担心
function add(item, list = [])多次调用会共用一个数组,那是 Python 的行为。JavaScript 每次调用都会重新执行[],每次都是新数组,不会互相污染。反过来,如果默认值是一个开销很大的计算,每次调用都会重新算一遍,需要自己做缓存。
面试官还会怎么问?
默认参数会影响 arguments 吗?
不会。arguments 只记录调用时真正传进来的实参。function f(a = 1) {} 调用 f() 时,a 是 1,但 arguments.length 是 0。另外只要函数用了默认参数,arguments 和形参之间就不再联动,改 a 不会影响 arguments[0]。
默认参数对 fn.length 有什么影响?
length 只统计第一个带默认值的参数之前的形参个数。function f(a, b = 1, c) {} 的 length 是 1,c 虽然没有默认值,但在 b 后面,也不计入。
解构参数的默认值怎么写?
function f({ page = 1, size = 10 } = {})。里面的默认值负责对象里缺少的字段,外面的 = {} 负责整个参数都没传的情况。少了外面这个 = {},调用 f() 时会因为解构 undefined 报 TypeError。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。