TypeScript数组与元组的深入解析
TypeScript 数组的基础概念
在 TypeScript 中,数组是一种用于存储多个值的数据结构。与 JavaScript 类似,TypeScript 的数组可以包含不同类型的数据,但通过类型注解,我们能更好地控制数组中元素的类型。
数组类型的定义方式
- 类型 + 方括号
最常见的定义数组类型的方式是在元素类型后面加上方括号
[]
。例如,定义一个存储数字的数组:
let numbers: number[] = [1, 2, 3];
这里,number[]
表示这是一个数组,数组中的每个元素都是 number
类型。
如果要定义一个字符串数组,可以这样写:
let names: string[] = ['Alice', 'Bob', 'Charlie'];
- 数组泛型
另一种定义数组类型的方式是使用数组泛型
Array<类型>
。例如,定义一个布尔值数组:
let booleans: Array<boolean> = [true, false, true];
Array<boolean>
与 boolean[]
的作用是一样的,都表示一个布尔值数组。
数组元素类型的多样性
- 单一类型数组 如前面的例子,我们可以创建只包含一种类型元素的数组,这有助于在开发过程中进行类型检查,减少错误。比如,以下代码会在 TypeScript 编译时报错:
let numbers: number[] = [1, 'two']; // 报错:类型“string”的参数不能赋给类型“number”的参数
- 联合类型数组 有时候,我们可能需要一个数组能存储多种类型的数据。这时可以使用联合类型。例如,定义一个数组,它可以存储数字或字符串:
let mixed: (number | string)[] = [1, 'two', 3];
在这个数组中,每个元素要么是 number
类型,要么是 string
类型。
TypeScript 数组的操作与方法
TypeScript 继承了 JavaScript 数组的众多方法,同时在类型检查的加持下,使用这些方法更加安全和可靠。
访问数组元素
通过索引可以访问数组中的元素。数组的索引从 0 开始。例如:
let numbers: number[] = [1, 2, 3];
console.log(numbers[0]); // 输出:1
如果访问不存在的索引,TypeScript 不会报错,但返回 undefined
。例如:
let numbers: number[] = [1, 2, 3];
console.log(numbers[3]); // 输出:undefined
修改数组元素
同样可以通过索引来修改数组中的元素。例如:
let numbers: number[] = [1, 2, 3];
numbers[1] = 4;
console.log(numbers); // 输出:[1, 4, 3]
但要注意,修改的元素类型必须与数组定义的类型一致,否则会报错。例如:
let numbers: number[] = [1, 2, 3];
numbers[1] = 'four'; // 报错:类型“string”的参数不能赋给类型“number”的参数
数组的常用方法
- push()
push()
方法用于在数组的末尾添加一个或多个元素,并返回新数组的长度。例如:
let numbers: number[] = [1, 2, 3];
let newLength = numbers.push(4);
console.log(numbers); // 输出:[1, 2, 3, 4]
console.log(newLength); // 输出:4
- pop()
pop()
方法用于删除并返回数组的最后一个元素。例如:
let numbers: number[] = [1, 2, 3];
let last = numbers.pop();
console.log(numbers); // 输出:[1, 2]
console.log(last); // 输出:3
- shift()
shift()
方法用于删除并返回数组的第一个元素。例如:
let numbers: number[] = [1, 2, 3];
let first = numbers.shift();
console.log(numbers); // 输出:[2, 3]
console.log(first); // 输出:1
- unshift()
unshift()
方法用于在数组的开头添加一个或多个元素,并返回新数组的长度。例如:
let numbers: number[] = [1, 2, 3];
let newLength = numbers.unshift(0);
console.log(numbers); // 输出:[0, 1, 2, 3]
console.log(newLength); // 输出:4
- splice()
splice()
方法用于在数组中添加或删除元素。它可以接受多个参数,第一个参数是起始位置,第二个参数是要删除的元素个数,后面的参数是要添加的元素。例如,删除数组中从索引 1 开始的 2 个元素:
let numbers: number[] = [1, 2, 3, 4];
let removed = numbers.splice(1, 2);
console.log(numbers); // 输出:[1, 4]
console.log(removed); // 输出:[2, 3]
如果要在数组中添加元素,可以这样:
let numbers: number[] = [1, 4];
numbers.splice(1, 0, 2, 3);
console.log(numbers); // 输出:[1, 2, 3, 4]
- concat()
concat()
方法用于合并两个或多个数组,并返回一个新数组。例如:
let arr1: number[] = [1, 2];
let arr2: number[] = [3, 4];
let combined = arr1.concat(arr2);
console.log(combined); // 输出:[1, 2, 3, 4]
- join()
join()
方法用于将数组中的所有元素连接成一个字符串。它接受一个可选的分隔符参数,默认分隔符是逗号。例如:
let names: string[] = ['Alice', 'Bob', 'Charlie'];
let str = names.join(' - ');
console.log(str); // 输出:Alice - Bob - Charlie
- reverse()
reverse()
方法用于反转数组中元素的顺序,并返回反转后的数组。例如:
let numbers: number[] = [1, 2, 3];
numbers.reverse();
console.log(numbers); // 输出:[3, 2, 1]
- sort()
sort()
方法用于对数组的元素进行排序。默认情况下,它会将元素转换为字符串,并按照 Unicode 码点进行排序。例如:
let numbers: number[] = [3, 1, 2];
numbers.sort();
console.log(numbers); // 输出:[1, 2, 3]
如果要进行自定义排序,可以传入一个比较函数。例如,按照从大到小的顺序排序:
let numbers: number[] = [3, 1, 2];
numbers.sort((a, b) => b - a);
console.log(numbers); // 输出:[3, 2, 1]
- forEach()
forEach()
方法用于对数组中的每个元素执行一次提供的函数。例如:
let numbers: number[] = [1, 2, 3];
numbers.forEach((number) => {
console.log(number * 2);
});
// 输出:
// 2
// 4
// 6
- map()
map()
方法用于创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。例如:
let numbers: number[] = [1, 2, 3];
let doubled = numbers.map((number) => number * 2);
console.log(doubled); // 输出:[2, 4, 6]
- filter()
filter()
方法用于创建一个新数组,其中包含通过所提供函数实现的测试的所有元素。例如,过滤出数组中的偶数:
let numbers: number[] = [1, 2, 3, 4];
let evens = numbers.filter((number) => number % 2 === 0);
console.log(evens); // 输出:[2, 4]
- reduce()
reduce()
方法对数组中的每个元素执行一个由您提供的“reducer”函数,将其结果汇总为单个返回值。例如,计算数组元素的总和:
let numbers: number[] = [1, 2, 3];
let sum = numbers.reduce((acc, number) => acc + number, 0);
console.log(sum); // 输出:6
这里,acc
是累加器,初始值为 0,number
是数组中的当前元素。
数组类型推断与类型拓宽
在 TypeScript 中,数组的类型推断和类型拓宽是两个重要的概念。
类型推断
当我们声明一个数组并初始化它时,TypeScript 会根据初始值推断数组的类型。例如:
let numbers = [1, 2, 3];
// numbers 的类型被推断为 number[]
如果数组中包含不同类型的元素,TypeScript 会推断出联合类型。例如:
let mixed = [1, 'two'];
// mixed 的类型被推断为 (number | string)[]
类型拓宽
类型拓宽是指在某些情况下,TypeScript 会将一个更具体的类型拓宽为一个更通用的类型。例如,当我们声明一个常量数组时:
const numbers = [1, 2, 3];
// numbers 的类型实际上是 readonly [1, 2, 3]
这里,numbers
的类型是一个只读数组,其元素类型是具体的数字字面量类型。但如果我们将这个数组赋值给一个变量,类型会被拓宽:
const numbers = [1, 2, 3];
let newNumbers = numbers;
// newNumbers 的类型被拓宽为 number[]
这种类型拓宽在一些场景下可能会导致意外的行为,所以在使用时需要注意。
多维数组
在 TypeScript 中,多维数组是指数组的元素又是数组。常见的多维数组是二维数组,它可以看作是一个矩阵。
二维数组的定义
- 使用类型 + 方括号 定义一个二维数字数组,可以这样写:
let matrix: number[][] = [
[1, 2],
[3, 4]
];
这里,number[][]
表示这是一个二维数组,外层数组的每个元素又是一个 number
类型的数组。
- 使用数组泛型 同样可以使用数组泛型来定义二维数组:
let matrix: Array<Array<number>> = [
[1, 2],
[3, 4]
];
访问和操作二维数组
访问二维数组的元素需要使用两个索引,第一个索引表示行,第二个索引表示列。例如:
let matrix: number[][] = [
[1, 2],
[3, 4]
];
console.log(matrix[0][1]); // 输出:2
修改二维数组元素的方式类似:
let matrix: number[][] = [
[1, 2],
[3, 4]
];
matrix[1][0] = 5;
console.log(matrix);
// 输出:
// [
// [1, 2],
// [5, 4]
// ]
TypeScript 元组的基础概念
元组是 TypeScript 中特有的一种数据结构,它与数组类似,但有一些重要的区别。元组允许我们创建一个固定长度且元素类型固定的数组。
元组的定义
定义一个元组时,需要在方括号内指定每个元素的类型。例如,定义一个包含字符串和数字的元组:
let tuple: [string, number] = ['Alice', 25];
这里,[string, number]
表示这是一个元组,第一个元素是 string
类型,第二个元素是 number
类型。
元组元素的访问和修改
与数组类似,通过索引可以访问元组中的元素。例如:
let tuple: [string, number] = ['Alice', 25];
console.log(tuple[0]); // 输出:Alice
console.log(tuple[1]); // 输出:25
元组元素也可以修改,但修改的元素类型必须与定义时一致。例如:
let tuple: [string, number] = ['Alice', 25];
tuple[1] = 26;
console.log(tuple); // 输出:['Alice', 26]
如果尝试修改为不匹配的类型,会报错:
let tuple: [string, number] = ['Alice', 25];
tuple[1] = 'twenty - six'; // 报错:类型“string”的参数不能赋给类型“number”的参数
元组的特性与应用场景
元组具有一些独特的特性,使其在某些场景下非常有用。
固定长度
元组的长度是固定的,一旦定义,就不能随意增加或减少元素。例如:
let tuple: [string, number] = ['Alice', 25];
// tuple.push('new value'); // 报错:属性“push”在类型“[string, number]”上不存在
这种固定长度的特性在需要明确表示一组特定数量值的场景中很有用,比如表示一个坐标点(x, y)。
元素类型固定
元组中每个元素的类型是固定的,这使得代码更加类型安全。例如,在函数返回值中使用元组可以明确返回值的结构。比如,一个函数返回用户名和用户年龄:
function getUserInfo(): [string, number] {
return ['Alice', 25];
}
let userInfo = getUserInfo();
console.log(userInfo[0]); // 输出:Alice
console.log(userInfo[1]); // 输出:25
解构赋值
元组非常适合解构赋值。通过解构赋值,可以方便地提取元组中的元素。例如:
let tuple: [string, number] = ['Alice', 25];
let [name, age] = tuple;
console.log(name); // 输出:Alice
console.log(age); // 输出:25
解构赋值还可以与默认值结合使用。例如:
let tuple: [string, number?] = ['Alice'];
let [name, age = 18] = tuple;
console.log(name); // 输出:Alice
console.log(age); // 输出:18
这里,number?
表示第二个元素是可选的,当元组中没有第二个元素时,解构赋值会使用默认值 18。
元组与数组的区别
虽然元组和数组都用于存储多个值,但它们有一些显著的区别。
长度
数组的长度可以动态变化,通过 push()
、pop()
、shift()
、unshift()
等方法可以增加或减少元素。而元组的长度是固定的,一旦定义,不能随意改变。
元素类型
数组中的元素类型可以是单一类型、联合类型等,但元素之间的类型约束相对宽松。元组中每个元素的类型是固定的,必须按照定义的顺序和类型来赋值和访问。
应用场景
数组适用于存储一组相同类型或多种类型但数量不固定的数据,比如存储用户列表、商品列表等。元组适用于需要明确表示一组固定数量且类型固定的数据,比如表示坐标点、函数返回一组特定结构的值等。
元组类型推断与类型拓宽
与数组类似,元组也存在类型推断和类型拓宽的情况。
类型推断
当我们声明一个元组并初始化它时,TypeScript 会根据初始值推断元组的类型。例如:
let tuple = ['Alice', 25];
// tuple 的类型被推断为 [string, number]
类型拓宽
在某些情况下,元组也会发生类型拓宽。例如,当我们将一个常量元组赋值给一个变量时:
const tuple = ['Alice', 25];
let newTuple = tuple;
// newTuple 的类型被拓宽为 [string, number]
// 但如果是常量元组,它实际上是 readonly [string, number] 类型
这种类型拓宽在使用时需要注意,特别是当涉及到对元组元素的修改操作时。
元组在函数中的应用
元组在函数的参数和返回值中有着广泛的应用。
函数参数
在函数参数中使用元组可以明确参数的结构和类型。例如,定义一个函数接收一个坐标点(元组)并计算到原点的距离:
function distanceFromOrigin([x, y]: [number, number]): number {
return Math.sqrt(x * x + y * y);
}
let point: [number, number] = [3, 4];
let dist = distanceFromOrigin(point);
console.log(dist); // 输出:5
函数返回值
如前面提到的,函数返回值使用元组可以清晰地表示返回值的结构。例如,一个函数从数据库中获取用户信息并以元组形式返回:
function getUser(): [string, number] {
// 模拟从数据库获取数据
return ['Bob', 30];
}
let [name, age] = getUser();
console.log(name); // 输出:Bob
console.log(age); // 输出:30
高级元组用法
除了基本的定义和使用,元组还有一些高级用法。
剩余元素
在 TypeScript 4.0 及以上版本中,元组支持剩余元素语法。例如,定义一个元组,前面两个元素是固定类型,后面可以有多个相同类型的元素:
let tuple: [string, number, ...boolean[]] = ['Alice', 25, true, false];
这里,...boolean[]
表示剩余的元素都是 boolean
类型的数组。
元组类型别名
我们可以使用类型别名来定义元组类型,这样可以提高代码的可读性和复用性。例如:
type UserInfo = [string, number];
function getUser(): UserInfo {
return ['Charlie', 35];
}
let user: UserInfo = getUser();
console.log(user[0]); // 输出:Charlie
console.log(user[1]); // 输出:35
数组与元组的相互转换
在实际开发中,有时需要将数组转换为元组,或者将元组转换为数组。
数组转元组
将数组转换为元组需要确保数组的长度和元素类型与元组定义一致。例如:
let arr: (string | number)[] = ['Alice', 25];
let tuple: [string, number] = arr as [string, number];
这里使用了类型断言 as
将数组断言为元组类型,但要注意,如果数组的实际内容与元组类型不匹配,可能会导致运行时错误。
元组转数组
将元组转换为数组可以使用数组的 from()
方法或展开运算符 ...
。例如:
let tuple: [string, number] = ['Alice', 25];
let arr1 = Array.from(tuple);
let arr2 = [...tuple];
console.log(arr1); // 输出:['Alice', 25]
console.log(arr2); // 输出:['Alice', 25]
通过对 TypeScript 数组和元组的深入解析,我们可以看到它们在不同场景下的强大功能和应用。合理使用数组和元组,能够提高代码的类型安全性和可读性,让我们的前端开发更加高效和稳健。在实际项目中,根据具体需求选择合适的数据结构,是写出高质量 TypeScript 代码的关键之一。同时,不断探索和实践这些特性,有助于我们更好地掌握 TypeScript 这门语言,提升开发技能。