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

TypeScript函数基础与高级用法

2024-08-317.3k 阅读

TypeScript函数基础

函数定义与参数类型

在TypeScript中,函数的定义与JavaScript类似,但可以为参数和返回值指定类型。基本的函数定义形式如下:

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

在上述代码中,add函数接受两个number类型的参数ab,并且返回一个number类型的值。如果传入的参数类型不符合定义,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?'));

greet函数中,message参数是可选的。当调用greet函数时,可以只传入name参数,也可以同时传入namemessage参数。

默认参数

除了可选参数,TypeScript还支持为参数设置默认值。当调用函数时,如果没有传入该参数的值,就会使用默认值。示例如下:

function multiply(a: number, b: number = 2) {
    return a * b;
}
console.log(multiply(3));
console.log(multiply(3, 4));

multiply函数中,b参数有一个默认值2。当只传入一个参数调用multiply函数时,b会使用默认值2

剩余参数

有时候,函数需要接受不确定数量的参数。在TypeScript中,可以使用剩余参数来实现这一点。剩余参数使用...语法,并且必须放在参数列表的最后。例如:

function sum(...numbers: number[]): number {
    let total = 0;
    for (let num of numbers) {
        total += num;
    }
    return total;
}
console.log(sum(1, 2, 3));
console.log(sum(4, 5, 6, 7));

sum函数中,...numbers表示接受任意数量的number类型参数,并将它们存储在一个数组中。

函数返回值类型

显式返回值类型声明

在前面的例子中,我们已经看到了如何显式声明函数的返回值类型。例如:

function subtract(a: number, b: number): number {
    return a - b;
}

这里明确声明了subtract函数返回一个number类型的值。显式声明返回值类型有助于代码的可读性和维护性,同时TypeScript编译器可以进行类型检查,确保函数实际返回的值与声明的类型一致。

隐式返回值类型推断

TypeScript编译器在很多情况下可以自动推断函数的返回值类型,即使没有显式声明。例如:

function getMessage() {
    return 'This is a message';
}

虽然没有显式声明返回值类型,但TypeScript可以推断出getMessage函数返回一个string类型的值。然而,为了代码的清晰和可维护性,建议在复杂函数或者返回值类型不明显的情况下,显式声明返回值类型。

函数类型

定义函数类型

在TypeScript中,可以像定义其他类型一样定义函数类型。函数类型由参数类型和返回值类型组成。例如:

type AddFunction = (a: number, b: number) => number;
function addNumbers(a: number, b: number): number {
    return a + b;
}
let add: AddFunction = addNumbers;

在上述代码中,首先定义了一个函数类型AddFunction,它接受两个number类型的参数并返回一个number类型的值。然后定义了一个符合该函数类型的addNumbers函数,并将其赋值给变量addadd的类型就是AddFunction

使用函数类型作为参数

函数类型可以作为其他函数的参数类型。这在实现回调函数等场景中非常有用。例如:

type Callback = (result: number) => void;
function calculate(a: number, b: number, callback: Callback) {
    let result = a + b;
    callback(result);
}
calculate(1, 2, (result) => {
    console.log(`The result is: ${result}`);
});

calculate函数中,第三个参数callback的类型是Callback,这是一个接受一个number类型参数且没有返回值的函数类型。

TypeScript函数高级用法

重载

函数重载允许一个函数根据不同的参数列表执行不同的逻辑。在TypeScript中,可以通过定义多个函数签名来实现重载。例如:

function print(value: string): void;
function print(value: number): void;
function print(value: any) {
    if (typeof value ==='string') {
        console.log(`String: ${value}`);
    } else if (typeof value === 'number') {
        console.log(`Number: ${value}`);
    }
}
print('Hello');
print(42);

在上述代码中,定义了两个函数签名print(value: string): voidprint(value: number): void,表示print函数可以接受string类型或number类型的参数。实际的函数实现可以处理不同类型的参数。

箭头函数

箭头函数是ES6引入的一种简洁的函数定义方式,在TypeScript中同样适用。箭头函数的语法更加简洁,并且在处理this上下文等方面有独特的行为。例如:

const multiply = (a: number, b: number) => a * b;
console.log(multiply(3, 4));

箭头函数没有自己的this,它的this继承自外层作用域。这在很多场景下可以避免this指向混乱的问题。例如:

const person = {
    name: 'Alice',
    greet: function() {
        setTimeout(() => {
            console.log(`Hello, ${this.name}`);
        }, 1000);
    }
};
person.greet();

setTimeout中使用箭头函数,this会指向person对象,而如果使用普通函数,this可能会指向全局对象或其他错误的对象。

函数的泛型

泛型是TypeScript的一个强大特性,它允许在定义函数、接口或类时使用类型参数。在函数中使用泛型可以使函数更加灵活,适用于多种类型。例如:

function identity<T>(arg: T): T {
    return arg;
}
let result1 = identity<number>(42);
let result2 = identity<string>('Hello');

identity函数中,<T>是一个类型参数,arg的类型是T,返回值的类型也是T。这样可以根据传入的实际类型来确定函数的具体行为,提高了代码的复用性。

函数与接口

可以使用接口来定义函数类型。例如:

interface Add {
    (a: number, b: number): number;
}
let add: Add = function(a: number, b: number): number {
    return a + b;
};

这里定义了一个接口Add,它描述了一个接受两个number类型参数并返回一个number类型值的函数。然后定义了一个符合该接口的函数add

函数装饰器

函数装饰器是一种特殊类型的声明,它可以被附加到类的方法声明上。装饰器可以修改类的方法行为,例如添加日志、验证等功能。以下是一个简单的函数装饰器示例:

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        console.log(`Calling method ${propertyKey} with arguments:`, args);
        const result = originalMethod.apply(this, args);
        console.log(`Method ${propertyKey} returned:`, result);
        return result;
    };
    return descriptor;
}
class Calculator {
    @log
    add(a: number, b: number) {
        return a + b;
    }
}
const calculator = new Calculator();
calculator.add(1, 2);

在上述代码中,定义了一个log装饰器,它会在调用被装饰的方法前后打印日志。通过@log将装饰器应用到Calculator类的add方法上。

函数的作用域与闭包

函数的作用域

在TypeScript中,函数有自己的作用域。变量在函数内部声明时,其作用域仅限于该函数内部。例如:

function example() {
    let localVar = 'This is a local variable';
    console.log(localVar);
}
// console.log(localVar); // 这里会报错,localVar超出作用域

example函数内部声明的localVar变量,在函数外部无法访问。

闭包

闭包是指函数可以访问并记住其词法作用域,即使函数在其原始作用域之外被调用。例如:

function outer() {
    let outerVar = 'This is from outer';
    function inner() {
        console.log(outerVar);
    }
    return inner;
}
let innerFunc = outer();
innerFunc();

在上述代码中,inner函数形成了一个闭包,它可以访问outer函数作用域内的outerVar变量,即使outer函数已经执行完毕。闭包在很多场景下非常有用,比如实现模块的私有变量等。

异步函数

async/await

在处理异步操作时,TypeScript支持async/await语法,这是基于ES2017的异步函数特性。async函数总是返回一个Promiseawait只能在async函数内部使用,用于暂停函数执行,直到Promise被解决(resolved)或被拒绝(rejected)。例如:

function delay(ms: number): Promise<void> {
    return new Promise((resolve) => {
        setTimeout(resolve, ms);
    });
}
async function asyncExample() {
    console.log('Start');
    await delay(2000);
    console.log('End');
}
asyncExample();

asyncExample函数中,await delay(2000)会暂停函数执行,直到delay返回的Promise被解决,这里是2秒后。这样可以让异步代码看起来更像同步代码,提高代码的可读性。

处理Promise错误

在使用async/await时,处理Promise的错误非常重要。可以使用try...catch块来捕获错误。例如:

function asyncError(): Promise<void> {
    return new Promise((_, reject) => {
        setTimeout(() => {
            reject(new Error('Async error'));
        }, 1000);
    });
}
async function handleError() {
    try {
        await asyncError();
    } catch (error) {
        console.error('Caught error:', error);
    }
}
handleError();

handleError函数中,try...catch块捕获了asyncError函数返回的Promise被拒绝时抛出的错误,并进行了处理。

函数类型兼容性

参数类型兼容性

在TypeScript中,函数参数类型的兼容性是双向协变的。这意味着,如果一个函数接受一个更具体类型的参数,那么它可以被赋值给一个接受更宽泛类型参数的函数类型变量。例如:

function greet(name: string) {
    console.log(`Hello, ${name}`);
}
function greetAny(value: any) {
    console.log(`Value: ${value}`);
}
let greeter1: (name: string) => void = greet;
let greeter2: (value: any) => void = greet;
let greeter3: (name: string) => void = greetAny; // 这是不允许的,因为参数类型不兼容

在上述代码中,greet函数接受string类型参数,greetAny函数接受any类型参数。greet函数可以赋值给接受any类型参数的函数类型变量,但反之则不行。

返回值类型兼容性

返回值类型的兼容性是逆变的。这意味着,如果一个函数返回一个更宽泛类型的值,那么它可以被赋值给一个返回更具体类型值的函数类型变量。例如:

function getString(): string {
    return 'Hello';
}
function getAny(): any {
    return 'Hello';
}
let getStr: () => string = getString;
let getAnyValue: () => any = getString;
let getStrValue: () => string = getAny;

在上述代码中,getString函数返回string类型值,getAny函数返回any类型值。getAny函数可以赋值给返回string类型值的函数类型变量。

函数的类型断言

有时候,TypeScript编译器无法准确推断函数的类型,或者我们知道函数的实际类型与推断的类型不同。这时可以使用类型断言来手动指定函数的类型。例如:

function callFunction(func: any, arg: any) {
    return func(arg);
}
function addNumbers(a: number, b: number): number {
    return a + b;
}
let result = callFunction(addNumbers as (a: number, b: number) => number, 1, 2);
console.log(result);

callFunction函数中,func参数的类型是any,为了确保addNumbers函数能正确调用,使用类型断言(a: number, b: number) => numberaddNumbers函数转换为合适的类型。

总结函数相关要点

TypeScript的函数特性为前端开发提供了更强大的类型检查和代码组织能力。从基础的函数定义、参数类型、返回值类型,到高级的重载、泛型、装饰器等特性,这些都有助于编写更健壮、可维护的代码。理解和熟练运用这些函数特性,对于提升前端开发的质量和效率至关重要。在实际开发中,根据不同的需求选择合适的函数特性来解决问题,能让代码更加清晰、简洁且易于理解。同时,注意函数的作用域、闭包以及异步处理等方面的知识,它们在构建复杂前端应用时起着关键作用。通过不断实践和深入学习,开发者可以更好地利用TypeScript函数的各种特性,打造出高质量的前端项目。