ES6 默认参数有什么特点?和手动兜底有什么区别?
简化版
默认参数是在形参位置给参数兜底,只有实参是 undefined 或未传时才会使用默认值;它比 x = x || value 更准确,因为不会误伤 0、空字符串和 false。
详细版
默认参数的核心规则是:缺参或传入 undefined 时生效,传入 null 不生效。
function createPage(page = 1, size = 20) {
return { page, size };
}
它和手动兜底的区别:
page = page || 1会把0当成无效值。- 默认参数只处理
undefined,语义更精确。 - 默认参数可以引用前面的参数。
- 使用默认参数会影响函数的
length。 - 默认参数区域有自己的作用域,不能随意引用后面的参数。
面试重点是讲清楚触发条件、求值时机、作用域和 length 变化。
完整版教学
一、默认参数解决的是“缺参”而不是“假值”
ES6 之前常见兜底写法是 x = x || defaultValue。
这个写法的问题是它把所有假值都当成缺失值,包括 0、''、false。
默认参数的判断更窄,只在参数值为 undefined 时触发。
function page(size = 20) {
return size;
}
page(); // 20
page(undefined); // 20
page(null); // null
page(0); // 0
数字例子很好记:分页 size=0 可能有业务含义。
如果用 size = size || 20,0 会被改成 20,这就是语义误判。
二、默认值是在调用时求值
默认参数不是函数定义时就固定下来。 每次调用函数时,如果该参数需要默认值,默认表达式才会被执行。 因此默认值可以是函数调用,也可以依赖当前环境。
let n = 0;
function nextId(id = ++n) {
return id;
}
console.log(nextId()); // 1
console.log(nextId()); // 2
console.log(nextId(10)); // 10
这个例子说明:第 3 次调用传了 10,默认表达式不会执行,所以 n 仍然是 2。
面试追问“默认参数什么时候计算”时,要答“调用时、且仅在需要默认值时”。
三、可以引用前面的参数,不能可靠引用后面的参数
参数初始化是从左到右进行的。 后面的默认参数可以使用前面已经初始化好的参数。 但前面的参数默认值不能引用后面的参数,因为后者还处在未初始化状态。
function area(width, height = width) {
return width * height;
}
area(5); // 25
function bad(x = y, y = 1) {
return x + y;
}
bad(); // ReferenceError
初始化流程可以画成:
实参进入
-> 初始化第 1 个形参
-> 初始化第 2 个形参
-> 初始化第 3 个形参
-> 进入函数体
所以默认参数不是普通变量声明的装饰,它参与了函数调用前的初始化过程。
四、默认参数会影响函数的 length
函数的 length 表示“第一个带默认值参数之前的形参数量”。
它经常被框架、工具函数或测试代码用来判断函数期望接收几个参数。
一旦从某个参数开始设置默认值,后面的参数不再计入 length。
function a(x, y) {}
function b(x, y = 1) {}
function c(x = 1, y) {}
console.log(a.length); // 2
console.log(b.length); // 1
console.log(c.length); // 0
| 函数 | length | 原因 |
|---|---|---|
a(x, y) | 2 | 没有默认参数 |
b(x, y = 1) | 1 | y 开始不计入 |
c(x = 1, y) | 0 | 第一个参数已有默认值 |
这不是日常业务最常用的点,但属于高频追问。
五、默认参数区域有独立作用域
当函数参数使用默认值时,参数初始化区域会形成一个独立作用域。 默认值表达式能访问外层变量,也能访问前面的参数。 函数体内部声明的变量不能被参数默认值提前访问。
let x = 1;
function demo(y = x) {
let x = 2;
return y;
}
demo(); // 1
这里 y = x 访问的是外层 x,不是函数体里的 let x = 2。
原因是函数体还没有开始执行。
这类题经常和作用域、暂时性死区一起考。
默认参数的判断口径很小:只管
undefined;它的执行位置很早:进入函数体之前。
六、工程中怎么用才不容易踩坑
默认参数适合表达“未传就使用默认配置”。
如果业务需要把 null 也视为缺失,需要显式处理,或者用空值合并运算符。
不要把复杂对象默认值和外部可变状态混在一起,否则读者很难判断每次调用得到的到底是什么。
function request(url, options = {}) {
const timeout = options.timeout ?? 3000;
return { url, timeout };
}
上面 options = {} 解决未传对象的问题,?? 3000 解决属性缺失的问题。
两层兜底分工清晰,比把所有判断塞进一个 || 表达式更可靠。
七、常见误区与追问
- 误区:默认参数会在传入
null时生效。 默认参数只在值为undefined时生效,null是一个明确传入的值。 - 误区:
x = x || 1和function f(x = 1)等价。 前者会覆盖0、false、空字符串,后者不会。 - 误区:默认参数在函数定义时求值。 默认表达式在每次调用时按需求值,不需要默认值时不会执行。
- 追问:为什么
function f(x = y, y = 1){}调用会报错? 参数从左到右初始化,x初始化时y还没有完成初始化。 - 追问:默认参数为什么影响
length?length只统计第一个默认参数之前的形参,便于表达最少必填参数数量。 - 追问:对象参数默认值应该怎么写? 常见写法是
options = {}配合属性级别的??,避免读取undefined.timeout报错。
八、加强记忆
记默认参数时抓四个关键词:undefined 触发、调用时求值、从左到右初始化、影响 length。实际答题可以用 0 被 || 误伤的例子切入,再补作用域和 length 追问,答案会比只说“给参数默认值”扎实很多。