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

Array、Tuple与ArrayLike在TypeScript中的选择

2021-01-234.3k 阅读

1. 认识 Array

在 TypeScript 中,Array 是最常见的数组类型。它表示一组有序的数据集合,这些数据可以是相同类型,也可以是多种类型(在某些情况下)。

1.1 定义方式

定义 Array 有两种常见方式。 第一种是使用泛型语法:

let numbers: number[] = [1, 2, 3];
let mixed: (string | number)[] = ['a', 1];

这里 number[] 表示一个由 number 类型元素组成的数组,而 (string | number)[] 表示一个元素可以是 string 或者 number 类型的数组。

第二种方式是使用 Array 构造函数:

let numbers2: Array<number> = new Array(1, 2, 3);
let mixed2: Array<string | number> = new Array('a', 1);

Array<number>number[] 本质上是等价的,这只是两种不同的书写方式。

1.2 常见操作方法

Array 类型有众多实用的方法。

  • push:用于向数组末尾添加一个或多个元素,并返回新数组的长度。
let fruits: string[] = ['apple'];
let newLength = fruits.push('banana');
console.log(fruits); // ['apple', 'banana']
console.log(newLength); // 2
  • pop:从数组末尾移除一个元素,并返回该元素。如果数组为空,返回 undefined
let fruits2: string[] = ['apple', 'banana'];
let removed = fruits2.pop();
console.log(fruits2); // ['apple']
console.log(removed); // 'banana'
  • shift:从数组开头移除一个元素,并返回该元素。如果数组为空,返回 undefined
let fruits3: string[] = ['apple', 'banana'];
let removed2 = fruits3.shift();
console.log(fruits3); // ['banana']
console.log(removed2); // 'apple'
  • unshift:向数组开头添加一个或多个元素,并返回新数组的长度。
let fruits4: string[] = ['banana'];
let newLength2 = fruits4.unshift('apple');
console.log(fruits4); // ['apple', 'banana']
console.log(newLength2); // 2
  • splice:用于插入、删除或替换数组的元素。
let numbers3: number[] = [1, 2, 3];
// 从索引1处删除1个元素,并插入4和5
let removedItems = numbers3.splice(1, 1, 4, 5);
console.log(numbers3); // [1, 4, 5, 3]
console.log(removedItems); // [2]
  • map:创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
let numbers4: number[] = [1, 2, 3];
let squared = numbers4.map((num) => num * num);
console.log(squared); // [1, 4, 9]
  • filter:创建一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
let numbers5: number[] = [1, 2, 3, 4, 5];
let evens = numbers5.filter((num) => num % 2 === 0);
console.log(evens); // [2, 4]
  • reduce:对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
let numbers6: number[] = [1, 2, 3];
let sum = numbers6.reduce((acc, num) => acc + num, 0);
console.log(sum); // 6

1.3 灵活的长度与元素类型

Array 的长度是动态可变的。可以随时添加或删除元素,这使得它在处理数据量不确定的场景中非常方便。

let dynamicArray: number[] = [];
dynamicArray.push(1);
dynamicArray.push(2);
dynamicArray.pop();

元素类型方面,虽然建议保持一致性,但如前文所述,也可以定义联合类型数组,不过这需要谨慎使用,因为可能会带来类型检查上的一些复杂性。例如:

let mixedArray: (string | number)[] = [];
mixedArray.push('a');
mixedArray.push(1);

在实际使用中,如果对数组元素的类型有严格要求,应尽量保持单一类型,以提高代码的可读性和可维护性。

2. 理解 Tuple

Tuple 是 TypeScript 中特有的类型,它用于表示一个固定长度的数组,其中每个元素的类型是已知的,且顺序是固定的。

2.1 定义与初始化

定义 Tuple 时,明确指定每个元素的类型和顺序。

let point: [number, number] = [10, 20];
let user: [string, number] = ['John', 30];

这里 point 是一个包含两个 number 类型元素的 Tuple,而 user 是一个第一个元素为 string 类型,第二个元素为 number 类型的 Tuple

2.2 访问与操作

Tuple 元素的访问和普通数组类似,通过索引。

let point2: [number, number] = [10, 20];
let x = point2[0];
let y = point2[1];
console.log(x); // 10
console.log(y); // 20

然而,Tuple 不支持像普通 Array 那样随意添加或删除元素,因为其长度是固定的。

let point3: [number, number] = [10, 20];
// 以下操作会报错
// point3.push(30); 
// point3.pop();

如果尝试进行这些操作,TypeScript 编译器会报错,提示该 Tuple 类型不具有这些方法。

2.3 类型推断与解构

Tuple 的类型推断很有用。例如:

let data = [1, 'a'];
// data 被推断为 [number, string]

Tuple 也支持解构赋值,这在同时获取多个值时非常方便。

let user2: [string, number] = ['Jane', 25];
let [name, age] = user2;
console.log(name); // 'Jane'
console.log(age); // 25

这种解构方式使得代码更加简洁易读,并且可以利用 Tuple 的类型信息进行有效的类型检查。

2.4 使用场景

Tuple 适用于那些元素数量和类型固定的场景。比如表示二维坐标、颜色值(例如 [red, green, blue],每个值是 number 类型),或者函数返回多个固定类型的值。

function getFullName(): [string, string] {
    return ['John', 'Doe'];
}
let [firstName, lastName] = getFullName();

在这个例子中,getFullName 函数返回一个包含两个 string 类型元素的 Tuple,调用者可以通过解构轻松获取两个值。

3. 探究 ArrayLike

ArrayLike 不是一个实际的类型,而是一种类型模式,它描述了一个对象具有类似数组的结构,即具有 length 属性和数值索引,但不一定具有完整的 Array 方法。

3.1 常见的 ArrayLike 类型

  • arguments:在 JavaScript 函数内部,arguments 对象是一个 ArrayLike。它包含了传递给函数的所有参数。
function sum() {
    let args: { length: number; [index: number]: number } = arguments;
    let total = 0;
    for (let i = 0; i < args.length; i++) {
        total += args[i];
    }
    return total;
}
let result = sum(1, 2, 3);
console.log(result); // 6

这里 argumentslength 属性和数值索引,但它没有像 mapfilterArray 方法。

  • NodeList:在 DOM 操作中,document.querySelectorAll 等方法返回的 NodeList 也是 ArrayLike
let elements: { length: number; [index: number]: HTMLElement } = document.querySelectorAll('div');
for (let i = 0; i < elements.length; i++) {
    elements[i].style.color = 'red';
}

NodeList 同样有 length 和数值索引,但不具备完整的 Array 方法集。

3.2 转换为 Array

由于 ArrayLike 对象不具备完整的 Array 方法,通常需要将其转换为真正的 Array。可以使用 Array.from 方法。

function sum2() {
    let args = Array.from(arguments);
    return args.reduce((acc, num) => acc + num, 0);
}
let result2 = sum2(1, 2, 3);
console.log(result2); // 6

对于 NodeList 也类似:

let elements2 = Array.from(document.querySelectorAll('div'));
elements2.forEach((element) => {
    element.style.color = 'blue';
});

Array.from 方法会将 ArrayLike 对象转换为真正的 Array,从而可以使用 Array 的各种方法。

3.3 类型定义与使用

在 TypeScript 中,可以这样定义一个 ArrayLike 类型:

interface ArrayLike<T> {
    length: number;
    [index: number]: T;
}
function processArrayLike(arr: ArrayLike<number>) {
    let total = 0;
    for (let i = 0; i < arr.length; i++) {
        total += arr[i];
    }
    return total;
}
let arrLike: { length: number; [index: number]: number } = { length: 3, 0: 1, 1: 2, 2: 3 };
let result3 = processArrayLike(arrLike);
console.log(result3); // 6

这里通过接口定义了 ArrayLike 类型,然后在函数中使用该类型进行处理。

4. Array、Tuple 与 ArrayLike 的选择

4.1 根据数据结构特性选择

  • 动态数据量与同类型元素:如果数据量在运行时可能会动态变化,并且元素类型相同,Array 是最佳选择。例如,存储用户上传的文件列表,文件数量不确定,且每个元素都是文件对象。
let files: File[] = [];
function handleFileUpload(file: File) {
    files.push(file);
}
  • 固定长度与特定类型组合:当需要表示一组固定数量且类型固定的元素时,Tuple 是合适的。比如函数返回一个包含用户名和用户 ID 的结果。
function getUserInfo(): [string, number] {
    return ['Alice', 123];
}
let [username, userId] = getUserInfo();
  • 类似数组结构但方法有限:如果处理的对象具有类似数组的结构(有 length 和数值索引),但不需要完整的 Array 方法集,ArrayLike 更合适。例如在处理 arguments 对象或 NodeList 时。
function logArguments() {
    let args: { length: number; [index: number]: any } = arguments;
    for (let i = 0; i < args.length; i++) {
        console.log(args[i]);
    }
}
logArguments(1, 'a', true);

4.2 从代码可读性和维护性角度

  • Array:由于其广泛使用和简洁的语法,在处理常见的动态数组场景时,代码可读性高。但如果数组元素类型不单一,可能会在维护时带来一些困惑,因为不同类型的元素可能需要不同的处理逻辑。
  • TupleTuple 的类型明确,元素顺序和类型固定,使得代码在涉及到固定组合数据时非常易读和维护。例如,在表示坐标或颜色值时,Tuple 能清晰地传达每个元素的含义。
  • ArrayLikeArrayLike 在处理类似数组但方法有限的对象时,能准确反映其特性。然而,如果过度使用 ArrayLike 而不转换为 Array,可能会导致代码中出现较多的手动遍历逻辑,降低代码的简洁性和可读性。

4.3 性能考量

  • ArrayArray 提供了丰富的内置方法,在大多数情况下,这些方法经过优化,性能较好。但如果频繁进行插入和删除操作,特别是在数组中间位置,可能会有一定的性能开销,因为需要移动元素。
  • TupleTuple 由于长度固定,在内存分配和访问效率上相对较高,特别是在元素数量较少且固定的情况下。它没有动态添加或删除元素的开销。
  • ArrayLikeArrayLike 本身不具备复杂的方法,其性能主要取决于如何使用。如果转换为 Array 并使用 Array 方法,性能与 Array 类似;如果手动遍历,性能可能会受到遍历逻辑的影响。

4.4 实际案例分析

  • 案例一:游戏开发中的坐标系统 在一个 2D 游戏开发中,需要表示游戏角色的坐标。坐标通常是一个固定的 [x, y] 组合,使用 Tuple 非常合适。
let playerPosition: [number, number] = [100, 200];
function movePlayer(dx: number, dy: number) {
    playerPosition[0] += dx;
    playerPosition[1] += dy;
}
movePlayer(10, 20);
console.log(playerPosition); // [110, 220]

这里使用 Tuple 能清晰地表示坐标的结构,并且在处理坐标相关操作时,代码简洁明了。

  • 案例二:电商购物车系统 在电商购物车系统中,购物车中的商品数量是动态变化的,每个商品都有相同的类型(比如 Product 类型),此时使用 Array 是合理的。
interface Product {
    id: number;
    name: string;
    price: number;
}
let cart: Product[] = [];
function addToCart(product: Product) {
    cart.push(product);
}
let product1: Product = { id: 1, name: 'Laptop', price: 1000 };
addToCart(product1);

Array 方便地处理了购物车中商品的动态添加操作。

  • 案例三:DOM 元素批量操作 在前端页面开发中,需要获取一组 div 元素并进行一些操作,document.querySelectorAll 返回的是 NodeList,属于 ArrayLike
let divs = document.querySelectorAll('div');
let divArray = Array.from(divs);
divArray.forEach((div) => {
    div.classList.add('active');
});

这里先使用 ArrayLikeNodeList 获取元素,然后根据需求转换为 Array 以使用 forEach 方法进行批量操作。

5. 总结与建议

在 TypeScript 编程中,ArrayTupleArrayLike 各有其适用场景。Array 以其动态灵活性适用于大多数常见的数组场景,Tuple 凭借其固定长度和明确类型组合在特定数据结构表示上表现出色,而 ArrayLike 则准确描述了那些类似数组但方法有限的对象。

为了写出高质量的代码,建议在设计数据结构时,充分考虑数据的特性,包括元素数量是否固定、类型是否一致以及对操作方法的需求。同时,从代码可读性、维护性和性能等多方面综合权衡,选择最合适的类型。这样不仅能提高代码的健壮性,还能让代码更易于理解和扩展。

在实际项目中,经常会遇到多种类型混合使用的情况。比如,可能会在一个 Array 中包含多个 Tuple,或者将 ArrayLike 转换为 Array 后与 Tuple 进行交互。这就要求开发者对这三种类型有深入的理解,以便能够在不同场景下做出明智的选择。

通过合理选择 ArrayTupleArrayLike,可以充分发挥 TypeScript 的类型系统优势,提升编程效率和代码质量,打造更加可靠和易于维护的软件系统。

希望通过本文的介绍和分析,读者能够更加清晰地了解这三种类型的区别和使用场景,在实际编程中做出更恰当的选择。

总的来说,深入理解 ArrayTupleArrayLike 并灵活运用它们,是 TypeScript 开发者提升编程技能和解决实际问题能力的重要一步。在日常编码过程中,不断实践和总结经验,将有助于更好地掌握这几种类型,编写出更优秀的 TypeScript 代码。