数据结构

前言

JavaScript 提供了一系列内置的数据结构,适用于各种应用场景。以下是JavaScript中主要的数据结构及其用途:

基础数据结构

  • 数组(Array)

    • 一组有序的元素,可以通过索引访问。支持多种内置方法,如 push(), pop(), shift(), unshift(), map(), filter(), reduce() 等。
    • 在 ES6 之后,新增了SetMap等集合类对象,丰富了数据结构的选择。
  • 对象(Object)

    • 键值对的集合,键是字符串或符号,值可以是任何数据类型。
    • 用于存储和组织相关数据,如哈希表、字典等。

现代集合数据结构

  • 集合(Set)

    • 一组唯一的元素,没有重复项,支持基本集合操作,如add(), delete(), has(), clear()等。
    • Set常用于需要唯一元素集合的场景,如去重、集合操作等。
  • 映射(Map)

    • 键值对的集合,键可以是任何数据类型(包括对象、数组等)。
    • 支持按插入顺序迭代,提供方法如set(), get(), delete(), has(), clear()等。
  • 弱集合(WeakSet)

    • 类似Set,但其元素是对对象的“弱引用”,不会阻止垃圾回收。
    • 适用于跟踪对象而不希望其阻止垃圾回收的场景。
  • 弱映射(WeakMap)

    • 类似Map,但键是对对象的“弱引用”。
    • 常用于对象的私有属性,或跟踪对象的附加数据。

函数相关数据结构

  • 生成器(Generator)

    • 使用function*定义,可以暂停并恢复执行,适合生成序列、迭代器等。
  • 迭代器(Iterator)

    • 一个遵循特定协议的对象,提供next()方法,可以在数据结构中进行迭代。

其他数据结构

  • Promise

    • 表示异步操作的最终结果。提供then()catch()方法,常用于处理异步代码。
  • Proxy

    • 用于创建对象的代理,可以定义自定义行为,如拦截属性访问、函数调用等。

这些数据结构的组合与使用,能帮助开发者解决各种复杂的数据处理问题。在选择合适的数据结构时,了解它们的特性和用途至关重要。

数组(Array)

数组(Array)是JavaScript中最常用的数据结构之一,用于存储有序的元素列表。数组的特点包括:

特性

  • 有序:数组中的元素按插入顺序排列。
  • 索引访问:可以通过索引访问元素,索引从0开始。
  • 动态大小:数组的大小可以动态改变,可以添加或删除元素。
  • 任意数据类型:数组中的元素可以是任何数据类型,包括数字、字符串、对象、函数、甚至其他数组。

创建数组

可以使用多种方式创建数组:

  • 字面量方式
    const arr = [1, 2, 3, 4, 5];
  • 使用 Array 构造函数
    const arr = new Array(5); // 创建一个长度为5的数组
    const arr2 = new Array(1, 2, 3); // 创建一个包含元素1、2、3的数组
    

常用方法

数组有许多内置方法,以下是一些常用的数组操作方法:

  • 添加与删除

    • push(element): 在数组末尾添加元素。
    • pop(): 删除数组末尾的元素。
    • shift(): 删除数组开头的元素。
    • unshift(element): 在数组开头添加元素。
    • splice(index, count, ...elements): 在指定索引处删除元素,并可插入新的元素。
  • 遍历与迭代

    • forEach(callback): 遍历数组,每个元素调用回调函数。
    • map(callback): 返回一个新数组,包含回调函数处理后的元素。
    • filter(callback): 返回一个新数组,包含满足回调函数条件的元素。
    • reduce(callback, initialValue): 累积数组中的元素,返回单个值。
  • 查找与检查

    • indexOf(element): 返回指定元素的索引,若找不到则返回-1。
    • includes(element): 检查数组中是否包含指定元素。
    • find(callback): 返回第一个满足回调函数条件的元素。
    • findIndex(callback): 返回第一个满足回调函数条件的元素的索引。
  • 其他操作

    • concat(array): 合并两个数组,返回新数组。
    • slice(start, end): 返回从startend(不包含end)的子数组。
    • join(separator): 将数组中的所有元素连接成字符串,使用指定分隔符。
    • reverse(): 反转数组顺序。
    • sort([compareFunction]): 对数组进行排序。

这些方法使得数组在JavaScript中非常灵活,可以用于许多数据处理任务。从简单的列表到复杂的数据结构,数组在JavaScript开发中扮演着重要的角色。

push

const arr = [10, 20, 30, 40]
console.log(arr) // [ 10, 20, 30, 40 ]

arr.push(50, 60)
console.log(arr) // [ 10, 20, 30, 40, 50, 60 ]

pop

const arr = [10, 20, 30]
console.log(arr) // [ 10, 20, 30 ]

x = arr.pop()
console.log(x) // 30
console.log(arr) // [ 10, 20 ]

map

const arr = [10, 20, 30]

console.log(arr.map(x => x + 1)) // [ 11, 21, 31 ]
console.log(arr) // [ 10, 20, 30 ]

filter

const arr = [10, 20, 30]

console.log(arr.filter(x => x / 10 % 2 === 0)) // [ 20 ]
console.log(arr) // [ 10, 20, 30 ]

forEach

迭代所有元素,无返回值

在 JavaScript 中,forEach 是一个数组方法,用于对数组中的每个元素执行一次提供的函数。与传统的 for 循环不同,forEach 不会返回一个新数组,它主要用于例如修改原数组或在每个元素上执行某些操作。

语法

array.forEach(function(element, index, array) {
    // 要执行的代码
});
  • element:当前数组元素。
  • index(可选):当前元素的索引。
  • array(可选):调用 forEach 的数组。

示例

基本用法
const array = [1, 2, 3, 4, 5];

array.forEach(function(element) {
    console.log(element);
});
// 输出: 1 2 3 4 5
使用索引
const array = ['a', 'b', 'c'];

array.forEach(function(element, index) {
    console.log(`元素: ${element}, 索引: ${index}`);
});
// 输出: 
// 元素: a, 索引: 0
// 元素: b, 索引: 1
// 元素: c, 索引: 2
使用箭头函数
const array = [10, 20, 30];

array.forEach((element, index) => {
    console.log(`索引 ${index} 的元素值为 ${element}`);
});
// 输出:
// 索引 0 的元素值为 10
// 索引 1 的元素值为 20
// 索引 2 的元素值为 30

注意事项

  1. 无法提前终止:与 forfor...of 循环不同,forEach 不能通过 break 语句提前终止。如果需要这种行为,可以使用 someevery 方法,它们可以在回调函数返回 true 时提前终止。

    const array = [1, 2, 3, 4, 5];
    
    array.some(function(element) {
        console.log(element);
        return element === 3;
    });
    // 输出: 1 2 3
    
  2. 不会修改原数组forEach 不会直接修改调用它的数组,但如果回调函数对数组进行了修改,则会反映在原数组中。

    const array = [1, 2, 3];
    
    array.forEach(function(element, index, array) {
        array[index] = element * 2;
    });
    
    console.log(array); // 输出: [2, 4, 6]
    
  3. 异步操作forEach 不适合处理异步操作,因为它不会等待回调函数中的异步操作完成。

    const array = [1, 2, 3];
    
    array.forEach(async (element) => {
        await new Promise(resolve => setTimeout(resolve, 1000));
        console.log(element);
    });
    // 输出:
    // 1
    // 2
    // 3
    // (每个数字之间的间隔为1秒,但它们不会按顺序输出)
    
  4. this 参数forEach 方法可以接受第二个参数,用于指定回调函数的 this 值。

    const obj = {
        multiplier: 2
    };
    
    const array = [1, 2, 3];
    
    array.forEach(function(element) {
        console.log(element * this.multiplier);
    }, obj);
    // 输出:
    // 2
    // 4
    // 6
    

总结

forEach 是一个方便的数组方法,适用于需要对数组的每个元素执行某些操作的场景。但需要注意它的一些局限性,特别是在处理异步操作或需要提前终止循环时。

数组练习题

题目一

  • 有一个数组:const arr = [1, 2, 3, 4, 5];,要求算出所有元素的平方值,且输出平方值是偶数且大于10的平方值

map + filter

const arr = [1, 2, 3, 4, 5];

// 所有元素的平方值
console.log(arr.map(x => x ** 2)); // [ 1, 4, 9, 16, 25 ]

// 所有元素的平方值,且输出平方值是偶数
console.log(arr.map(x => x ** 2).filter(x => x % 2 ===0)); // [ 4, 16 ]

// 所有元素的平方值,且输出平方值是偶数,且大于10的平方值
console.log(arr.map(x => x ** 2).filter(x => x % 2 ===0).filter(x => x > 10)); // [ 16 ]
// 简便写法,使用逻辑与:
console.log(arr.map(x => x ** 2).filter(x => x % 2 === 0 && x > 10)); // [ 16 ]

forEach

使用 forEach 可以实现相同的功能,但 forEach 本身并不会返回一个新数组,因此你需要手动创建一个结果数组并在遍历过程中将符合条件的元素添加到这个数组中。下面是通过 forEach 实现的示例:

const arr = [1, 2, 3, 4, 5];

const result = [];
arr.forEach(x => {
    const squared = x ** 2;
    if (squared % 2 === 0 && squared > 10) {
        result.push(squared);
    }
});

console.log(result); // [ 16 ]

在这里,我们首先创建一个空数组 result,然后使用 forEach 遍历原始数组 arr。对于每一个元素,我们计算它的平方值并检查是否满足两个条件:是偶数且大于10。如果满足条件,就将其添加到 result 数组中。最后,打印 result 数组即可。

最佳方案

  • 减少计算
const arr = [1, 2, 3, 4, 5];

// filter + map
const point = Math.sqrt(10);
console.log(arr.filter(x => (x > point) && !(x & 1)).map(x => x ** 2)); // [ 16 ]

// forEach
const newArr = []
arr.forEach(x => {
    if ((x > point) && !(x & 1)) {
        newArr.push(x ** 2)
    }
}
);
console.log(newArr) // [ 16 ]

对象(Object)

对象(Object)是JavaScript中的一种基本数据结构,主要用来表示键值对的集合。对象非常灵活,广泛应用于数据存储、组织和建模。

对象的特点

  • 键值对:对象由一组键值对组成,其中键是唯一的,值可以是任何数据类型。
  • 无序:在 ES2015 之前,对象的键没有固定的顺序。ES2015 及以后版本,键的顺序遵循一定的规则:首先按数字键排序,然后按插入顺序保存字符串和符号键。
  • 动态结构:可以动态添加、修改或删除键值对。

创建对象

有多种方式创建对象:

  • 对象字面量
    const person = {
      name: 'Alice',
      age: 25,
      occupation: 'Engineer'
    };
  • new Object() 构造函数
    const obj = new Object();
    obj.name = 'Bob';
    obj.age = 30;
  • Object.create() 方法
    const prototype = { greet: () => 'Hello!' };
    const obj = Object.create(prototype);

访问与操作对象

可以通过两种方式访问和操作对象的属性:

  • 点表示法:适用于键符合变量命名规则的情况。
    console.log(person.name); // 'Alice'
    person.age = 26;
  • 方括号表示法:适用于动态键名、非标准变量名等情况。
    const key = 'occupation';
    console.log(person[key]); // 'Engineer'
    person['country'] = 'USA'; // 动态添加属性
    

常用对象方法

对象提供了许多操作和检查的方法,这里列举了一些常用的对象相关方法:

  • 获取键、值、键值对

    • Object.keys(obj): 返回对象的所有键,结果是数组。
    • Object.values(obj): 返回对象的所有值,结果是数组。
    • Object.entries(obj): 返回对象的所有键值对,结果是二维数组。
  • 对象比较

    • Object.is(value1, value2): 判断两个值是否严格相等,包括 NaN 与 NaN 相等。
  • 对象复制和合并

    • Object.assign(target, ...sources): 将源对象的属性复制到目标对象上,返回目标对象。
    • ...spread: ES6 语法糖,可以快速复制和合并对象。
  • 检查对象的属性

    • obj.hasOwnProperty(key): 检查对象是否有指定的属性。
    • key in obj: 检查对象是否包含指定的键。

示例1

const obj = {
    a:100,
    b:200,
    c:300
};

for (let k in obj) {
    console.log(k, obj[k]);
};
/*
a 100
b 200
c 300
*/

console.log(Object.keys(obj)) // [ 'a', 'b', 'c' ]
console.log(Object.values(obj)) // [ 100, 200, 300 ]
console.log(Object.entries(obj)) // [ [ 'a', 100 ], [ 'b', 200 ], [ 'c', 300 ] ]

面向对象编程

JavaScript 中对象也可以用于面向对象编程(OOP)。可以使用构造函数或 ES6 的类来创建对象,并为其定义属性和方法。

  • 构造函数

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    
    const alice = new Person('Alice', 25);
  • 类与继承

    class Animal {
      constructor(name) {
        this.name = name;
      }
    
      speak() {
        console.log(`${this.name} makes a noise.`);
      }
    }
    
    class Dog extends Animal {
      speak() {
        console.log(`${this.name} barks.`);
      }
    }
    
    const dog = new Dog('Rex');
    dog.speak(); // 'Rex barks.'
    

通过对象和相关工具,可以实现多种编程范式,支持灵活的数据处理和组织结构。

弱集合(WeakSet)

WeakSet 是 JavaScript 中一种特殊类型的集合,和普通 Set 类似,但有一些关键的不同点,主要在于对对象的 “弱引用” 特性。以下是关于 WeakSet 的详细解释:

WeakSet 的特点

  • 仅包含对象WeakSet 中的元素必须是对象,不能包含原始值(如数字、字符串、布尔值等)。
  • 弱引用WeakSet 对内部元素持有 “弱引用”,这意味着如果没有其他强引用指向这些对象,它们会被垃圾回收器回收。
  • 不可迭代:由于弱引用的特性,WeakSet 无法被直接迭代或转换为其他可迭代结构。
  • 无重复元素WeakSet 中的元素是唯一的,不能有重复项。

使用场景

WeakSet 适合在需要跟踪对象,而不希望其阻止垃圾回收的场景中使用。例如:

  • 管理对象的存在性WeakSet 可用于跟踪一组对象,便于检查其存在性,而不会阻止这些对象的垃圾回收。
  • 对象缓存:当需要创建缓存,但希望缓存中的对象能被自动垃圾回收时,WeakSet 是一个不错的选择。

创建 WeakSet

可以使用 WeakSet 构造函数创建一个新的弱集合,并可选地传入一个包含对象的可迭代对象进行初始化:

const weakSet = new WeakSet();
const obj1 = {};
const obj2 = {};

weakSet.add(obj1);
weakSet.add(obj2);

console.log(weakSet.has(obj1)); // true

常用方法

WeakSet 提供了以下常用方法:

  • add(object):将对象添加到弱集合中。
  • has(object):检查对象是否在弱集合中。
  • delete(object):从弱集合中删除对象。
const ws = new WeakSet();
const obj = {};

ws.add(obj);
console.log(ws.has(obj)); // true

ws.delete(obj);
console.log(ws.has(obj)); // false

不可迭代

与普通 Set 不同,WeakSet 无法迭代,也没有 size 属性。由于其中的对象可能随时被垃圾回收,这使得无法保证迭代的稳定性。这是 WeakSetSet 之间的主要区别之一。

总体而言,WeakSet 是一种灵活的数据结构,适用于管理对象的存在性,同时保持对垃圾回收的支持。在需要这些特性的场景中,WeakSet 是一个很好的选择。

弱映射(WeakMap)

WeakMap 是 JavaScript 中一种特殊类型的映射结构,和普通 Map 类似,但有一些独特的特征,使其在处理对象与附加数据之间的关联时非常有用。以下是关于 WeakMap 的详细解释:

WeakMap 的特点

  • 键是弱引用WeakMap 的键是对对象的弱引用,这意味着如果没有其他强引用指向这些对象,它们会被垃圾回收。这使得 WeakMap 不会阻止对象被垃圾回收。
  • 不可迭代:由于键的弱引用特性,WeakMap 无法被迭代或转换为其他可迭代结构,且没有 size 属性。
  • 键只能是对象WeakMap 中的键必须是对象,不能是原始值(如数字、字符串、布尔值等)。

使用场景

WeakMap 适用于存储与对象相关的私有数据、元数据、缓存等场景,尤其在需要对象自动垃圾回收时:

  • 私有数据存储:可以用 WeakMap 存储对象的私有属性,而不影响对象的垃圾回收。
  • 缓存:在对象生命周期内需要临时存储额外数据时,WeakMap 是理想选择。
  • 元数据管理:可以将元数据关联到对象,而无需修改对象自身的结构。

创建 WeakMap

可以使用 WeakMap 构造函数创建一个新的弱映射,并可选地传入一个包含键值对的可迭代对象进行初始化:

const weakMap = new WeakMap();
const obj1 = {};
const obj2 = {};

weakMap.set(obj1, "Value 1");
weakMap.set(obj2, "Value 2");

console.log(weakMap.get(obj1)); // "Value 1"

常用方法

WeakMap 提供了以下常用方法:

  • set(key, value):为对象设置键值对。
  • get(key):根据对象键返回关联的值。
  • delete(key):删除对象键及其关联的值。
  • has(key):检查对象键是否在弱映射中。
const wm = new WeakMap();
const obj = {};

wm.set(obj, "some data");
console.log(wm.get(obj)); // "some data"

wm.delete(obj);
console.log(wm.has(obj)); // false

不可迭代

WeakMap 不支持迭代,也没有 keys()values()entries() 方法。这是因为其中的对象键可能随时被垃圾回收,无法保证迭代的稳定性。这与普通 Map 的最大区别之一。

WeakMap 的使用要点

  • 保持键的可回收性WeakMap 的主要优势在于不会阻止对象被垃圾回收。因此,务必避免创建对键的强引用,否则会失去 WeakMap 的优势。
  • 避免滥用:由于 WeakMap 无法迭代,适用于特定场景,而不适合所有映射需求。

WeakMap 提供了一种关联对象与附加数据的灵活方式,确保对象的生命周期不会受到影响。适用于存储与对象相关的私有数据、元数据、缓存等场景。

生成器(Generator)

生成器(Generator)是JavaScript中的一种特殊类型的函数,可以在执行过程中暂停,并且可以在之后恢复。这种特性使生成器在处理惰性计算、流数据、迭代和协程等场景中非常有用。以下是关于生成器的详细解释:

生成器的定义

生成器使用 function* 语法定义,星号 * 表示这是一个生成器函数。生成器函数可以使用 yield 关键字来暂停执行,并返回值给调用者。

function* counter() {
  let count = 0;
  while (true) {
    yield count++;
  }
}

const gen = counter();

生成器的行为

生成器函数的返回值是一个生成器对象,提供 next()return()throw() 方法来控制生成器的执行。

  • next():恢复生成器的执行,从上一个 yield 位置继续,返回一个包含 valuedone 的对象。
  • return():终止生成器的执行,并可选择返回一个值。
  • throw():向生成器抛出异常,允许在生成器内部处理错误。
const gen = counter();

console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }

在这个例子中,生成器会无限生成递增的数字,直到调用 return()throw()

yieldyield*

  • yield:用于暂停生成器的执行,并返回值给调用者。生成器在此暂停,直到 next() 再次被调用。
  • yield*:用于在一个生成器中委托另一个生成器,允许嵌套生成器的执行。
function* numbers() {
  yield 1;
  yield 2;
}

function* moreNumbers() {
  yield* numbers(); // 委托给 'numbers' 生成器
  yield 3;
}

const gen = moreNumbers();
console.log([...gen]); // [1, 2, 3]

生成器的应用

生成器在许多场景中有用,包括:

  • 惰性计算:生成器可以生成大量数据而不会立即计算,有助于处理大数据集。
  • 迭代器:生成器实现了迭代器接口,可用于构建自定义迭代器。
  • 协程:生成器可以通过 yieldnext() 实现协作式多任务处理。
  • 数据流:生成器可以处理连续的数据流,适用于实现惰性流处理。

生成器是JavaScript中强大的工具,为编写更灵活、更高效的代码提供了可能性。通过暂停和恢复的能力,生成器在许多编程场景中扮演了重要角色。

迭代器(Iterator)

迭代器(Iterator)是JavaScript中的一种接口,它定义了一种协议,用于访问集合中的元素。这种协议允许在数据结构中逐个迭代元素,提供了一种通用的方式来遍历集合。迭代器在遍历数组、对象、生成器等各种数据结构时非常有用。

迭代器协议

迭代器协议规定,迭代器必须实现一个名为 next() 的方法,该方法返回一个对象,包含以下属性:

  • value:当前迭代的值。
  • done:一个布尔值,指示迭代是否结束。

使用迭代器

可以通过调用 next() 方法获取迭代器的下一个值:

const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator](); // 获取数组的迭代器

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

在这个例子中,arr[Symbol.iterator]() 返回一个迭代器,可以通过调用 next() 方法逐个获取数组的元素,直到 donetrue

可迭代对象

一个对象要成为可迭代的,必须实现迭代器接口。这通常通过实现 Symbol.iterator 方法来完成。标准数据结构如数组、字符串、MapSet 等都默认实现了这个接口。

const str = "hello";
const strIterator = str[Symbol.iterator]();

console.log(strIterator.next()); // { value: 'h', done: false }

自定义迭代器

可以通过实现 Symbol.iterator 方法来创建自定义迭代器。这允许为任何对象定义迭代行为。

const range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    let current = this.from;
    let end = this.to;
    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (let num of range) {
  console.log(num); // 1, 2, 3, 4, 5
}

在这个例子中,range 是一个自定义对象,通过实现 Symbol.iterator,可以使用 for...of 循环迭代它的元素。

生成器与迭代器

生成器函数 function* 提供了一个简便的方式来创建迭代器。生成器函数会自动生成迭代器,并支持使用 yield 关键字来定义迭代逻辑。

function* simpleGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = simpleGenerator();

console.log(gen.next()); // { value: 1, done: false }

迭代器的应用

迭代器的常见应用包括:

  • 遍历数据结构:如数组、字符串、MapSet等。
  • 生成器:生成器默认实现了迭代器接口,可以用于生成序列数据。
  • 自定义迭代逻辑:可以为自定义对象实现迭代器,实现复杂的迭代逻辑。

迭代器提供了一种一致的方式来遍历数据,允许在处理复杂数据结构时保持代码的简洁和可维护性。

Promise

Promise 是 JavaScript 中用于处理异步操作的对象,它可以表示一个异步操作的最终结果,无论是成功还是失败。Promise 的概念有助于解决回调地狱和异步代码的复杂性。以下是对 Promise 的详细解释:

Promise 的状态

一个 Promise 对象有三种状态:

  • pending:初始状态,表示异步操作尚未完成。
  • fulfilled:异步操作成功,Promise 有了一个结果。
  • rejected:异步操作失败,Promise 有了一个拒绝的原因。

一旦 Promise 状态改变为 fulfilledrejected,就不会再变化。这种不可逆性是 Promise 的关键特性之一。

创建 Promise

可以使用 Promise 构造函数创建一个新的 Promise 对象,并提供一个执行器函数,该函数接收两个参数:resolvereject,用于改变 Promise 的状态。

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true; // 模拟异步操作结果
    if (success) {
      resolve("Operation successful!");
    } else {
      reject(new Error("Operation failed!"));
    }
  }, 1000);
});

在这个例子中,myPromise 是一个 Promise 对象,模拟了一个异步操作,1秒后会调用 resolvereject

使用 Promise

Promise 对象提供了方法 then()catch()finally() 来处理异步操作的结果。

  • then(onFulfilled, onRejected):当 Promise 状态变为 fulfilled 时,执行 onFulfilled;当状态变为 rejected 时,执行 onRejected
  • catch(onRejected):等价于 then(null, onRejected),用于处理 Promise 的拒绝。
  • finally(callback):无论 Promise 成功或失败,都会执行 callback,适合用于清理操作。
myPromise
  .then((result) => {
    console.log(result); // "Operation successful!"
  })
  .catch((error) => {
    console.error(error); // 仅当操作失败时才会执行
  })
  .finally(() => {
    console.log("Cleanup"); // 无论成功或失败都会执行
  });

Promise

Promise 可以被链接起来,这样可以处理一系列异步操作,且每个 then() 的返回值会成为下一个 then() 的输入。这种链式结构简化了复杂的异步流程。

function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Data fetched");
    }, 1000);
  });
}

fetchData()
  .then((data) => {
    console.log(data); // "Data fetched"
    return "Next operation";
  })
  .then((result) => {
    console.log(result); // "Next operation"
  });

Promise.all()Promise.race()

Promise 还提供了用于并行处理的静态方法:

  • Promise.all(promises):接受一个 Promise 数组,返回一个 Promise,当所有 Promise 都成功时,该 Promise 状态变为 fulfilled;若有一个失败,该 Promise 状态变为 rejected
  • Promise.race(promises):接受一个 Promise 数组,返回第一个状态改变的 Promise
const p1 = new Promise((resolve) => setTimeout(() => resolve("P1 done"), 1000));
const p2 = new Promise((resolve) => setTimeout(() => resolve("P2 done"), 2000));

Promise.all([p1, p2])
  .then((results) => {
    console.log(results); // ["P1 done", "P2 done"]
  })
  .catch((error) => {
    console.error(error);
  });

Promise.race([p1, p2])
  .then((result) => {
    console.log(result); // "P1 done"
  });

Promise 是 JavaScript 处理异步操作的基础,通过 then()catch()finally() 提供了一种清晰的方式来管理异步逻辑。与 async/await 相结合,可以进一步简化异步代码。

Proxy

Proxy 是 JavaScript 中的一种内置对象,它允许你创建对象的代理,并通过拦截各种操作来定义自定义行为。Proxy 在许多场景中有用,比如验证数据、跟踪对象变化、实现访问控制等。

Proxy 的定义

Proxy 构造函数接收两个参数:目标对象(target)和处理程序对象(handler)。处理程序对象定义了要拦截的操作和相应的行为。

const target = { name: 'Alice', age: 25 };
const handler = {
  get(obj, prop) {
    return prop in obj ? obj[prop] : 'Property does not exist';
  },
  set(obj, prop, value) {
    if (prop === 'age' && value < 0) {
      throw new Error('Age must be positive');
    }
    obj[prop] = value;
    return true; // 必须返回 true 表示成功
  }
};

const proxy = new Proxy(target, handler);

在这个例子中,处理程序对象定义了 getset 两个拦截器,分别用于自定义属性的获取和设置行为。

常用拦截器

Proxy 提供了多种可以拦截的操作,以下是一些常用的拦截器:

  • get(obj, prop, receiver):拦截属性的获取操作。
  • set(obj, prop, value, receiver):拦截属性的设置操作。
  • has(obj, prop):拦截 in 操作。
  • deleteProperty(obj, prop):拦截属性的删除操作。
  • ownKeys(obj):拦截 Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()
  • apply(target, thisArg, args):拦截函数的调用操作。
  • construct(target, args, newTarget):拦截 new 操作。

Proxy 的应用

Proxy 的灵活性使其适用于各种场景,以下是一些常见的应用:

  • 数据验证:可以使用 Proxy 在属性设置时进行验证。
  • 访问控制:可以拦截属性访问,确保只能在特定条件下访问。
  • 跟踪对象变化:通过拦截属性设置、删除等操作,可以跟踪对象的变化。
  • 虚拟属性:可以通过拦截 get 操作,动态生成属性值。
  • 函数代理:可以通过拦截 apply 操作,代理函数的调用。
const person = {
  name: 'Alice',
  age: 25
};

const validator = {
  set(obj, prop, value) {
    if (prop === 'age' && (typeof value !== 'number' || value < 0)) {
      throw new Error('Invalid age');
    }
    obj[prop] = value;
    return true;
  }
};

const personProxy = new Proxy(person, validator);

try {
  personProxy.age = -10; // 抛出错误 "Invalid age"
} catch (error) {
  console.error(error.message); // "Invalid age"
}

personProxy.age = 30; // 成功设置 age 为 30

在这个例子中,Proxy 用于验证对象的属性设置,以确保 age 属性为非负数。

注意事项

使用 Proxy 时需要注意以下几点:

  • 性能:拦截器的过度使用可能影响性能,因为每次访问都会触发拦截器。
  • 透明度Proxy 的行为可能导致代码的行为与预期不同,因此需要确保处理程序的逻辑清晰且可理解。
  • 代理陷阱:在某些情况下,代理可能会引起意想不到的行为,例如递归代理或过度代理。

Proxy 是一个强大的工具,提供了自定义对象行为的能力。通过理解其拦截机制和应用场景,可以在开发中充分利用其特性。