MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript可选参数与默认参数的使用技巧

2021-07-162.9k 阅读

可选参数

在前端开发中,当我们使用 TypeScript 编写函数时,有时某些参数并不是每次调用函数时都必须提供的。这时候,可选参数就派上了用场。

定义可选参数

在 TypeScript 里,我们通过在参数名后面加上 ? 来表示该参数是可选的。例如,我们有一个函数用于打印用户信息,其中用户的年龄可能是未知的,此时年龄参数就可以设为可选参数:

function printUserInfo(name: string, age?: number) {
    let message = `Name: ${name}`;
    if (age) {
        message += `, Age: ${age}`;
    }
    console.log(message);
}

printUserInfo('Alice');
printUserInfo('Bob', 30);

在上述代码中,printUserInfo 函数接受两个参数,name 是必选参数,age 是可选参数。我们可以只传入 name 调用函数,也可以同时传入 nameage

可选参数的位置限制

在 TypeScript 中,可选参数必须跟在必选参数之后。如果将可选参数放在必选参数之前,会导致编译错误。例如:

// 错误示例
function wrongOrder(age?: number, name: string) {
    let message = `Name: ${name}`;
    if (age) {
        message += `, Age: ${age}`;
    }
    console.log(message);
}
// 报错:A required parameter cannot follow an optional parameter.

上述代码中,age 作为可选参数放在了 name 这个必选参数之前,这是不允许的。这样的限制是为了确保函数调用时参数的传递逻辑清晰,避免混淆。

可选参数的类型检查

虽然可选参数在调用函数时可以不传递,但在函数内部使用时,我们需要进行类型检查,以防止潜在的运行时错误。比如,我们在上面的 printUserInfo 函数中,在使用 age 之前,先通过 if (age) 检查 age 是否有值。另外,TypeScript 会在编译阶段对可选参数的类型进行严格检查。假设我们错误地传入了一个非数字类型的值给 age 参数:

printUserInfo('Charlie', 'twenty');
// 报错:Argument of type '"twenty"' is not assignable to parameter of type 'number | undefined'.

TypeScript 会明确指出传入的 'twenty' 类型与 age 参数期望的 number | undefined 类型不匹配,这有助于我们在开发阶段尽早发现错误。

可选参数与函数重载

在涉及函数重载的场景中,可选参数也需要特别注意。函数重载允许我们为同一个函数定义多个不同的参数列表和返回类型。例如,我们有一个函数 add,既可以接受两个数字参数进行相加,也可以只接受一个数字参数,返回该数字与默认值 10 的和:

function add(a: number, b: number): number;
function add(a: number, b?: number): number {
    if (b) {
        return a + b;
    } else {
        return a + 10;
    }
}

let result1 = add(5, 3);
let result2 = add(7);

在上述代码中,首先定义了两个函数签名,第一个签名要求传入两个数字参数,第二个签名允许第二个参数可选。然后在函数实现中,根据 b 是否存在来执行不同的逻辑。这种方式在实际开发中非常实用,能够满足多种调用需求,同时保证类型安全。

默认参数

除了可选参数,TypeScript 还支持为参数设置默认值,即默认参数。默认参数在函数调用时如果没有传入相应的值,就会使用预先设定的默认值。

定义默认参数

我们通过在参数名后面使用 = 来为参数指定默认值。例如,我们有一个函数用于计算两个数的乘积,如果第二个数没有传入,就默认与 1 相乘:

function multiply(a: number, b = 1) {
    return a * b;
}

let product1 = multiply(5);
let product2 = multiply(5, 3);

multiply 函数中,b 参数设置了默认值 1。当我们调用 multiply(5) 时,b 会使用默认值 1,计算结果为 5;当调用 multiply(5, 3) 时,b 则使用传入的值 3,计算结果为 15。

默认参数的位置

与可选参数不同,默认参数可以放在参数列表的任意位置。例如:

function greet(name = 'Guest', message: string) {
    return `Hello, ${name}! ${message}`;
}

let greeting1 = greet('Alice', 'Welcome to our site');
let greeting2 = greet(undefined, 'Have a nice day');

greet 函数中,name 参数设置了默认值 'Guest',并且放在了 message 参数之前。在调用函数时,我们可以通过传入 undefined 来触发 name 使用默认值。

默认参数的类型推导

TypeScript 会根据默认值的类型来推导参数的类型。例如:

function divide(a: number, b = 2) {
    return a / b;
}
// 这里 b 的类型被推导为 number

由于 b 的默认值是 2,一个数字类型,所以 TypeScript 会自动将 b 的类型推导为 number。当然,我们也可以显式地指定参数类型,即使有默认值:

function divide(a: number, b: number = 2) {
    return a / b;
}

这种显式指定类型的方式在一些复杂的场景下可以让代码意图更加清晰,特别是当默认值的类型可能存在歧义时。

默认参数与作用域

在函数内部,默认参数表达式在函数调用时才会被求值,并且其求值的作用域是函数体内部。例如:

let base = 10;
function addToBase(num: number, offset = base + 5) {
    return num + offset;
}

let result = addToBase(3);
// 这里 offset 的值为 15,因为在函数调用时 base 的值为 10
base = 20;
let anotherResult = addToBase(3);
// 这里 offset 的值仍然为 15,因为 offset 的求值在函数调用时就已经确定

addToBase 函数中,offset 的默认值依赖于外部变量 base。在函数调用时,offset 会根据当时 base 的值进行计算,并且这个计算结果在函数调用期间是固定的,不会因为外部 base 值的改变而改变。

默认参数与函数重载

在函数重载的情况下使用默认参数需要小心。默认参数可能会影响函数重载的解析。例如:

function printValue(value: string): void;
function printValue(value: number, prefix = 'Number: '): void {
    console.log(prefix + value);
}

printValue('Hello');
printValue(5);

在上述代码中,第一个函数签名只接受一个字符串参数,第二个函数签名接受一个数字参数并且有一个默认参数 prefix。当我们调用 printValue('Hello') 时,会匹配第一个函数签名;当调用 printValue(5) 时,会匹配第二个函数签名并使用 prefix 的默认值。如果我们不小心定义了冲突的函数签名,就可能导致编译错误。比如:

function printValue(value: string): void;
function printValue(value: number, prefix: string = 'Number: '): void;
function printValue(value: any) {
    console.log(value);
}
// 报错:A 'default' initializer in an overload declaration is not permitted.

这里在函数重载声明中使用默认参数会导致错误,因为这可能会使函数重载的解析逻辑变得不明确。

可选参数与默认参数的混合使用

在实际开发中,我们经常会遇到需要同时使用可选参数和默认参数的场景。

合理组合

例如,我们有一个函数用于生成 HTML 元素,其中元素的文本内容是必选的,元素的类名是可选的,如果类名未传入则使用默认值 'default - class'

function createElement(tag: string, text: string, className?: string = 'default - class') {
    let element = document.createElement(tag);
    element.textContent = text;
    if (className) {
        element.className = className;
    } else {
        element.className = 'default - class';
    }
    return element;
}

let p1 = createElement('p', 'This is a paragraph');
let p2 = createElement('p', 'Another paragraph', 'custom - class');

createElement 函数中,className 既是可选参数,又有默认值。这样的设计既允许调用者不传入 className 而使用默认值,也允许传入自定义的 className

注意事项

当混合使用可选参数和默认参数时,要注意参数的顺序和类型检查。例如,确保默认参数的类型与可选参数的类型相匹配,避免出现类型错误。同时,也要注意函数调用时参数传递的逻辑,确保代码的可读性和可维护性。比如,如果有多个可选参数和默认参数,合理安排它们的顺序,使得函数调用时参数的意图更加清晰。例如:

function configureWidget(width: number, height: number, color?: string = 'black', isVisible?: boolean = true) {
    // 配置部件的逻辑
}

configureWidget(100, 200);
configureWidget(150, 250, 'red');
configureWidget(200, 300, 'blue', false);

configureWidget 函数中,按照参数的重要性和相关性安排了顺序。widthheight 是必选参数,因为它们对于定义部件的基本尺寸至关重要。color 是可选参数且有默认值,因为部件可以使用默认颜色。isVisible 同样是可选参数且有默认值,因为部件默认是可见的。这样的顺序安排使得在调用函数时,参数的传递逻辑更加自然和易于理解。

高级应用场景

函数柯里化中的应用

函数柯里化是指将一个多参数函数转换为一系列单参数函数的技术。可选参数和默认参数在函数柯里化中有着有趣的应用。例如,我们有一个函数用于计算三个数的乘积:

function multiplyThree(a: number, b: number, c: number) {
    return a * b * c;
}

// 使用柯里化和可选参数、默认参数
function curriedMultiply(a: number) {
    return function(b?: number, c: number = 1) {
        if (b === undefined) {
            return function(_c: number) {
                return a * _c;
            };
        } else {
            return a * b * c;
        }
    };
}

let multiplyByFive = curriedMultiply(5);
let result1 = multiplyByFive(3, 2);
let multiplyByFiveAndThen = multiplyByFive();
let result2 = multiplyByFiveAndThen(4);

在上述代码中,curriedMultiply 函数返回一个内部函数,这个内部函数利用了可选参数和默认参数的特性。如果只传入一个参数 a,返回的函数可以继续接受 bc 参数(c 有默认值 1)。如果第二次调用时没有传入 b,则返回一个只接受 c 参数的函数。这样就实现了灵活的柯里化操作,增加了函数的复用性和灵活性。

与接口和类型别名结合使用

在 TypeScript 中,我们经常会将函数参数与接口或类型别名结合使用,以提高代码的可维护性和类型安全性。例如,我们定义一个接口来表示用户信息,然后在函数中使用这个接口,并结合可选参数和默认参数:

interface User {
    name: string;
    age?: number;
    email?: string;
}

function displayUser(user: User, message: string = 'User information') {
    let info = `${message}: ${user.name}`;
    if (user.age) {
        info += `, Age: ${user.age}`;
    }
    if (user.email) {
        info += `, Email: ${user.email}`;
    }
    console.log(info);
}

let user1: User = { name: 'David' };
let user2: User = { name: 'Ella', age: 25, email: 'ella@example.com' };

displayUser(user1);
displayUser(user2, 'Detailed user info');

在上述代码中,User 接口定义了用户信息的结构,其中 ageemail 是可选属性。displayUser 函数接受一个 User 类型的对象和一个可选的 message 参数,通过这种方式,我们可以方便地处理和展示用户信息,并且在代码的不同部分可以复用 User 接口,保证类型的一致性。

在 React 组件中的应用

在 React 开发中,TypeScript 的可选参数和默认参数也有广泛的应用。例如,我们定义一个简单的 React 组件,用于显示一个按钮,按钮的文本是必选的,按钮的样式类名是可选的且有默认值:

import React from'react';

interface ButtonProps {
    text: string;
    className?: string;
}

const Button: React.FC<ButtonProps> = ({ text, className = 'default - button' }) => {
    return <button className={className}>{text}</button>;
};

export default Button;

在上述代码中,ButtonProps 接口定义了组件的属性,text 是必选属性,className 是可选属性且有默认值。在组件的实现中,通过解构赋值的方式获取属性值,并使用默认参数的特性为 className 设置默认值。这样在使用这个按钮组件时,调用者可以根据需要传入自定义的样式类名,或者使用默认的样式类名。

性能考虑

编译时优化

在 TypeScript 编译过程中,可选参数和默认参数的使用不会对编译后的 JavaScript 代码性能产生直接的负面影响。TypeScript 编译器会进行优化,在生成的 JavaScript 代码中,对于可选参数和默认参数的处理是高效的。例如,对于默认参数,编译器会在函数内部生成相应的逻辑来判断是否传入了参数值,如果未传入则使用默认值,这种逻辑的生成是经过优化的,不会引入过多的性能开销。

运行时影响

在运行时,可选参数和默认参数的使用也不会带来显著的性能问题。当函数调用时,检查可选参数是否存在以及使用默认参数的操作都是相对轻量级的。例如,在前面提到的 printUserInfo 函数中,检查 age 是否存在并使用相应逻辑的操作在现代 JavaScript 引擎中执行速度非常快。然而,如果在函数内部对可选参数或默认参数进行复杂的计算或操作,可能会影响函数的执行性能。比如,如果默认参数的计算涉及到复杂的算法或大量的数据处理,那么在每次函数调用时都会执行这些计算,可能会导致性能下降。因此,在设置默认参数时,应该尽量保证默认值的计算是简单和高效的。

代码复杂度与性能平衡

虽然可选参数和默认参数为我们编写代码提供了很大的便利,但过度使用可能会增加代码的复杂度。复杂的参数组合和逻辑判断可能会使代码的可读性和可维护性下降,间接影响开发效率和潜在的性能。例如,如果一个函数有多个可选参数和默认参数,并且它们之间存在复杂的依赖关系,那么在调用函数时很难快速理解每个参数的作用和预期值。在这种情况下,我们需要在代码的便利性和复杂度之间找到平衡。可以考虑将复杂的参数逻辑封装到单独的函数或模块中,以提高代码的清晰度和性能。

常见错误与解决方法

可选参数未检查导致的错误

在使用可选参数时,最常见的错误之一是在函数内部未对可选参数进行检查就直接使用。例如:

function calculateArea(radius?: number) {
    return Math.PI * radius * radius;
    // 报错:Object is possibly 'undefined'.
}

在上述代码中,radius 是可选参数,但在函数内部直接使用 radius 进行计算,而没有检查 radius 是否存在,这会导致编译错误。解决方法是在使用 radius 之前进行检查:

function calculateArea(radius?: number) {
    if (radius) {
        return Math.PI * radius * radius;
    }
    return 0;
}

通过这种方式,我们可以避免在 radiusundefined 时出现运行时错误。

默认参数类型不匹配错误

当设置默认参数时,如果默认值的类型与参数预期类型不匹配,会导致编译错误。例如:

function divideNumbers(a: number, b: number = '2') {
    return a / b;
    // 报错:Type '"2"' is not assignable to type 'number'.
}

在上述代码中,b 的默认值是字符串 '2',与参数预期的 number 类型不匹配。解决方法是确保默认值的类型正确:

function divideNumbers(a: number, b: number = 2) {
    return a / b;
}

函数重载与默认参数冲突错误

如前面提到的,在函数重载时使用默认参数可能会导致冲突错误。例如:

function formatDate(date: Date): string;
function formatDate(date: string, format: string = 'yyyy - mm - dd'): string;
function formatDate(date: any) {
    // 具体实现
}
// 报错:A 'default' initializer in an overload declaration is not permitted.

解决这种错误的方法是将默认参数放在函数实现中,而不是在函数重载声明中:

function formatDate(date: Date): string;
function formatDate(date: string): string;
function formatDate(date: any, format: string = 'yyyy - mm - dd') {
    // 具体实现
}

通过这种方式,既可以实现函数重载的功能,又能合理使用默认参数。

最佳实践

参数设计原则

在设计函数的参数时,应该遵循一些原则。首先,参数的数量应该尽量保持合理,避免函数接受过多的参数,导致函数的调用和维护变得复杂。如果确实需要多个参数,可以考虑将相关参数封装成对象,使用对象解构来获取参数值,这样可以提高代码的可读性和可维护性。例如:

function drawRectangle({ x, y, width, height, color = 'black' }: {
    x: number;
    y: number;
    width: number;
    height: number;
    color?: string;
}) {
    // 绘制矩形的逻辑
}

drawRectangle({ x: 10, y: 20, width: 100, height: 50 });
drawRectangle({ x: 30, y: 40, width: 150, height: 80, color:'red' });

在上述代码中,将矩形的相关参数封装成一个对象,并使用对象解构来获取参数值,同时为 color 参数设置了默认值。这样在调用函数时,参数的意义更加明确,并且可以方便地添加或修改参数。

文档化参数

对于函数的参数,尤其是可选参数和默认参数,应该进行充分的文档化。可以使用 JSDoc 等工具来为函数添加注释,说明每个参数的作用、类型、是否可选以及默认值的含义。例如:

/**
 * 计算两个数的和
 * @param a 第一个数字
 * @param b 第二个数字,可选,默认值为 0
 * @returns 两个数的和
 */
function sum(a: number, b?: number) {
    if (b === undefined) {
        b = 0;
    }
    return a + b;
}

通过这样的文档化,其他开发人员在使用这个函数时可以快速了解参数的相关信息,减少错误的发生。

测试参数逻辑

在编写测试用例时,要特别关注可选参数和默认参数的逻辑。确保函数在各种参数组合下都能正确运行。例如,对于前面的 printUserInfo 函数,我们可以编写如下测试用例:

import { expect } from 'chai';

function printUserInfo(name: string, age?: number) {
    let message = `Name: ${name}`;
    if (age) {
        message += `, Age: ${age}`;
    }
    return message;
}

describe('printUserInfo', () => {
    it('should print only name when age is not provided', () => {
        let result = printUserInfo('Frank');
        expect(result).to.equal('Name: Frank');
    });

    it('should print name and age when age is provided', () => {
        let result = printUserInfo('Grace', 28);
        expect(result).to.equal('Name: Grace, Age: 28');
    });
});

通过这样的测试用例,可以确保函数在不同参数情况下的正确性,提高代码的质量。