四、JavaScript 内置对象(二)

151 阅读7分钟

前言

上一篇文章 介绍了与原始值类型相关的内置对象,以及 MathDate 这两个常用的内置对象。本文将介绍 JavaScript 中数据结构相关的内置对象,包括 ArrayMapWeakMapSetWeakSet,对于 Int8ArrayUint8Array 这样的 TypedArray 只会简单介绍。

1. 数据结构

1.1 Array

Array 在 JavaScript 中算是最常用的数据结构,它跟其它编程语言里的 Array 有很大的区别。

  • JavaScript 数组内的元素可以是不同的数据类型
  • JavaScript 数组的 length 可以直接修改
  • JavaScript 数组可以通过 push/pop/shift/unshift/splice 方法来直接给数组追加/删除元素
const arr = [];

arr.length = 2; // 直接将数组长度修改为 2,空位填充 `undefined`
arr.push(3); // 从数组尾部添加元素,数组长度增加 1,变为 3
arr.unshift(-1); // 从数组头部添加元素,数组长度增加 1,变为 4
arr.pop(); // 取出数组尾部的第 1 个元素,数组长度减少 1,变为 3
arr.shift(); // 取出数组头部的第 1 个元素,数组长度减少 1,变为 2

JavaScript 中还有一种数组叫做 "类型数组 (TypedArray)",与之对应的是 ArrayBufferInt8ArrayUint8ArrayInt16ArrayUint16Array 等。

创建方式

数组可以通过多种方式创建:

  • 字面量 (最常用)
  • 使用 Array.form(source)
  • 使用构造函数 Array (强烈建议 要用这种方式)

使用与操作

JavaScript Array 提供了 forEach, map, reduce, pop, push, shift, unshift 等丰富的实例方法,以及 length 这个唯一的实例属性。此外,Array 构造函数也提供了 from, of, isArray 这三个静态方法,用于数组创建和数组判断。这些方法和属性的使用,在开发过程中多翻一翻文档就好,不用死记硬背。

1.2 Map

Map 是用于保存 key/value 映射关系的一种数据结构,它和 Object 很像,但两者还是有很大的区别,这在 MDN Map-描述 里进行了详细的对比。

Map 的常用操作:

  • 使用 set(key, value) 方法插入值
  • 使用 get(key) 方法获取值
  • 使用 delete(key) 方法来删除值
  • 使用 clear() 方法来清空整个 Map 实例
const map = new Map();
const objKey = {};
map.set(objKey, "这是一个对象 key 的属性");
map.set(1, "这个值的 key 是数字 1");
console.log(map.get(objKey)); // "这是一个对象 key 的属性"

使用场景:

  • 需要记录非 string/symbol 键的键值对映射时
  • 需要一个非常 “纯净” 的键值对映射容器时
  • 需要保证 key 的遍历顺序时
  • 需要记录键值对映射数量时
  • 需要对键值对进行频繁的增删时
  • 键值对映射不需要序列化时

1.3 WeakMap

WeakMapkey 只能是对象,且对这个对象是弱引用的 (这个对象可以在不被其他地方引用时被回收),它的值可以是任意类型的。

WeakMap 只有 get, set, delete, has 4个实例方法,使用起来很简单,当然,它的使用场景也相对较少,你可以查阅 Mdn WeakMap 来做一个更细致一点的了解。

1.4 Set

Set 相当于是一个容器,它可以存储任意类型的值,但无论是原始值还是引用值,都只会出现一次。

  • 原始值只会出现一次
  • 引用值只会出现相同引用

Set 中的元素不能直接访问,因此它没有 get() 方法,只有 add, clear, delete, has, entries, forEach, keys, values 方法,以及 size 属性。

keys 方法是 values 方法的别名

const set = new Set();
set.add({}); // set.size = 1;
set.add({}); // set.size = 2;
console.log(set.has({})); // false

set.clear();

const obj = {};
set.add(obj); // set.size = 1
set.add(obj); // set.size = 1
console.log(set.has(obj)); // true

// 为什么会是上面这样的结果呢?
// 因为不同的对象字面量,是不同的引用值

Set 最常用的一个场景就是元素去重:

const arr = ["1", 1, 1, "1"];
const arr2 = Array.from(new Set(arr));
console.log(arr2); // ["1", 1]

1.5 WeakSet

WeakSet 也是一个容器,但它只能存储对象,且对这个对象是弱引用。

WeakSet 只有 add, delete, has 三个方法。

你可以通过在浏览器 devtools Console 面板上依次输入下面的代码来体验一下 WeakMapMap 的差别

const set = new Set();
set.add({});

const weakset = new WeakSet();
weakset.add({});

console.log(set);
console.log(weakset);

看到了结果之后,按下快捷键 Ctrl + L 清空控制台,再在控制台输入下面的代码

console.log(set);
console.log(weakset);

你会发现 set 中有一个元素,而 weakset 是空的,没有元素。

2. 工具对象

2.1 Promise

Promise 是 JavaScript 中最早的真正的异步编程方案,Promise 对象实例用于表示一个异步操作的最终结果及其结果值。如果结果是完成,则包含完成返回的值;如果结果是失败,则包含失败返回的结果值。

一个 Promise 对象实例有三种状态:

  • pendding: 初始状态,也是过程持续状态
  • fulfilled:成功时的结果状态,即通过 resolve 返回时的状态,此时可以通过 then 的第一个回调来获取到返回值
  • rejected:失败时的结果状态,即通过 reject 返回时的状态,此时可以通过 then 的第二个回调来获取到返回值,也可以通过 catch 来获取到返回值。

一旦 Promise 对象实例的状态到达了结果状态,无论是 fulfilled 还是 rejected,其结果状态、结果值就不会再改变。

Promise 对象实例只有 then, catchfinally 三个方法,且这三个方法返回值都是 Promise 对象实例。

  • 利用这个特性,Promise 可以很轻松地实现链式调用。
const p = new Promise((resolve) => {
  resolve("响应成功");
});

p.then((result) => {
  console.log("第一个 then 收到响应:", result);
  return "再次响应成功";
})
  .then((result) => {
    console.log("第二个 then 收到响应:", result);
    throw new Error("抛出了一个错误");
  })
  .then((result) => {
    console.log("第三个 then 收到响应:", result);
  })
  .catch((error) => {
    console.log("捕获到错误:", error);
  });

// 最终的输出结果为
// 第一个 then 收到响应: 响应成功
// 第二个 then 收到响应: 再次响应成功
// 捕获到错误: Error: 抛出了一个错误

再看和上一段代码类似的:

const p = new Promise((resolve) => {
  resolve("响应成功");
});

p.then((result) => {
  console.log("第一个 then 收到响应:", result);
  throw new Error("在第一个 then 里抛出了一个错误");
  // return "再次响应成功";
})
  .then((result) => {
    console.log("第二个 then 收到响应:", result);
    // 这个异常不会被抛出,因为前一个 promise 抛出了一个异常,所以不会执行这个 then 回调
    throw new Error("抛出了一个错误");
  })
  .then(
    (result) => {
      console.log("第三个 then 收到响应:", result);
      return "第三次响应成功";
    },
    // 这个是用来捕获前面未被捕获的 promise 的异常
    (error) => {
      console.log("捕获到错误:", error);
      return "错误捕获里的返回值";
    }
  )
  .then((result) => {
    console.log("第四个 then 收到响应:", result);
  })
  .catch((error) => {
    console.log("最终捕获错误:", error);
  });

// 此时最终的输出为
// 第一个 then 收到响应: 响应成功
// 捕获到错误: Error: 在第一个 then 里抛出了一个错误
// 第四个 then 收到响应: 错误捕获里的返回值

Promise 的静态方法有 all, allSettled, any, race, reject, resolve 共6个,它们的作用各有特点,这些方法的作用可以查阅文档 MDN Promise

  • all, allSettled, any, race 的参数都是一组 Promise 对象实例
  • resolvereject 通常是用来将一个函数的返回值包装成 Promise 对象实例

Promise 最重要的是理解概念,知道基本的使用方式

然后是掌握除 resolvereject 之外的静态方法

最后是明白 Promise 对象实例的 then/catch/finnaly 回调函数都是产生的 微任务

2.2 Proxy/Reflect

Proxy 即 “代理”,它代理的目标是 Object,用于实现对象基本操作 (属性取值/属性赋值/可枚举性/方法调用等) 的拦截和自定义,Vue3 的响应式就是基于 Proxy 去做的。

Reflect 即 “反射”,它的作用是提供一系列静态方法来操作对象,最常用的场景就是用在 Proxyhandler 中,用以操作代理对象。

ProxyReflect 在日常的业务开发中用得特别少,一般在做一些底层开发的时候才会用到,因此只需要了解一下这两个概念即可。

推荐一篇文章,让你更好地理解 ReflectJavaScript 中的 Reflect 反射

总结

本文先简单介绍了 JavaScript 里的几个关键数据结构内置对象:Array, Map, WeakMap, Set, WeakSet。这些内置对象里,最常用的是 Array,它的实例方法也是最多的,功能非常强大。Map/WeakMap 用来存储键值对,它们常用于需要记录键值对映射关系的场景。Set/WeakSet 是数据容器,它们里面不会存在相同的值 (原始值/引用值)。

然后介绍了 JavaScript 异步编程最重要的对象:Promise,并简单演示了 Promise 的使用。

最后介绍了 JavaScript 元编程里比较重要的两个对象:ProxyReflect