从“防坑”到优雅:ES6 默认参数的实战精髓
你有没有写过这样的代码?
function greet(name, time) { name = name || 'Guest'; time = time || 'morning'; console.log(`Good ${time}, ${name}!`); }或者更复杂的:
if (!options) options = {}; const host = options.host || 'localhost'; const port = options.port || 8080;这些写法在 ES5 时代司空见惯,但它们有个通病——不够精准,容易踩坑。比如传了个0或者'',结果被误判为“假值”,默认值就冒出来了,而你却毫无察觉。
直到 ES6 来了。
它没搞什么惊天动地的大变革,而是悄悄给函数参数加了个“保险”:默认参数(Default Parameters)。这看似是个小功能,实则彻底改变了我们设计函数接口的方式。
为什么说它是“必看”?因为它解决了真问题
在现代 JavaScript 开发中,无论是写一个 React 组件、Node.js 中间件,还是封装一个工具函数,你几乎总会遇到这类需求:
“这个参数可以不传,不传的时候就用某个默认值。”
以前我们靠||,现在我们可以直接写:
function greet(name = 'Guest', time = 'morning') { console.log(`Good ${time}, ${name}!`); }就这么简单?是的。但它背后的逻辑,远比表面复杂。
它到底什么时候生效?
关键点来了:默认参数只在参数为undefined时触发。
这意味着:
| 传入值 | 是否使用默认值? |
|---|---|
undefined | ✅ 是 |
null | ❌ 否 |
0 | ❌ 否 |
'' | ❌ 否 |
false | ❌ 否 |
对比一下老式||写法就知道区别有多大了:
// 老方法:会把 0、''、false 都当成“无效”处理 time = time || 'morning'; // 如果 time 是 '',也会变成 'morning' // 新方法:只有 undefined 才用默认值 function greet(time = 'morning') { ... }这才是真正的“按需兜底”。
惰性求值:别急着执行,默认值可以很聪明
默认参数不只是能写常量,还能写表达式,甚至函数调用——而且是惰性的。
什么意思?来看个例子:
function log(msg = generateDefaultMessage()) { console.log(msg); } function generateDefaultMessage() { console.log('Generating fallback message...'); return 'Oops!'; } log(); // 先输出 "Generating fallback...",再输出 "Oops!" log('Hello'); // 只输出 "Hello",generateDefaultMessage 根本没执行!看到了吗?generateDefaultMessage()只在需要时才执行。这对于那些耗时的操作(比如生成 UUID、读取配置、发起请求)特别有用——你不传参我才去算,你传了我就省事了。
这叫“懒加载思维”,用在参数上,刚刚好。
参数之间能“说话”:后面的可以依赖前面的
有时候,参数之间有逻辑关系。比如画布宽度定了,高度想默认是宽度的一半。
ES6 允许你在后面参数的默认值里引用前面的:
function createCanvas(width, height = width / 2) { return { width, height }; } createCanvas(100); // { width: 100, height: 50 } createCanvas(100, 80); // { width: 100, height: 80 }但注意:不能反向引用。下面这段是错的:
// ❌ 报错:Cannot access 'height' before initialization function badFunc(height = width * 2, width) { ... }顺序很重要。先定义的才能被后使用的看到。
真正的杀手级组合:解构 + 默认参数
如果你的函数接受一堆可选配置,最优雅的方式不是传七八个参数,而是传一个对象,并用解构 + 默认值来处理。
function connect({ host = 'localhost', port = 3000, secure = false } = {}) { const protocol = secure ? 'https' : 'http'; console.log(`Connecting to ${protocol}://${host}:${port}`); }重点在最后那个= {}—— 它给整个解构对象设了个默认值。
如果没有它,当你调用connect()时,相当于对undefined解构,直接报错:
// ❌ TypeError: Cannot destructure property 'host' of 'undefined' connect();加上= {},哪怕你不传任何参数,也能安全进入函数体,所有字段都走各自的默认值。
这种模式在 Axios、Express、React props 处理中随处可见。它是现代 JS 接口设计的标准范式。
和 Rest 参数搭档:灵活处理不定参数
Rest 参数(...args)用来收集剩余参数,和默认参数搭配也很自然:
function multiply(factor = 2, ...numbers) { return numbers.map(n => n * factor); } multiply(); // [] —— 没有数字可乘 multiply(3); // [] —— factor 是 3,但没有 numbers multiply(3, 1, 2, 4); // [3, 6, 12]这里factor有了默认值,就算用户只传了一堆数字没指定倍数,也不会出错。
不过要注意:带默认值的参数不能放在 rest 参数后面。
// ❌ SyntaxError function bad(...numbers, factor = 2) { ... }因为 rest 已经把剩下的全拿走了,哪还有“后面”的参数?
实战场景:一个日志函数的进化之路
让我们看一个真实案例:如何用默认参数写出健壮又易用的日志函数。
初始版本(ES5 风格)
function log(message, options) { if (!options) options = {}; const level = options.level || 'info'; const timestamp = options.timestamp || new Date().toISOString(); const output = options.output || console.log; const entry = `[${timestamp}] ${level.toUpperCase()}: ${message}`; output(entry); }问题不少:
-options必须手动初始化;
-||无法区分null和'error';
- 代码冗长,意图不清晰。
进化版(ES6 + 解构 + 默认参数)
function log( message, { level = 'info', timestamp = new Date().toISOString(), output = console.log } = {} ) { const entry = `[${timestamp}] ${level.toUpperCase()}: ${message}`; output(entry); }现在你可以这样调用:
log('App started'); // 自动补全 info 级别、当前时间、console 输出 log('DB error', { level: 'error', output: alert }); // 只改想要的部分,其他保持默认代码更短了,但表达力更强了。每个参数的默认行为一目了然。
常见陷阱与最佳实践
⚠️ 陷阱1:忘了给解构对象设默认值
// ❌ 危险! function config({ port, host }) { ... } config(); // TypeError!✅ 正确做法:
function config({ port, host } = {}) { ... }⚠️ 陷阱2:在默认值里做副作用操作
// ❌ 不推荐:每次调用都会尝试修改 DOM function render(el = document.getElementById('app')) { ... }虽然语法合法,但会让函数变得不可预测。除非明确需要,否则避免在默认值中执行查询、发送请求等副作用。
✅ 推荐做法:把副作用留在函数体内,或通过工厂函数控制。
⚠️ 陷阱3:误以为null会触发默认值
function foo(x = 10) { return x; } foo(null); // null,不是 10!记住:只有undefined触发默认值。如果你希望null也走默认逻辑,得自己处理:
function foo(x) { if (x == null) x = 10; // 处理 null 和 undefined ... }写在最后:这不是语法糖,是设计哲学的升级
很多人把默认参数当成“语法糖”,觉得不过是少写了两行判断。但真正用起来你会发现,它带来的是一种思维方式的转变:
- 从前:我得检查参数有没有传;
- 现在:我在定义接口契约——“这个参数,不传也没关系”。
它让函数变得更“宽容”,也让调用者更自由。你可以只关心你要改的部分,其余交给默认值。
更重要的是,它推动我们写出更清晰、更可维护的 API。尤其是配合解构,实现了类似“命名参数”的效果,彻底摆脱了参数顺序的束缚。
所以,掌握默认参数,不只是学会一个语法,而是迈入现代 JavaScript 开发的第一步。
如果你还在用||处理默认值,不妨停下来想想:是不是该升级一下了?
如果你已经熟练使用,欢迎在评论区分享你的高阶技巧——比如结合工厂函数动态生成默认值,或是如何在 TypeScript 中类型推导这些默认项。