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

TypeScript可选参数与默认参数的综合应用场景分析

2022-09-042.5k 阅读

一、TypeScript 可选参数基础

在前端开发中,当我们定义函数时,并非所有参数都是必填的。TypeScript 允许我们声明可选参数,这样在调用函数时可以选择性地提供这些参数。可选参数通过在参数名后添加问号(?)来表示。

function greet(name: string, message?: string) {
    if (message) {
        return `Hello, ${name}! ${message}`;
    }
    return `Hello, ${name}!`;
}

console.log(greet('Alice'));
console.log(greet('Bob', 'How are you?'));

在上述代码中,message 参数是可选的。当我们调用 greet 函数时,可以只传入 name 参数,也可以同时传入 namemessage 参数。如果传入了 message 参数,函数会在问候语后添加该消息;否则,只返回基本的问候语。

二、默认参数的概念

默认参数是指在函数定义时为参数提供一个默认值。当调用函数时,如果没有传递该参数,函数将使用默认值。在 TypeScript 中,默认参数的定义方式与 JavaScript 类似,即在参数名后使用赋值语句指定默认值。

function calculateArea(radius: number, pi = 3.14159) {
    return pi * radius * radius;
}

console.log(calculateArea(5));
console.log(calculateArea(5, 3.14));

calculateArea 函数中,pi 参数有一个默认值 3.14159。如果调用函数时没有提供 pi 的值,函数将使用默认值进行面积计算;如果提供了 pi 的值,则使用传入的值。

三、可选参数与默认参数的区别

  1. 定义方式
    • 可选参数通过在参数名后加问号(?)定义,而默认参数通过在参数名后使用赋值语句定义。
    • 例如:
// 可选参数
function func1(param1: string, param2?: number) {}

// 默认参数
function func2(param1: string, param2 = 10) {}
  1. 调用时的行为
    • 对于可选参数,调用函数时可以完全省略该参数。而对于默认参数,虽然可以省略不写,但它实际上是作为函数调用的一部分存在的,只是使用了默认值。
    • 例如:
function optionalFunc(a: string, b?: number) {
    console.log(a, b);
}

function defaultFunc(a: string, b = 10) {
    console.log(a, b);
}

optionalFunc('test');
defaultFunc('test');

在上述代码中,optionalFunc 调用时 b 可以不存在,而 defaultFunc 调用时 b 实际上是存在的,只是值为默认的 10。 3. 类型推断 - 可选参数的类型推断相对宽松,因为它可以不存在。默认参数的类型推断则基于默认值的类型。 - 例如:

function optionalWithInference(a: string, b?: number) {
    if (b) {
        return a + b.toString();
    }
    return a;
}

function defaultWithInference(a: string, b = 'default') {
    return a + b;
}

optionalWithInference 函数中,由于 b 是可选的,我们在使用 b 前需要进行存在性检查。而在 defaultWithInference 函数中,b 的类型被推断为 string,因为默认值是字符串。

四、可选参数与默认参数的综合应用场景

  1. 构建灵活的 UI 组件 在前端开发中,UI 组件常常需要根据不同的场景展示不同的样式或行为。例如,我们创建一个按钮组件,它可能有不同的颜色主题和文本内容。
interface ButtonProps {
    text: string;
    theme?: 'primary' | 'secondary' | 'danger';
    disabled?: boolean;
}

function Button({ text, theme = 'primary', disabled = false }: ButtonProps) {
    const themeClass = theme === 'primary'? 'btn-primary' : theme === 'secondary'? 'btn-secondary' : 'btn-danger';
    return `<button class="${themeClass} ${disabled? 'disabled' : ''}">${text}</button>`;
}

console.log(Button({ text: 'Click me' }));
console.log(Button({ text: 'Cancel', theme:'secondary' }));
console.log(Button({ text: 'Delete', theme: 'danger', disabled: true }));

在上述代码中,themedisabled 是可选参数,并且提供了默认值。通过这种方式,我们可以轻松地根据不同的需求创建具有不同样式和状态的按钮,既保证了灵活性,又不需要在每次使用时都指定所有参数。

  1. 数据获取与处理函数 在处理 API 数据获取时,我们常常需要根据不同的条件来请求数据。例如,我们有一个获取用户列表的函数,可以根据页码和每页数量来分页获取数据,同时还可以提供一个搜索关键词来过滤数据。
interface User {
    id: number;
    name: string;
    email: string;
}

async function fetchUsers(page: number = 1, perPage: number = 10, search?: string): Promise<User[]> {
    // 模拟 API 请求
    const response = await fetch(`https://example.com/api/users?page=${page}&perPage=${perPage}${search? `&search=${search}` : ''}`);
    return response.json();
}

fetchUsers().then(users => console.log(users));
fetchUsers(2, 20).then(users => console.log(users));
fetchUsers(1, 10, 'john').then(users => console.log(users));

在这个例子中,pageperPage 有默认值,search 是可选参数。这样,我们可以根据不同的需求调用该函数,比如获取第一页默认数量的数据,或者根据特定的页码、每页数量以及搜索关键词来获取过滤后的数据。

  1. 函数式编程与组合函数 在函数式编程中,我们经常创建一些可以组合使用的函数。这些函数可能有一些参数是可选的,并且有默认值,以便在不同的组合场景下使用。
function add(a: number, b: number) {
    return a + b;
}

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

function operate(a: number, b: number, operator: (a: number, b: number) => number = add) {
    return operator(a, b);
}

console.log(operate(2, 3));
console.log(operate(2, 3, multiply));

在上述代码中,operate 函数接受两个数字和一个操作函数作为参数,操作函数参数有默认值 add。这样,我们既可以使用默认的加法操作,也可以传入其他操作函数(如 multiply)来进行不同的运算,体现了函数的灵活性和可组合性。

  1. 事件处理函数 在前端开发中,事件处理函数常常需要处理不同的事件对象属性。例如,在处理鼠标点击事件时,我们可能需要获取点击的坐标,但有些情况下我们只关心事件本身,而不需要坐标信息。
function handleClick(event: MouseEvent, logCoords: boolean = false) {
    if (logCoords) {
        console.log(`Clicked at x: ${event.clientX}, y: ${event.clientY}`);
    } else {
        console.log('Clicked');
    }
}

document.addEventListener('click', (event) => handleClick(event));
document.addEventListener('click', (event) => handleClick(event, true));

在这个例子中,logCoords 是一个默认参数。当我们只关心点击事件时,可以不传入 logCoords,函数将只打印“Clicked”。当我们需要获取点击坐标时,传入 true,函数将打印坐标信息。

五、注意事项与最佳实践

  1. 参数顺序 在函数定义中,最好将必填参数放在前面,可选参数和默认参数放在后面。这样可以提高代码的可读性,并且符合大多数开发者的习惯。
// 推荐
function func1(a: string, b?: number, c = 'default') {}

// 不推荐
function func2(b?: number, c = 'default', a: string) {}
  1. 避免过度使用默认参数 虽然默认参数提供了很大的便利性,但过度使用可能会导致代码难以理解和维护。尽量确保默认参数的使用是合理且必要的,对于复杂的逻辑,考虑使用其他设计模式来处理。
  2. 类型兼容性 在使用可选参数和默认参数时,要确保参数类型的兼容性。特别是当使用默认参数时,要注意默认值的类型与函数内部逻辑的一致性。
function func3(a: number, b = 'default') {
    // 错误,类型不兼容
    return a + b;
}

在上述代码中,anumber 类型,而 bstring 类型,直接相加会导致类型错误。

  1. 文档化 对于包含可选参数和默认参数的函数,一定要提供清晰的文档说明。这样其他开发者在使用你的函数时,能够清楚地了解每个参数的作用、是否可选以及默认值的含义。
/**
 * 计算两个数的运算结果
 * @param a 第一个数字
 * @param b 第二个数字
 * @param operator 运算函数,默认是加法
 * @returns 运算结果
 */
function operate(a: number, b: number, operator: (a: number, b: number) => number = add) {
    return operator(a, b);
}

六、结合接口与类型别名

  1. 使用接口定义参数对象 在处理多个可选参数和默认参数时,使用接口来定义参数对象可以提高代码的可读性和可维护性。
interface LoginOptions {
    username: string;
    password: string;
    rememberMe?: boolean;
    captcha?: string;
}

function login({ username, password, rememberMe = false, captcha }: LoginOptions) {
    // 登录逻辑
    console.log(`Logging in user ${username} with rememberMe: ${rememberMe} and captcha: ${captcha}`);
}

login({ username: 'user1', password: 'pass1' });
login({ username: 'user2', password: 'pass2', rememberMe: true, captcha: '1234' });

在上述代码中,LoginOptions 接口定义了登录所需的参数,其中 rememberMecaptcha 是可选参数。通过将参数对象作为函数参数,我们可以更清晰地组织和传递参数。

  1. 类型别名的应用 类型别名也可以用于定义包含可选参数和默认参数的函数类型,这在函数作为参数传递或返回值时非常有用。
type MathOperation = (a: number, b: number, multiplier?: number) => number;

function performOperation(operation: MathOperation, a: number, b: number) {
    return operation(a, b);
}

function addWithMultiplier(a: number, b: number, multiplier = 1) {
    return (a + b) * multiplier;
}

console.log(performOperation(addWithMultiplier, 2, 3));
console.log(performOperation((a, b, multiplier = 2) => (a + b) * multiplier, 2, 3));

在这个例子中,MathOperation 类型别名定义了一个函数类型,其中 multiplier 是可选参数。performOperation 函数接受一个符合 MathOperation 类型的函数作为参数,并调用它进行计算。

七、与 JavaScript 的交互

  1. 调用 JavaScript 函数 当在 TypeScript 项目中调用 JavaScript 函数时,如果该函数有可选参数或默认参数,TypeScript 会根据 JavaScript 的行为进行类型推断。

假设我们有一个 JavaScript 文件 utils.js

function sum(a, b = 10) {
    return a + b;
}

module.exports = { sum };

在 TypeScript 文件中调用:

import { sum } from './utils.js';

console.log(sum(5));
console.log(sum(5, 20));

TypeScript 会正确识别 sum 函数的 b 参数有默认值,并允许我们按照 JavaScript 的方式调用。 2. 将 TypeScript 函数暴露给 JavaScript 如果我们在 TypeScript 中定义了一个带有可选参数或默认参数的函数,并希望在 JavaScript 中使用,需要注意编译后的代码。TypeScript 会将默认参数的逻辑转换为 JavaScript 兼容的形式。

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

export { greet };

编译后的 JavaScript 代码:

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

exports.greet = greet;

JavaScript 代码可以正常调用这个函数:

const { greet } = require('./greet.js');
console.log(greet('Alice'));
console.log(greet('Bob', 'Hi'));

八、在 React 开发中的应用

  1. React 组件属性 在 React 开发中,组件的属性常常包含可选参数和默认值。例如,我们创建一个 Card 组件,它可以有标题、内容和可选的样式类。
import React from'react';

interface CardProps {
    title: string;
    content: string;
    className?: string;
}

const Card: React.FC<CardProps> = ({ title, content, className = 'card' }) => {
    return (
        <div className={className}>
            <h2>{title}</h2>
            <p>{content}</p>
        </div>
    );
};

export default Card;

在使用 Card 组件时:

import React from'react';
import Card from './Card';

const App: React.FC = () => {
    return (
        <div>
            <Card title="Default Card" content="This is the content of the default card" />
            <Card title="Styled Card" content="This is a styled card" className="card-styled" />
        </div>
    );
};

export default App;

在上述代码中,className 是可选参数并提供了默认值。这样,我们可以轻松地根据需要为 Card 组件添加自定义样式,而不需要每次都指定 className。 2. React Hook 的参数 React Hook 也可以利用可选参数和默认参数来提供更灵活的功能。例如,我们创建一个自定义 Hook 来获取数据,并可以根据不同的条件进行缓存和重试。

import { useState, useEffect } from'react';

interface FetchOptions {
    cacheTime?: number;
    maxRetries?: number;
}

function useFetch<T>(url: string, options: FetchOptions = { cacheTime: 60000, maxRetries: 3 }) {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);

    useEffect(() => {
        let cache: T | null = null;
        let timer: NodeJS.Timeout | null = null;
        let retries = 0;

        const fetchData = async () => {
            if (cache && Date.now() - (timer as number) < options.cacheTime) {
                setData(cache);
                setLoading(false);
                return;
            }

            try {
                const response = await fetch(url);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const result = await response.json();
                cache = result;
                timer = Date.now();
                setData(result);
            } catch (e) {
                if (retries < options.maxRetries) {
                    retries++;
                    await new Promise(resolve => setTimeout(resolve, 1000));
                    await fetchData();
                } else {
                    setError((e as Error).message);
                }
            } finally {
                setLoading(false);
            }
        };

        fetchData();

        return () => {
            if (timer) {
                clearTimeout(timer);
            }
        };
    }, [url, options]);

    return { data, loading, error };
}

在使用这个 Hook 时:

import React from'react';
import useFetch from './useFetch';

const App: React.FC = () => {
    const { data, loading, error } = useFetch('https://example.com/api/data');
    const { data: customData, loading: customLoading, error: customError } = useFetch('https://example.com/api/other-data', { cacheTime: 120000, maxRetries: 5 });

    if (loading) {
        return <div>Loading...</div>;
    }

    if (error) {
        return <div>Error: {error}</div>;
    }

    return (
        <div>
            <h1>Data</h1>
            <pre>{JSON.stringify(data, null, 2)}</pre>
            <h1>Custom Data</h1>
            <pre>{JSON.stringify(customData, null, 2)}</pre>
        </div>
    );
};

export default App;

在这个例子中,useFetch Hook 接受一个 URL 和可选的 FetchOptions 参数,其中 cacheTimemaxRetries 有默认值。通过这种方式,我们可以根据不同的需求配置数据获取的行为,提高了代码的复用性和灵活性。

九、在 Vue 开发中的应用

  1. Vue 组件属性 在 Vue 开发中,我们可以在组件定义中使用可选参数和默认参数来设置组件的属性。例如,创建一个 Dialog 组件,它有标题、内容、是否显示以及关闭按钮的文本。
<template>
    <div v-if="visible" class="dialog">
        <h2>{{ title }}</h2>
        <p>{{ content }}</p>
        <button @click="close">{{ closeText }}</button>
    </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';

interface DialogProps {
    title: string;
    content: string;
    visible: boolean;
    closeText?: string;
}

export default defineComponent({
    name: 'Dialog',
    props: {
        title: {
            type: String,
            required: true
        },
        content: {
            type: String,
            required: true
        },
        visible: {
            type: Boolean,
            required: true
        },
        closeText: {
            type: String,
            default: 'Close'
        }
    },
    methods: {
        close() {
            this.$emit('close');
        }
    }
});
</script>

<style scoped>
.dialog {
    border: 1px solid #ccc;
    padding: 10px;
}
</style>

在使用 Dialog 组件时:

<template>
    <div>
        <Dialog :title="dialogTitle" :content="dialogContent" :visible="isDialogVisible" @close="handleClose" />
        <button @click="toggleDialog">Toggle Dialog</button>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref } from 'vue';
import Dialog from './Dialog.vue';

export default defineComponent({
    name: 'App',
    components: {
        Dialog
    },
    setup() {
        const dialogTitle = ref('My Dialog');
        const dialogContent = ref('This is the content of the dialog');
        const isDialogVisible = ref(false);

        const toggleDialog = () => {
            isDialogVisible.value =!isDialogVisible.value;
        };

        const handleClose = () => {
            isDialogVisible.value = false;
        };

        return {
            dialogTitle,
            dialogContent,
            isDialogVisible,
            toggleDialog,
            handleClose
        };
    }
});
</script>

<style scoped>
button {
    margin-top: 10px;
}
</style>

在上述代码中,closeText 是可选参数并提供了默认值。这样,在使用 Dialog 组件时,如果不需要自定义关闭按钮文本,可以使用默认值。

  1. Vue 指令的参数 Vue 指令也可以利用可选参数和默认参数来实现更灵活的功能。例如,我们创建一个自定义指令 v-highlight,它可以根据条件高亮元素,并可以指定高亮的颜色。
<template>
    <div>
        <p v-highlight="true">This text will be highlighted with default color</p>
        <p v-highlight="{ value: true, color:'red' }">This text will be highlighted with red color</p>
    </div>
</template>

<script lang="ts">
import { defineComponent, Directive } from 'vue';

interface HighlightOptions {
    value: boolean;
    color?: string;
}

const highlightDirective: Directive = {
    mounted(el, binding) {
        const { value, color = 'yellow' } = binding.value as HighlightOptions;
        if (value) {
            el.style.backgroundColor = color;
        }
    }
};

export default defineComponent({
    name: 'App',
    directives: {
        highlight: highlightDirective
    }
});
</script>

<style scoped>

</style>

在上述代码中,v-highlight 指令接受一个对象作为参数,其中 color 是可选参数并提供了默认值 yellow。这样,在使用指令时,可以根据需要指定高亮颜色,也可以使用默认颜色。

通过以上在不同前端框架中的应用示例,我们可以看到可选参数和默认参数在 TypeScript 前端开发中具有广泛的用途,能够极大地提高代码的灵活性和可维护性。无论是构建 UI 组件、处理数据获取,还是实现特定的业务逻辑,合理运用可选参数和默认参数都能使我们的代码更加优雅和高效。