TypeScript数组类型的操作与实践
数组类型的基础声明
在 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']
这里 fruits
是 string
类型的数组,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']
这些方法的类型推断都是基于数组本身的元素类型。
数组的遍历与迭代器
- for 循环遍历 这是最基本的遍历数组的方式。
let numbers3: number[] = [1, 2, 3];
for (let i = 0; i < numbers3.length; i++) {
console.log(numbers3[i]);
}
在这个 for
循环中,通过索引 i
访问数组中的每个元素。
- for...of 循环
for...of
循环更简洁,它直接迭代数组的元素而不是索引。
for (let num of numbers3) {
console.log(num);
}
for...of
循环背后使用了迭代器协议。数组默认实现了迭代器协议,所以可以直接使用 for...of
循环。
- forEach 方法
forEach
方法用于对数组的每个元素执行一次提供的函数。
numbers3.forEach((num) => {
console.log(num);
});
forEach
方法接受一个回调函数,回调函数的参数就是数组中的每个元素。
- 迭代器与生成器
迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。数组有默认的迭代器,通过
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
或函数结束。
数组的高阶函数
- map 方法
map
方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
let numbers4: number[] = [1, 2, 3];
let squaredNumbers = numbers4.map((num) => num * num);
console.log(squaredNumbers); // [1, 4, 9]
map
方法返回的新数组元素类型和回调函数的返回值类型一致。
- 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
值,决定当前元素是否应该被包含在新数组中。
- 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
。
- 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
。
- 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
数组类型与接口和类型别名
- 使用接口定义数组类型 可以通过接口来定义数组类型,这样可以增加代码的可读性和可维护性。例如:
interface NumberArray {
[index: number]: number;
}
let myNumbers: NumberArray = [1, 2, 3];
这里 NumberArray
接口定义了一个数组类型,其索引为数字类型,元素类型为 number
。
- 使用类型别名定义数组类型 类型别名也可以用于定义数组类型,和接口类似,但语法略有不同。
type StringArray = string[];
let myStrings: StringArray = ['a', 'b', 'c'];
使用类型别名定义数组类型更加简洁,而且类型别名还可以用于定义联合类型、交叉类型等复杂类型。例如:
type MixedArray = (number | string)[];
let mixed: MixedArray = [1, 'two'];
在函数中使用数组类型
- 函数参数为数组类型 当函数接受数组作为参数时,需要明确指定数组的类型。例如:
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
类型的数组作为参数,并返回数组元素的总和。
- 函数返回值为数组类型 函数也可以返回数组类型。例如:
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 数组类型有了更深入的理解和掌握,可以在实际项目中更加灵活和准确地使用数组类型。无论是简单的数组声明,还是复杂的数组操作,都能应对自如。