默认情况下,TypeScript 对函数参数数量的检查是非常严苛的:定义了多少个参数,调用时就必须老老实实传多少个参数,多一个会报错,少一个也会报错。
但在实际开发中,业务逻辑往往是没有这么死板的。比如我们封装一个 “构建用户全名” 的函数,用户的姓氏(firstName)是必填的,但名字(lastName)可能由于各种原因暂时没有提供。
面对这种 “可传可不传” 的参数需求,TypeScript 为我们提供了两种解决方案:可选参数和默认参数。
TypeScript 函数的可选参数 (?)
在 TypeScript 中,如果想让函数的某个参数变成 “可选” 的,我们只需要在该参数名的后面加上一个问号 “?” 即可。
语法:
// 传统函数声明
function 函数名(必选参数: 类型, 可选参数?: 类型): 返回值类型 {
// 函数体逻辑
}
// 箭头函数声明
const 函数名 = (必选参数: 类型, 可选参数?: 类型): 返回值类型 => {
// 函数体逻辑
};说明:
到目前为止,关于 “可选” 这种操作,其实我们已经接触过 3 次了。小伙伴们可以对比理解一下。
示例 1:使用可选参数
// 场景:生成欢迎语,称呼(title)是可选的
const greetUser = (name: string, title?: string): string => {
if (title) {
return `Hello, ${title} ${name}`;
} else {
return `Hello, ${name}`;
}
};
// 只传必填参数
const user1: string = greetUser("Jack");
console.log(user1);
// 传满 2 个参数
const user2: string = greetUser("Jack", "Boss");
console.log(user2);运行结果如下。
Hello, Jack
Hello, Boss Jack分析:
在这个例子中,title 是一个可选参数。当我们只传入 "Jack" 时,TypeScript 不再报错,而是默默地将未传入的 title 赋值为 undefined。因此我们必须在函数体内使用 if (title) 来进行安全判断才行。
在使用可选参数时,有一条非常容易踩坑的铁律:可选参数必须放在所有必选参数的后面。
如果我们把可选参数放在了必选参数的前面,那么 TypeScript 会立马报错,比如:
// 报错:必选参数不能位于可选参数后
const greetUserError = (title?: string, name: string) => { ... }这其实很好理解,如果你前面没传值,后面的值传进来了,那么 TypeScript 编译器怎么知道你传的值到底是给前面那个参数的,还是给后面那个参数的呢?所以,可选参数必须统统往后放。
可选参数的底层逻辑
在前面我们学过,开启严格模式后,变量不能被随意赋值为空。那为什么这里的 lastName 没传值(变成了 undefined)却没有报错呢?
实际上,当我们在参数后面加上 “?” 后,TypeScript 编译器会在底层自动帮你做一次 “联合类型” 的转换。对于 title?: string 来说,TypeScript 眼中看到的真实类型其实是:
title: string | undefined这也意味着,除了完全不传这个参数,我们甚至可以显式地传入一个 undefined,这在 TypeScript 看来也是绝对合法的:
// 显式传入 undefined,等同于没传
const user3: string = greetUser("Lucy", undefined);TypeScript 函数的默认参数 (=)
除了可选参数,还有一种很常见的业务场景:如果用户没传这个参数,我们希望系统自动给它一个 “默认值”,而不是干巴巴的 undefined。
在 TypeScript 中(其实 ES6+ 中也是一样),我们可以直接使用等号 “=” 来给参数设置默认值。
语法:
// 传统函数声明
function 函数名(参数名: 类型 = 默认值): 返回值类型 {
// 函数体逻辑
}
// 箭头函数声明
const 函数名 = (参数名: 类型 = 默认值): 返回值类型 => {
// 函数体逻辑
};示例 2:使用默认参数
// 场景:生成欢迎语,为 title 参数设置默认值为 "Guest"(访客)
const greetUser = (name: string, title: string = "Guest"): string => {
return `Hello, ${title} ${name}`;
};
// 不传第 2 个参数,触发默认值
const finalMsg1 = greetUser("Jack");
console.log("最终欢迎语 A:", finalMsg1);
// 传入第 2 个参数,覆盖默认值
const finalMsg2 = greetUser("Jack", "Boss");
console.log("最终欢迎语 B:", finalMsg2);运行结果如下。
最终欢迎语 A:Hello, Guest Jack
最终欢迎语 B:Hello, Boss Jack分析:
当参数有了默认值之后,我们在调用时就可以选择性地忽略它。而且 TypeScript 非常聪明,如果我们给了默认值 "Guest",它就能通过 “类型推断” 知道 title 是个 string 类型,所以我们在参数里写成 title = "Guest",甚至都不用写 : string,代码变得更加清爽。
TypeScript 默认参数的 “奇葩” 站位
我们前面刚说了,可选参数(?)必须放到参数列表的最后面。那么,默认参数(=)也必须放在最后面吗?答案是:不需要。默认参数可以放在参数列表的任何位置。
但是,这就引出了一个非常经典的高级前端面试题:如果带默认值的参数放在了前面,我们在调用时,又该如何跳过它,去传后面的必填参数呢?
示例 3:默认参数在前面的 “越过” 技巧
// 场景:生成欢迎语,“默认参数 title” 放在 “必选参数 name” 的前面
const greetUser = (title: string = "Guest", name: string): string => {
return `Hello, ${title} ${name}`;
};
// 必须显式传入 undefined 来触发 title 的默认值
const result2 = greetUser(undefined, "Jack");
console.log(result2);运行结果如下。
Hello, Guest Jack分析:
在这个例子中,我们在定义函数时,“默认参数 title” 放在了 “必选参数 name” 的前面。因此在调用函数时,第 1 个参数必须显式传入 undefined 来触发默认值。
如果我们直接传一个字符串(如下所示),此时该字符串会塞给 title,然后导致 name 缺失。
// 报错:应有 2 个参数,但获得 1 个
const result1 = greetUser("Jack");在 TypeScript 的底层机制中,只有当我们确切地传入 undefined 时,编译器才会说:“哦,你不想给这个参数传值,那我就用它的默认值吧。” 这个小技巧在封装一些多参数的复杂底层工具库时非常有用。
最后,我们来总结一下这两种参数:
- 可选参数(?):侧重于 “有没有”。如果没传,则底层就是 undefined。其中,可选参数必须放在参数列表的最后。
- 默认参数(=):侧重于 “用什么”。如果没传,就用预设好的默认值。其中,默认参数可以放在任何位置。
