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

TypeScript可选和默认参数的使用

2023-08-034.6k 阅读

TypeScript 中的可选参数

在 TypeScript 编程中,函数的参数有时并非总是必需的。比如,我们可能有一个函数用于获取用户信息,通常情况下需要用户名作为参数,但在某些特殊场景下,比如从当前登录会话中自动获取用户信息时,用户名参数就不是必须的。这时候,可选参数就派上用场了。

定义可选参数的语法

在 TypeScript 里,定义可选参数非常简单,只需在参数名后面加上一个问号 ?。例如:

function greet(name?: string) {
    if (name) {
        console.log(`Hello, ${name}!`);
    } else {
        console.log('Hello, stranger!');
    }
}

greet(); 
greet('John'); 

在上述代码中,greet 函数的 name 参数是可选的。当我们调用 greet 函数时,可以不传参数,此时函数会输出 Hello, stranger!;也可以传入一个字符串参数,函数则会输出 Hello, [传入的名字]!

可选参数的类型检查

TypeScript 会对可选参数进行严格的类型检查。即使参数是可选的,但一旦传入,其类型必须符合定义。例如:

function printNumber(num?: number) {
    if (num) {
        console.log(num.toFixed(2));
    }
}

printNumber(); 
printNumber(10); 
printNumber('ten'); 

在这段代码中,当我们尝试调用 printNumber('ten') 时,TypeScript 编译器会报错,因为 printNumber 函数期望的可选参数 num 类型是 number,而我们传入了一个 string 类型的值。

可选参数与必选参数的顺序

在 TypeScript 函数定义中,可选参数必须放在必选参数之后。例如:

function calculate(a: number, b: number, operation?: string) {
    if (operation === '+') {
        return a + b;
    } else if (operation === '-') {
        return a - b;
    } else {
        return a * b;
    }
}

console.log(calculate(2, 3)); 
console.log(calculate(2, 3, '+')); 

calculate 函数中,ab 是必选参数,operation 是可选参数。如果将可选参数放在必选参数之前,TypeScript 编译器会报错。因为在调用函数时,参数是按照顺序匹配的,如果可选参数在前,编译器无法确定后续传入的参数应该匹配哪个必选参数。

TypeScript 中的默认参数

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

设置默认参数的语法

在 TypeScript 中,为参数设置默认值的语法很直观,直接在参数定义后使用 = 赋值即可。例如:

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

console.log(multiply(5)); 
console.log(multiply(5, 3)); 

multiply 函数中,b 参数有一个默认值 1。当我们调用 multiply(5) 时,由于没有传入 b 的值,函数会使用默认值 1,即相当于调用 multiply(5, 1),返回 5。而当我们调用 multiply(5, 3) 时,函数会使用传入的 3 作为 b 的值,返回 15

默认参数的类型推导

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

function greet(name: string = 'Guest') {
    console.log(`Hello, ${name}!`);
}

greet(); 
greet('Alice'); 

greet 函数中,由于默认值 Guest 是字符串类型,所以 name 参数的类型被推导为 string。即使我们在定义函数时没有显式声明 name 的类型为 string,TypeScript 也能正确推断并进行类型检查。

默认参数与函数重载

在使用函数重载时,默认参数需要特别注意。函数重载允许我们为同一个函数定义多个不同的参数列表和返回类型。当存在默认参数时,可能会影响重载的解析。例如:

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

console.log(addNumbers(5)); 
console.log(addNumbers(5, 3)); 

在上述代码中,我们定义了两个 addNumbers 函数的重载。第一个重载定义了两个必选的 number 类型参数,第二个重载定义了一个必选和一个可选的 number 类型参数。由于第二个重载中的 b 参数有默认值处理逻辑,所以在调用 addNumbers(5) 时,会匹配第二个重载;调用 addNumbers(5, 3) 时,两个重载都能匹配,但 TypeScript 会选择最精确的匹配,即第一个重载。

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

在实际编程中,我们可能会遇到既需要可选参数又需要默认参数的情况。TypeScript 很好地支持了这种混合使用。

混合使用的示例

function displayInfo(name?: string, age: number = 18) {
    if (name) {
        console.log(`${name} is ${age} years old.`);
    } else {
        console.log(`The person is ${age} years old.`);
    }
}

displayInfo(); 
displayInfo('Bob'); 
displayInfo('Bob', 25); 

displayInfo 函数中,name 是可选参数,age 是带有默认值 18 的参数。当我们调用 displayInfo() 时,会使用 age 的默认值 18,输出 The person is 18 years old.;调用 displayInfo('Bob') 时,同样使用 age 的默认值,输出 Bob is 18 years old.;调用 displayInfo('Bob', 25) 时,则会根据传入的参数输出 Bob is 25 years old.

混合使用时的注意事项

在混合使用可选参数和默认参数时,同样要遵循可选参数放在必选参数(包括有默认值的必选参数)之后的规则。例如,以下代码是错误的:

function wrongFunction(name: string = 'Default', age?: number) {
    // 代码逻辑
} 

TypeScript 编译器会报错,因为 age 作为可选参数放在了有默认值的 name 参数之后。正确的做法是将 age 参数放在 name 参数之前,即 function correctFunction(age?: number, name: string = 'Default') {... }

可选参数和默认参数在接口和类型别名中的应用

在 TypeScript 中,接口和类型别名常用于定义对象的形状或函数类型。可选参数和默认参数在这些场景下也有相应的应用方式。

在接口定义函数类型中的应用

interface MathOperation {
    (a: number, b: number, operation?: string): number;
}

const calculate: MathOperation = (a, b, operation) => {
    if (operation === '+') {
        return a + b;
    } else if (operation === '-') {
        return a - b;
    } else {
        return a * b;
    }
};

console.log(calculate(2, 3)); 
console.log(calculate(2, 3, '+')); 

在上述代码中,我们定义了一个接口 MathOperation,它描述了一个函数类型,其中 operation 参数是可选的。然后我们创建了一个符合该接口的函数 calculate。这种方式使得我们可以通过接口来规范函数的参数类型,包括可选参数的类型。

在类型别名定义函数类型中的应用

type StringFormatter = (text: string, prefix?: string, suffix?: string) => string;

const formatText: StringFormatter = (text, prefix, suffix) => {
    let result = text;
    if (prefix) {
        result = prefix + result;
    }
    if (suffix) {
        result = result + suffix;
    }
    return result;
};

console.log(formatText('Hello')); 
console.log(formatText('Hello', 'Prefix - ')); 
console.log(formatText('Hello', 'Prefix - ', ' - Suffix')); 

这里我们使用类型别名 StringFormatter 定义了一个函数类型,其中 prefixsuffix 都是可选参数。通过类型别名,我们可以方便地复用这个函数类型定义,并且 TypeScript 会根据类型别名对函数实现进行严格的类型检查。

可选参数和默认参数对代码可维护性和扩展性的影响

提高代码的可维护性

使用可选参数和默认参数可以使代码更加清晰和易于维护。例如,在一个大型项目中,如果有一个函数经常被调用,但在某些情况下需要传入不同的参数组合。使用可选参数和默认参数,我们可以避免为每种参数组合都定义一个单独的函数。

function loadData(url: string, options?: { method: string; headers?: any }) {
    // 这里可以根据传入的 options 来决定如何加载数据
    let requestOptions: any = { method: 'GET' };
    if (options) {
        requestOptions.method = options.method;
        if (options.headers) {
            requestOptions.headers = options.headers;
        }
    }
    // 实际的加载数据逻辑,这里省略
    console.log(`Loading data from ${url} with options: ${JSON.stringify(requestOptions)}`);
}

loadData('https://example.com/api/data'); 
loadData('https://example.com/api/data', { method: 'POST' }); 
loadData('https://example.com/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); 

loadData 函数中,options 是一个可选参数,它是一个对象,其中 method 是必选属性,headers 是可选属性。这种方式使得函数调用更加灵活,同时也使得代码在维护时,对于不同的调用场景一目了然,只需要在函数内部根据传入的参数进行相应处理即可,而不需要在项目的多个地方重复编写类似的逻辑。

增强代码的扩展性

可选参数和默认参数也有助于代码的扩展性。当项目需求发生变化,需要在函数中添加新的参数时,如果合理使用可选参数和默认参数,就可以减少对现有代码调用处的修改。

function sendEmail(to: string, subject: string, body: string, cc?: string[], bcc?: string[], priority?: 'high' | 'low' | 'normal') {
    // 发送邮件的逻辑,这里省略
    console.log(`Sending email to ${to} with subject: ${subject}, body: ${body}`);
    if (cc) {
        console.log(`CC: ${cc.join(', ')}`);
    }
    if (bcc) {
        console.log(`BCC: ${bcc.join(', ')}`);
    }
    if (priority) {
        console.log(`Priority: ${priority}`);
    }
}

sendEmail('recipient@example.com', 'Important Message', 'This is the body of the email'); 
sendEmail('recipient@example.com', 'Another Message', 'Another body', ['cc1@example.com']); 
sendEmail('recipient@example.com', 'Urgent Message', 'Urgent body', ['cc1@example.com'], ['bcc1@example.com'], 'high'); 

假设最初 sendEmail 函数只需要 tosubjectbody 三个参数来发送邮件。随着项目发展,可能需要支持抄送(cc)、密送(bcc)以及设置邮件优先级(priority)。通过将新添加的参数设置为可选参数,我们可以在不影响现有代码调用的情况下,轻松扩展函数的功能。调用处如果不需要新功能,就可以继续按照原来的方式调用函数;如果需要新功能,只需要传入相应的参数即可。

可选参数和默认参数在不同编程场景中的应用实例

在工具函数中的应用

function debounce(func: Function, delay: number = 300) {
    let timer: NodeJS.Timeout;
    return function (...args: any[]) {
        if (timer) {
            clearTimeout(timer);
        }
        timer = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

function expensiveOperation() {
    console.log('Performing expensive operation...');
}

const debouncedOperation = debounce(expensiveOperation, 500);

// 多次调用 debouncedOperation 不会立即执行 expensiveOperation,而是在最后一次调用后的 500ms 执行
debouncedOperation();
debouncedOperation();

在上述代码中,debounce 函数是一个常用的工具函数,用于防止函数在短时间内被频繁调用。它接受一个函数 func 和一个可选的延迟时间 delay,默认值为 300 毫秒。这里的默认参数 delay 使得该工具函数在大多数常见场景下可以直接使用默认延迟,而在有特殊需求时,又可以传入自定义的延迟时间。

在类方法中的应用

class Logger {
    private prefix: string;

    constructor(prefix: string = '[Default]') {
        this.prefix = prefix;
    }

    log(message: string, level: 'info' | 'error' | 'warn' = 'info') {
        let levelPrefix = '';
        if (level === 'error') {
            levelPrefix = '[ERROR] ';
        } else if (level === 'warn') {
            levelPrefix = '[WARN] ';
        } else {
            levelPrefix = '[INFO] ';
        }
        console.log(`${this.prefix} ${levelPrefix}${message}`);
    }
}

const logger = new Logger('[MyApp]');
logger.log('This is an info message'); 
logger.log('This is a warning', 'warn'); 
logger.log('This is an error', 'error'); 

Logger 类中,构造函数的 prefix 参数有一个默认值 [Default],这使得在创建 Logger 实例时,如果没有传入自定义的前缀,就会使用默认前缀。log 方法的 level 参数也有一个默认值 info,这样在大多数情况下记录普通信息时,不需要显式传入 info 级别,而在记录警告或错误信息时,则可以传入相应的级别。

在 React 组件中的应用(以函数式组件为例)

import React from'react';

interface ButtonProps {
    text: string;
    onClick: () => void;
    disabled?: boolean;
    className?: string;
}

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

const handleClick = () => {
    console.log('Button clicked!');
};

export default () => {
    return (
        <div>
            <Button text="Click me" onClick={handleClick} />
            <Button text="Disabled button" onClick={handleClick} disabled={true} />
            <Button text="Styled button" onClick={handleClick} className="btn btn-primary" />
        </div>
    );
};

在这个 React 函数式组件 Button 中,disabledclassName 是可选参数且有默认值。这使得在使用该组件时,大多数情况下可以使用默认的启用状态和空的类名。只有在需要特定的禁用状态或添加自定义样式类时,才传入相应的值。这种方式提高了组件的复用性和灵活性,使得组件在不同的页面和场景中都能方便地使用。

可选参数和默认参数的性能考虑

函数调用的性能

从函数调用的性能角度来看,可选参数和默认参数本身并不会带来显著的性能开销。在 JavaScript(TypeScript 最终会编译为 JavaScript)中,函数调用时参数的传递和处理是一个相对高效的过程。当函数定义了可选参数或默认参数时,引擎在执行函数时会按照既定的规则进行参数匹配和默认值设置。例如,对于有默认参数的函数,引擎会在函数执行时检查是否传入了相应参数,如果没有传入则使用默认值,这个检查和赋值操作的开销非常小,在大多数应用场景下可以忽略不计。

function add(a: number, b: number = 0) {
    return a + b;
}

let start = Date.now();
for (let i = 0; i < 1000000; i++) {
    add(i);
}
let end = Date.now();
console.log(`Time taken: ${end - start} ms`); 

在上述代码中,我们对一个带有默认参数的函数进行了一百万次调用,并记录了调用所花费的时间。通过实际测试可以发现,即使进行大量调用,默认参数带来的性能影响几乎可以忽略不计。

代码编译和打包的性能

在 TypeScript 项目的编译和打包过程中,可选参数和默认参数也不会对性能产生明显的负面影响。TypeScript 编译器在编译时会对函数参数进行类型检查和处理,包括可选参数和默认参数的相关逻辑。这个过程是编译器正常的工作流程,虽然会增加一定的编译时间,但对于现代的编译器和开发工具来说,这种增加的时间通常也是可以接受的。 在打包阶段,如果使用像 Webpack 这样的工具,打包过程主要关注的是模块的解析、合并和压缩等操作,可选参数和默认参数在这个阶段同样不会成为性能瓶颈。除非项目规模极其庞大,函数定义和调用极其复杂,否则不必过于担心可选参数和默认参数对编译和打包性能的影响。

可选参数和默认参数与其他 TypeScript 特性的结合

与剩余参数的结合

TypeScript 中的剩余参数允许我们将多个参数收集到一个数组中。它可以与可选参数和默认参数很好地结合使用。

function sumNumbers(a: number, b: number = 1, ...rest: number[]) {
    let total = a + b;
    for (let num of rest) {
        total += num;
    }
    return total;
}

console.log(sumNumbers(2)); 
console.log(sumNumbers(2, 3)); 
console.log(sumNumbers(2, 3, 4, 5)); 

sumNumbers 函数中,a 是必选参数,b 是带有默认值的参数,rest 是剩余参数。这种结合方式使得函数可以接受不同数量的参数,并且在处理参数时更加灵活。当调用 sumNumbers(2) 时,b 使用默认值 1rest 为空数组;当调用 sumNumbers(2, 3, 4, 5) 时,a2b3rest 包含 45

与泛型的结合

泛型是 TypeScript 中非常强大的特性,它可以与可选参数和默认参数结合,进一步增强函数的灵活性和复用性。

function identity<T>(value: T, message?: string): T {
    if (message) {
        console.log(message);
    }
    return value;
}

let result1 = identity(10); 
let result2 = identity('Hello', 'This is a string'); 

identity 函数中,T 是一个泛型类型参数,value 是必选参数,message 是可选参数。通过泛型,我们可以让函数接受任何类型的值,并在需要时输出一条可选的消息。这种结合方式使得函数可以在不同类型的数据上复用相同的逻辑,同时又能根据具体需求决定是否传入可选参数。

最佳实践与常见错误避免

最佳实践

  1. 合理使用默认参数:在函数参数有合理的默认值时,应优先使用默认参数。这样可以简化函数调用,提高代码的可读性。例如,在一个格式化日期的函数中,如果大多数情况下使用本地时区,可以将时区参数设置为默认值。
function formatDate(date: Date, format: string = 'YYYY - MM - DD', timezone: string = 'local') {
    // 日期格式化逻辑,这里省略
    return `Formatted date: ${date.toISOString()}`;
}
  1. 谨慎使用可选参数:可选参数应在真正必要时使用,避免过度使用导致函数逻辑复杂难以理解。如果一个函数的可选参数过多,可能需要重新设计函数,将其拆分成多个更专注的函数。
  2. 文档化参数:无论是可选参数还是默认参数,都应该在函数的文档中清晰地说明其用途、默认值(如果有)以及可能的取值范围。这样可以帮助其他开发者更好地理解和使用该函数。例如:
/**
 * 发送 HTTP 请求
 * @param url 请求的 URL
 * @param options 可选的请求选项,默认值为 { method: 'GET', headers: {} }
 * @returns 响应数据
 */
function sendRequest(url: string, options?: { method: string; headers: any }): Promise<any> {
    // 请求逻辑,这里省略
}

常见错误避免

  1. 忽略类型检查:在使用可选参数和默认参数时,要确保传入的参数类型与定义的类型一致。即使参数是可选的,一旦传入,也必须符合类型要求。例如,不要将一个 string 类型的值传入期望为 number 类型的可选参数。
  2. 可选参数顺序错误:牢记可选参数必须放在必选参数之后,否则会导致编译错误。例如,function wrongOrder(optional?: string, required: number) {... } 这样的定义是错误的,应该改为 function correctOrder(required: number, optional?: string) {... }
  3. 默认参数在重载中的问题:在函数重载时,要注意默认参数可能对重载解析产生的影响。确保重载定义清晰,避免出现意外的函数调用匹配。例如,在定义重载时,要考虑不同参数组合下函数的行为和返回类型,避免模糊不清的定义。

通过深入理解和正确使用 TypeScript 中的可选参数和默认参数,我们可以编写出更加灵活、可读和可维护的代码。在实际项目中,根据具体的需求和场景,合理运用这些特性,能够显著提高开发效率和代码质量。同时,注意与其他 TypeScript 特性的结合使用以及遵循最佳实践,避免常见错误,将有助于打造出高质量的 TypeScript 项目。