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

TypeScript数组类型的操作与实践

2022-05-224.1k 阅读

数组类型的基础声明

在 TypeScript 中,声明数组类型有两种常见方式。

第一种方式是在元素类型后面加上 [],例如声明一个数字数组:

let numbers: number[] = [1, 2, 3];

这里 number[] 表示这是一个元素类型为 number 的数组。

第二种方式是使用数组泛型 Array<类型>,同样以数字数组为例:

let numbers2: Array<number> = [4, 5, 6];

这两种声明方式本质上是等效的,你可以根据个人喜好选择使用。

如果数组中元素类型不同,可以声明为联合类型数组。比如,一个既包含数字又包含字符串的数组:

let mixedArray: (number | string)[] = [1, 'two', 3];

这里 (number | string)[] 表示数组元素要么是 number 类型,要么是 string 类型。

只读数组

有时候我们希望数组一旦初始化后,其内容就不能被修改,这时候可以使用只读数组。只读数组使用 readonly 关键字声明。例如:

let readonlyNumbers: readonly number[] = [1, 2, 3];
// readonlyNumbers[0] = 4; // 这行代码会报错,因为只读数组不能被修改

上述代码中,readonlyNumbers 数组在声明后就不能再修改其元素值。这在一些场景下非常有用,比如传递一些不应该被修改的数据集合时。

元组类型

元组是一种特殊的数组,它的元素类型和数量都是固定的。元组类型声明时需要指定每个位置元素的类型。例如:

let tuple: [string, number] = ['hello', 42];

这里 [string, number] 表示元组第一个元素是 string 类型,第二个元素是 number 类型。

元组元素的访问和普通数组一样通过索引,但要注意索引值不能超出元组定义的范围,否则会报错。例如:

let tuple2: [boolean, string];
tuple2 = [true, 'world'];
console.log(tuple2[0]); // true
console.log(tuple2[1]); // world
// console.log(tuple2[2]); // 这行代码会报错,索引越界

元组也支持解构赋值,方便地提取元组中的元素:

let [boolValue, strValue] = tuple2;
console.log(boolValue); // true
console.log(strValue); // world

数组方法的类型推断

TypeScript 对数组的方法有很好的类型推断。以 push 方法为例,它用于向数组末尾添加一个或多个元素,并返回新数组的长度。

let fruits: string[] = ['apple', 'banana'];
let newLength = fruits.push('cherry');
console.log(newLength); // 3
console.log(fruits); // ['apple', 'banana', 'cherry']

这里 fruitsstring 类型的数组,push 方法传入的参数也必须是 string 类型,因为类型系统会根据数组的元素类型进行推断。

pop 方法用于删除并返回数组的最后一个元素。

let removedFruit = fruits.pop();
console.log(removedFruit); // cherry
console.log(fruits); // ['apple', 'banana']

pop 方法返回值的类型和数组元素类型一致,在这个例子中就是 string 类型。

shift 方法用于删除并返回数组的第一个元素,unshift 方法则是在数组开头添加一个或多个元素,并返回新数组的长度。

let shiftedFruit = fruits.shift();
console.log(shiftedFruit); // apple
console.log(fruits); // ['banana']

let newLength2 = fruits.unshift('grape');
console.log(newLength2); // 2
console.log(fruits); // ['grape', 'banana']

这些方法的类型推断都是基于数组本身的元素类型。

数组的遍历与迭代器

  1. for 循环遍历 这是最基本的遍历数组的方式。
let numbers3: number[] = [1, 2, 3];
for (let i = 0; i < numbers3.length; i++) {
    console.log(numbers3[i]);
}

在这个 for 循环中,通过索引 i 访问数组中的每个元素。

  1. for...of 循环 for...of 循环更简洁,它直接迭代数组的元素而不是索引。
for (let num of numbers3) {
    console.log(num);
}

for...of 循环背后使用了迭代器协议。数组默认实现了迭代器协议,所以可以直接使用 for...of 循环。

  1. forEach 方法 forEach 方法用于对数组的每个元素执行一次提供的函数。
numbers3.forEach((num) => {
    console.log(num);
});

forEach 方法接受一个回调函数,回调函数的参数就是数组中的每个元素。

  1. 迭代器与生成器 迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。数组有默认的迭代器,通过 Symbol.iterator 属性访问。
let iterator = numbers3[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
    console.log(result.value);
    result = iterator.next();
}

生成器是一种特殊的函数,它返回一个迭代器。通过 function* 语法定义。例如:

function* numberGenerator() {
    yield 1;
    yield 2;
    yield 3;
}
let gen = numberGenerator();
let genResult = gen.next();
while (!genResult.done) {
    console.log(genResult.value);
    genResult = gen.next();
}

这里 yield 关键字暂停生成器函数的执行,并返回一个值。每次调用 next 方法时,生成器函数从暂停的地方继续执行,直到下一个 yield 或函数结束。

数组的高阶函数

  1. map 方法 map 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
let numbers4: number[] = [1, 2, 3];
let squaredNumbers = numbers4.map((num) => num * num);
console.log(squaredNumbers); // [1, 4, 9]

map 方法返回的新数组元素类型和回调函数的返回值类型一致。

  1. filter 方法 filter 方法创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
let numbers5: number[] = [1, 2, 3, 4, 5];
let evenNumbers = numbers5.filter((num) => num % 2 === 0);
console.log(evenNumbers); // [2, 4]

filter 方法的回调函数返回一个 boolean 值,决定当前元素是否应该被包含在新数组中。

  1. reduce 方法 reduce 方法对数组中的每个元素执行一个由你提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。
let numbers6: number[] = [1, 2, 3];
let sum = numbers6.reduce((acc, num) => acc + num, 0);
console.log(sum); // 6

reduce 方法接受两个参数,第一个是 reducer 函数,reducer 函数接受两个参数:累加器 acc 和当前元素 num。第二个参数是初始值 0。如果没有提供初始值,reduce 会从数组的第二个元素开始,第一个元素作为初始的 acc

  1. some 方法 some 方法测试数组中是不是至少有 1 个元素通过了被提供的函数测试。它返回一个 boolean 值。
let numbers7: number[] = [1, 2, 3];
let hasEven = numbers7.some((num) => num % 2 === 0);
console.log(hasEven); // true

如果数组中有任何一个元素满足回调函数的条件,some 方法就返回 true,否则返回 false

  1. every 方法 every 方法测试数组的所有元素是否都通过了指定函数的测试。
let numbers8: number[] = [2, 4, 6];
let allEven = numbers8.every((num) => num % 2 === 0);
console.log(allEven); // true

只有当数组中的所有元素都满足回调函数的条件时,every 方法才返回 true,否则返回 false

多维数组

多维数组本质上是数组的数组。在 TypeScript 中声明多维数组时,需要相应地增加 []。例如,声明一个二维数组(矩阵):

let matrix: number[][] = [
    [1, 2],
    [3, 4]
];

这里 number[][] 表示这是一个二维数组,其内部数组的元素类型为 number

访问二维数组的元素需要使用双重索引。例如:

console.log(matrix[0][0]); // 1
console.log(matrix[1][1]); // 4

对于更高维度的数组,声明和访问方式类似,只是增加更多的 [] 和索引。例如三维数组:

let threeDArray: number[][][] = [
    [
        [1, 2],
        [3, 4]
    ],
    [
        [5, 6],
        [7, 8]
    ]
];
console.log(threeDArray[0][0][0]); // 1
console.log(threeDArray[1][1][1]); // 8

数组类型与接口和类型别名

  1. 使用接口定义数组类型 可以通过接口来定义数组类型,这样可以增加代码的可读性和可维护性。例如:
interface NumberArray {
    [index: number]: number;
}
let myNumbers: NumberArray = [1, 2, 3];

这里 NumberArray 接口定义了一个数组类型,其索引为数字类型,元素类型为 number

  1. 使用类型别名定义数组类型 类型别名也可以用于定义数组类型,和接口类似,但语法略有不同。
type StringArray = string[];
let myStrings: StringArray = ['a', 'b', 'c'];

使用类型别名定义数组类型更加简洁,而且类型别名还可以用于定义联合类型、交叉类型等复杂类型。例如:

type MixedArray = (number | string)[];
let mixed: MixedArray = [1, 'two'];

在函数中使用数组类型

  1. 函数参数为数组类型 当函数接受数组作为参数时,需要明确指定数组的类型。例如:
function sumArray(numbers: number[]): number {
    let sum = 0;
    for (let num of numbers) {
        sum += num;
    }
    return sum;
}
let resultSum = sumArray([1, 2, 3]);
console.log(resultSum); // 6

这里 sumArray 函数接受一个 number 类型的数组作为参数,并返回数组元素的总和。

  1. 函数返回值为数组类型 函数也可以返回数组类型。例如:
function getEvenNumbers(numbers: number[]): number[] {
    return numbers.filter((num) => num % 2 === 0);
}
let evens = getEvenNumbers([1, 2, 3, 4, 5]);
console.log(evens); // [2, 4]

getEvenNumbers 函数接受一个 number 类型的数组,返回其中的偶数组成的新数组。

数组类型与泛型函数

泛型函数可以处理不同类型的数组,增加函数的复用性。例如:

function identity<T>(arg: T[]): T[] {
    return arg;
}
let numbers9: number[] = [1, 2, 3];
let resultIdentity = identity(numbers9);
console.log(resultIdentity); // [1, 2, 3]

let strings: string[] = ['a', 'b', 'c'];
let resultIdentity2 = identity(strings);
console.log(resultIdentity2); // ['a', 'b', 'c']

这里 identity 函数是一个泛型函数,T 是类型参数。函数接受一个 T 类型的数组,并返回同样类型的数组。在调用函数时,TypeScript 会根据传入的实际参数类型推断 T 的具体类型。

数组类型的类型断言

有时候,TypeScript 不能准确推断数组的类型,这时候可以使用类型断言。例如:

let value: any = [1, 2, 3];
let numbers10 = value as number[];
console.log(numbers10[0]); // 1

这里 value 初始类型为 any,通过类型断言 as number[] 将其转换为 number 类型的数组。但要注意,类型断言需要开发者自己确保实际类型和断言类型一致,否则可能会导致运行时错误。

数组类型在面向对象编程中的应用

在类中可以使用数组类型来存储相关的数据。例如:

class Student {
    private scores: number[];
    constructor() {
        this.scores = [];
    }
    addScore(score: number) {
        this.scores.push(score);
    }
    getAverageScore(): number {
        let sum = this.scores.reduce((acc, score) => acc + score, 0);
        return sum / this.scores.length;
    }
}
let student = new Student();
student.addScore(80);
student.addScore(90);
let average = student.getAverageScore();
console.log(average); // 85

在这个 Student 类中,scores 属性是一个 number 类型的数组,用于存储学生的成绩。addScore 方法向数组中添加成绩,getAverageScore 方法计算并返回平均成绩。

数组类型与模块

在 TypeScript 模块中,数组类型也经常被使用。例如,在一个模块中定义一个函数,接受和返回数组类型:

// mathUtils.ts
export function squareArray(numbers: number[]): number[] {
    return numbers.map((num) => num * num);
}
// main.ts
import { squareArray } from './mathUtils';
let numbers11: number[] = [1, 2, 3];
let squared = squareArray(numbers11);
console.log(squared); // [1, 4, 9]

在这个例子中,mathUtils 模块导出了 squareArray 函数,该函数接受一个 number 类型的数组并返回一个新的 number 类型数组,其中每个元素是原数组对应元素的平方。在 main.ts 中导入并使用了这个函数。

通过以上对 TypeScript 数组类型的各种操作与实践的介绍,相信你对 TypeScript 数组类型有了更深入的理解和掌握,可以在实际项目中更加灵活和准确地使用数组类型。无论是简单的数组声明,还是复杂的数组操作,都能应对自如。