# 对象的扩展

本节实验我们来探索一下,在 ES6 中对象相关的新变化。

# 知识点

  • 对象字面量
  • 对象的扩展运算符
  • 对象的新增方法

# 对象字面量

对象字面量就是使用 {} 去定义对象。

在 ES6 中,对象字面量有了许多增强的写法,这里会给大家介绍以下三种新写法:

  • 属性的简洁表示法
  • 方法的简洁表示法
  • 属性名表达式

# 属性的简洁表示法

在 ES6 之前我们可能会像下面这样来定义:

const name = "闷墩儿";
const age = 2;
const dog = { name: name, age: age };

在上面代码中,定义了一个名为 dog 的对象,其属性名与属性值的变量名相同,但在定义的对象还是要重复写两遍。

有了 ES6 之后,我们可以使用属性初始化的简写语法,消除这种属性名称与局部变量之间的重复书写。简洁表示法如下所示:

const dog = { name, age };

新建一个 index.html 文件,我们可以尝试在控制台输出上面代码中的对象,看看其结果是否相同。

console.log(dog);

在控制台我们可以看到输出都是:

1678553619346

# 方法的简洁表示法

除了属性有简写的形式外,方法也有简洁的表示法。😉

在 ES6 之前,如果要为对象添加方法,必须通过指定名称并完整定义函数,我们可能会像下面这样来定义:

const name = "闷墩儿";
const dog = {
  run: function () {
    return name + "在公园里奔跑!";
  },
};

有了 ES6 之后,就可以不用冒号和 function 关键字了。我们可以用以下简洁表示法:

const name = "闷墩儿";
const dog = {
  run() {
    return `${name}在公园里奔跑!`;
  },
};

新建一个 index1.html 文件,我们可以尝试在控制台输出上面代码中的方法,看看其结果是否相同。

console.log(dog.run());

在控制台我们可以看到输出都是:

1678553759159

# 属性名表达式

在 ES6 之前,我们只能使用标识符的方式来作为属性名,例如:

dog.name = "闷墩儿";

而在 ES6 之后,我们还可以使用表达式来作为属性名,例如:

dog["n" + "ame"] = "闷墩儿";

新建一个 index2.html 文件,在控制台输出 dog 对象,其结果为:

1678553834223

我们还可以将定义的模版字面量放入 [] 中,例如:

const key = `name`;
const dog = {
  [key]: "闷墩儿",
};

定义在 [] 中的属性说明该属性是可以被计算的动态属性,其内容是可以被计算的,最后会转换成字符串,这提高了代码的灵活性。

let temp = "temp";
const key = `name${temp}`;
let dog = {
    key: "闷墩儿",
};
console.log(dog); // {key: ' 闷墩儿 '}
dog = {
    [key]: "闷墩儿",
};
console.log(dog); // {nametemp: ' 闷墩儿 '}
// dog = {
//     'a' + 'b': "闷墩儿", // 报错
// };
dog = {
    ['a' + 'b']: "闷墩儿", // 报错
};
console.log(dog); // {ab: ' 闷墩儿 '}
dog = {
    'a': "闷墩儿", // 这里键为字母开头时是否使用引号都可,下面可以用 dog.a 和 dog ['a']
};
console.log(dog.a); // 闷墩儿

# 对象的扩展运算符

在对象中引入扩展运算符后,我们可以用来遍历参数对象中的所有属性,并将其拷贝到新对象中。

概念听起来复复杂杂滴 🤔️ 我们来看个例子。

新建 index3.html 文件。

let obj1 = { species: "柯基", name: "闷墩儿", age: 2 };
let obj2 = { ...obj1 };
console.log(obj2);

我们在控制台可以看到 obj2 也拥有 obj1 的属性和属性值。

1678553857126

还可以使用扩展运算符将两个对象合并到一个新对象中。举个例子:

let obj1 = { species: "柯基", name: "闷墩儿", age: 2 };
let obj2 = { food: "狗粮" };
let obj3 = { ...obj1, ...obj2 };
console.log(obj3);

在控制台可以看到 obj1obj2 的属性和属性值都是拷贝到 obj3 里面了。

1678553868243

# 对象的新增方法

在 ES6 中新增了一些 Object 全局对象的方法,其一些操作更简便,在这里会给大家介绍以下几种方法:

  • Object.is
  • Object.assign
  • 遍历对象

# Object.is

在 ES6 之前,如果我们要判断两个值是否相等,可以用 == 或者 === ,但是这两种判断方式都存在一些缺点。我们来看看下面的代码:

console.log(-0 == +0); // true
console.log(-0 === +0); // true
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(7 == "7"); // true

控制台输出结果如下:

1678553901794

从输出的结果,我们可以看出其中的问题所在:

  • 在 JavaScript 引擎中, -0+0 代表两个完全不同的实体,而使用 ===== 的判断结果却是相等的。
  • ===== 对于 NaN 的判断都是 false
  • 使用 == ,判断整型 7 和字符串 7 的结果是 true

基于上述这些缺点,在 ES6 中提出了同值相等的算法,就是使用 Object.is 来比较两个值是否相等。

新建一个 index4.html 文件。

console.log(Object.is(-0, +0)); // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(7 == "7")); // false

控制台输出结果如下:

1678553913649

# Object.assign

在 ES6 之前对象组合中我们往往会用到 mixin() 方法,其方法的作用就是一个对象接受另一个对象的属性和方法。

在 ES6 中引入了 Object.assign 来合并对象,该方法一个对象可以接受任意多个对象的属性和方法,我们来看个例子。

新建一个 index5.html 文件。

const obj1 = { name: "闷墩儿", food: "狗粮" };
const obj2 = { age: 2, hobby: "跑圈圈" };
const obj3 = { food: "鸡胸肉", color: "黑白黄" };
Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中
console.log(obj1);

在控制台可以看到以下输出结果:

1678553933784

同学们有没有发现,在 obj1obj3 中有相同名称的属性名 food ,从输出结果可以看到 obj1 中的 food 属性被覆盖了,这一点也需要同学们注意哦,被合并的对象中出现同名属性,后面的对象会覆盖前面的对象中的属性值。

还有一点需要大家注意一下哦~

这就是合并方式是一种浅拷贝,也就是说如果被合并对象中的属性发生变化,合并后的对象不会继承修改的属性,我们来看个例子:

let obj1 = { name: "闷墩儿", food: "狗粮" };
let obj2 = { age: 2, hobby: "跑圈圈" };
let obj3 = { color: "黑白黄" };
Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中
console.log(obj1);
obj2.hobby = "游泳";
console.log(obj2);
console.log(obj1);

控制台输出如下:

1678553947130

从上图我们可以看到,修改 obj2hobby 属性后, obj1 没有继承。

# 实验总结

本节实验给大家介绍了对象相关的扩展,这里我们来总结一下:

  • 对象字面量:
    • 属性的简洁表示法
    • 方法的简洁表示法
    • 属性名表达式
  • 对象的扩展运算符:遍历对象的属性和属性值。
  • 对象的新增方法
    • Object.is:判断两个值是否相等。
    • Object.assign:合并多个对象。

# Set 和 Map

本节实验会给大家介绍 Set 和 Map 对象。

# 知识点

  • Set 对象
  • Map 对象

# Set 对象

Set 是 ES6 提供的一种新的数据结构,其结构与数组类似,但与数组不同的是 Set 里面不允许存放相同的元素,也就是说 Set 中的每个值都是独一无二的。

概念清楚了,我们来看看如何创建 Set 对象。

Set 对象有两种创建形式:

  • 不带参数的 Set。
let s = new Set();
  • 带参数的 Set。
let s = new Set(argument1, argument1,...);

接下来我们一一举例。

# 创建不带参数的 Set

新建一个 index.html 文件。

首先,创建一个空的 Set 对象。

let s = new Set();

然后,我们使用 add() 方法往 Set 对象里加入一些元素。

s.add(1);
s.add(2);
s.add(2);
s.add(3);
s.add(3);
console.log(s);

在控制台输出,你会发现,重复的元素只保留了一个。

1678554030865

其执行过程原理如下动图所示:

1678554080279

在 set 中可以放入不同类型的值,比如 s.add(1);s.add("hello");

# Set 相关的方法

我们已经知道了使用 add() 方法可以往 Set 中添加元素。我们还可以对 Set 进行删除等操作。

在 Set 中使用 delete() 方法来移除指定元素。其使用格式为:

Set.delete(element);

需要注意的是, delete() 里面的参数是要删除的元素,而不是其索引。

我们举个例子看看具体是怎么用的。

新建一个 index2.html 文件,在文件中写入以下内容:

let dogs = new Set(["柯基", "博美", "秋田犬", "藏獒"]);
dogs.delete("秋田犬");
console.log(dogs);

在控制台可以看到「秋田犬」不在 Set 中了:

1678554161236

我们还可以使用 has() 方法来检查某个元素是否存在于 Set 中。修改代码如下:

let dogs = new Set(["柯基", "博美", "秋田犬", "藏獒"]);
dogs.delete("秋田犬");
console.log(dogs.has("柯基"));
console.log(dogs.has("秋田犬"));

在控制台可以看到如下输出:

1678554185835

若我们想删除 Set 中的所有数据,可以使用 clear() 方法。

let dogs = new Set(["柯基", "博美", "秋田犬", "藏獒"]);
dogs.clear();
console.log(dogs);

在控制台可以看到如下输出:

1678554197895

可以看到 Set 已经空了。

如果我们还想遍历一下 Set 中的元素,该怎么做呢?🤔

来跟着我继续学习吧!

# Set 的遍历

我们使用 forEach() 方法可以遍历 Set 中的元素。

其使用格式为:

Set.prototype.forEach(callback[,thisArg])

参数说明如下:

  • callback 是 Set 中每个元素要执行的回调函数。
  • thisArg 是回调函数执行过程中的 this 值。

话不多说,我们来看一看实际的用法吧!

新建一个 index3.html 文件,在该文件中写入以下内容。

let dogs = new Set(["柯基", "博美", "比熊"]);
dogs.forEach(function details(values) {
  console.log(`Hello,我是一只小${values}`);
});

在控制台可以看到如下输出:

1678554224980

在上面代码中,我们遍历了 dogs ,在 forEach 方法里,给回调函数命名为 details ,该回调函数接收一个参数 values ,就是 Set 对象中的值,我们遍历 Set 中的值并在控制台中输出。

我们还可以改写成下面这个样子~

let dogs = new Set(["柯基", "博美", "比熊"]);
// 回调函数
function details(values) {
  console.log(`Hello,我是一只小${values}`);
}
dogs.forEach(details);

# WeakSet

Set 实例和变量在存储数据方面的内存分配和垃圾回收机制是一样的。这么说未免有些抽象不太好理解,接下来我们结合例子去看。

如果 Set 实例中的引用一直存在,垃圾回收就不能释放该对象的存储空间,即使你并没有用到它。例如:

let s = new Set();
let obj1 = {};
let obj2 = {};
s.add(obj1);
s.add(obj2);
console.log(s.size); // 2
obj1 = null;
console.log(s.size); // 2

1678554257271

上面代码中,先声明了一个空的 Set 对象 s ,然后调用 add 方法向 s 中添加两个空对象元素,控制台打印 s 的元素个数为 2,证明 Set 对象中给空对象也分配了内存;接着把对象 obj1 设为 null,再次打印 s 的元素个数,仍然为 2,证明 Set 实例 s 中元素 1 占用的内存并没有被释放掉。

小 tips:同学们知道什么垃圾回收吗?这里简单的说一下。在 JavaScript 中,当你创建了一个值,分配给你相应的内存空间,值不需要了,就释放掉分配的空间,这就是垃圾回收了。

针对这个缺陷,ES6 又给我们提供了另一种 Set,叫做 WeakSet

WeakSet 也叫做弱引用 Set,如果将其存储的对象设为了 null,相当于是删除了该对象,当垃圾回收机运行时,会释放掉被删除对象占用的空间。

我们来看看 WeakSet 是怎样使用的。

新建一个 index4.html 文件,在文件中写入以下内容:

let s = new WeakSet();
let obj = { msg: "同学们好!" };
s.add(obj);
console.log(s.has(obj));
s.delete(obj);
console.log(s.has(obj));

在控制台显示如下:

1678554271509

如果你打印出定义的实例对象,会发现输出为 undefined。

我们针对前面举的 ”Set 中 null 元素占用的内存无法被释放 “的小例子做如下修改。

let s = new WeakSet();
let obj1 = {};
let obj2 = {};
s.add(obj1);
s.add(obj2);
console.log(s.size);
obj1 = null;
console.log(s.size);

此时的输出都为 undefined 的了。

1678554282280

下面给大家说一说,Set 与 WeakSet 的区别:

  • WeakSet 的成员只能是对象且都是弱引用。

    在 WeakSet 中,add () 方法中不能传入非对象参数,若传入会报错。

  • 在 WeakSet 中,给 has () 和 delete () 方法传入非对象参数,虽然不会报错,但是会返回 false。
  • WeakSet 对象没有 size 属性,不能被遍历。

    由于 WeakSet 里面存储的都是弱引用,内部有多少个成员,取决于垃圾回收机制有没有运行。运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。

# Map 对象

对象,我们不陌生吧!在 ES6 之前,对象是创建键值对数据结构的主要方式,但对象在使用上有一些局限性。

  1. 在对象中的键名只能是字符串、数字或者 Symbol。
  2. 对象不可以直接使用 forEach 方法遍历。

Map 的出现就解决了上述问题。

Map 是 ES6 中一种存储许多键值对的有序列表,其键值对可以是任意数据类型。Map 是有序的,它会按照键值插入的顺序来排列。

我们先来看看如何来创建 Map 对象。

其语法格式为:

let map = new Map([iterable]);

Map 对象可以接收一个由键值对组成的可叠对象。

我们来举个例子~ 👻

打开我们的线上环境,新建一个 index5.html 文件,创建一个 Map 类型的变量 bookstore

let bookstore = new Map();

# set 方法添加数据

使用 set() 方法可以向对象中添加数据。其使用格式为:

map.set(key:value);

index5.html 文件中增加以下代码:

bookstore.set([1, 2, 3], "书籍");
bookstore.set(false, "日用品");
bookstore.set(3, "化妆品");
console.log(bookstore);
  • bookstore.set([1,2,3],"书籍") 中创建了数组类型的键值 [1,2,3] ,其值为普通字符串 “书籍”。
  • bookstore.set(false,"日用品") 中创建了布尔类型的键值 false ,其值为普通字符串 “日用品”。
  • bookstore.set(3,"test"); 中创建了整数类型的键值 3,其值为普通的字符串 “化妆品”。

在控制台可以看到 Map 对布尔类型和数组类型作为键值也是支持的。

1678554311952

# get 方法从集合中获取数据

我们要获取集合中的数据,使用 get() 方法即可。

其使用格式为:

map.get(key);

我们来获取一下 bookstore 集合中的数据吧~

console.log(bookstore.get(false));

在控制台可以输出 false 键值映射的数据。

1678554322359

# 其他常用方法

除了上方提到的 set()get() 方法,在 Map 中,还有下面三种方法比较常用。

  • has() 用来判断指定键名对应的数据是否存在于当前集合中。
  • delete() 用来删除指定键名的数据。
  • clear() 用来清空集合中的数据。

我们来举个例子~

let bookstore = new Map();
bookstore.set("《活着》", "余华");
bookstore.set("《平凡的世界》", "路遥");
bookstore.set("《三体》", "刘欣慈");
bookstore.set("《猫和老鼠》", "电影");
console.log(`《活着》是否存在:${bookstore.has("《活着》")}`);
bookstore.delete("《猫和老鼠》");
console.log(`《猫和老鼠》是否存在:${bookstore.has("《猫和老鼠》")}`);

在控制台可以看到如下输出:

1678554343708

在上面代码中:

  • 使用 bookstore.has("《活着》") 判断 bookstore 对象中是否存在键值为 《活着》 的数据,很明显是存在的,所以输出为 true。
  • 使用 bookstore.delete("《猫和老鼠》") 删除了电影 《猫和老鼠》 ,所以使用 has() 方法判断它是否存在输出为 false。

接着,我们再使用 clear() 方法将上面代码的 bookstore 对象的数据清空掉。

bookstore.clear();
console.log(bookstore);

从控制台输出的结果,我们看到 bookstore 对象成了一个空对象。

1678554358303

# Map 的遍历

与对象不同,Map 可以使用 forEach() 方法来遍历数据值。

在讲 forEach() 方法之前,先给大家说一说如何在创建 Map 时就为其赋初始值。在前面的例子中,我们都是创建了一个空的 Map,然后使用 set() 方法往里面添加的值。现在我们来创建并初始化一个带数据的 Map。

语法格式如下:

let map = new Map([[key1,value1],[key2,value2]...]);

我们来举个例子~

新建一个 index6.html 文件,在文件中写入以下内容:

let userName = new Map([
  [1, "小红"],
  [2, "小蓝"],
  [3, "小白"],
]);
console.log(userName);

在控制台可以看到创建成功了。

1678554378360

接下来我们遍历 userName 中的值。

此外,Map 还有一个 forEach() 方法,与数组的 forEach() 方法类似,可以实现对 Map 实例的遍历。

map.forEach(callback(value, key, ownerMap));

callback 是一个回调函数,其函数包含三个参数:

  • value:本次循环所读取的元素的数据。
  • key:本次循环所读取的元素的键名。
  • ownerMap:Map 集合本身。

我们将上面例子中的 userName 使用 forEach 遍历如下:

let userName = new Map([
  [1, "小红"],
  [2, "小蓝"],
  [3, "小白"],
]);
userName.forEach(function (value, key, ownerMap) {
  console.log(`userName 中的值有: ${value}`);
  console.log(`userName 中的键有:${key}`);
  console.log(ownerMap);
});

控制台显示如下:

1678554394288

# 实验总结

本节实验给大家介绍了 Set 对象和 Map 对象相关的知识点。这里我们来总结一下~

Set 对象:

Set 与数组之间的区别:

  • Set 是一个可以存储数据的对象,你可以在其中添加或者删除数据,并循环访问 Set。但是 Set 中没有索引,也不能存放重复的值,数组与之相反。

Set 与 WeakSet 的区别:

  • Set 是强引用,创建的对象不能被垃圾回收,WeakSet 是弱引用,创建的对象可以被垃圾回收。

Map 对象:

  • Map 可以创建任意数据类型的键值对,打破了对象键名类型限制的局限性。
  • 我们可以使用 forEach() 方法来遍历 Map,而对象不能。
  • 我们可以使用 set()get()has()delete()clear() 等方法来操作 Map。

# 异步编程

那什么是异步编程?要理解这个概念,我们先从掌握 Promise 对象开始,因为它是实现异步编程的利器。掌握 Promise 对象之后,会给大家介绍异步编程中的重要关键字。

让我们一起进入异步编程的世界吧~

# 知识点

  • Promise 对象基础应用
  • Promise 对象中的方法
  • async 关键字
  • await 关键字

# Promise 对象基础应用

关于 Promise 对象的基础应用,本文会给大家介绍以下几个方面的内容:

  • 地狱式回调
  • 定义 Promise 对象
  • Promise 对象的 then 方法
  • 解决地狱式回调

# 地狱式回调

在日常开发中,往往会遇到这样的需求:通过接口 1 的返回值,去获取接口 2 的数据,然后,再通过接口 2 的返回值,获取接口 3 的数据。即每次请求接口数据时,都需要使用上一次的返回值。为了实现这个需求,通常会使用回调函数来完成,即把函数作为参数进行层层嵌套。其实现代码如下:

首先,打开我们的线上环境,新建一个 index.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

var outnum = function (n, callback) {
  setTimeout(function () {
    console.log(n);
    callback();
  }, 1000);
};
outnum("1", function () {
  outnum("2", function () {
    outnum("3", function () {
      console.log("0");
    });
  });
});

上述代码执行后的效果如下图所示:

1678254938017

在上述代码中我们发现,虽然可以通过回调函数层层嵌套的形式达到最终数据请求的目的,但代码结构不甚明朗,可读性极差,这就是传说中的回调地狱。如果你还想了解更多回调地狱的内容,可以点击下面的扩展阅读。 扩展阅读

# 定义 Promise 对象

为了解决这种地狱式的回调,可以使用 Promise 对象,且代码更优雅,由于 Promise 对象是一个构造函数,因此,必须通过实例化来生成,它的定义格式如下代码:

let p = new Promise(function (resolve, reject) {
  // 此处做一个异步的事情
});

在定义格式的代码中,需要说明的几个问题:

  • 在实例化中,参数为函数,函数中又有两个用于回调的函数。
  • 两个回调函数中, resolve 为异步执行成功时的回调,其参数可以传递执行的结果。
  • reject 为异步执行失败时的回调,其参数可以传递失败的错误信息。

使用 resolvereject 方法传递出去的参数被谁接收到了,是以何种方式接收的?接下来说下 then 方法。

# Promise 对象的 then 方法

Promise 对象实例化后,可以调用 then 方法获取两个回调函数中的传参值,该方法接收两个回调函数作为参数,第一个参数是必选参数,表示异步成功后执行的 resolve 回调函数,第二个参数是可选参数,表示异步失败后执行的 reject 回调函数,它的调用格式如下:

p.then(
  function () {},
  function () {}
);

回调函数带参数的格式如下:

p.then(
  function (v) {},
  function (e) {}
);

其中参数 v 值表示 resolve 回调函数中的参数值, e 值表示 reject 回调函数中的参数值,如下列代码所示:

let n = 6;
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    if (n > 5) {
      resolve(n);
    } else {
      reject("必须大于5");
    }
  });
});
p2.then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);
// 执行代码后,由于 n 值大于 5 ,因此,在控制台中输出数字 6 。

此外,一个 then 方法被执行后,如果仍然返回一个 Promise 对象,则可以继续再执行 then 方法,形成链式写法效果,代码如下所示:

p1.then(function (v) {
  return p1;
}).then(function (v) {
  return p1;
});

# 解决地狱式回调

在学习完 Promise 对象的定义和 then 方法调用后,接下来,我们使用 Promise 来实现开头提到的需求,从而解决由此引起的回调地狱问题,实现过程如下:

首先,打开我们的线上环境,新建一个 index2.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script<span> </span> 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

var outnum = function (order) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(order);
      resolve();
    }, 1000);
  });
};
outnum("1")
  .then(function () {
    return outnum("2");
  })
  .then(function () {
    return outnum("3");
  })
  .then(function () {
    console.log("0");
  });

执行上述代码之后的页面效果与使用地狱式回调方式是完全一样的,但 Promise 对象实现的代码可读性更强,还可以很方便地取到异步执行后传递的参数值,因此,这种代码的实现方式,更适合在异步编程中使用。

# Promise 对象中的方法

Promise 对象中有以下两个常用的方法:

  • Promise.all 方法
  • Promise.race 方法

# Promise.all 方法

常开发过程中,往往会遇到这种问题,当首次加载某个页面时,由于数据庞大需要分别同时发送多个异步请求向服务器获取数据,最终所有数据返回之后做下一步操作(如 “隐藏页面的加载 loading 动画”)。由于很难捕获这些异步请求全部成功的时机,导致这个需求实现起来相当困难。难道就没有解决办法了吗?🤔 这时使用 Promise.all 方法就可以解决这种问题。

# 使用格式

Promise.all 方法中的参数是一个数组,数组中的每个元素是实例化后的 Promise 对象,格式如下代码:

Promise.all([p1,p2,p3,...]).then(res=>{
  // 所有请求成功后的操作步骤
},error=>{
  // 某一个请求失败后的操作步骤
});

上述代码中,p1、p2、p3 都是实例化后的 Promise 对象,并且该方法可以通过链式写法直接调用 Promise.all 中的 then<span> </span> 方法,当全部的实例化对象都执行成功后,进入 then 方法的第一个执行成功的回调函数中,函数参数是每个任务执行成功后的结果,以数组形式保存,如下图所示:

1678255107091

如果在调用 Promise.all 方法时,有一个 Promise 实例对象(比如:p1)的任务执行失败了,则会直接进入 Promise.all 后的 then 方法的失败回调函数中,如下图所示:

1678255121350

通过 Promise.all 方法可以并列完成多个异步的请求,只有当全部请求成功后,才进入 then 方法中的成功回调函数中,否则,进入失败的回调函数中,因此,当首次加载页面时,可以将各种的异步请求放入 Promise.all 方法中,如果全部完成,则在 then 方法中的成功回调函数中执行下步操作,否则,直接进入失败回调函数中。

# 实战应用

下列通过一个实战来演示 Promise.all 方法的使用过程,功能说明:

  1. 定义一个函数 p1,返回一个 Promise 对象,在返回过程中,执行一个延时操作,定义一个参数 n ,如果参数 n 大于 0 ,则返回该数据,否则,则返回 “ 不能小于 0“ 的字符信息。
  2. 调用 Promise.all 方法,使用不同的参数,调用三次 p1 函数,当全部执行成功或有一个执行失败后,分别查看控制台的输出信息。

为了实现这个功能,首先,打开我们的线上环境,新建一个 index3.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

function p1(n) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      if (n > 0) {
        resolve(n);
      } else {
        reject("不能小于0");
      }
    }, 1000);
  });
}

先传入三个执行成功的任务,在新建页面中的 script 元素中添加如下代码:

Promise.all([p1(5), p1(6), p1(7)]).then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);

上述代码执行后的效果如下图所示:

1678255146770

从上述效果可以看出,如全部任务执行成功,则将各个执行结果保存在数组中,可以通过 then 方法中的成功回调函数返回。

此外,传入一个执行失败的任务,二个执行成功的任务,在新建页面中的 script 元素中再添加如下代码,

Promise.all([p1(5), p1(-2), p1(7)]).then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);

上述代码执行后的效果如下图所示:

1678255163706

从上述效果可以看出,如有一个任务执行失败,则通过 then 方法中的失败回调函数返回错误信息。

# Promise.race 方法

Promise.all 方法不同, Promise.race 方法是多个 Promise 实例化对象在比赛, 执行最快的那个任务的结果,将返回给 then 方法中的对应回调函数中,通过这种方式,可以检测页面中某个请求是否超时,并输出相关的提示信息。

# 使用格式

Promise.all 方法一样, Promise.race 中的参数也是一个数组,每个元素也是实例化后的 Promise 对象,格式如下代码:

Promise.race([p1,p2,p3,...]).then(
    function(v){
      // 获取最快任务成功时的返回值
  },
  function(){
      // 获取最快任务失败时的返回值
  }
)

# 实战应用

下列通过一个实战来演示 Promise.race 方法的使用过程,功能说明:

  1. 定义一个模拟异步请求的函数,返回一个 Promise 对象,在返回过程中,执行一个延时 3 秒的操作,请求成功后,则返回一个” 请求成功 “ 的字样。
  2. 再定义一个超时请求的函数,返回一个 Promise 对象,在返回过程中,执行一个延时 5 秒的操作,如果超过 5 秒,则返回一个” 请求超时 “ 的字样。
  3. 调用 Promise.race 方法,添加这 2 个 Promise 对象,当请求大于 5 秒和小于 5 秒时,分别查看控制台的输出信息。

为了实现这个功能,首先,打开我们的线上环境,新建一个 index4.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

function timeOut() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject("请求超时");
    }, 5000);
  });
}

先定义一个延时小于 5 秒的任务,在新建页面中的 script 元素中添加如下代码:

function loadData() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve("请求成功");
    }, 3000);
  });
}

接下来调用 Promise.race() 方法,获取并在显示执行最快任务的返回内容,在新建页面中的 script 元素中添加如下代码:

Promise.race([loadData(), timeOut()]).then(
  function (d) {
    console.log(d);
  },
  function (e) {
    console.log(e);
  }
);

上述代码执行后的效果如下图所示:

1678255199613

由于 loadData 函数的延时时间小于请求超时的延时时间,因此,该任务执行最快,所以在控制台显示 ” 请求成功 “ 的信息。

如果将 loadData 函数的延时时间修改为 6 秒,即将 loadData 函数的代码修改为如下代码:

function loadData() {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve("请求成功");
    }, 6000);
  });
}

其他代码不变,页面执行后的效果如下图所示:

1678255212992

由于 timeOut 函数的延时时间小于请求超时的延时时间,因此,该任务执行最快,所以在控制台显示 ” 请求超时 “ 的信息。

# Promise.then 方法的缺点

Promise 对象虽然很优雅地解决了地狱回调的情形,使代码更简洁和易读,但通过 then 方法取值时,代码还是不够时尚和前沿,多层嵌套取值时也不够高效,如下列代码所示:

var p = function (msg) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(msg);
    }, 1000);
  });
};
p("明明")
  .then(function (v) {
    return p(v + ",男");
  })
  .then(function (v) {
    return p(v + ",今年18岁");
  })
  .then(function (v) {
    console.log(v);
  });

在上述代码中, then 方法在取值和传值时,如果层级多时,它的代码的结构并不易读,下面我们通过 asyncawait 来解决这个问题,一起来看下它们的用法~

# async 关键字和 await 关键字

Promise 对象虽然很优雅地解决了地狱回调的情形,使代码更简洁和易读,但通过 then 方法取值时,代码还是不够时尚和前沿,多层嵌套取值时也不够高效,如下列代码所示:

var p = function (msg) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(msg);
    }, 1000);
  });
};
p("明明")
  .then(function (v) {
    return p(v + ",男");
  })
  .then(function (v) {
    return p(v + ",今年18岁");
  })
  .then(function (v) {
    console.log(v);
  });

在上述代码中, then 方法在取值和传值时,如果层级多时,它的代码的结构并不易读,下面我们通过 asyncawait 来解决这个问题,一起来看下它们的用法~

# async 关键字

async 英文单词的意思是异步,虽然它是 ES7 中新增加的一个关键字,但它的本质是一种语法糖写法(语法糖是一种简化后的代码写化,它能方便程序员的代码开发), async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值,下面通过一个简单示例来说明它的使用。

首先,打开我们的线上环境,新建一个 index5.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

async function fn() {
  return "12345";
}
fn().then((val) => {
  console.log(val);
});

在上述代码中,定义一个名称为 fn 的函数,但由于在函数前添加了关键字 async ,使这个函数将返回一个 Promise 对象,因此,函数执行后,可以直接调用 then 方法;同时, fn 函数中的返回值,就是 then 方法中,执行成功回调函数时的参数值,因此,执行上述代码后,将在页面的控制台输出 “12345” 字符,效果如下所示:

1678255295987

通过上述示例,我们明确以下两点:

  • 使用 async 关键字定义的函数,将会返回一个 Promise 对象。
  • 函数中有返回值,则相当于执行了 Promise.resolve(返回值) 函数,没有返回值,则相当于执行了 Promise.resolve() 函数。

虽然 async 关键字简化了我们之前实现异步请求中返回 Promise 实例对象的那一步,直接返回了一个 Promise 对象,但是仍然需要在 then 方法中处理异步获取到的数据。有没有什么办法可以继续优化呢?比如省去 then 方法的调用,让异步操作写起来更像同步操作那么简洁明了?答案就是 —— await ,接下来我们来介绍下它的用法。

# await 关键字

await 可以理解为 async wait 的简写,表示等待异步执行完成, await 必须在 async 定义的函数中,不能单独使用, await 后可以返回任意的表达式,如果是正常内容,则直接执行,如果是异步请求,必须等待请求完成后,才会执行下面的代码,来看下列代码。

首先,打开已经构建好的 index6.html 文件,找到 script 元素,先注释原有代码,再添加如下代码:

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
// 一个用于正常输出内容的函数
function log() {
  console.log("2.正在操作");
}
async function fn() {
  console.log("1.开始");
  await log();
  let p1 = await p("3.异步请求");
  console.log(p1);
  console.log("4.结束");
}
fn();

执行上述代码后,页面在控制台输出的效果如下所示:

1678255331413

根据页面效果,源代码解析如下:

  • fn 函数执行后,首先,会按照代码执行流程,先输出 “1. 开始”。
  • 其次,对于没有异步请求的内容,在 await 后面都将会正常输出,因此,再输出 “2. 正在操作”。
  • 如果 await 后面是异步请求,那么,必须等待请求完成并获取结果后,才会向下执行。
  • 根据上述分析,由于 方法 p 是一个异步请求,因此,必须等待它执行完成后,并将返回值赋给变量 p1,再执行向下代码。
  • 所以,最后的执行顺序是,先输出 “3. 异步请求”,再输出 "4. 结束",在 async 函数中的执行顺序,如下图所示。

1678255346358

# 多层嵌套传参数的优化

基于 await 的特性,可以将异步请求的代码变成同步请求时的书写格式,代码会更加优雅,特别是处理多层需要嵌套传参时,使用 await 的方式,代码会更少,更易于阅读,如下列需求。

# 需求分析

需要发送三次异步请求,第一次请求,成功后获取返回 1,并将该值作为参数并加 2,发送第二次请求,成功后获取返回值,并将该值作为参数并加 3,发送第三次请求,成功后输出全部的返回值,如果三次请求都成功了,则在控制台输出 “登录成功!” 的字样。

# 实现代码

首先,打开我们的线上环境,新建一个 index7.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  let p1 = await p("1");
  let p2 = await p(p1 + "2");
  let p3 = await p(p2 + "3");
  console.log(p3);
  console.log("登录成功!");
}
fn();

上述代码执行后的效果如下图所示:

1678255373757

从上述页面效果可以看出,在 fn 函数中,第一次发送请求时,返回值为 “1”,并保存在变量 p1 中,然后,将变量 p1 作为参数,并加 “2” 发送第二次请求,返回值为 “12”,并保存在变量 p2 中,然后,将变量 p2 值作为参数,并加 ”3“ 发送第三次请求,返回值为 ”123“,并保存在变量 p3 中,最后,在控制台输出的内容是 p3 的值,即字符 “123”,同时,输出 “登录成功!” 的字样。

# 多个并列异步请求的调优

await 在处理多个异步请求时,如果请求之间有嵌套关系,可以一次一次按顺序发送请求,但是如果各个请求之间无任何关联,则可以将这些请求借助 Promise.all 一次性并列发送,使用 await 关键字获取请求的结果,并根据返回的结果进行下一步操作。如下列需求。

# 需求分析

页面首次加载时,将会发送三次无任何关联的异步请求,当这三次请求成功后,在控制台输出 “隐藏加载动画!” 字样。

# 实现代码

为了实现这个功能,首先,打开我们的线上环境,新建一个 index8.html 文件,再使用快捷键方式生成模版,并在 body 元素中添加 script 元素,如下代码所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script></script>
  </body>
</html>

其次,在新建页面中的 script 元素中添加如下代码:

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  await Promise.all([p("a"), p("b"), p("c")]);
  console.log("隐藏加载动画!");
}
fn();

上述代码执行后的效果如下图所示:

1678255408280

在上述实现的代码中,方法 Promise.all 中每个实例化的 Promise 对象,都会以并行的方式发送异步请求,当所有请求都成功后,才会去执行输出字符内容的代码,许多初次学习 async 的同学们,可能会将 fn 函数的内容修改成如下代码所示:

async function fn() {
  await p("a");
  await p("b");
  await p("c");
  console.log("隐藏加载动画!");
}
fn2();

需要说明的是,无论是函数修改之前还是修改之后 ,都使用了 asyncawait ,并且两次的执行结果都是一样的,但在 fn 函数修改之前,所有的异步请求都是并行发送的,而在函数修改之后,所有的异步请求都是按顺序执行的。

从性能上来看, fn 函数修改之前的异步请求并发执行明显高于修改之后的阻塞式异步请求,因此,虽然,我们学习了 asyncawait ,但也不能完全取代 Promise 对象,需要结合实际需求场景去使用。

# 模块化

HI,小伙伴们,又见面了!今天我们讲点啥呢?😜 很多同学在查看资料时,经常会看到 exportimport 这样的单词,那它们究竟是什么东东?我们应该如何去使用它呢?带着这些问题,来开启我们今天的学习之旅~

# 知识点

  • export
  • import

# export

模块化开发项目是目前的主流趋势,它是将复杂的功能分解成各自独立的子模块,所有子模块按照一种方式进行组合,最终完成复杂功能的过程,它的优势是各模块是独立工作的,更利于复用和维护,同时更有利于节略资源,提高效率。

基于模块化开发的趋势,在前端项目开发时,各个功能都是独立开发的,如登录模块,注册模块和公用模块等,它们都是一个个单独的文件,如果登录模块想要访问公用模块中的某个方法,则需要公用模块开放这个方法,并制定访问的标准,而这时就需要使用 ES6 中新添加的关键字 export 了,功能如下图所示:

1678554520185

关键字 export 可以将一个模块中的方法、变量和其他功能从模块中输出,允许其他需要的模块按指定的标准进行访问,没有使用关键字 export 输出的模块内容,是封闭的,其它模块无法访问到它,下面介绍关键字 export 几种输出的方式 。

  • 可以直接输出一个模块文件中的变量,如下代码:
export let name = "小蓝同学";
export let age = 18;
let work = "一直在写代码呢!";

在上述代码中,由于变量 nameage 之前都使用了输出关键字 export ,因此,它们都可以被其他模块访问,由于变量 work 之前没有添加关键字 export ,所以,其他的模块无法访问到这个变量。

上述代码的这种写法,还可以合并成一个对象,并使用关键字 export 一次性输出,修改后的代码如下:

let name = "小蓝同学";
let age = 18;
let work = "一直在写代码呢!";
export { name, age };

修改后的这种方法更加有利于一次性地批量输出多项内容,经常在开发中使用。

  • 关键字 export 除能输出模块中的变量外,还可以输出模块中的方法,两者的输出格式都是相同的,如下代码:
function say() {
  console.log("我的名字叫小蓝");
}
function act() {
  console.log("我的工作是写代码");
}
export function log() {
  console.log("说点什么。。。");
}
export { say, act };

关键字 export 在输出方法时,不要去添加执行方法的括号,只要输出方法的名称就可以,否则就会报错;此外,在输出内容的过程中,还可以使用关键字 as 来修改输出后的名称,修改后的代码如下:

export {
    say(), // 报错
    act as userWork
}

在上述代码中,由于在输出 say 方法时,添加了括号,表示执行该方法,因此,代码报错;同时,在输出 act 方法时,添加了关键字 as ,它的作用是给输出的功能取一个别名,因此,其他模块在使用输出的 act 方法时,只能访问它的别名 userWork

关键字 export 是输出模块中的变量和方法,那用什么来接收这些输出的内容呢?这时就需要使用另一个关键字 import

# import

与关键字 export 相对应, import 的功能是输入已经使用关键字 export 输出的内容,它们是对应关系, export 负责输出,而 import 则用于接受输出的内容,即负责输入,功能如下图所示:

1678554563738

关键字 import 在输入模块中加载输出模块的变量时,可以使用大括号包裹全部变量名,各个变量之间使用逗号分割,再通过 from 指定输出模块的路径,这个路径可以是绝对的,也可以是相对的,代码格式如下:

import { 变量1,变量2,变量3...} from 输出模块位置

在上述格式代码中,大括号中的变量 1,变量 2 也可以通过关键字 as 取一个别名,格式如下:

import { 变量1 as a1,变量2 as fn,变量3...} from 输出模块位置

取了别名之后,在输入模块中,只能使用这个别名,而不能再使用原先的名称,否则,将会出现变量未定义的错误信息。

下面我们通过一个简单的示例来演示关键字 exportimport 的使用方法。

打开我们的线上环境,新建一个 outfile.js 文件,并在文件中添加如下代码:

let strSex = "女";
let strName = "小蓝";
let strLike = "就喜欢写点代码";
function log(v) {
  console.log(v);
}
export { strSex, strName, strLike, log };

outfile.js 文件是一个输出模块,通过关键字 export 输出变量和函数,用于输入模块的接收,接下来,再新建一个 index.html 文件,使用快捷键方式生成模版,在 body 元素中添加 script 元素,并在 script 元素中添加逻辑代码,页面完整的代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { strName, strSex, strLike, log } from "./outfile.js";
      console.log(strName + "," + strSex + "," + strLike);
      log("写代码,是一种情怀");
    </script>
  </body>
</html>

新建的 index.html 页面在浏览器中执行的效果如下图所示:

1678554579344

针对 index.html 页面 script 元素中的代码,解析如下:

  • 由于在页面的 script 元素中,需要执行关键字 import ,因此,必须将 script 元素的 type 属性值设置为 module ,表示代码中允许使用模块输入和输出的关键字,进行模块化代码开发。
  • 虽然使用 import 输入的是 log 函数名称,但它本质上是一个与输出模块相关联的真实函数,因此,在输入的页面中,可以直接像普通函数一样去使用这个输入的函数。

# 数据排序的模块开发

在 ES6 中,引入关键字 exportimport 的最终目的是为项目的模块提供衔接上的支撑,从而为模块化开发项目提供保障,在模块化开发项目的过程中,开发人员可以将各个功能分解成各个子类模块,各个子类模块通过关键字 exportimport 进行相互衔接,其效果如下图所示:

1678554624287

在了解了 ES6 中如何对项目进行模块化开发后,下面我们通过一个真实的案例开发,来实际应用下,其需求如下:

# 需求分析

使用模块化开发的方式,分解各个功能模块,实现一个数字型数组的正反排序效果。

# 实现代码

首先,打开我们的线上环境,新建一个 data.js 文件,作为项目中的数据模块,在文件中添加如下代码:

let _data = [12, 18, 16, 20];
export { _data as data };

在数据模块代码中,定义一个名称为 _data 的数组,并使用关键字 export 将该数组输出,用于逻辑模块的使用;接下来, 新建一个 logic.js 文件,作为项项目中的逻辑模块,在文件中添加如下代码:

import { data } from "./data.js";
function sort(blnS) {
  if (blnS)
    return data.sort(function (a, b) {
      return a - b;
    });
  else
    return data.sort(function (a, b) {
      return b - a;
    });
}
export { sort };

在逻辑模块代码中,先使用 import 输入数据模块中的数组,再定义一个 sort 函数,对加载的数组进行逻辑操作,通过布尔值 blnS 控制排序的方向,最后,再使用关键字 export 将该函数输出,用于视图模块的使用。

最后,新建一个 index2.html 文件,作为项目中的视图模块,在页面中使用快捷键方式生成模版,在 body 元素中添加 script 元素,并在 script 元素中添加逻辑代码,页面完整的代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      import { sort } from "./logic.js";
      console.log("升序: " + sort(true));
      console.log("降序: " + sort(false));
    </script>
  </body>
</html>

新建的 index2.html 页面在浏览器中执行的效果如下图所示:

1678554644836

在页面 index2.html 中,先使用 import 输入逻辑模块中的 sort 函数,再直接在控制台中执行函数,完成数据排序的效果输出。

# Proxy

本节实验我们要学习操作对象的 API---Proxy(代理)。

快跟我一起去探索 Proxy 的世界吧!

# 知识点

  • 什么是 Proxy
  • Proxy 的实例方法

# 什么是 Proxy

Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后通过对象的代理对象进行操作。也可以理解为在外界与对象之间建立了一道门,外界要访问该对象必须先打开这道门,如果想要获得打开该门的钥匙,就要遵守一个访问 “条约”,允许对来访人员进行改造( 提供一种机制:可以对外界的访问进行过滤和改写 )。

用 Proxy 创建代理需要传入两个参数:目标对象(target)和处理程序(handler)。语法格式如下:

var proxy = new Proxy(target, handler);

参数说明如下:

  • target:要拦截的目标对象。
  • handler:制定拦截行为的对象。

清楚了 Proxy 的概念之后,我们一起来创建一个简单的代理吧~

新建一个 index.html 文件。

let target = {};
let proxy = new Proxy(target, {});
proxy.name = "闷墩儿";
console.log(proxy.name);
console.log(target.name);
target.name = "憨憨";
console.log(proxy.name);
console.log(target.name);

在控制台输出结果如下所示:

1678554704289

在上面的例子中 proxy 的所有操作都转发给了 target 对象,当在 proxy 上创建了 name 属性之后,相应的 target 上也会创建 name 属性。由于 target.nameproxy.name 都是引用的 target.name ,所以当修改了 target.name 的值后, proxy.name 的值也会修改。

# Proxy 的实例方法

Proxy 的代理拦截方法如下表所示:

拦截方法方法说明
get(target, propKey, receiver)拦截对象属性的读取。
set(target, propKey, value, receiver)拦截对象属性的设置。
has(target, propKey)拦截 propKey in proxy 的操作。
deleteProperty(target, propKey)拦截 delete proxy [propKey] 的操作。
ownKeys(target)拦截 Object.getOwnPropertyNames (proxy)、Object.getOwnPropertySymbols (proxy)、Object.keys (proxy)、for...in 循环,返回一个数组。
getOwnPropertyDescriptor(target, propKey)拦截 Object.getOwnPropertyDescriptor (proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc)拦截 Object.defineProperty (proxy, propKey, propDesc)、Object.defineProperties (proxy, propDescs),返回一个布尔值。
preventExtensions(target)拦截 Object.preventExtensions (proxy),返回一个布尔值。
getPrototypeOf(target)拦截 Object.getPrototypeOf (proxy),返回一个对象。
isExtensible(target)拦截 Object.isExtensible (proxy),返回一个布尔值。
setPrototypeOf(target, proto)拦截 Object.setPrototypeOf (proxy, proto),返回一个布尔值。
apply(target, object, args)拦截 Proxy 实例作为函数调用的操作。
construct(target, args)拦截 Proxy 实例作为构造函数调用的操作。

如此多的拦截操作方法!在本文中我们就不一一举例了,只给大家介绍最常用的四种拦截方法:

  • get(target, propKey, receiver)
  • set(target, propKey, value, receiver)
  • has(target, propKey)
  • ownKeys(target)

下面我们一一来学习吧~

# get(target, propKey, receiver)

在 JavaScript 中,当我们去访问一个对象中不存在的属性时,不会报错,而是返回一个 undefined。如下例所示:

let dog = {};
console.log(dog.name);

1678554749956

这样的模式在大型的代码库中可能会导致严重的问题。ES6 中为我们提供了 get 方法,在访问对象之前检验一下是否存在你要访问的属性,该方法接受三个参数,具体说明如下:

  • target:被读取属性的目标对象。
  • propKey:要读取的属性键值。
  • receiver:操作发生的对象。

新建一个 index1.html 文件,在文件中写入以下内容:

let dog = {
  name: "闷墩儿",
};
var proxy = new Proxy(dog, {
  get(target, propKey) {
    // 遍历目标对象的属性键值
    if (propKey in target) {
      return target[propKey]; // 返回相应的属性值
    } else {
      throw new ReferenceError(propKey + " 属性不存在");
    }
  },
});
console.log("访问 dog 对象中的 name 属性值为:" + proxy.name);
console.log("访问不存在的 age 属性:" + proxy.age);

在控制台可以看见访问 age 属性就报错了。

1678554762519

1678606074196

说明只有通过 proxy 去访问对象的属性,才会起到拦截效果。

上面是对所有属性访问进行拦截并统一处理了,如果想对属性针对性的处理呢?在拦截器内加上 if 判断: if(propKey == "kkk") {}

# set(target, propKey, value, receiver)

如果要创建一个只接受数字作为属性值的对象,那么在创建属性时,必须判断该值是否是数字,若不是数字应该报错。我们使用 set 方法就可以实现这个需求。

set 方法接受四个参数,具体说明如下:

  • target:用于接收属性的目标对象。
  • propKey:要写入的属性键值。
  • value:要写入的属性值。
  • receiver:操作发生的对象。

新建一个 index2.html 文件,在文件中写入以下内容:

let validator = {
  set(target, propKey, value) {
    if (propKey === "age") {
      // 判断 age 属性值是否时数字
      if (!Number.isInteger(value)) {
        throw new TypeError("狗狗的年龄只能是整型哦!");
      }
    }
    target[propKey] = value;
    return true;
  },
};
let dog = new Proxy({}, validator);
console.log((dog.age = "22"));

从控制台的输出结果可以看出,当 age 属性值设为字符串时,抛出错误。

1678554782803

# has(target, propKey)

在 ES6 之前如果我们要判断某个属性是否在该对象中,可以使用 in 来判断。例如:

let dog = {
  name: "闷墩儿",
};
console.log("name" in dog);
console.log("valueOf" in dog);

在控制台你会看到两个都输出了 true

1678554801650

这时候同学们可能有疑问了,明明上面的 dog 对象中只有 name 属性,为什么 valueOf 会被判为 true 呢?🤔️

这是因为 valueOf 是一个继承自 object 的原型属性

而在 has 方法中可以拦截这些操作,返回不一样的值。

has 方法接收两个参数,具体说明如下:

  • target:读取属性的目标对象。
  • propKey:要检查的属性键值。

新建一个 index3.html 文件,在文件中写入以下内容:

let dog = {
  name: "闷墩儿",
  age: 2,
};
let handler = {
  has(target, propKey) {
    if (propKey == "age" && target[propKey] < 5) {
      console.log(`${target.name}的年龄小于 5 岁哦!`);
      return true;
    }
  },
};
let proxy = new Proxy(dog, handler);
console.log("age" in proxy);

在控制台可以看到以下输出:

1678554813981

# ownKeys(target)

ownKeys 方法用于拦截对象自身属性的读取操作,具体可以拦截以下四种操作:

  • Object.getOwnPropertyNames()
  • Object.getOwnPropertySymbols()
  • Object.keys()
  • for...in

下面我们举一个拦截 for...in 的例子吧~

新建一个 index4.html 文件,在文件中写入以下内容:

let dog = {
  name: "闷墩儿",
  age: 2,
  food: "狗罐头",
};
const proxy = new Proxy(dog, {
  ownKeys() {
    return ["name", "color"];
  },
});
for (let key in proxy) {
  console.log(key); // 输出 name
}

在控制台的输出如下:

1678554833074

从上图我们可以看到只输出了 name 属性,这是因为在 dog 对象中不包含 color 属性。

# 实验总结

本节实验给大家介绍了用于对象拦截操作的 Proxy,并给大家介绍了它的拦截方法。在实验最后我们对常用的四种拦截方法进行了练习。这里我们可以来回顾一下这四种方法:

  • get (target, propKey, receiver):拦截对象属性的读取。
  • set (target, propKey, value, receiver):拦截对象属性的设置。
  • has (target, propKey):拦截 propKey in proxy 的操作。
  • ownKeys (target):拦截 Object.getOwnPropertyNames (proxy)、Object.getOwnPropertySymbols (proxy)、Object.keys (proxy)、for...in 循环。