jstips 第 12 期:ES6 伪强制参数(Pseudomandatory Parameters)——用默认参数实现必传校验
jstips 第 12 期:ES6 伪强制参数(Pseudomandatory Parameters)——用默认参数实现必传校验
导读
在 JavaScript 中,函数的每一个参数天然都是可选的,调用方少传参数不会报错,只会得到 undefined。本文基于 jstips 仓库第 12 期技巧(见 _posts/zh_CN/javascript/2016-01-12-pseudomandatory-parameters-in-es6-functions.md,英文原版见 _posts/en/javascript/2016-01-12-pseudomandatory-parameters-in-es6-functions.md),讲解如何借助 ES6 的参数默认值(default parameter)特性,在完全不污染函数体(不写任何 if 判断)的前提下,让参数变得"伪强制"(pseudomandatory):漏传参数时立即抛出带有明确信息的 Error。读完本篇,你将掌握这一模式的原理、边界行为,以及它在日常 API 设计与参数校验中的实战用法。
背景:为什么需要"伪强制参数"
在许多编程语言中(如 Java、C#、Python 的部分场景),函数的参数默认是强制必传的,开发者需要显式声明某个参数为可选。而 JavaScript 恰恰相反:每一个参数都是可选的——调用时不传的参数会被赋值为 undefined,函数照常执行,不会抛出任何错误。
这种"宽容"在小型脚本中很便利,但在大型代码库或作为公共 API 对外暴露时,漏传关键参数往往会导致难以排查的 TypeError 或静默的错误结果。传统做法是在函数体内手动检查:
function getSum(a, b) {
if (a === undefined) {
throw new Error('a is not defined');
}
if (b === undefined) {
throw new Error('b is not defined');
}
return a + b;
}
这种做法的问题在于:校验逻辑与业务逻辑混杂在函数体中,每增加一个参数就要多写一段判断,函数体被不断"污染"。
核心方案:默认参数 + 抛错函数的组合
ES6 的参数默认值特性允许为参数指定默认值——当参数为 undefined 时(未传入或显式传入 undefined),默认值表达式会被求值。jstips 第 12 期的思路正是利用这一点:把默认值设为一个"立刻抛错"的函数调用,让缺失参数自动触发异常。
原文档给出的完整示例:
const _err = function( message ){
throw new Error( message );
}
const getSum = (a = _err('a is not defined'), b = _err('b is not defined')) => a + b
getSum( 10 ) // throws Error, b is not defined
getSum( undefined, 10 ) // throws Error, a is not defined
逐行拆解
_err是一个立即抛出Error的工具函数,唯一职责是构造并抛出异常;getSum是一个箭头函数(关于箭头函数可参考 jstips 第 14 期 _posts/en/javascript/2016-01-14-fat-arrow-functions.md),它的两个形参a、b都设置了默认值,而默认值表达式就是_err(...)的调用;- 当调用
getSum(10)时,a正常接收到10,而b为undefined,于是b的默认值表达式_err('b is not defined')被求值,抛出Error: b is not defined; - 当调用
getSum(undefined, 10)时,a显式传入了undefined——注意,显式传undefined同样会触发默认值——于是抛出Error: a is not defined。
这套组合的意义在于:函数体本身零校验代码,参数契约直接写在签名里,任何阅读签名的人一眼就能看出哪些参数是必须的。
深入原理:默认参数的求值时机与触发条件
要正确使用这个模式,需要精确理解 ES6 默认参数的几条行为规则:
1. 只有 undefined 会触发默认值
默认值仅在参数值为 undefined 时生效,触发场景包括:
- 调用时完全未传该位置参数;
- 调用时显式传入
undefined(如上例getSum(undefined, 10))。
而 null 不会触发默认值。若调用 getSum(null, 10),a 会拿到 null,_err 不会被调用,随后 null + 10 计算得到 10,校验被静默绕过。这与 jstips 第 5 期 _posts/zh_CN/javascript/2016-01-05-differences-between-undefined-and-null.md 中强调的 undefined 与 null 语义差异一脉相承:在需要"必传校验"的场景,应引导调用方用 undefined 表达"未提供",而不是 null。
2. 默认值表达式是惰性求值的
默认值表达式(如 _err('a is not defined'))只在参数缺失时才会被求值,且是每次调用时动态求值。也就是说,参数齐全的正常调用不会产生任何 _err 调用的开销,校验成本为零。
3. 默认值可以引用前面的参数
默认值表达式中可以引用同参数列表中更靠前的参数,这让"第二个参数必传,且依赖第一个参数"这类更复杂的约束成为可能。
边界情况与模式变体
变体一:普通函数同样适用
原文档示例用的是箭头函数,但默认参数特性对 function 声明的普通函数、方法、构造函数完全通用:
const _err = (message) => { throw new Error(message); };
function divide(dividend = _err('dividend is not defined'),
divisor = _err('divisor is not defined')) {
return dividend / divisor;
}
甚至可以把 _err 定义成更通用的"必需参数"标记:
const required = (name) => { throw new Error(`Parameter "${name}" is required`); };
function connect(url = required('url'), timeout = 1000) {
// url 必传,timeout 可选,默认 1000
return { url, timeout };
}
这种写法将"哪些参数必传、哪些可选、可选的默认值是什么"完整地暴露在签名上,可读性极佳。
变体二:与解构默认参数组合
当函数接收的是一个配置对象时,可以将"伪强制"思想与解构默认参数结合——jstips 第 43 期 _posts/en/javascript/2016-02-12-use-destructuring-in-function-parameters.md 详细讲解了参数解构与默认参数的搭配。例如要求 options 对象中必须包含 token 字段:
function request({ token = required('token'), url = required('url') } = {}) {
// 缺少 token 或 url 时抛出对应错误
return { token, url };
}
request({ token: 'abc' }); // Error: Parameter "url" is required
request(); // Error: Parameter "token" is required
注意 = {} 兜底:当整个实参对象缺失时,先回退到 {} 再解构,此时属性默认值(即 required(...) 调用)才会触发,从而抛出精确的错误信息——这正是第 43 期文档中 = {} 兜底场景与本期"伪强制"思想的结合点。
边界提示:默认值不会拦截 null
再次强调:若调用方传入 null(如 request({ token: null, url: 'x' })),属性默认值同样不会触发。对于严格要求"非空"的场景,可在 required 之外再配合显式校验,或者约定 API 语义为"必须使用 undefined 表示缺失"。
在仓库中的位置与延伸阅读
本篇技巧是 jstips 仓库第 12 期(tip-number: 12),在项目 README.md 的 Tips 列表中以 "Pseudomandatory parameters in ES6 functions" 登记,并提供了中文(zh_CN)、繁体中文(zh_TW)、西班牙语(es_ES)等多个语言版本(分别位于 _posts/zh_TW/javascript/2016-01-12-pseudomandatory-parameters-in-es6-functions.md 与 _posts/es_ES/javascript/2016-01-12-pseudomandatory-parameters-in-es6-functions.md),可用于对照学习。
与本期主题相关的仓库内延伸阅读:
- 第 43 期:在函数参数中使用解构赋值——解构 + 默认参数组合的完整行为(含
= {}兜底); - 第 5 期:undefined 与 null 的区别——理解为什么默认值只对
undefined生效; - 第 54 期:可选参数与可选回调——另一种处理"参数可多可少"的经典手法,可与默认参数方案对比取舍;
- 第 11 期:变量提升——
const/箭头函数与var声明的提升差异,有助于理解_err的声明位置是否安全。
小结
"伪强制参数"模式用一句话概括:把默认参数当作钩子,钩住一个必然抛错的函数。它的核心价值在于:
- 参数契约显式化——必传参数一眼可见;
- 函数体零污染——不写任何手工校验分支;
- 零性能损耗——默认值惰性求值,正常调用不触发。
使用时只需牢记两个前提:默认值只在参数为 undefined 时触发,因此调用方应以 undefined 而非 null 表示"未提供";若 API 需要更复杂的约束(如类型校验、非空校验),仍需要在此基础上叠加其他校验手段。这个模式非常适合库函数、公共 API 的参数契约声明,是 ES6 默认参数特性最实用的落地场景之一。