书籍目录
1- ECMAScript 6 简介
2- 环境搭建
3- let和const
4- 解构赋值
5- 数值的扩展
6- 字符串的扩展
7- 数组的扩展
8- 函数的扩展
9- 对象的扩展
10- Symbol
11- Proxy和Reflect
12- Set和Map
13- Iterator和for...of
14- Genrator函数
15- Promise
16- async/await
17- Class
18- 装饰器
19- 模块
122
ES6入门教程
免费
共19小节
ES6(ECMAScript 2015)是JavaScript的第六个版本,引入了许多新特性和语法改进,使得 JavaScript 更加现代化、强大和易用。本教程将为你提供一个简单的入门指南,帮助你了解 ES6 的一些核心概念和常用特性。
离线

云端电子书创作 [官方]

私信

1-ECMAScript 6 简介

ECMAScript 6(简称 ES6 或 ES2015)是 JavaScript 编程语言的一个重要版本,它于2015年发布。它引入了许多令人兴奋的新特性和改进,使得 JavaScript 变得更加现代化、强大和易于维护。

1.1 ECMAScript 和 JavaScript

ECMAScript(简称ES)是一种标准化的脚本语言,由ECMA国际组织负责制定和维护。ECMAScript定义了一套规范,用于描述编写Web应用程序的脚本语言的特性、语法和行为。

JavaScript 是一种基于 ECMAScript 规范的脚本语言,最初由网景(Netscape)开发,并在并在1995年发布。JavaScript 是一种面向对象、动态类型的语言,常用于在Web浏览器中实现交互式的功能和效果。随后 JavaScript 发展成为一种通用的脚本语言,不仅可以在网页中使用,还可以在服务器端、桌面应用和移动应用中运行。

JavaScript 是 ECMAScript 规范的一种具体实现。JavaScript 实现了 ECMAScript 规定的语法、数据类型、操作符和内置对象,同时还提供了与浏览器交互的API(如DOM和BOM)。JavaScript 还包括其他非 ECMAScript 规定的功能,例如对异步请求的支持(Ajax)和针对网页设计的特定API(如Canvas和WebGL)。

随着时间的推移,ECMAScript规范不断发展和更新,引入了新的语法和功能,以满足开发者日益复杂的需求。各个 JavaScript 引擎(如V8引擎、SpiderMonkey引擎)会根据最新的 ECMAScript 规范实现和优化自己的 JavaScript 解释器或编译器。

1.2 ECMAScript 历史

ECMAScript 6 是一个重要的 ECMAScript 版本,它引入了许多新特性和改进。以下是ECMAScript 6的主要历史里程碑:

  1. 1996年:首次发布 ECMAScript 1。它是 JavaScript 的第一个版本,定义了基本的语法结构、数据类型、操作符和控制流程等。

  2. 1998年:发布 ECMAScript 2,主要是对第一版的一些错误进行修正。

  3. 1999年:发布 ECMAScript 3,是 JavaScript 在浏览器中广泛使用的版本。它引入了许多新特性,如正则表达式、异常处理、更严格的错误处理、新的数据类型和对象等。

  4. 2000年:发布 ECMAScript 4 草案,计划引入一系列新特性,如类、模块、命名空间、类型检查等。但由于各大浏览器厂商的意见分歧和实现复杂度,ECMAScript 4最终被搁置。

  5. 2009年:发布 ECMAScript 5,是一个相对较小的更新,主要是为了修复已知的问题,并引入一些新特性,如严格模式、JSON对象、数组方法(如forEach、map、filter)等。

  6. 2011年:发布 ECMAScript 5.1,主要是对 ECMAScript 5 的一些错误进行修正和澄清。

  7. 2015年:发布 ECMAScript 6,也称为 ES6 或 ES2015,是一个重要的 ECMAScript 版本。它引入了许多新特性和改进,如箭头函数、模板字面量、解构赋值、类和模块等。ES6的发布标志着 JavaScript 的一个重要分水岭,为 JavaScript 语言带来了现代化和更强大的功能。

2-环境搭建

目前大多数大多数现代浏览器已经支持 ES6 的大部分特性,包括Chrome、Firefox、Safari、Opera等;其中只有IE尚不支持 ES6。因此可以直接在浏览器中编写和运行 ES6 代码。

2.1 浏览器环境

以 Chrome 浏览器为例,我们编写一个 hello.html 文件。

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>ES6</title>
</head>
<body>
    <script>
        const hello = () => {
			let msg = "Hello World!";
            console.log(msg);
        }

        hello();
    </script>
</body>
</html>

然后右键用 Chrome 浏览器打开,并打开 Chrome 开发者工具,在控制台就可以看到代码运行结果。

图1 浏览器运行ES6代码

2.2 Node环境

如果你希望在本地开发环境中运行 ES6 代码,您需要安装 Node.js。请在官网https://node.js.org 下载安装,推荐使用LTS版本(长期支持版本)进行安装。

2.2.1 下载并安装

点击官网的 Download。然后根据自己的操作系统选择对应的安装包下载进行安装。

图2 Node官网

  

查看node版本:node -v

查看npm版本:npm -v

图3 查看node和npm版本

在nodejs的安装目录下创建 node_globalnode_cache 文件夹。

管理员身份打开cmd,运行下面命令:

npm config set prefix "D:\nodejs\node_global"
npm config set prefix "D:\nodejs\node_cache"

2.2.2 环境变量

Windows 的设置相对比较复杂,这里主要以 Windows 设置为例。

Windows 操作系统下,通过 系统 > 高级系统设置 > 环境变量 来进行环境变量设置。

  • 在系统变量中新建 变量名 NODE_PATH,变量值 D:\nodejs\node_global\node_modules

  • 在系统变量 path > 编辑 里添加 D:\nodejs和%NODE_PATH%

  • 在用户变量 path > 编辑,新增 D:\nodejs\node_global

2.2.3 设置镜像地址

在cmd中输入以下命令:

npm config set registry https://registry.npm.taobao.org

查看是否配置成功,在cmd中输入 npm config get registry 是否能成功输出淘宝的镜像地址。

2.2.4 Node运行ES6

我们创建一个 hello.js 文件,然后输入以下代码:

const hello = () => {
	let msg = "Hello World!";
	console.log(msg);
}

hello();

然后我们使用 终端/CMD 输入 node hello.js命令来打开这个 hello.js 文件,就可以看到运行结果。

ydcq@ydcqdeMac-mini ~ % node /Users/ydcq/Desktop/hello.js 
Hello World!

注意 node 后面接的是 hello.js 的具体路径。

2.3 Babel转码

为了确保 ES6 代码在旧版JavaScript引擎中能够正常运行,你需要使用一个转换工具,如Babel。Babel可以将 ES6 代码转换为向后兼容的 JavaScript 代码。可以使用Babel CLI(命令行界面)或Babel API(编程接口)来进行代码转换。

  1. Babel依赖项

    安装Babel及其相关的插件和预设。打开终端或命令提示符,并在项目的根目录下执行以下命令来安装Babel CLI和相关的依赖项:

    npm install --save-dev @babel/core @babel/cli @babel/preset-env
    
  2. Babel配置文件

    创建一个名为.babelrc的文件,并在其中指定Babel的配置,在这里,我们将使用 @babel/preset-env 预设,它会自动根据目标环境进行适当的转换。例如:

    {
      "presets": ["@babel/preset-env"]
    }
    
  3. 转码ES6代码

    现在,可以使用Babel来转码ES6代码了。在终端或命令提示符中,使用以下命令来转码指定的文件或目录:

    babel src --out-dir dist
    

    这将把 src 目录下的 ES6 代码转换为向后兼容的代码,并输出到 dist 目录中。

    如果你在项目的 package.json 文件中已经配置了 scripts 字段,您也可以在其中添加一个脚本来运行Babel:

    "scripts": {
      "build": "babel src --out-dir dist"
    }
    

    然后,在终端或命令提示符中,执行以下命令来运行Babel脚本:

    npm run build
    
  4. 验证转码结果

    Babel会将ES6代码转换为向后兼容的代码,并将其输出到指定的目录中。你可以查看转码后的文件,确保代码已经成功地被转换和输出。

通过以上步骤,你就可以在项目中使用 Babel 并将 ES6+ 语法转换为 ES5 语法。这将确保你的代码在大多数现代浏览器中都能够正常运行。如果你需要进行更高级的配置,可以参考 Babel 官方文档

2.4 VSCode

VSCode(全称:Visual Studio Code)是一款由微软开发的免费、开源的轻量级代码编辑器。它具有高亮显示,代码补全等功能,此外插件市场还有非常丰富的扩展支持。

2.4.1 VSCode安装

VSCode 官网 https://code.visualstudio.com ,根据自己的操作系统下载对应的安装包。

图4 VSCode官网

双击下载好的安装包并勾选同意按钮,点击下一步进行安装。

图5 开始安装

根据自己的喜好设置安装到磁盘的位置。

图6 安装磁盘位置

勾选对应的选项点击下一步继续安装等待安装。

图7 继续安装

最后点击完成。

图8 点击完成

安装完成后的界面。

图9 完成安装

2.4.2 设置简体中文

我们发现刚才安装的 VSCode 还是英文的,接下来我们就要设置它的中文界面。

点击下面的图标,然后输入 chinese,选择第一个,然后点击 install 进行安装。

 
图10 简体中文插件

完成后根据提示重启 VSCode 就能显示中文了。

图11 简体中文界面

2.5 在线编程

我们也可以使用云端源想在线编程工具去运行 ES6 代码,在线编程工具已经集成了 Node.js 环境,可以省去很多安装和配置的步骤,真正做到了开箱即用。下面就让我们来看一看具体的操作吧。

  1. 进入官网首页https://www.ydcode.cn/ ,点击开始进入

    图12 云端源想官网首页

  2. 在课程首页点击在线编程

    图13 点击在线编程

  3. 开始创建新的项目

    图14 新建项目

  4. 完成项目创建,我们只使用 Node.js 环境,选择Vue项目足够我们使用了。

    图15 完成创建项目

  5. 打开创建的项目,等待项目加载并正常打开。

    图16 打开创建的项目

  6. 我们在在线编程下面的终端输入 node -vnpm -v,我们可以看到已经能够正常输出版本号了,是不是很方便。

    图17 运行node命令

  7. 我们创建一个 demo.js,并在里面填入 console.log("Hello World!"); 这段代码。

    图18 新建文件

    图19 创建demo.js

    图20 编写代码

  8. 然后我们在下面的终端输入 node demo.js,命令。

    图21 运行demo.js

注意:
1. 输入命令请在英文输入法下进行。
2. 在线编程长时间不操作会断开连接,你需要点击左上角返回然后再重新进入项目才能继续操作。

3-let和const

ES6 中新增了 letconst 两个关键字,用于声明变量。它们相比于传统的 var 关键字,具有更加严格的作用域规则和特定的行为。下面就让我们学习 letconst 的使用。

3.1 let

在 ES5 中只有一个 var 变量,由于 var 变量存在着诸多的缺陷,所以 ES6 新增了 let 关键字,它与 var 用法相似,但也存在着些许差异,以下就让我们看看 let 的用法吧。

3.1.1 基本用法

let 关键字用于声明块级作用域的变量,其作用域仅限于声明变量的代码块内。例如:

{
  console.log(x); // ReferenceError: x is not defined
  let x = 10;
}

在上面的示例中,x是在语句块内部声明的变量,因此在该块之外的代码中无法访问它。

还有for循环的就非常适合使用 let 关键字,例如:

for (let i = 0; i < 10; i++) {}

console.log(i); // ReferenceError: i is not defined

这样for循环的变量i不会影响到其他的变量,它的作用域只在for循环内起作用。

3.1.2 块级作用域

在 ES6 之前,JavaScript 只有全局作用域和函数作用域,没有块级作用域。这意味着在if语句、for循环等代码块内部声明的变量会泄漏到外部作用域中。例如:

if (true) {
	var x = 10;
}
console.log(x); // 输出 10,x泄漏到了外部作用域

在上面的例子中,变量x在if语句的块级作用域内部声明,但在if语句外部的作用域中仍然可以访问到它。

除了if语句,for循环等也可以创建块级作用域。例如:

for (var i = 0; i < 5; i++) {
}
console.log(i); // 输出 5,i泄漏到了外部作用域

上述代码中的变量i只是用来控制循环的,但是在循环结束后,它并没有随着循环的结束而消失,而是泄漏到了外面,成了全局变量。

ES6 引入了 let 关键字,它们可以在声明变量时将变量绑定到所在的块级作用域。这意味着在块级作用域内声明的变量只能在该块级作用域内部访问,而在外部作用域是无法访问的。

function example() {
  if (true) {
    let x = 10;
    console.log(x); // 输出 10
  }
  console.log(x); // 报错,x未定义
}

example();

在上面的例子中,变量x使用let关键字声明,在if语句的块级作用域内有效,但在if语句外部的作用域中无法访问。因此,第二个 console.log 语句会抛出一个错误。

使用块级作用域可以有效地控制变量和常量的作用范围,避免了变量提升和命名冲突等问题。它还提供了更好的代码封装和隔离,增强了代码的可读性和可维护性。

3.1.3 暂时性死区

暂时性死区是指在使用 let 关键字声明变量时,变量会绑定到声明所在的块级作用域,而不再受外部作用域的影响。

console.log(x); // ReferenceError: Cannot access 'x' before initialization

if (true) {
	let x = 10;
}

在上面的例子中,由于变量 x 使用了 let 关键字声明,它会被绑定到当前的块级作用域,即在声明之前访问 x 会触发暂时性死区,导致抛出一个 Cannot access 'x' before initialization 的错误。

暂时性死区的特点是,在变量声明之前,该变量是无法被访问或使用的。如果在暂时性死区中尝试访问变量,就会抛出一个错误。

3.1.4 不存在变量提升

变量提升就是还没有定义这个变量,但是并不影响我们使用这个未定义的变量,变量提升其实提升的是声明。JavaScript 引擎会把变量声明提升到当前作用域的最顶端,但是不包括变量赋值的部分。

console.log(x); // undefined
var x = 10;

在上面的例子中,尽管变量x在输出语句之前被声明,但是由于变量提升的作用,输出结果为undefined,而不是“未定义”。

let 不会再像 var 那样出现变量提升的问题。例如:

console.log(name);
let name = "张三"; // ReferenceError: Cannot access 'name' before initialization

所以在 ES6 中的 let 变量都必须要先声明再使用,否则就会报错影响程序继续执行。

3.1.5 不允许重复声明

使用 let 关键字声明变量时,不允许在同一个作用域内重复声明同名的变量。否则,就会抛出一个SyntaxError错误。

if (true) {
	let x = 10;
	let x = 20; // SyntaxError: Identifier 'x' has already been declared
}

3.2 const

constlet 使用上基本类似,只不过 const 用来声明常量,一旦声明,变量的值就不允许再次修改。例如:

const name = "张三";
name = "李四"; // TypeError: Assignment to constant variable.
console.log(name);

注意:
1.使用 const 声明变量后面必须接一个初始值,否则就会报错:SyntaxError: Missing initializer in const declaration
2.const 同样存在块级作用域,使用 const 声明的变量只能在这个块级作用域范围内使用。
3.const 常量不存在变量提升,同样存在暂时性死区、不允许重复声明等特性。

3.3 全局对象的属性

全局对象指的是最顶层的对象,即在浏览器中为 window 对象,在 Node.js 环境中为 global 对象。
在 ES5 中,全局对象的属性和全局变量是等价的。例如:

window.name = "张三";
console.log(name); // 张三

name = "李四";
console.log(window.name); // 李四

var age = 18;
console.log(window.age); // 18

在上面的例子中,全局对象的属性赋值和全局变量的赋值其实是同一件事。所以由var声明的变量就是全局对象的属性。

但是需要注意的是,ES6 中由 letconst 声明的变量并不属于全局对象的属性。

注意:如果 cosnt 变量是引用类型,那么这个引用类型变量的属性是可以修改的,因为这个变量存的是内存地址,修改这个对象的属性并不会改变 cosnt 变量内存地址的引用。

3.4 var、let和const的区别

varletconst 都可以声明变量,它们在作用域、可变性和变量提升等方面有以下区别:

  1. 作用域:

    • var:使用 var 关键字声明的变量具有函数作用域,即在声明的函数内部有效。如果在函数外部声明的话,它将成为全局变量。

    • let和const:使用 letconst 关键字声明的变量具有块级作用域,只在最近的块级作用域内有效。块级作用域可以是if语句、for循环、函数等花括号括起来的代码块。

  2. 变量提升:

    • var:使用var声明的变量会进行变量提升,即在函数或全局作用域内,无论变量声明在哪里,都会被提升到所在作用域的顶部。但是,变量的赋值不会被提升,只有声明会被提升。

    • let和const:使用 letconst 声明的变量不存在变量提升,letconst 声明的变量不会被初始化,直到变量声明的位置被执行到才会进行赋值。

  3. 可变性:

    • var和let:使用 varlet 声明的变量是可变的,可以随时重新赋值。

    • const:使用 const 声明的变量是常量,一旦被赋值后,就不能再修改其值。const 声明的变量必须在声明时进行初始化。

  4. 重复声明:

    • var:可以在同一个作用域内多次使用 var 关键字声明同名的变量,这样后面的声明会覆盖前面的声明。

    • let和const:在同一个作用域内,不允许使用 letconst 关键字重复声明同名的变量,否则会报错。

4-解构赋值

ES6 引入了一种便捷的语法,叫做解构赋值(Destructuring Assignment),用于从数组或对象中提取值,并赋给对应的变量。下面详细介绍一下 ES6 变量的解构赋值的用法和特性。

4.1 数组的解构赋值

下面是数组解构赋值的用法和示例:

4.1.1 基本用法

数组解构赋值可以通过 模式匹配(只要等号两边的模式相同,那么左边的变量就会被赋给对应的值。)的方式将数组中的值赋给变量。例如:

const arr = [1, 2, 3];
const [a, b, c] = arr;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

如果解构失败,那么变量的值就是undefined,例如:

const arr = [1];
const [a, b, c] = arr;
console.log(a); // 1
console.log(b); // undefined
console.log(c); // undefined

4.1.2 跳过某项值

如果只想获取部分值,可以使用逗号 , 跳过相应的位置。例如:

let [a, , c] = [1, 2, 3];
console.log(a); // 1
console.log(c); // 3

还可以控制解构变量的个数来跳过后面的值。例如:

let [a, b] = [1, 2, 3];
console.log(a); // 1
console.log(b); // 2

像上面的两个例子都属于是不完全解构。

4.1.3 默认值

如果解构失败,可以为变量设置一个默认值。例如:

const arr = [1];
const [a, b = 2, c = 3] = arr;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3

注意:如果数组中的一个值是 null 的话是不会触发默认值的,因为 null === undefined 的结果是 false

4.2 对象的解构赋值

下面是对象解构赋值的用法和示例:

4.2.1 基本用法

对象解构赋值可以通过模式匹配的方式将对象中的属性值赋给变量。例如:

const obj = { x: 1, y: 2 };
const { x, y } = obj;
console.log(x); // 1
console.log(y); // 2

4.2.2 默认值

在解构赋值中,可以为变量设置默认值。当解构的值为undefined时,会使用默认值。例如:

const { x = 0, y = 0 } = { x: undefined, y: 2 };
console.log(x); // 0
console.log(y); // 2

4.2.3 重命名属性

我们还可以对变量的属性进行重新命名。例如:

const obj = { x: 1, y: 2 };
const { x: a, y: b } = obj;
console.log(a); // 1
console.log(b); // 2

注意:对象的解构赋值的内部机制是先找到同名的属性,然后再将对象的属性赋值给对应的变量。真正赋值的对象的属性,而不是对应的变量。

4.3 剩余运算符

剩余运算符(Rest Operator),也被称为扩展运算符(Spread Operator),在解构赋值中常用于获取数组或对象中剩余的值。

在数组解构中,剩余运算符以三个点 ... 的形式出现,并放置在最后一个变量名之前,用于获取数组中剩余的元素。例如:

const arr = [1, 2, 3, 4, 5];
const [a, b, ...rest] = arr;
console.log(a); // 1
console.log(b); // 2
console.log(rest); // [3, 4, 5]

在对象解构中,剩余运算符同样以三个点 ... 的形式出现,并放置在最后一个变量名之前,用于获取对象中剩余的属性。例如:

const obj = { x: 1, y: 2, z: 3 };
const { x, ...rest } = obj;
console.log(x); // 1
console.log(rest); // { y: 2, z: 3 }

剩余运算符可以很方便地获取数组或对象中的剩余元素或属性,并将它们收集到新的变量中。这在处理一些动态长度的数据时非常有用,能够让代码更加简洁和灵活。

4.4 嵌套解构

解构赋值可以嵌套使用,以便从嵌套的数组或对象中提取值。例如:

const arr = [1, [2, 3], 4];
const [a, [b, c], d] = arr;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // 4

const obj = { x: 1, y: { z: 2 } };
const { x, y: { z } } = obj;
console.log(x); // 1
console.log(z); // 2

4.5 字符串的解构赋值

字符串解构赋值是一种用于将字符串拆分为单个字符的方法。它可以将字符串的每个字符分配给一个变量。

const str = "hello";
const [a, b, c, d, e] = str;
console.log(a); // "h"
console.log(b); // "e"
console.log(c); // "l"
console.log(d); // "l"
console.log(e); // "o"

4.6 用途

解构赋值在实际开发中有很多用途,包括交换变量的值、获取函数返回值(函数多返回值等)、函数参数的定义和对象属性的赋值等。它可以使代码更简洁、可读性更强,并提高开发效率。

5-数值的扩展

ES6 对数值类型进行了一些扩展,引入了一些新的特性和方法,以提供更好的数值处理能力。以下是 ES6 中数值的扩展内容:

5.1 二进制和八进制表示法

ES6 引入了二进制(使用0b或0B开头)和八进制(使用0o或0O开头)字面量的表示方式,使得我们可以直接使用这些进制表示数值。

例如 0b1010表示十进制的100o777表示十进制的511

console.log(0b1010); // 10
console.log(0o777); // 511

5.2 Number对象

Number 对象是一个全局对象,用于进行数值相关操作和处理。它提供了一些静态方法和属性,用于数值的转换、判断、格式化等操作。

5.2.1 类型转换

Number() 构造函数可以将任意类型数据转换为数值类型。如果参数不能被转换为数值,则返回 NaN

console.log(Number("123")); // 123
console.log(Number("123.45")); // 123.45
console.log(Number("abc")); // NaN
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN

parseInt()parseFloat() 可以将字符串转换为整形数类型和浮点数类型,如果转换失败,则返回 NaN

// 整形
console.log(parseInt("123")); // 123
console.log(parseInt("123.45")); // 123
console.log(parseInt("abc")); // NaN
console.log(parseInt("0x10")); // 16
console.log(parseInt("010")); // 8

// 浮点型
console.log(parseFloat("123")); // 123
console.log(parseFloat("123.45")); // 123.45
console.log(parseFloat("abc")); // NaN
console.log(parseFloat("1.23e5")); // 123000

注意:在进行类型转换时要注意数据类型的精度和范围问题,以免出现不符合预期的结果。

5.2.2 Number.isFinite()和Number.isNaN()

Number.isFinite() 用来判断一个数值是否有限。如果参数是有限数值(非Infinity和NaN),则返回true;否则返回false

console.log(Number.isFinite(123)); // true
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isFinite(NaN)); // false

Number.isNaN() 用于判断一个值是否为NaN。如果参数是NaN,则返回true;否则返回false

console.log(Number.isNaN(NaN)); // true
console.log(Number.isNaN(123)); // false
console.log(Number.isNaN("abc")); // false

5.2.3 Number.isInteger()

判断一个值是否为整数。如果参数是整数,则返回true;否则返回false

console.log(Number.isInteger(123)); // true
console.log(Number.isInteger(123.45)); // false
console.log(Number.isInteger("123")); // false

5.2.4 Number.isSafeInteger()

JavaScript 可以准确表示和操作的整数范围:-2^53+12^53-1(不包括两个边界值),超出这个范围的整数可能无法准确表示。

Number.isSafeInteger() 用来判断一个数值是否为安全整数。

console.log(Number.isSafeInteger(123)); // true
console.log(Number.isSafeInteger(Math.pow(2, 53))); // false
console.log(Number.isSafeInteger(9007199254740991)); // true

5.2.5 Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER

Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER 属性分别用来获取最大和最小的安全整数。

console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991

5.2.6 Number.EPSILON

Number.MAX_SAFE_INTEGER 属性表示 1 与大于 1 的最小的非零数值的差别。

console.log(0.1 + 0.2 === 0.3); // false
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // true

5.3 Math对象

Math 对象是 JavaScript 中的一个内置对象,提供了一系列数学相关的静态方法和属性。ES6 在 Math 对象上新增了17个与数学相关的静态方法。下面是对 Math 对象的一些常用方法和属性的介绍:

5.3.1 普通计算

  • Math.cbrt(x):用于计算一个数的立方根。

    console.log(Math.cbrt(27)); // 3
    console.log(Math.cbrt(-27)); // -3
    
  • Math.hypot(…args):用于计算所有参数的平方和的平方根。

    console.log(Math.hypot(3, 4)); // 5
    console.log(Math.hypot(1, 2, 3)); // 3.7416573867739413
    
  • Math.imul(x, y):用于计算两个数的32位整数乘积,返回结果的低32位部分。

    console.log(Math.imul(2 ** 16, 2 ** 16)); // 0
    console.log(Math.imul(2 ** 16 - 1, 2 ** 16 - 1)); // 1
    
  • Math.clz32(x):用于计算一个数的32位二进制形式前导零的个数。

    console.log(Math.clz32(1)); // 31
    console.log(Math.clz32(2 ** 32 - 1)); // 0
    

5.3.2 数字处理

  • Math.trunc(x):用于去除一个数的小数部分,返回整数部分。

    console.log(Math.trunc(3.14)); // 3
    console.log(Math.trunc(-3.14)); // -3
    console.log(Math.trunc("1.5")); // 1
    
  • Math.fround(x):用于将一个数转换成单精度浮点数。

    console.log(Math.fround(1.2)); // 1.2000000476837158
    console.log(Math.fround(2 ** 24 + 1)); // 16777217
    

5.3.3 判断

  • Math.sign(x):用于判断一个数的符号,返回一个表示符号的数字。如果参数是正数,则返回1;如果参数是负数,则返回-1;如果参数是0,则返回0;如果参数是NaN,则返回NaN

    console.log(Math.sign(3)); // 1
    console.log(Math.sign(-3)); // -1
    console.log(Math.sign(0)); // 0
    console.log(Math.sign(NaN)); // NaN
    

5.3.5 对数方法

  • Math.log10(x):用于计算一个数的以10为底的对数。

    console.log(Math.log10(100)); // 2
    console.log(Math.log10(1000)); // 3
    
  • Math.log2(x):用于计算一个数的以2为底的对数。

    console.log(Math.log2(8)); // 3
    console.log(Math.log2(16)); // 4
    
  • Math.log1p(x):用于计算一个数加1后的自然对数。

    console.log(Math.log1p(Math.E - 1)); // 1
    console.log(Math.log1p(-1)); // NaN
    
  • Math.expm1(x):用于计算e的x次方减去1后的值。

    console.log(Math.expm1(0)); // 0
    console.log(Math.expm1(1)); // 1.718281828459045
    

5.3.6 双曲函数方法

  • Math.sinh(x):用于计算一个数的双曲正弦值。

    console.log(Math.sinh(0)); // 0
    console.log(Math.sinh(1)); // 1.1752011936438014
    
  • Math.cosh(x):用于计算一个数的双曲余弦值。

    console.log(Math.cosh(0)); // 1
    console.log(Math.cosh(1)); // 1.5430806348152437
    
  • Math.tanh(x):用于计算一个数的双曲正切值。

    console.log(Math.tanh(0)); // 0
    console.log(Math.tanh(1)); // 0.7615941559557649
    
  • Math.asinh(x):用于返回一个数的双曲正弦反函数值。

    console.log(Math.asinh(1)); // 0.8813735870195429
    
  • Math.acosh(x):用于返回一个数的双曲余弦反函数值。

    console.log(Math.acosh(2));
    
  • Math.atanh(x):用于返回一个数的双曲正切反函数值。

    console.log(Math.atanh(0.5)); // 0.5493061443340548
    

5.3.7 指数算符

ES6 中引入了指数运算符 **,用于进行指数运算。该运算符可以替代传统的指数运算 Math.pow() 方法。

使用指数运算符时,其语法为 base ** exponent,其中 base 表示底数,exponent 表示指数。例如,2 ** 3 表示计算 2 的 3 次方。

除了基本的指数运算,它还支持一些特殊的情况,例如:

  • exponent 为小数时,会计算出 base 的小数次幂。
  • base 为负数且 exponent 为小数时,会计算出虚数。
// 计算 2 的 3 次方
console.log(2 ** 3); // 输出 8

// 计算 2 的 0.5 次方,即开平方
console.log(2 ** 0.5); // 输出 1.4142135623730951

// 计算 -1 的 0.5 次方,会得到一个虚数
console.log((-1) ** 0.5); // 输出 NaN

需要注意的是,指数算符的优先级高于加减乘除等运算符,但低于括号运算符。因此,在进行复合运算时,需要通过括号来确定正确的运算顺序。例如:

let a = 2;
a **= 3 + 2; // 这里会先计算 3 + 2,再进行幂运算
console.log(a); // 输出 32

let b = (2 + 2) ** 3; // 这里会先计算括号内的加法,再进行幂运算
console.log(b); // 输出 64

6-字符串的扩展

ES6 在字符串的处理上引入了一些扩展和改进。下面将详细介绍 ES6 字符串的扩展功能:

6.1 Unicode

ES6 允许在字符串中使用 Unicode 转义表示法,即 \u{} 语法。这个语法可以用来表示任意 Unicode 码点(Code Point),不受限于 16 位的 BMP 码点范围。例如:

console.log("\u{1F600}"); // 输出 😀

ES6 引入了一些新的字符串方法,如 codePointAt()fromCodePoint() 等,用于处理 Code Point 相关的操作。其中 codePointAt() 方法返回指定位置的 Code Point 值,而 fromCodePoint() 方法则将 Code Point 值转换为字符。例如:

const str = "😀🍎";
console.log(str.codePointAt(0)); // 输出 128512
console.log(String.fromCodePoint(128512)); // 输出 😀

ES6 引入了 String.prototype[@@iterator]() 方法,使得字符串的遍历器接口成为一个可选项,可以通过 for...of 循环进行遍历。这个遍历器会依次返回每个字符的 Code Point 值。例如:

const str = "😀🍎";
for (let codePoint of str) {
  console.log(codePoint);
}
// 输出:
// 128512
// 127822

ES6 引入了正则表达式的 u 修饰符,用于处理 Unicode 字符串。在 u 修饰符的作用下,正则表达式会将匹配模式视为 Unicode 模式,从而正确处理 Code Point 大于 16 位的字符。例如:

const str = "Hello, 😀!";
console.log(str.match(/^.+$/u)); // 输出 ["Hello, 😀!"]

6.2 字符串遍历器接口

ES6 引入了字符串的遍历器接口,使得字符串可以使用 for...of 循环进行遍历。例如:

const str = "Hello";
for (let char of str) {
  console.log(char);
}
// 输出:
// H
// e
// l
// l
// o

使用字符串遍历器接口的好处是,它可以直接遍历字符串中的每个字符,而不需要手动拆分字符串或使用索引来访问单个字符。这样简化了字符串的处理和操作。

需要注意的是,字符串遍历器接口返回的是字符串的 Unicode 码点(Code Point)值,而不是字符编码单元(Code Unit)。这意味着对于包含 Unicode 码点大于 16 位的字符,遍历器会正确地返回完整的字符。

6.3 字符串方法扩展

ES6 还引入了一些新的字符串方法,如 startsWith()endsWith()includes() 等。

6.3.1 startsWith()、endsWith()和includes()

startsWith()endsWith()includes() 三个方法用于判断字符串的开始、结束或包含关系。例如:

const str = "Hello, world!";
console.log(str.startsWith("Hello")); // 输出 true
console.log(str.endsWith("!")); // 输出 true
console.log(str.includes("world")); // 输出 true

这三个方法都支持第二个参数,表示字符串开始搜索的位置。

const str = "Hello, world!";
console.log(str.startsWith("world", 7)); // 输出 true
console.log(str.endsWith("Hello", 5)); // 输出 true
console.log(str.includes("Hello"), 6); // 输出 false

6.3.2 repeat()

repeat() 方法返回一个新的字符串,用于复制字符串指定次数。

const str = "Hello, world!";
console.log(str.repeat(3)); // 输出 Hello, world!Hello, world!Hello, world!

6.3.3 at()

at() 接受一个整数参数,表示要获取的字符位置。如果该位置超出字符串的范围,则返回 undefined。如果该位置处的字符是一个 Unicode 码点大于 16 位的字符,那么返回的字符会正确地包含该字符的所有编码单元。

const str = '😀🍎abc';

console.log(str.at(0)); // 输出 "😀"
console.log(str.at(1)); // 输出 "🍎"
console.log(str.at(2)); // 输出 "a"

6.4 模版字符串

ES6 引入了模板字符串(Template Strings)的概念,它是一种新的字符串语法,可以更方便地创建多行字符串和字符串插值。

6.4.1 模板字面量

模板字面量是一种更灵活和直观的字符串表示方法,使用反引号 `` 包围。它允许在字符串中插入表达式,并支持多行字符串的书写。使用 ${} 语法可以插入变量或表达式。例如:

const name = "Alice";
const age = 25;
const message = `My name is ${name} and I am ${age} years old.`;
console.log(message);
// 输出:My name is Alice and I am 25 years old.

6.4.2 多行字符串

ES6 允许使用反引号 `` 创建多行字符串,而不需要使用 \n 进行换行。例如:

const multiline = `This is a
multi-line
string.`;
console.log(multiline);
/*
输出:
This is a
multi-line
string.
*/

6.4.3 字符串插值

模板字面量中的 ${} 语法不仅可以插入变量,还可以插入任意 JavaScript 表达式。例如:

const a = 5;
const b = 10;
const sum = `The sum of ${a} and ${b} is ${a + b}.`;
console.log(sum);
// 输出:The sum of 5 and 10 is 15.

6.5 标签模版

标签模板(Tagged Templates)允许自定义模板字面量的解析方式。在模板字面量前面加上一个函数名,该函数会接收解析后的字符串和插值表达式作为参数,并返回处理后的结果。这个特性可以用于实现自定义的字符串处理逻辑。例如:

function uppercase(strings, ...values) {
  let result = "";
  for (let i = 0; i < strings.length; i++) {
    result += strings[i].toUpperCase();
    if (i < values.length) {
      result += values[i];
    }
  }
  return result;
}

const name = "Alice";
const age = 25;
const message = uppercase`My name is ${name} and I am ${age} years old.`;
console.log(message);
// 输出:MY NAME IS ALICE AND I AM 25 YEARS OLD.

7-数组的扩展

ES6 引入了许多有用的数组扩展功能,使得数组操作更加方便和灵活。下面是一些常用的 ES6 数组扩展。

7.1 Array.from()

ES6 中的 Array.from() 是一个用于将可迭代对象(iterable)或类数组对象(array-like object)转换为真正的数组的静态方法。它提供了一种简便的方式来创建新的数组,并且可以在转换过程中对原始数据进行一些处理。

Array.from() 的语法如下:

Array.from(iterable, [mapFn], [thisArg])

参数说明:

  • iterable:必需,要转换为数组的可迭代对象或类数组对象。
  • mapFn(可选):一个映射函数,用于对每个元素进行处理后返回新的值。
  • thisArg(可选):执行映射函数时的this值。
  1. 将字符串转换为字符数组

    const str = 'hello';
    const arr = Array.from(str);
    // arr: ['h', 'e', 'l', 'l', 'o']
    
  2. 使用映射函数

    将类数组对象转换为数组,并对每个元素进行处理:

    const obj = {
      length: 3,
      0: 1,
      1: 2,
      2: 3
    };
    const arr = Array.from(obj, (x) => x * 2);
    // arr: [2, 4, 6]
    
  3. 使用映射函数和thisArg参数:

    在使用Array.from方法时,如果希望在映射函数中使用this关键字引用其他对象,可以通过thisArg参数来设置。

    const set = new Set([1, 2, 3]);
    const arr = Array.from(set, function(x) {
      return x * this.multiplier;
    }, { multiplier: 2 });
    console.log(arr); // [2, 4, 6]
    
  4. Set和Map转换为数组

    后面要学习的SetMap等可迭代对象可以通过 Array.from 方法直接转换为数组。

    const set = new Set([1, 2, 3]);
    console.log(Array.from(set)); // [1, 2, 3]
    
    const map = new Map();
    map.set('1', 'Tom');
    map.set('2', 'John');
    map.set('3', 'Julie');
    console.log(Array.from(map)); // [['1', 'Tom'], ['2', 'John'], ['3', 'Julie']]
    

注意:Array.from() 方法返回一个新的数组,并不修改原始的可迭代对象或类数组对象。

7.2 Array.of()

Array.of() 方法用于创建一个具有可变数量参数的新数组实例。

// 1. 创建包含指定元素的数组:
const arr1 = Array.of(1, 2, 3, 4, 5);
console.log(arr1); // [1, 2, 3, 4, 5]

// 2. 创建只包含一个元素的数组:
const arr2 = Array.of(10);
console.log(arr2); // [10]

// 3. 创建包含多个不同类型的元素的数组:
const arr3 = Array.of('hello', 42, true, { name: 'John' });
console.log(arr3); // ['hello', 42, true, { name: 'John' }]

注意:与Array构造函数的行为不同,当传递单个参数时,Array.of() 会将该参数作为唯一的元素值,而不会根据参数的类型进行特殊处理。

Array.of() 相比,Array构造函数在以下情况下表现不同:

// Array()
const arr1 = Array(5); // arr1: [ , , , , ]
const arr2 = Array(5, 10); // arr2: [5, 10]

// Array.of()
const arr3 = Array.of(5); // arr3: [5]
const arr4 = Array.of(5, 10); // arr4: [5, 10]

在上述示例中,Array(5) 创建了一个长度为5的空数组,而 Array(5, 10) 创建了一个包含两个元素的数组,元素值分别为5和10。

7.3 填充

  • copyWithin()

    copyWithin() 是一个用于在数组内部进行元素拷贝的方法。它将数组的一部分元素复制到数组的指定位置,覆盖原来的值,并返回修改后的数组。

    copyWithin() 的语法如下:

    array.copyWithin(target, start, end)
    

    参数说明:

    • target:必需,指定拷贝的目标位置,即被覆盖的起始位置。
    • start(可选):指定拷贝的起始位置,默认为0。
    • end(可选):指定拷贝的结束位置,默认为数组的长度。

    示例代码:

    // 1.在数组内部进行元素拷贝:
    
    const arr1 = [1, 2, 3, 4, 5];
    arr1.copyWithin(2, 0, 2);
    console.log(arr1); // [1, 2, 1, 2, 5]
    
    // 2.使用负数索引进行拷贝:
    const arr2 = [1, 2, 3, 4, 5];
    arr2.copyWithin(-3, -5, -2);
    console.log(arr2); // [1, 2, 1, 2, 3]
    

    注意:copyWithin() 方法会修改原始数组,并且返回修改后的数组。它对原始数组进行的操作是基于索引位置的拷贝,而不是插入或删除元素。如果目标位置超出了数组的长度,拷贝将会停止。

    此外,copyWithin() 方法还支持链式调用,可以连续进行多次拷贝操作。例如:

    const arr = [1, 2, 3, 4, 5];
    arr.copyWithin(0, 1).copyWithin(2, 0, 2);
    console.log(arr); // [1, 2, 1, 2, 3]
    
  • fill()

    fill() 是一个用于填充数组元素的方法。它将数组的所有元素都替换为指定的值,并返回修改后的数组。

    fill() 的语法如下:

    array.fill(value, [start], [end])
    

    参数说明:

    • value:必需,用于填充数组元素的值。
    • start(可选):指定填充的起始位置,默认为0。
    • end(可选):指定填充的结束位置,默认为数组的长度。

    示例代码:

    // 1.填充数组元素:
    const arr = [1, 2, 3, 4, 5];
    arr.fill(0); 
    console.log(arr); // [0, 0, 0, 0, 0]
    
    // 2.指定填充的起始位置和结束位置:
    arr.fill(0, 2, 4);
    console.log(arr); // [1, 2, 0, 0, 5]
    

    注意:fill() 方法会修改原始数组,并且返回修改后的数组。它对原始数组进行的操作是基于索引位置的填充,而不是插入或删除元素。如果填充范围超出了数组的长度,填充将会停止。

    fill() 方法还支持链式调用,可以连续进行多次填充操作。例如:

    const arr = [1, 2, 3, 4, 5];
    arr.fill(0).fill(10, 2, 4);
    console.log(arr); // [10, 10, 0, 0, 0]
    

7.4 查找

find() 方法用于查找满足条件的第一个元素,并返回该元素;findIndex() 方法用于查找满足条件的第一个元素的索引,并返回该索引。

find() 和 findIndex() 语法如下:

array.find(callback(element, index, array), [thisArg])
array.findIndex(callback(element, index, array), [thisArg])

参数说明:

  • callback:必需,用于测试每个元素的函数,接受3个参数:
    • element:当前被处理的元素。
    • index:当前元素的索引。
    • array:调用findIndex方法的数组。
  • thisArg(可选):执行回调函数时的this值,默认为undefined

使用示例:

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

// find()
const result = arr.find(item => item > 3);
console.log(result); // 4

// findIndex()
const resultIndex = arr.findIndex(item => item > 3);]
console.log(3); // 3

注意:find()findIndex() 都是从数组的左侧开始查找,一旦找到满足条件的元素,即停止搜索。它们都支持链式调用,并且可以通过 thisArg 参数指定回调函数的执行上下文。

7.5 includes()

Array.includes() 是一个用于检查数组是否包含指定元素的方法。它返回一个布尔值,表示数组是否包含该元素。

includes() 的语法如下:

array.includes(value, [fromIndex])

参数说明:

  • value:必需,要检查的元素值。
  • fromIndex(可选):指定搜索的起始位置,默认为0。如果指定的 fromIndex 为负数,则表示从数组末尾开始计算的偏移量。

示例代码:

// 1. 检查数组是否包含某个元素:
const arr = [1, 2, 3, 4, 5];
const hasElement = arr.includes(3); 
console.log(hasElement); // hasElement: true

// 2.指定起始位置进行搜索:
const hasElement2 = arr.includes(3, 2);
console.log(hasElement2); // hasElement: true

// 3.使用负数的起始位置:
const arr = [1, 2, 3, 4, 5];
const hasElement3 = arr.includes(3, -3);
console.log(hasElement3); // hasElement: true

在上面的示例中,includes() 方法会从倒数第3个元素开始搜索数组中是否包含值为3的元素,并返回 true。

注意:includes() 方法使用严格相等运算符===进行比较。这意味着它不会进行隐式类型转换。只有在数组中找到一个严格相等于指定值的元素时,才会返回 true。如果要检查数组中是否存在某个对象引用,需要保证对象引用相同。

includes() 方法还可以用来检查 NaN 的存在,因为 NaN 与自身不相等。例如:

const arr = [1, 2, NaN, 4, 5];
const hasNaN = arr.includes(NaN);
console.log(hasNaN); // hasNaN: true

在上面的示例中,includes() 方法会检查数组中是否包含 NaN,并返回 true

7.6 转换

  • flat()

    flat() 方法用于将多维数组“扁平化”,即将多层嵌套的数组转换为一维数组。语法:array.flat([depth])。其中,depth参数可选,表示要扁平化的层数。如果不指定该参数,则默认为1,即只扁平化一层。

    const arr = [1, 2, [3, 4]];
    const flatArr = arr.flat();
    console.log(flatArr); // [1, 2, 3, 4]
    

    在上面的示例中,我们定义了一个嵌套了一层的数组arr,其中包含了两个数字和一个嵌套了一层的数组。我们调用 flat() 方法将其扁平化,得到了一个一维数组flatArr,其中包含了原数组中的所有元素。

    注意:flat()方法返回的是一个新的数组,而不是修改原数组。如果原数组中的元素本身也是数组,那么它们也会被扁平化到结果数组中。例如:

    const arr = [1, [2, [3, 4]]];
    const flatArr = arr.flat(2);
    console.log(flatArr); // [1, 2, 3, 4]
    

    在上面的示例中,我们定义了一个嵌套了两层的数组arr,其中包含了一个数字和两个嵌套了两层的数组。我们调用 flat(2) 方法将其扁平化到最大深度,得到了一个一维数组flatArr,其中包含了原数组中的所有元素。

    如果原数组中的元素有空位,那么它们会被跳过,不会出现在结果数组中。例如:

    const arr = [1, 2, , 4];
    const flatArr = arr.flat();
    console.log(flatArr); // [1, 2, 4]
    

    在上面的示例中,我们定义了一个包含空位的数组arr,其中第三个元素是一个空位。我们调用 flat() 方法将其扁平化,得到了一个一维数组flatArr,其中包含了原数组中的非空元素。

    flat() 方法可以用来将多维数组扁平化,特别适用于处理嵌套结构的数据。

  • flatMap()

    flatMap() 方法结合了 map() flat() 两个方法的功能,可以对数组中的每个元素进行映射操作,并将结果“扁平化”得到一个新的一维数组。语法:array.flatMap(callback, [thisArg])。其中,callback参数是一个函数,用于对数组中的每个元素进行映射操作。该函数会被传入三个参数:当前元素的值、当前元素的索引和原数组本身。函数返回值会成为新的数组的一部分。

    const arr = [1, 2, 3];
    const flatMapArr = arr.flatMap(x => [x * 2]);
    console.log(flatMapArr); // [2, 4, 6]
    

    在上面的示例中,我们定义了一个数组arr,其中包含了三个数字。我们调用 flatMap() 方法对每个元素进行映射操作,将每个元素乘以2并放入一个新的数组中。由于结果数组中的每个元素都是一个数组,因此调用 flatMap() 方法后会将这些数组“扁平化”,得到一个新的一维数组flatMapArr,其中包含了所有映射结果。

    需要注意的是,如果映射函数返回的是一个数组,那么这个数组会被“扁平化”到结果数组中。例如:

    const arr = [1, 2, 3];
    const flatMapArr = arr.flatMap(x => [[x * 2]]);
    console.log(flatMapArr); // [[2], [4], [6]]
    

    flatMap()方法可以用来对数组中的每个元素进行映射操作,并将结果“扁平化”得到一个新的一维数组。它提供了一种方便的方式来处理数组中的嵌套结构数据。

  • map()、flat()、flatMap()的区别

    map()flat()flatMap() 它们的区别如下:

    • map()方法:

      • 对数组中的每个元素进行操作,返回一个新的数组。
      • 新数组中的元素数量与原数组相同。
      • 新数组中的元素顺序与原数组相同。
      • 不会对原数组造成影响。
    • flat()方法:

      • 将多维数组“扁平化”,即将多层嵌套的数组转换为一维数组。
      • 返回一个新的一维数组。
      • 不会改变原数组的结构。
    • flatMap()方法:

      • 对数组中的每个元素进行映射操作,并将结果“扁平化”得到一个新的一维数组。
      • 返回一个新的一维数组。
      • 可以代替map()和flat()的组合使用。
      • 不会改变原数组的结构。
    const arr = [[1, 2], [3], [4, 5, 6]];
    
    // 使用 map() 方法将每个数组中的元素加一,返回一个新的二维数组
    const mapArr = arr.map(innerArr => innerArr.map(x => x + 1));
    console.log(mapArr); // [[2, 3], [4], [5, 6, 7]]
    
    // 使用 flat() 方法将二维数组“扁平化”,返回一个新的一维数组
    const flatArr = arr.flat();
    console.log(flatArr); // [1, 2, 3, 4, 5, 6]
    
    // 使用 flatMap() 方法将每个数组中的元素加一,并将结果“扁平化”,返回一个新的一维数组
    const flatMapArr = arr.flatMap(innerArr => innerArr.map(x => x + 1));
    console.log(flatMapArr); // [2, 3, 4, 5, 6, 7]
    

    在上面的例子中,我们使用 map() 方法对每个内部数组中的元素进行操作,返回一个新的二维数组。使用 flat() 方法将这个二维数组“扁平化”得到一个新的一维数组。使用 flatMap() 方法可以将这两个步骤合并为一步,优化了代码。

7.7 遍历

ES6 中引入了迭代器(Iterator)的概念,它是一种统一的遍历机制,用于遍历数据结构中的元素。迭代器提供了一种标准的方式来访问集合中的每一个元素,而不需要关心集合的具体实现。

使用迭代器的好处是,可以通过统一的方式遍历不同类型的数据结构,例如:数组、字符串、Map、Set等。

下面是数组的一些遍历方法:

  • entries()

    entries() 是 ES6 中新增的一个用于获取数组键值对的迭代器方法。它返回一个新的数组迭代器对象,该迭代器对象可以依次返回数组的每个键值对。

    const arr = ['a', 'b', 'c'];
    const iterator = arr.entries();
    for (const [index, value] of iterator) {
      console.log(index, value);
    }
    /* 输出:
    0 "a"
    1 "b"
    2 "c"
    */
    

    在上面的示例中,entries() 方法会返回一个新的迭代器对象 iterator。然后我们使用 for...of 循环遍历迭代器对象,每次迭代都会返回一个数组,其中第一个元素是当前项的索引,第二个元素是当前项的值。

  • keys()

    keys() 方法用于返回一个新的数组迭代器对象,该对象可以依次返回数组的每个索引键。

    const arr = ['a', 'b', 'c'];
    const iterator = arr.keys();
    // for (const key of iterator) {
    //   console.log(key);
    // }
    let result = iterator.next();
    while (!result.done) {
      console.log(result.value);
      result = iterator.next();
    }
    /* 输出:
    0
    1
    2
    */
    

    在上面的示例中,我们使用while循环遍历迭代器对象,并通过判断done属性是否为true来判断是否已经迭代完毕。每次迭代都会打印出当前的索引键。

  • values()

    values() 方法用于返回一个新的数组迭代器对象,该对象可以依次返回数组的每个值。

    const arr = [1, 2, 3];
    const iterator = arr.values();
    // for (const value of iterator) {
    //   console.log(value);
    // }
    let result = iterator.next();
    while (!result.done) {
      console.log(result.value);
      result = iterator.next();
    }
    /* 输出:
    1
    2
    3
    */
    

    在上面的示例中,我们使用while循环遍历迭代器对象,并通过判断done属性是否为true来判断是否已经迭代完毕。每次迭代都会打印出当前的值。

entries() 方法与其他数组迭代器方法如 keys()values() 相比,在实际应用中用得相对较少。但是,它可以方便地获取数组的键值对,同时也可以用于实现一些高级的数组操作。

8-函数的扩展

ES6 引入了许多新的函数扩展,包括箭头函数、默认参数、剩余参数、扩展操作符、解构赋值等。

8.1 默认参数

默认参数可以在函数定义时为参数设置默认值,如果调用时没有传递参数,则会使用默认值。

function sayHello(name = 'World') {
  console.log(`Hello, ${name}!`);
}

sayHello(); // 输出:Hello, World!
sayHello('Alice'); // 输出:Hello, Alice!

注意:当传递的参数值为 null 或者空字符串时,将不会使用默认值,而会使用实际的参数值。

默认参数可以与其他类型的参数一起使用,并且可以设置默认值的参数位于参数列表的任意位置。但是,函数的默认参数必须位于非默认参数的后面。

function foo(a = 1, b) {
  // ...
}

foo(); // 报错:Missing initializer in destructuring declaration

8.2 剩余参数

剩余参数可以使用 ... 操作符来表示,它允许我们将不确定数量的参数表示为一个数组。

function sum(...numbers) {
  let total = 0;
  for (let number of numbers) {
    total += number;
  }
  return total;
}

console.log(sum(1, 2, 3, 4, 5)); // 输出:15

注意:剩余参数只能出现在函数参数列表的最后一个位置,并且一个函数只能有一个剩余参数。

8.3 扩展操作符

扩展操作符可以使用 ... 操作符来表示,它允许我们将一个数组“展开”为多个参数。

在函数调用中使用扩展操作符:

function multiply(x, y, z) {
	return x * y * z;
}

const numbers = [2, 3, 4];

console.log(multiply(...numbers)); // 输出:24

在这个示例中,我们定义了一个名为 multiply 的函数,它接收三个参数并返回它们的乘积。使用扩展操作符 ... 将数组 numbers 展开为单独的参数,即 multiply(2, 3, 4),函数计算并返回乘积 24。

注意:在函数调用中,扩展操作符必须处于参数列表的最后位置。

剩余运算符和扩展运算符的区别:

  • 剩余运算符:用于收集多个参数或提取剩余的数组元素。

  • 扩展运算符:用于将数组或对象展开为独立的参数,或将一个数组或对象的元素/属性展开到另一个数组或对象中。

8.4 name属性

在 ES6 中,函数对象的 name 属性用于获取函数的名称。这个属性是一个只读字符串,表示函数的名称。它提供了一种方便的方式来获取函数的名称,无论是命名函数还是匿名函数。

对于命名函数,name 属性将返回函数的实际名称:

function myFunction() {
  // 函数逻辑
}

console.log(myFunction.name); // "myFunction"

对于匿名函数,name 属性将返回一个空字符串:

const myFunction = function() {
  // 函数逻辑
};

console.log(myFunction.name); // ""

需要注意的是,箭头函数是匿名函数的一种特殊形式,它们没有自己的 name 属性。当使用箭头函数时,name 属性将从外部上下文中继承,通常是变量名或属性名:

const myArrowFunction = () => {
  // 函数逻辑
};

console.log(myArrowFunction.name); // "myArrowFunction"

8.5 箭头函数

ES6 中的箭头函数(arrow function)是一种新的函数声明方式,它提供了更简洁的语法和更清晰的上下文绑定。箭头函数通常用于替代传统的匿名函数表达式,尤其是在需要保留当前上下文的情况下。

箭头函数的语法很简单,它使用箭头 => 来分隔参数列表和函数体,如下所示:

// 传统的匿名函数表达式
const myFunction1 = function(param1, param2) {
  // 函数体
};

// 箭头函数表达式
const myFunction2 = (param1, param2) => {
  // 函数体
};

可以看到,箭头函数将原本需要用 function 关键字声明的函数,简化为一个带参数列表和箭头 => 的表达式。同时,箭头函数的函数体可以是一个单独的表达式或一个代码块。

另外,在箭头函数中,this 关键字的指向和普通函数不同,箭头函数会捕获定义时上下文的 this 值,并在函数执行时使用这个值。这意味着在箭头函数中,this 的值不受函数执行方式的影响。例如:

const obj = {
  name: 'Alice',
  greet: function() {
    setTimeout(() => {
      console.log(`Hello, my name is ${this.name}`);
    }, 1000);
  }
};

obj.greet(); // 输出 "Hello, my name is Alice"(1 秒后输出)

在这个例子中,我们定义了一个对象 obj,它有一个属性 name 和一个方法 greet。在 greet 中,我们使用了箭头函数来定义 setTimeout 的回调函数,并使用 ${this.name} 来引用对象的 name 属性。由于箭头函数会捕获定义时上下文的 this 值,所以在回调函数中,this.name 指向的是 obj 对象的 name 属性。

8.6 解构赋值

解构赋值可以让我们从数组或对象中提取值,并将它们赋给变量。

  1. 数组解构赋值:

    function myFunction([item1, item2]) {
      // 使用解构得到的变量
      console.log(item1);
      console.log(item2);
    }
    
    myFunction(['value1', 'value2']);  // 输出 "value1" 和 "value2"
    

    在这个例子中,我们定义了一个函数 myFunction,它的参数是一个数组解构模式 [item1, item2]。当调用该函数时,我们传入了一个具有相应元素的数组 ['value1', 'value2'],函数内部会自动根据解构模式将数组的元素值赋给解构出的变量 item1item2

  2. 对象解构赋值:

    function myFunction({prop1, prop2}) {
      // 使用解构得到的变量
      console.log(prop1);
      console.log(prop2);
    }
    
    myFunction({prop1: 'value1', prop2: 'value2'});  // 输出 "value1" 和 "value2"
    

    在这个例子中,我们定义了一个函数 myFunction,它的参数是一个对象解构模式 {prop1, prop2}。当调用该函数时,我们传入了一个具有相应属性的对象 {prop1: 'value1', prop2: 'value2'},函数内部会自动根据解构模式将对象的属性值赋给解构出的变量 prop1prop2

9-对象的扩展

ES6 对象的扩展引入了一些新的语法和功能,使得对象的创建和操作更加方便和灵活。下面是一些 ES6 对象扩展的详细介绍:

9.1 简洁的属性表达

当属性名和变量名相同时,可以使用简洁表示法。

const x = 10;
const y = 20;

// ES5
// const obj = { x: x, y: y };

// ES6
const obj = { x, y };

console.log(obj); // 输出 { x: 10, y: 20 }

9.2 可计算属性名

在对象字面量中使用方括号 [] 来定义属性名,可以使用表达式或变量。

const propName = 'name';

const obj = {
  [propName]: 'John',
  age: 25
};

console.log(obj); // 输出 { name: 'John', age: 25 }

9.3 属性名简洁方法

在对象字面量中定义方法时,可以直接使用简洁的方式,省略冒号和 function 关键字。

// ES5
// const obj = {
//   sayHello: function() {
//     console.log('Hello!');
//   }
// };

// ES6
const obj = {
  sayHello() {
    console.log('Hello!');
  }
};

obj.sayHello(); // 输出 "Hello!"

9.4 对象的可迭代性和解构赋值

  • 对象可以通过 Object.entries()Object.keys() 方法返回可迭代的键值对和键名,方便进行遍历。

  • 对象也可以使用解构赋值的方式将属性解构为变量。

const obj = { x: 1, y: 2 };

// 遍历对象的键值对
for (const [key, value] of Object.entries(obj)) {
  console.log(`${key}: ${value}`);
}
// 输出 "x: 1" 和 "y: 2"

// 解构赋值
const { x, y } = obj;
console.log(x); // 输出 1
console.log(y); // 输出 2

9.5 Object.is()

Object.is() 是一个用于比较两个值是否相等的方法。它与传统的相等比较运算符 == 和严格相等比较运算符 === 有一些不同之处。语法:Object.is(value1, value2);,其中,value1value2 是要进行比较的两个值。

Object.is() 方法的比较规则如下:

  1. 对于大多数情况下,Object.is(value1, value2) 的行为与 value1 === value2 相同。

  2. 不同之处在于 Object.is() 对于以下特殊值的处理:

    • NaN 和 NaN 被认为是相等的。

    • +0 和 -0 被认为是不相等的。

console.log(Object.is(1, 1)); // 输出 true
console.log(Object.is(NaN, NaN)); // 输出 true
console.log(NaN === NaN); // 输出 false

console.log(Object.is(0, -0)); // 输出 false
console.log(Object.is(+0, -0)); // 输出 false
console.log(0 === -0); // 输出 true

9.6 Object.assign()

Object.assign() 方法用于将多个源对象的属性复制到目标对象中。

const target = { a: 1 };
const source = { b: 2, c: 3 };

const newObj = Object.assign(target, source);

console.log(newObj); // 输出 { a: 1, b: 2, c: 3 }

注意:Object.assign() 方法只能复制可枚举的属性,包括自有属性和继承属性。不可枚举的属性、Symbol 类型的属性以及原型链上的属性都不会被复制。同时,该方法也无法复制对象的方法。如果需要复制对象的方法,可以使用 Object.getPrototypeOf()Object.setPrototypeOf() 方法。

9.7 扩展运算符

扩展运算符 ... 可以将一个对象的属性扩展到另一个对象中,或者用于创建新的对象。

const person = { name: 'Alice', age: 25 };
const personWithDetails = { ...person, city: 'New York' }; // 对象字面量中使用扩展操作符

console.log(personWithDetails); // 输出:{ name: 'Alice', age: 25, city: 'New York' }

在这个示例中,我们有一个包含 nameage 属性的对象 person。使用扩展操作符 ...person 对象展开,并在对象字面量中添加额外的属性 city,得到新的对象 personWithDetails。打印 personWithDetails 可以看到它包含了所有展开的属性,即 { name: 'Alice', age: 25, city: 'New York' }

注意:扩展操作符只能用于可迭代的对象(如数组或字符串),而不能用于普通的对象。

9.8 对象属性的遍历

ES6 引入了以下几种方法来遍历对象的属性。

  1. for…in循环

    const obj = { a: 1, b: 2 };
    for (let prop in obj) {
    	console.log(prop, obj[prop]);
    }
    // Output: "a 1", "b 2"
    
  2. Object.keys()方法

    使用 Object.keys() 方法可以获取对象自身所有可枚举的属性名,返回一个数组。不会遍历原型链上的属性。

    const obj = { a: 1, b: 2 };
    const keys = Object.keys(obj);
    console.log(keys); // Output: ["a", "b"]
    
  3. Object.values() 方法

    使用 Object.values() 方法可以获取对象自身所有可枚举的属性值,返回一个数组。不会遍历原型链上的属性。

    const obj = { a: 1, b: 2 };
    const values = Object.values(obj);
    console.log(values); // Output: [1, 2]
    
  4. Object.entries()方法

    使用 Object.entries() 方法可以获取对象自身所有可枚举的属性值,返回一个数组。不会遍历原型链上的属性。

    const obj = { a: 1, b: 2 };
    const entries = Object.entries(obj);
    console.log(entries); // Output: [["a", 1], ["b", 2]]
    
  5. Object.getOwnPropertyNames()

    Object.getOwnPropertyNames() 方法返回一个包含对象自身所有属性(包括不可枚举属性)的数组,属性的顺序与对象中定义的顺序一致。

    const obj = { a: 1, b: 2 };
    const propNames = Object.getOwnPropertyNames(obj);
    console.log(propNames); // Output: ["a", "b"]
    
  6. Object.getOwnPropertySymbols()

    Object.getOwnPropertySymbols() 方法是获取对象的私有 Symbol 属性的一种方式。它返回一个包含给定对象所有 Symbol 属性的数组,这些属性是对象自己拥有的,而不是从原型链继承来的。

    const obj = {
      [Symbol('foo')]: 'foo value',
      bar: 'bar value'
    };
    
    const symbols = Object.getOwnPropertySymbols(obj);
    console.log(symbols); // [Symbol(foo)]
    
    const fooSymbol = symbols[0];
    console.log(obj[fooSymbol]); // foo value
    
  7. Reflect.ownKeys()

    Reflect.ownKeys() 方法返回一个包含对象自身所有属性(包括可枚举和不可枚举属性)的数组,属性的顺序与对象中定义的顺序一致。

    const obj = { a: 1, b: 2 };
    const keys = Reflect.ownKeys(obj);
    console.log(keys); // Output: ["a", "b"]
    

9.9 __proto__属性

在ES6中,对象有一个特殊的 __proto__ 属性,它指向该对象的原型。每个对象都有一个 __proto__ 属性,包括所有的函数、数组和自定义对象。

__proto__ 属性指向对象的原型,它可以用来继承原型上的属性和方法。当我们访问一个对象属性或方法时,如果对象本身没有这个属性或方法,就会去查找它的原型上是否有这个属性或方法,如果还没有就会一直沿着原型链往上查找,直到找到 Object.prototype 为止。

ES6之前,访问对象的原型通常使用 Object.getPrototypeOf() 方法,但是这个方法只能获取原型,不能设置原型。而在 ES6 中,可以使用 __proto__ 属性来设置和获取对象的原型,如下所示:

const obj1 = { a: 1 };
const obj2 = { b: 2 };

obj1.__proto__ = obj2; // 将 obj2 设置为 obj1 的原型
console.log(obj1.b); // 输出: 2

上面的代码中,我们将 obj2 设置为 obj1 的原型,因此当我们访问 obj1b 属性时,会沿着原型链查找到 obj2,并获取到它的属性值2。

9.10 Object.getPrototypeOf() 和 Object.setPrototypeOf()

  • Object.getPrototypeOf() 用于获取指定对象的原型(prototype)。它接受一个参数,即要获取原型的对象,并返回该对象的原型。

    const obj = {};
    const proto = Object.getPrototypeOf(obj);
    console.log(proto); // 输出: Object {}
    
    const array = [];
    const proto2 = Object.getPrototypeOf(array);
    console.log(proto2); // 输出: Array []
    

    在上述示例中,我们使用 Object.getPrototypeOf() 方法分别获取了 obj 对象和 array 数组的原型。protoproto2 变量分别保存了这两个对象的原型。

  • Object.setPrototypeOf() 用于设置指定对象的原型。它接受两个参数,第一个参数是要设置原型的对象,第二个参数是要设置的原型对象。

    const obj = {};
    const proto = { name: 'John' };
    Object.setPrototypeOf(obj, proto);
    
    console.log(Object.getPrototypeOf(obj)); // 输出: { name: 'John' }
    

    在上述示例中,我们使用 Object.setPrototypeOf() 方法将 obj 对象的原型设置为 proto 对象。然后,通过使用 Object.getPrototypeOf() 方法验证原型是否已被成功设置。

    注意:尽管 Object.setPrototypeOf() 方法可以设置对象的原型,但在性能上可能会有一些损失,并且在某些情况下可能影响到 JavaScript 引擎的优化。因此,如果可能的话,最好在创建对象时就使用 Object.create() 方法来指定原型,而不是使用 Object.setPrototypeOf() 进行动态设置原型。

10-Symbol

ES6 引入了一种新的原始数据类型 Symbol(符号),它是一种独一无二且不可变的数据类型。Symbol 的主要作用是创建具有唯一标识符的属性名,这些属性名可以用于对象的属性和方法。

Symbol 的特点如下:

  • 独一无二性:每个通过 Symbol 函数生成的 Symbol 值都是唯一的,不会与其他任何值相等。

  • 不可变性:Symbol 值一旦生成,就不能被修改或重写。

10.1 基本用法

使用 Symbol 的语法为:Symbol([description]),其中可选的 description 参数是一个字符串,用于描述该 Symbol 值,主要用于调试目的。

let symbol1 = Symbol();
console.log(symbol1); // Symbol()
console.log(typeof symbol1); // symbol

let symbol2 = Symbol();
console.log(symbol2); // Symbol()
console.log(typeof symbol2); // symbol

// 带描述的Symbol
let symbol3 = Symbol("Hello"); // Symbol(Hello)
console.log(helloSymbol3); // Symbol(Hello)

let symbol4 = Symbol("Hello"); // Symbol(Hello)
console.log(helloSymbol4); // Symbol(Hello)

console.log(symbol1 === symbol2); // false
console.log(symbol3 === symbol4); // false

上述代码中的 Symbol() 函数生成的 Symbol 值都是独一无二的,尽管 symbol1、symbol2、 symbol3 和 symbol4 看起来一样,但是它们并不相等。

10.2 作为属性名

Symbol 可以用于创建唯一属性名。通过使用 Symbol 作为对象的属性名,可以确保属性名的唯一性,避免与其他属性名冲突。

  • 创建唯一属性名

    const mySymbol = Symbol('mySymbol');
    const obj = {};
    obj[mySymbol] = 'Hello Symbol';
    console.log(obj[mySymbol]); // 输出: Hello Symbol
    
  • 避免属性名冲突

    const firstName = Symbol('firstName');
    const lastName = Symbol('lastName');
    
    const person = {
      [firstName]: 'John',
      [lastName]: 'Doe'
    };
    
    console.log(person[firstName]); // 输出: John
    console.log(person[lastName]); // 输出: Doe
    

注意:使用Symbol作为属性名时,无法通过点符号 . 来访问属性,因为Symbol属性名是独一无二的,不属于对象的可枚举属性。可以使用方括号 [] 语法来访问、设置和删除Symbol属性。

const symbolKey = Symbol('symbolKey');
// 第一种写法
const obj = {};
obj[symbolKey] = 'Hello Symbol';

// 第二种写法
// const obj = {
//   [symbolKey] = 'Hello Symbol';
// };

// 第三种写法
// const obj = {};
// Object.defineProperty(obj, symbolKey, {value: "Hello Symbol"});

// 不能使用点符号访问Symbol属性
console.log(obj.symbolKey); // 输出: undefined

// 使用方括号语法访问、设置和删除Symbol属性
console.log(obj[symbolKey]); // 输出: Hello Symbol
delete obj[symbolKey];
console.log(obj[symbolKey]); // 输出: undefined

10.3 作为对象的 key

使用 Symbol 作为属性名时,该属性不会出现在 for...inObject.keys()、Object.getOwnPropertyNames()JSON.stringify() 等操作中。只能通过 Object.getOwnPropertySymbols() 方法获得。

const s = Symbol('mySymbol');
const obj = {
  name: 'John',
  [s]: 123,
};

console.log(obj); // 输出: {name: 'John', Symbol(mySymbol): 123}

const symbols = Object.getOwnPropertySymbols(obj);
console.log(symbols); // 输出: [Symbol(mySymbol)]

const value = obj[s];
console.log(value); // 输出: 123

注意:以上三种方法都只能遍历对象自身的属性,而不能遍历从原型链继承的属性。如果要一次遍历对象自身和从原型链继承的所有属性,可以使用 for...in 循环。

10.4 Symbol.for()和Symbol.keyFor()

  • Symbol.for()

    Symbol.for(key) 方法会先在全局 Symbol 注册表中搜索具有给定 keySymbol,如果找到则返回该 Symbol,否则创建一个新的 Symbol 并注册到全局 Symbol 注册表中。返回的 Symbol 在全局注册表中是唯一的,可以被多个对象共享使用。

    const s1 = Symbol.for('mySymbol');
    const s2 = Symbol.for('mySymbol');
    
    console.log(s1 === s2); // 输出: true
    

    在上述示例中,我们调用 Symbol.for() 方法时传入同样key的 'mySymbol',返回的两个 Symbol 值是相同的,因为它们都是在全局注册表中搜索到的同一个 Symbol

  • Symbol.keyFor()

    Symbol.keyFor(sym) 方法接收一个 Symbol 值作为参数,返回其在全局注册表中对应的 key。如果该 Symbol 不在全局注册表中,则返回 undefined

    const s1 = Symbol.for('mySymbol');
    const key1 = Symbol.keyFor(s1);
    console.log(key1); // 输出: 'mySymbol'
    
    const s2 = Symbol('mySymbol');
    const key2 = Symbol.keyFor(s2);
    console.log(key2); // 输出: undefined
    

    在上述示例中,我们首先使用 Symbol.for() 方法创建一个全局 Symbol,并通过 Symbol.keyFor() 方法获取其对应的 key。然后使用普通的 Symbol() 方法创建一个非全局的 Symbol,并尝试使用 Symbol.keyFor() 获取其对应的 key,结果为 undefined

注意:Symbol.for() 创建的 Symbol 会一直存在于全局注册表中,除非手动调用 Symbol.for(key) 方法删除。因此在使用 Symbol.for() 时要注意不要意外创建大量不必要的全局 Symbol,以免占用过多内存。

10.5 内置Symbol值

ES6 中引入了许多内置的 Symbol 值,以提供对语言的扩展和自定义。下面是一些 ES6 内置的 Symbol 值:

内置 Symbol 值 描述
Symbol.iterator 表示对象是可迭代的(iterable),可以使用 for…of 循环进行迭代。
Symbol.asyncIterator 表示对象是一个异步可迭代的对象,可以在 for await…of 循环中进行异步迭代。
Symbol.match 表示对象具有用于匹配的正则表达式方法,如 String.prototype.match()。
Symbol.replace 表示对象具有用于替换的正则表达式方法,如 String.prototype.replace()。
Symbol.search 表示对象具有用于搜索的正则表达式方法,如 String.prototype.search()。
Symbol.split 表示对象具有用于拆分的正则表达式方法,如 String.prototype.split()。
Symbol.hasInstance 表示对象是另一个对象的实例,用于自定义 instanceof 操作符的行为。
Symbol.isConcatSpreadable 表示对象在通过数组的 concat() 方法连接时,是否扁平展开。
Symbol.species 表示对象的构造函数,用于创建派生对象。
Symbol.toPrimitive 表示对象如何被转换为原始值。
Symbol.unscopables 一个对象,其中的属性名称对应的值为布尔类型,用于指定哪些属性在使用 with 语句时应该被排除。
Symbol.toStringTag 表示对象的默认字符串描述,可以通过重写 Object.prototype.toString() 方法来自定义。
Symbol.isPrototypeOf 用于检查一个对象是否存在于另一个对象的原型链上。
Symbol.getOwnPropertySymbols 返回一个包含对象自身的所有 Symbol 属性的数组。
Symbol.for 将一个字符串转换为全局 Symbol,并在全局 Symbol 注册表中搜索或创建该 Symbol。

下面是一些常见的 ES6 内置的 Symbol 值及其示例:

// 1.Symbol.iterator
const myArray = [1, 2, 3];
const iterator = myArray[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 }

// 2.Symbol.match
class MyMatcher {
  constructor(value) {
    this.value = value;
  }
  [Symbol.match](string) {
    return string.indexOf(this.value) >= 0 ? true : false;
  }
}

console.log('Hello world'.match(new MyMatcher('Hello'))); // true
console.log('Hello world'.match(new MyMatcher('Goodbye'))); // false

// 3.Symbol.species
class MyArray extends Array {
  static get [Symbol.species]() { return Array; }
}

const myArray = new MyArray(1, 2, 3);
const mappedArray = myArray.map(x => x * 2);

console.log(mappedArray instanceof MyArray); // false
console.log(mappedArray instanceof Array); // true

// 4.Symbol.toStringTag
class MyClass {
  get [Symbol.toStringTag]() { return 'MyClass'; }
}

const myInstance = new MyClass();
console.log(myInstance.toString()); // "[object MyClass]"

11-Proxy和Reflect

ES6 中引入了两个新的内置对象:ProxyReflect。这两个对象提供了对对象的代理和反射操作的能力,可以用于拦截、定制和增强对象的行为。

11.1 Proxy

Proxy 对象用于创建一个代理对象,可以拦截并定制对象上的各种操作。它接收两个参数:目标对象(被代理的对象)和处理程序(一个包含拦截方法的对象)。拦截方法允许我们在目标对象上进行各种操作的拦截和自定义。

const target = {
  name: 'Alice',
  age: 25
};

const handler = {
  get(target, property) {
    console.log(`Accessing property: ${property}`);
    return target[property];
  },
  set(target, property, value) {
    console.log(`Setting property: ${property} to ${value}`);
    target[property] = value;
  }
};

const proxy = new Proxy(target, handler);

console.log(proxy.name); // Accessing property: name, Alice
proxy.age = 30; // Setting property: age to 30
console.log(proxy.age); // Accessing property: age, 30

11.1.1 实例方法

  • get(target, propKey, receiver)

    用于拦截对目标对象属性的读取操作。拦截器方法 get 会在读取目标对象的属性时被调用,可以对读取操作进行拦截和自定义处理。它接收三个参数,分别是目标对象、属性键和代理对象(或继承代理对象的对象),并返回对应的属性值。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      get(target, propKey, receiver) {
    	console.log(`Accessing property: ${propKey}`);
    	return target[propKey];
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    console.log(proxy.name); // Accessing property: name, Alice
    console.log(proxy.age); // Accessing property: age, 25
    

    在上面的示例中,handler 对象的 get 方法被调用了两次,分别对 nameage 属性的读取进行拦截。在拦截器方法内部,可以根据需要进行一些自定义操作。

    注意:如果目标对象的属性值是一个对象,那么该对象同样会被代理,也就是说 receiver 参数会传递到下一级的代理中。这样可以形成一个代理链,对多层嵌套的对象都进行拦截和处理。

    const target = {
      name: 'Alice',
      address: {
    	city: 'New York',
    	country: 'USA'
      }
    };
    
    const handler = {
      get(target, propKey, receiver) {
    	// 实现私有属性读取保护
        if(propKey[0] === '_'){
          throw new Erro(`Invalid attempt to get private "${propKey}"`);
        }
    	console.log(`Accessing property: ${propKey}`);
    	return target[propKey];
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    console.log(proxy.name); // Accessing property: name, Alice
    console.log(proxy.address.city); // Accessing property: address, Accessing property: city, New York
    console.log(proxy.address.country); // Accessing property: address, Accessing property: country, USA
    

    在上面的示例中,handler 对象的 get 方法被调用了四次,分别对 nameaddresscitycountry 属性的读取进行拦截,最后返回对应的属性值。

  • set(target, propKey, value, receiver)

    用于拦截对目标对象属性的赋值操作。拦截器方法 set 会在对目标对象的属性进行赋值时被调用,可以对赋值操作进行拦截和自定义处理。它接收四个参数,分别是目标对象、属性键、属性值和代理对象(或继承代理对象的对象),并返回一个布尔值表示赋值是否成功。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      set(target, propKey, value, receiver) {
    	console.log(`Setting property: ${propKey}=${value}`);
    	if (propKey === 'age') {
          if (!Number.isInteger(value)) {
            throw new TypeError('The age is not an integer!');
          }
          if (value > 200) {
            throw new RangeError('The age seems invalid!');
          }
        }
    	// 对于满足条件的 age 属性以及其他属性,直接保存
    	return target[propKey] = value;
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    proxy.name = 'Bob'; // Setting property: name=Bob
    console.log(proxy.name); // Bob
    

    在上面的示例中,handler 对象的 set 方法被调用了一次,对 name 属性的赋值进行拦截。在拦截器方法内部,可以根据需要进行一些自定义操作。

    注意:如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不会起作用。

  • has(target, propKey)

    用于拦截对目标对象是否具有某个属性的判断操作。拦截器方法 has 会在判断目标对象是否具有某个属性时被调用,可以对判断操作进行拦截和自定义处理。它接收两个参数,分别是目标对象和属性键,并返回一个布尔值表示目标对象是否具有该属性。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      has(target, propKey) {
    	console.log(`Checking property existence: ${propKey}`);
    	return propKey in target;
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    console.log('name' in proxy); // Checking property existence: name, true
    console.log('address' in proxy); // Checking property existence: address, false
    

    在上面的示例中,handler 对象的 has() 方法被调用了两次,分别对 nameaddress 属性的存在性进行判断。

  • enumerate(target)

    enumerate 方法用来拦截 for...in 循环。

    const handler = {
      enumerate (target) {
    	return Object.keys(target).filter(key => key[0] !== '_')[Symbol.iterator]();
      }
    }
    const target = { prop: 'foo', _bar: 'baz', _prop: 'foo' }
    const proxy = new Proxy(target, handler)
    for (let key in proxy) {
      console.log(key);
      // "prop"
    }
    

    上面代码中,enumerate 方法取出原对象的所有属性名,将其中第一个字符等于下划线的都过滤掉,然后返回这些符合条件的属性名的一个遍历器对象,供 for...in 循环消费。

    注意:enumerate()Proxy对象的has方法区分,后者用来拦截in操作符,对for...in循环无效。

  • ownKeys(target)

    用来拦截获取对象自身属性键的操作。它会在以下情况下被调用:

    1. 当使用 Object.getOwnPropertyNames() 方法获取对象自身的属性键时;
    2. 当使用 Object.getOwnPropertySymbols() 方法获取对象自身的 Symbol 属性键时;
    3. 当使用 Reflect.ownKeys() 方法获取对象自身的所有属性键时。

    ownKeys 方法接受一个参数,即目标对象(被代理的对象),并应该返回一个数组或类数组对象,包含目标对象自身属性的键。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      ownKeys(target) {
    	console.log('Intercepting ownKeys');
    	return ['age']; // 控制返回的属性键
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    console.log(Object.getOwnPropertyNames(proxy)); // 输出:["age"]
    console.log(Object.getOwnPropertySymbols(proxy)); // 输出:[]
    console.log(Reflect.ownKeys(proxy)); // 输出:["age"]
    

    在上面的示例中,我们通过 ownKeys 方法拦截获取对象自身属性键的操作。在拦截器方法内部,返回了一个固定的属性键数组 ['age']。这样就控制了返回的属性键。

    注意:ownKeys 只能拦截对对象自身属性键的获取操作,无法拦截对原型链上的属性键的获取。如果你想拦截对所有属性键的获取操作,包括原型链上的属性键,可以考虑使用 getOwnPropertyNames 方法结合 Reflect.ownKeys() 方法来实现。

  • deleteProperty(target, propKey)

    用来拦截对象属性的删除操作。它会在以下情况下被调用:

    1. 当使用 delete 操作符删除对象属性时;
    2. 当使用 Reflect.deleteProperty() 方法删除对象属性时。

    deleteProperty 方法接受两个参数,第一个参数是目标对象(被代理的对象),第二个参数是被删除的属性键。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      deleteProperty(target, propKey) {
    	console.log(`Intercepting deleteProperty('${propKey}')`);
    	if (propKey === 'name') {
    	  console.log('Cannot delete name property');
    	  return false; // 阻止删除 name 属性
    	}
    	return true;
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    // 使用delete操作符删除name属性
    console.log(delete proxy.name); // 输出:Cannot delete name property, false
    console.log(proxy); // 输出:{ name: 'Alice', age: 25 }
    
    // 使用delete操作符删除age属性
    console.log(delete proxy.age); // 输出:true
    console.log(proxy); // 输出:{ name: 'Alice' }
    

    在上面的示例中,我们通过 deleteProperty 方法拦截对象属性的删除操作。在拦截器方法内部,判断如果要删除的属性键是 name,则阻止删除并返回 false。其他情况下,直接返回 true 表示允许删除属性。

    注意:无法通过 deleteProperty 方法拦截使用 Object.defineProperty() 方法定义的访问器属性的删除操作。这是因为访问器属性实际上并不是对象的属性,而是通过 gettersetter 函数来访问和设置的。如果要拦截访问器属性的删除操作,可以考虑使用 defineProperty 方法结合 getOwnPropertyDescriptorReflect.deleteProperty() 方法来实现。

  • defineProperty(target, propKey, propDesc)

    用来拦截对象属性的定义操作。它会在以下情况下被调用:

    1. 当使用 Object.defineProperty() 方法定义对象属性时;
    2. 当使用 Reflect.defineProperty() 方法定义对象属性时。

    defineProperty 方法接受三个参数,第一个参数是目标对象(被代理的对象),第二个参数是被定义的属性键,第三个参数是被定义的属性描述符。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      defineProperty(target, propKey, propDesc) {
    	console.log(`Intercepting defineProperty('${propKey}')`);
    	console.log('Property descriptor:', propDesc);
    	return true;
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    Object.defineProperty(proxy, 'gender', {
      value: 'female',
      writable: false,
      enumerable: true,
      configurable: true
    });
    
    console.log(proxy.gender); // 输出:female
    proxy.gender = 'male'; // 抛出 TypeError: Cannot assign to read only property 'gender' of object '#<Object>'
    console.log(proxy); // 输出:{ name: 'Alice', age: 25, gender: 'female' }
    

    在上面的示例中,我们通过 defineProperty 方法拦截对象属性的定义操作。defineProperty 返回 false 会导致添加新属性抛出错误。

    注意:无法通过 defineProperty 方法拦截使用 Object.defineProperties() 方法同时定义多个对象属性的操作。如果要拦截这种情况,可以考虑使用 defineProperty 方法结合 getOwnPropertyDescriptorReflect.defineProperty() 方法来实现。

  • defineProperties(proxy, propDesc)

    用来拦截对象属性的批量定义操作。它会在以下情况下被调用:

    1. 当使用 Object.defineProperties() 方法定义多个对象属性时;
    2. 当使用 Reflect.defineProperties() 方法定义多个对象属性时。

    defineProperties 方法接受两个参数,第一个参数是目标对象(被代理的对象),第二个参数是一个对象,其中包含了要定义的多个属性。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      defineProperties(target, propDesc) {
    	console.log('Intercepting defineProperties');
    	console.log('Property descriptors:', propDesc);
    	return true;
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    Object.defineProperties(proxy, {
      gender: {
    	value: 'female',
    	writable: false,
    	enumerable: true,
    	configurable: true
      },
      address: {
    	value: '123 Main St.',
    	writable: true,
    	enumerable: true,
    	configurable: false
      }
    });
    
    console.log(proxy.gender); // 输出:female
    proxy.gender = 'male'; // 抛出 TypeError: Cannot assign to read only property 'gender' of object '#<Object>'
    console.log(proxy); // 输出:{ name: 'Alice', age: 25, gender: 'female', address: '123 Main St.' }
    

    在上面的示例中,我们通过 defineProperties 方法拦截对象属性的批量定义操作。在拦截器方法内部,我们可以自定义处理逻辑。

  • getOwnPropertyDescriptor(target, propKey)

    用于拦截获取对象属性描述符的操作。它会在以下情况下被调用:

    1. 当使用 Object.getOwnPropertyDescriptor() 方法获取对象属性描述符时;
    2. 当使用 Reflect.getOwnPropertyDescriptor() 方法获取对象属性描述符时。

    getOwnPropertyDescriptor 方法接受两个参数,第一个参数是目标对象(被代理的对象),第二个参数是要获取属性描述符的属性键。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      getOwnPropertyDescriptor(target, propKey) {
    	console.log(`Intercepting getOwnPropertyDescriptor('${propKey}')`);
    	return Reflect.getOwnPropertyDescriptor(target, propKey);
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    const descriptor = Object.getOwnPropertyDescriptor(proxy, 'name');
    console.log(descriptor);
    

    在上面的示例中,我们通过 getOwnPropertyDescriptor 方法拦截获取对象属性描述符的操作。在拦截器方法内部,输出被获取的属性键,并调用 Reflect.getOwnPropertyDescriptor(target, propKey) 方法实现实际的获取操作。

    注意:
    1.拦截器方法应该返回一个属性描述符对象或者 undefined。如果返回 undefined,则表示目标对象不存在指定属性,或者属性不可枚举。如果返回一个属性描述符对象,它必须包含 valuewritableenumerableconfigurable 属性。
    2.无法通过 getOwnPropertyDescriptor 方法拦截使用 Object.getOwnPropertyDescriptors() 方法获取多个属性描述符的操作。如果要拦截这种情况,可以考虑使用 getOwnPropertyDescriptor 方法结合 definePropertyReflect.getOwnPropertyDescriptor() 方法来实现。

  • getPrototypeOf(target)

    用于拦截获取对象原型的操作。它会在以下情况下被调用:

    1. 当使用 Object.getPrototypeOf() 方法获取对象原型时;
    2. 当使用 Reflect.getPrototypeOf() 方法获取对象原型时。

    getPrototypeOf 方法接受两个参数,第一个参数是目标对象(被代理的对象)。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      getPrototypeOf(target) {
    	console.log('Intercepting getPrototypeOf');
    	return Reflect.getPrototypeOf(target);
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    const prototype = Object.getPrototypeOf(proxy);
    console.log(prototype);
    

    注意:
    1.拦截器方法应该返回一个属性描述符对象或者 undefined。如果返回 undefined,则表示目标对象不存在指定属性,或者属性不可枚举。如果返回一个属性描述符对象,它必须包含 valuewritableenumerableconfigurable 属性。
    2.无法通过 getPrototypeOf 方法拦截使用 Object.prototype.__proto__Object.prototype.isPrototypeOf()instanceof 运算符等方式来获取对象原型的操作。如果要拦截这种情况,可以考虑使用 getPrototypeOf 方法结合 setPrototypeOfReflect.getPrototypeOf() 方法来实现。

  • isExtensible(target)

    用于拦截判断目标对象是否可扩展的操作。它会在以下情况下被调用:

    1. 当使用 Object.isExtensible() 判断目标对象是否可扩展时;
    2. 当调用 Object.preventExtensions() 方法阻止目标对象扩展时。

    isExtensible(target) 方法接受一个参数,即目标对象(被代理的对象),它会返回一个布尔值,表示目标对象是否可扩展。如果返回 false,则表示目标对象不能再添加新的属性或方法。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      isExtensible(target) {
    	console.log('Intercepting isExtensible');
    	return true;
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    console.log(Object.isExtensible(proxy)); // 输出:true
    
    Object.preventExtensions(proxy);
    
    console.log(Object.isExtensible(proxy)); // 输出:false
    

    注意:
    1.拦截器方法应该返回一个布尔值。如果返回 false,则会阻止目标对象扩展;如果返回 true,则不会阻止目标对象扩展。
    2.无法通过 isExtensible 方法拦截使用 Object.freeze()Object.seal()Object.defineProperty()Object.defineProperties()Reflect.defineProperty()Reflect.defineProperties()Reflect.preventExtensions() 等方法来影响目标对象是否可扩展的操作。如果要拦截这些操作,可以考虑使用 preventExtensionsdefineProperty 方法组合实现。

  • preventExtensions(target)

    用于拦截阻止目标对象扩展的操作。当使用 Object.preventExtensions() 阻止目标对象扩展时被调用。preventExtensions(target) 方法接受一个参数,即目标对象(被代理的对象),它会返回一个布尔值,表示是否成功阻止目标对象扩展。如果返回 false,则表示目标对象已经被阻止扩展了。

    const target = {
      name: 'Alice',
      age: 25
    };
    
    const handler = {
      preventExtensions(target) {
    	console.log('Intercepting preventExtensions');
    	Object.freeze(target);
    	return true;
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    console.log(Object.isExtensible(proxy)); // 输出:true
    
    Object.preventExtensions(proxy);
    
    console.log(Object.isExtensible(proxy)); // 输出:false
    
    proxy.gender = 'female'; // 抛出 TypeError 异常
    
    console.log(proxy.gender); // 输出:undefined
    

    注意:
    1.拦截器方法应该返回一个布尔值。如果返回 false,则表示阻止扩展失败,即目标对象已经被阻止扩展了;如果返回 true,则表示阻止扩展成功。
    2.无法通过 preventExtensions 方法拦截使用 Object.seal()Object.defineProperty()Object.defineProperties()Reflect.defineProperty()Reflect.defineProperties()Reflect.preventExtensions() 等方法来影响目标对象是否可扩展的操作。如果要拦截这些操作,可以考虑使用 definePropertyisExtensible 方法组合实现。

  • setPrototypeOf

    用于拦截设置目标对象原型的操作。当使用 Object.setPrototypeOf() 设置目标对象原型时被调用。setPrototypeOf(target, prototype) 方法接受两个参数:目标对象(被代理的对象)和原型对象,它会返回一个布尔值,表示是否成功设置目标对象的原型。如果返回 false,则表示设置原型失败,目标对象的原型不会被修改。

    const target = {};
    
    const handler = {
      setPrototypeOf(target, prototype) {
    	console.log('Intercepting setPrototypeOf');
    	return Reflect.setPrototypeOf(target, prototype);
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    const prototype = {
      sayHello() {
    	console.log('Hello');
      }
    };
    
    Object.setPrototypeOf(proxy, prototype);
    
    proxy.sayHello(); // 输出:Hello
    

    在上面的示例中,我们通过 setPrototypeOf 方法拦截了设置目标对象原型的操作。在拦截器方法内部,输出拦截行为,并调用 Reflect.setPrototypeOf() 方法实现实际的操作。

    注意:
    1.拦截器方法应该返回一个布尔值。如果返回 false,则表示设置原型失败,即目标对象的原型不会被修改;如果返回 true,则表示设置原型成功。
    2.无法通过 setPrototypeOf 方法拦截使用 Object.create()Object.assign()Reflect.construct() 等方法来影响目标对象原型的操作。如果要拦截这些操作,可以考虑使用 getPrototypeOfsetPrototypeOf 方法组合实现。

  • apply(target, ctx, args)

    用于拦截函数的调用操作。当调用目标对象作为函数时被调用。apply(target, ctx, args) 方法接受三个参数:目标对象(被代理的函数)、函数调用时的上下文对象(即 this 值)和一个数组参数(即函数调用时传递的参数列表)。它可以用来拦截函数的调用,并在调用前后执行自定义的逻辑。

    const target = function (name) {
      console.log(`Hello, ${name}!`);
    };
    
    const handler = {
      apply(target, ctx, args) {
    	console.log('Intercepting apply');
    	console.log(`Arguments: ${args.join(', ')}`);
    	return Reflect.apply(target, ctx, args);
      }
    };
    
    const proxy = new Proxy(target, handler);
    
    proxy('Alice'); // 输出:Hello, Alice!
    

    注意:
    1.拦截器方法应该返回函数调用的结果。可以使用 Reflect.apply() 方法将函数调用操作委托给目标对象进行实际的调用。
    2.需要注意的是,apply 方法只能拦截函数的调用操作,对于作为构造函数使用的函数调用,应该使用 construct 方法进行拦截。

  • construct(target, args)

    用于拦截对目标对象的构造函数调用操作。当使用 new 关键字调用目标对象时,construct 方法会被触发。construct(target, args) 方法接受两个参数:目标对象(被代理的构造函数)和一个数组参数(即 new 调用时传递的参数列表)。它可以用来拦截构造函数的调用,并在构造函数调用前后执行自定义的逻辑。

    class Person {
      constructor(name) {
    	this.name = name;
      }
    }
    
    const handler = {
      construct(target, args) {
    	console.log('Intercepting construct');
    	console.log(`Arguments: ${args.join(', ')}`);
    	return Reflect.construct(target, args);
      }
    };
    
    const proxy = new Proxy(Person, handler);
    
    const person = new proxy('Alice'); // 输出:Intercepting construct
    								   //       Arguments: Alice
    
    console.log(person.name); // 输出:Alice
    

    在上面的示例中,我们通过 construct 方法拦截了对构造函数的调用操作。在拦截器方法内部,我们可以自定义处理逻辑,这里只是简单地输出拦截行为,并调用 Reflect.construct() 方法实现实际的构造函数调用。

    注意:拦截器方法应该返回一个对象,作为构造函数调用的结果。可以使用 Reflect.construct() 方法将构造函数调用操作委托给目标对象进行实际的构造。

11.1.2 Proxy.revocable()

Proxy.revocable() 用于创建一个可撤销的代理对象。它接受两个参数:目标对象(要被代理的对象)和处理器对象(用于定义代理行为的对象)。它返回一个包含 proxyrevoke 两个属性的对象,其中 proxy 是一个代理对象,revoke 是一个函数,用于撤销该代理对象。

const target = {
  name: 'Alice'
};

const handler = {};

const { proxy, revoke } = Proxy.revocable(target, handler);

console.log(proxy.name); // 输出:Alice

revoke();

console.log(proxy.name); // 抛出 TypeError: Cannot perform 'get' on a proxy that has been revoked

在上面的示例中,我们使用 Proxy.revocable() 创建了一个可撤销的代理对象。通过解构赋值得到的 proxy 是一个代理对象,可以像普通对象一样访问其属性。而 revoke 是一个函数,调用它可以撤销代理对象。

注意,在调用 revoke() 之后,再对代理对象进行任何操作都会抛出错误。撤销后的代理对象不能再被使用。

11.2 Reflect

Reflect 对象提供了一组静态方法,用于执行与对象相关的操作。它的方法与一些操作符和内置函数是对应的,提供了更统一、易读且可继承的 API。

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const args = ['Alice', 25];

const person = Reflect.construct(Person, args);
console.log(person); // Person { name: 'Alice', age: 25 }

Reflect.set(person, 'age', 30);
console.log(Reflect.get(person, 'age')); // 30

下面是 Reflect 对象的一些常用静态方法:

  • Reflect.get(target, property, [receiver]): 获取目标对象的属性值。
  • Reflect.set(target, property, value, [receiver]): 设置目标对象的属性值。
  • Reflect.has(target, property): 检查目标对象是否具有指定的属性(是 key in obj 指令的函数化。)。
  • Reflect.deleteProperty(target, property): 删除目标对象的属性。
  • Reflect.construct(target, argumentsList, [newTarget]): 调用目标对象作为构造函数创建一个新实例。
  • Reflect.apply(target, thisArgument, argumentsList): 调用目标对象的方法,并指定方法的上下文和参数。
  • Reflect.defineProperty(target, property, attributes): 定义目标对象的属性。
  • Reflect.getOwnPropertyDescriptor(target, property): 获取目标对象的属性描述符。
  • Reflect.isExtensible(target): 检查目标对象是否可扩展。
  • Reflect.preventExtensions(target): 阻止目标对象进一步扩展。
  • Reflect.ownKeys(target): 获取目标对象所有自身的属性键,包括字符串键和符号键。

这些静态方法可以替代传统的操作符、Object 对象的方法以及一些 ProxyObject.defineProperty 的使用场景,提供了更加统一和一致的接口。它们通常返回布尔值或执行结果,并且在操作失败时会抛出错误。

// Reflect.set
const obj = {};
Reflect.set(obj, 'foo', 'bar');
console.log(obj.foo); // 输出:bar

const obj2 = {};
Reflect.set(obj2, 'foo', 'bar');
console.log(obj2.foo); // 输出:bar2

// Reflect.has
const obj3 = { foo: 'bar' };
const hasProperty = Reflect.has(obj3, 'foo');
console.log(hasProperty); // 输出:true

// Reflect.deleteProperty
const obj4 = { foo: 'bar' };
Reflect.deleteProperty(obj4, 'foo');
console.log(obj4.foo); // 输出:undefined

// Reflect.construct
class Person {
  constructor(name) {
    this.name = name;
  }
}

const args = ['Alice'];
const instance = Reflect.construct(Person, args);
console.log(instance.name); // 输出:Alice

// Reflect.apply
const obj5 = {
  greet(greeting) {
    console.log(greeting);
  }
};

Reflect.apply(obj5.greet, obj, ['Hello']); // 输出:Hello

ReflectProxy经常被拿来组合使用,Reflect 对象的方法和 Proxy 对象的方法是一一相对应的,所以 Proxy 对象的方法可以通过调用 Reflect 对象的方法获取默认行为,然后进行额外操作。

const cache = new Map();

const proxy = new Proxy({}, {
  get(target, property) {
    if (cache.has(property)) {
      console.log('从缓存中获取数据');
      return cache.get(property);
    } else {
      console.log('访问实际对象并将数据加入缓存');
      const value = Reflect.get(target, property);
      cache.set(property, value);
      return value;
    }
  },
  set(target, property, value) {
    console.log('设置实际对象的属性值,并更新缓存');
    Reflect.set(target, property, value);
    cache.set(property, value);
  },
  deleteProperty(target, property) {
    console.log('删除实际对象的属性,并从缓存中移除');
    Reflect.deleteProperty(target, property);
    cache.delete(property);
  }
});

proxy.name = 'Alice'; // 设置实际对象的属性值,并更新缓存
console.log(proxy.name); // 访问实际对象并将数据加入缓存,输出:Alice
console.log(proxy.name); // 从缓存中获取数据,输出:Alice

proxy.age = 25; // 设置实际对象的属性值,并更新缓存
console.log(proxy.age); // 访问实际对象并将数据加入缓存,输出:25
console.log(proxy.age); // 从缓存中获取数据,输出:25

delete proxy.name; // 删除实际对象的属性,并从缓存中移除
console.log(proxy.name); // 访问实际对象并将数据加入缓存,输出:undefined
console.log(proxy.name); // 从缓存中获取数据,输出:undefined

在上述示例中,我们创建了一个代理对象 proxy,它拦截了对属性的访问、设置和删除操作。当需要获取属性值时,首先检查缓存中是否存在该属性的值,如果存在,则直接返回缓存中的值;如果不存在,则从实际对象中获取属性值,并将其加入缓存。而当设置或删除属性时,不仅会更新实际对象的属性值,还会更新缓存。

通过使用 Reflect 方法,我们可以在代理对象的陷阱中执行相应的操作,并利用 Map 来作为缓存容器。这样,在多次访问同一个属性时,可以从缓存中获取值,避免了每次都访问实际对象的开销。

12-Set和Map

ES6 中新增了两个数据结构,分别是 SetMap。它们都是类似于数组和对象的数据结构,但有着不同的特点和用途。

12.1 Set

Set 是一种无序且唯一的集合,其中每个元素只出现一次。

12.1.1 基本用法

可以用 new Set() 来创建一个空的 Set,也可以传入一个数组来初始化。

const set = new Set([1, 2, 3]);
console.log(set); // 输出:Set(3) {1, 2, 3}

12.1.2 常用属性和方法

  • 属性:

    Set.prototype.size: 返回 Set 集合中的元素个数。

  • 方法:

    • Set.prototype.add(value): 向 Set 集合中添加一个元素,并返回该 Set 集合。
    • Set.prototype.delete(value): 从 Set 集合中删除指定的元素,并返回一个布尔值,表示是否成功删除。
    • Set.prototype.has(value): 判断 Set 集合中是否存在指定元素,返回一个布尔值。
    • Set.prototype.clear(): 清空 Set 集合中的所有元素。
    • Set.prototype.forEach(callbackFn[, thisArg]): 使用指定的回调函数遍历 Set 集合中的每个元素。
    • Set.prototype.values(): 返回一个新的迭代器对象,按插入顺序包含 Set 集合中的所有元素的值。
    • Set.prototype.keys(): 返回一个新的迭代器对象,按插入顺序包含 Set 集合中的所有元素的键。
    • Set.prototype.entries(): 返回一个新的迭代器对象,按插入顺序包含 Set 集合中的所有元素的键值对。

下面是 Set 常用方法的示例代码:

const set = new Set();

set.add('apple');
set.add('banana');
set.add('orange');

console.log(set.size); // 输出:3

console.log(set.has('apple')); // 输出:true
console.log(set.has('grape')); // 输出:false

set.delete('banana');
console.log(set.size); // 输出:2

set.forEach((value) => {
  console.log(value);
});
// 输出:
// apple
// orange

for (let value of set.values()) {
  console.log(value);
}
// 输出:
// apple
// orange

for (let entry of set.entries()) {
  console.log(entry);
}
// 输出:
// ["apple", "apple"]
// ["orange", "orange"]

set.clear();
console.log(set.size); // 输出:0

注意:Set 集合中的元素是按照插入顺序进行存储的,并且 Set 集合中的元素是唯一的,不会存在重复的元素。在使用 Set 时,可以通过上述属性和方法来操作集合中的元素,实现对唯一值的快速查找、添加、删除等操作。

12.1.3 遍历Set

遍历 Set 可以使用 Set.prototype.forEach() 方法、for...of 循环或者迭代器方法 Set.prototype.values()Set.prototype.keys()Set.prototype.entries()

const set = new Set([1, 2, 3]);

// 使用 forEach() 方法遍历 Set
set.forEach((value) => {
  console.log(value);
});
// 输出:
// 1
// 2
// 3

// 使用 for...of 循环遍历 Set
for (let value of set) {
  console.log(value);
}
// 输出:
// 1
// 2
// 3

// 使用迭代器方法 values() 遍历 Set
for (let value of set.values()) {
  console.log(value);
}
// 输出:
// 1
// 2
// 3

// 使用迭代器方法 keys() 遍历 Set
for (let key of set.keys()) {
  console.log(key);
}
// 输出:
// 1
// 2
// 3

// 使用迭代器方法 entries() 遍历 Set
for (let entry of set.entries()) {
  console.log(entry);
}
// 输出:
// [1, 1]
// [2, 2]
// [3, 3]

12.1.4 Set对象的作用

  1. 去重

    const arr = [1, 2, 3, 4, 2, 3, 1];
    const uniqueArr = [...new Set(arr)];
    console.log(uniqueArr); // 输出: [1, 2, 3, 4]
    
  2. 并集

    const set1 = new Set([1, 2, 3]);
    const set2 = new Set([3, 4, 5]);
    
    const unionSet = new Set([...set1, ...set2]);
    console.log([...unionSet]); // 输出: [1, 2, 3, 4, 5]
    
  3. 交集

    const set1 = new Set([1, 2, 3]);
    const set2 = new Set([2, 3, 4]);
    
    const intersectionSet = new Set([...set1].filter(x => set2.has(x)));
    console.log([...intersectionSet]); // 输出: [2, 3]
    
  4. 差集

    const set1 = new Set([1, 2, 3]);
    const set2 = new Set([2, 3, 4]);
    
    const differenceSet = new Set([...set1].filter(x => !set2.has(x)));
    console.log([...differenceSet]); // 输出: [1]
    

12.1.5 Set与其他数据类型的转换

可以使用一些方法将 Set 与其他数据类型进行转换。以下是一些常见的转换方式:

  1. Set 转换为数组:可以使用扩展运算符 ...Array.from() 方法将 Set 转换为数组。

    const set = new Set([1, 2, 3]);
    const array1 = [...set];
    console.log(array1); // 输出: [1, 2, 3]
    
    const array2 = Array.from(set);
    console.log(array2); // 输出: [1, 2, 3]
    
  2. 数组转换为 Set:可以通过将数组作为参数传递给 Set 构造函数来将数组转换为 Set

    const array = [1, 2, 3];
    const set = new Set(array);
    console.log(set); // 输出: Set { 1, 2, 3 }
    
  3. Set 转换为字符串:可以先将 Set 转换为数组,然后使用数组的 join() 方法将其转换为字符串。

    const set = new Set([1, 2, 3]);
    const array = [...set];
    const string = array.join(', ');
    console.log(string); // 输出: "1, 2, 3"
    
  4. 字符串转换为 Set:可以将字符串转换为数组,然后使用数组去重的方式创建一个新的 Set

    const string = 'abcabc';
    const set = new Set(string);
    console.log(set); // 输出: Set { 'a', 'b', 'c' }
    

12.1.6 WeakSet

ES6 中的 WeakSet 是一种特殊类型的集合,它只能包含对象,并且对其包含的对象是弱引用。这意味着如果一个对象在其他地方没有被引用,那么它将会被垃圾回收,即使它存在于 WeakSet 中。

WeakSet 具有以下特点:

  • WeakSet 不能包含原始值,只能包含对象。
  • WeakSet 中的对象是弱引用的,垃圾回收机制可以自动回收不再被其他地方引用的对象。
  • WeakSet 是无序的,不支持迭代。
const weakSet = new WeakSet();

// 添加对象到 WeakSet
const obj1 = { name: 'Object 1' };
const obj2 = { name: 'Object 2' };

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

// 检查 WeakSet 是否包含某个对象
console.log(weakSet.has(obj1)); // 输出: true
console.log(weakSet.has(obj2)); // 输出: true

// 从 WeakSet 中移除对象
weakSet.delete(obj1);

// 再次检查对象是否存在于 WeakSet
console.log(weakSet.has(obj1)); // 输出: false

12.2 Map

Map 是一种键值对的集合,其中每个键对应一个值。

12.2.1 基本用法

可以用 new Map() 来创建一个空的 Map,也可以传入一个数组来初始化 Map

const map = new Map([
  ['key1', 'value1'],
  ['key2', 'value2'],
  ['key3', 'value3']
]);
console.log(map); // 输出:Map(3) {"key1" => "value1", "key2" => "value2", "key3" => "value3"}

12.2.2 常用属性和方法

  • 属性:

    size:返回 Map 中键值对的数量。

  • 方法:

    • set(key, value):向 Map 中添加键值对。
    • get(key):根据键获取对应的值。
    • has(key):检查 Map 中是否存在指定的键。
    • delete(key):根据键删除对应的键值对。
    • clear():清空 Map 中的所有键值对。
    • keys():返回一个包含 Map 中所有键的迭代器。
    • values():返回一个包含 Map 中所有值的迭代器。
    • entries():返回一个包含 Map 中所有键值对的迭代器。
    • forEach(callbackFn, thisArg):遍历 Map 中的每个键值对,并执行回调函数。
const map = new Map();

// 添加键值对
map.set('key1', 'value1');
map.set('key2', 'value2');

// 获取值
console.log(map.get('key1')); // 输出: value1

// 检查键是否存在
console.log(map.has('key2')); // 输出: true

// 删除键值对
map.delete('key1');

// 清空 Map
map.clear();

// 遍历 Map
map.set('key1', 'value1');
map.set('key2', 'value2');

map.forEach((value, key) => {
  console.log(key, value);
});

// 获取所有键的迭代器
const keysIterator = map.keys();
for (let key of keysIterator) {
  console.log(key);
}

// 获取所有值的迭代器
const valuesIterator = map.values();
for (let value of valuesIterator) {
  console.log(value);
}

// 获取所有键值对的迭代器
const entriesIterator = map.entries();
for (let entry of entriesIterator) {
  console.log(entry[0], entry[1]);
}

注意:Map 中的键可以是任何类型的值,包括原始类型和对象。在使用时,需要注意键的唯一性。

12.2.3 遍历Map

使用 for...of 循环遍历 Map.entries() 方法返回的迭代器,获取键值对数组并逐一进行处理。

  1. 使用 for...of 循环遍历 Map.entries() 方法返回的迭代器,获取键值对数组并逐一进行处理。

    const map = new Map();
    map.set('key1', 'value1');
    map.set('key2', 'value2');
    
    for (const [key, value] of map.entries()) {
      console.log(`${key} = ${value}`);
    }
    // 输出:
    // key1 = value1
    // key2 = value2
    
  2. 使用 forEach() 方法遍历 Map,该方法接受一个回调函数作为参数,该回调函数接受三个参数:值、键和 Map 对象本身。

    const map = new Map();
    map.set('key1', 'value1');
    map.set('key2', 'value2');
    
    map.forEach((value, key, map) => {
      console.log(`${key} = ${value}`);
    });
    // 输出:
    // key1 = value1
    // key2 = value2
    
  3. 遍历 Map 的 keys()values() 方法返回的迭代器。

    const map = new Map();
    map.set('key1', 'value1');
    map.set('key2', 'value2');
    
    for (const key of map.keys()) {
      console.log(key);
    }
    // 输出:
    // key1
    // key2
    
    for (const value of map.values()) {
      console.log(value);
    }
    // 输出:
    // value1
    // value2
    

12.2.4 Map与其他数据类型的转换

Map 与其他数据类型之间的转换可以使用以下方法:

  1. Map 转数组

    可以使用 Array.from() 方法将 Map 转换为数组。该方法接受一个可迭代对象作为参数,并返回一个新的数组,其中包含可迭代对象中的所有元素。

    const map = new Map();
    map.set('key1', 'value1');
    map.set('key2', 'value2');
    
    const arr = Array.from(map);
    console.log(arr);
    // 输出: [ ['key1', 'value1'], ['key2', 'value2'] ]
    
  2. 数组转 Map

    可以使用 new Map() 构造函数和数组作为参数来创建一个新的 Map 对象。数组中的每个元素都应该是键值对数组,第一个元素表示键,第二个元素表示值。

    const arr = [ ['key1', 'value1'], ['key2', 'value2'] ];
    
    const map = new Map(arr);
    console.log(map);
    // 输出: Map { 'key1' => 'value1', 'key2' => 'value2' }
    
  3. Map 转对象

    可以使用对象解构语法和扩展运算符将 Map 转换为对象。在这种情况下,Map 中的键必须是字符串类型。

    const map = new Map();
    map.set('key1', 'value1');
    map.set('key2', 'value2');
    
    const obj = {};
    for (const [key, value] of map.entries()) {
      obj[key] = value;
    }
    console.log(obj);
    // 输出: { key1: 'value1', key2: 'value2' }
    
  4. 对象转 Map

    可以使用 new Map() 构造函数和对象的键值对数组作为参数来创建一个新的 Map 对象。

    const obj = { key1: 'value1', key2: 'value2' };
    const arr = Object.entries(obj);
    
    const map = new Map(arr);
    console.log(map);
    // 输出: Map { 'key1' => 'value1', 'key2' => 'value2' }
    
  5. Map 转 JSON

    可以通过先将 Map 转换为普通对象,然后使用 JSON.stringify() 方法将对象转换为 JSON 字符串。

    const map = new Map();
    map.set('key1', 'value1');
    map.set('key2', 'value2');
    
    const obj = Object.fromEntries(map);
    const jsonString = JSON.stringify(obj);
    
    console.log(jsonString);
    // 输出: {"key1":"value1","key2":"value2"}
    
  6. JSON 转 Map

    可以使用 JSON.parse() 方法将 JSON 字符串解析为普通对象,然后使用 Object.entries() 方法将对象转换为键值对数组,最后使用 new Map() 构造函数将键值对数组转换为 Map 对象。

    const jsonString = '{"key1":"value1","key2":"value2"}';
    
    const obj = JSON.parse(jsonString);
    const entries = Object.entries(obj);
    const map = new Map(entries);
    
    console.log(map);
    // 输出: Map { 'key1' => 'value1', 'key2' => 'value2' }
    

12.2.5 WeakMap

WeakMap 是 ES6 中新增的数据类型之一,它是 Map 的一种变体,与 Map 类似,也是一个键值对集合,其中键是弱引用的,而值可以是任意类型的 JavaScript 值。

WeakMapMap 的区别在于,WeakMap 中的键必须是对象或继承自对象的类型,而不能是基本数据类型;另外,由于键是弱引用的,当键所引用的对象被垃圾回收时,键值对会自动从 WeakMap 中删除,因此 WeakMap 不支持遍历和清空操作。

const wm = new WeakMap();

const key1 = { id: 1 };
const value1 = { name: 'Alice' };

wm.set(key1, value1);

console.log(wm.get(key1)); // 输出: { name: 'Alice' }

console.log(wm.has(key1)); // 输出: true

wm.delete(key1);

console.log(wm.has(key1)); // 输出: false

注意:由于 WeakMap 中的键是弱引用的,因此不能通过键来判断 WeakMap 中是否存在某个键值对,应该使用 has() 方法来进行判断。另外,在使用 WeakMap 时,需要确保键所引用的对象不会被意外地清除,否则可能会导致程序出现异常。

13-Iterator和for…of

ES6 引入了迭代器(Iterator)和 for…of 循环,用于更方便地遍历数据集合。

13.1 Iterator接口

Iterator 接口包括一个名为 next() 的方法,它返回一个包含 valuedone 两个属性的对象。其中,value 表示当前迭代的值,done 表示是否已经遍历到最后一个值。

Iterator 接口的定义如下:

interface Iterator {
  next(): IteratorResult;
}

IteratorResult 是一个包含 value 和 done 两个属性的对象:

interface IteratorResult {
  value: any;
  done: boolean;
}

ES6 规定,为了让一个对象成为可迭代对象,需要在对象上实现 @@iterator 方法,该方法返回一个迭代器对象。可以使用 Symbol.iterator 常量来定义该方法。

13.1.1 迭代器

迭代器(Iterator)是指实现了 next() 方法的对象,每次调用 next() 方法都会返回一个包含 valuedone 属性的对象。其中,value 表示当前迭代到的值,done 表示迭代是否结束。

function makeIterator(arr) {
  let index = 0;
  return {
    next: function() {
      if (index < arr.length) {
        return { value: arr[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const iterator = makeIterator([1, 2, 3]);
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 }

在上面的例子中,我们定义了一个迭代器 makeIterator(),它接受一个数组作为参数,并返回一个迭代器对象。该迭代器对象包含一个 next() 方法,用于遍历数组中的元素。

在调用 next() 方法时,如果还有元素没有遍历,则返回一个包含当前元素值和 done 属性为 false 的对象;如果已经遍历完所有元素,则返回一个包含 valueundefineddonetrue 的对象。

13.1.2 可迭代对象

可迭代对象是指实现 Iterator 接口的 [Symbol.iterator]() 方法的对象,该方法返回一个迭代器对象。当使用 for...of 循环或扩展运算符来遍历可迭代对象时,会自动调用该方法获取迭代器对象,然后使用迭代器对象来遍历对象中的元素。

可迭代对象可以是数组、字符串、Set、Map 等内置的 JavaScript 对象,也可以是用户自定义的对象。

const iterable = {
  values: [1, 2, 3],
  [Symbol.iterator]: function() { // [Symbol.iterator]() {...}
    let index = 0;
    const values = this.values;
    return {
      next: function() {
        if (index < values.length) {
          return { value: values[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (const value of iterable) {
  console.log(value);
}

在上面的例子中,我们定义了一个可迭代对象 iterable,它包含一个数组 values 和一个 [Symbol.iterator]() 方法。在该方法中返回一个迭代器对象,用于遍历数组中的元素。

for...of 循环中,我们可以直接使用 iterable 来遍历对象中的元素。在执行循环时,会自动调用 iterable[Symbol.iterator]() 方法来获取迭代器对象,然后使用迭代器对象来遍历元素。

迭代器接口提供了一种统一的方式来遍历数据集合,使得代码更加简洁和可读。同时,由于可迭代对象和迭代器对象都是基于约定而非继承的机制实现的,因此它们可以适用于任意类型的数据集合,使得 JavaScript 更加灵活和高效。

13.1.3 调用迭代器接口的场合

ES6 中的 Iterator 接口可以在以下场合中使用:

  1. for…of 循环:for...of 循环是遍历可迭代对象的常用方式,它会自动调用可迭代对象的迭代器接口来遍历对象的元素。

    const iterable = [1, 2, 3];
    
    for (const value of iterable) {
      console.log(value);
    }
    // 输出: 1
    // 输出: 2
    // 输出: 3
    

    在这个例子中,通过使用 for...of 循环遍历了数组 iterable 中的每个元素,实际上会自动调用数组的迭代器接口来完成遍历。

  2. 扩展运算符:扩展运算符可以将可迭代对象展开成单独的元素,同样会自动调用可迭代对象的迭代器接口来获取元素。

    const iterable = [1, 2, 3];
    const newArray = [...iterable];
    console.log(newArray); // 输出: [1, 2, 3]
    

    在这个例子中,使用扩展运算符将数组 iterable 展开成单独的元素,并赋值给新的数组 newArray,这个操作会自动调用数组的迭代器接口来获取元素。

  3. 解构赋值:解构赋值可以从可迭代对象中提取值并赋值给变量,同样也会自动调用可迭代对象的迭代器接口来获取值。

    const iterable = [1, 2, 3];
    const [a, b, c] = iterable;
    console.log(a, b, c); // 输出: 1 2 3
    

    在这个例子中,使用解构赋值从数组 iterable 中提取值并分别赋值给变量 abc,这个操作会自动调用数组的迭代器接口来获取值。

手动调用迭代器对象的 next() 方法来逐个获取可迭代对象的元素。手动调用迭代器对象的 next() 方法来逐个获取数组 iterable 的元素,并输出每次调用 next() 方法后得到的值。

由于数组的遍历也会调用 Iterator 接口,因此接受数组作为参数的一些 JavaScript 内置方法也会调用 Iterator 接口来实现遍历。

以下是一些接受数组参数的场合,它们会自动调用数组的迭代器接口:

  • for…of

  • Array.from()、Array.prototype.forEach()等

  • Map()、Set()、WeakSet()、WeakMap()等

  • Promise.all()

  • Promise.race()

13.2 for…of

for...of 循环是 ES6 中用于遍历可迭代对象的语法结构。它可以遍历具有迭代器的任何可迭代对象,如:数组、字符串、Set、Map 等。

for…of 语法格式如下:

for (variable of iterable) {
  // code block to be executed
}

其中,variable 是一个变量名,用于存储每次循环迭代到的值;iterable 是一个可迭代对象,用于遍历其中的元素。

13.2.1 迭代数组

for...of 可以直接用于迭代数组中的元素。每次循环迭代时,将数组中的元素依次赋值给指定的变量。

const arr = [1, 2, 3];
for (const value of arr) {
  console.log(value);
}
// Output:
// 1
// 2
// 3

13.2.2 迭代 Set 和 Map

在 ES6 中,SetMap 是两种常用的数据结构,它们都是可迭代对象,可以使用 for...of 循环进行迭代遍历。

// 迭代 Set
const mySet = new Set([1, 2, 3]);

for (const value of mySet) {
  console.log(value);
}
// Output:
// 1
// 2
// 3

// 迭代 Map
const myMap = new Map();
myMap.set('name', 'John');
myMap.set('age', 30);

for (const [key, value] of myMap) {
  console.log(key, value);
}
// Output:
// name John
// age 30

13.2.3 迭代类数组对象

  • 迭代 argument 对象

    通过 arguments 对象可以拿到在调用函数时拿到传递的参数,arguments 是一个类数组,通过 for...of 循环可以直接用于迭代 arguments 对象。

    function sum() {
      let result = 0;
      for (const arg of arguments) {
    	result += arg;
      }
      return result;
    }
    
    console.log(sum(1, 2, 3, 4, 5)); // 输出:15
    
  • 迭代 DOM 元素集合

    可以使用 for...of 循环来迭代 DOM 元素集合。DOM 元素集合是一个类数组对象,通常是通过 querySelectorAll() 或类似方法获取的。

    const elements = document.querySelectorAll('.my-class');
    
    for (const element of elements) {
      console.log(element);
    }
    

    注意:for...of 循环只能用于可迭代对象,而 DOM 元素集合是实时的、动态的集合,它们在每次访问时都会重新计算。因此,如果在迭代过程中修改了 DOM 元素集合,可能会导致意外的结果或错误。

13.2.4 forEach、for、for…in和for…of的区别

ES6 中的forEachforfor...infor...of都是用于迭代数组或类数组对象的循环结构,但它们在语法和功能上存在一些区别。

  1. forEach:
  • 语法:array.forEach(callback[, thisArg])
  • 功能:forEach方法会对数组中的每个元素执行一次提供的回调函数。回调函数接受三个参数:当前元素的值、当前索引和被遍历的数组。
  • 特点:forEach方法无法使用breakreturn来中断循环,也无法修改原数组。
  1. for循环:
  • 语法:for (初始化; 条件; 更新) { 循环体 }
  • 功能:for循环是一种通用的循环结构,可以用于任何迭代需求。通过手动控制循环变量,可以在循环的不同步骤中执行各种操作。
  • 特点:可以使用breakcontinue关键字来中断循环或跳过当前迭代。
  1. for…in循环:
  • 语法:for (variable in object) { 循环体 }
  • 功能:for...in循环用于遍历对象的可枚举属性(包括继承的属性)。变量variable在每次迭代中表示对象的一个属性名。
  • 特点:for...in循环遍历对象的属性,不适用于数组和类数组对象的迭代。此外,它会遍历原型链上的属性,可能会包含非预期的属性。
  1. for…of循环:
  • 语法:for (variable of iterable) { 循环体 }
  • 功能:for...of循环用于迭代可迭代对象(如数组、字符串、Set、Map等)。变量variable在每次迭代中表示可迭代对象的一个元素值。
  • 特点:for...of循环只能用于迭代可迭代对象,不适用于普通对象。它提供了一种简洁的方式来遍历集合,并且支持breakcontinue关键字来中断循环或跳过当前迭代。

总结:

  • forEach 是数组方法,用于对每个元素执行回调函数。
  • for 循环是通用的循环结构,适用于任何迭代需求。
  • for...in 循环用于遍历对象的可枚举属性。
  • for...of 循环用于迭代可迭代对象的元素值。

14-Genrator函数

ES6 引入了生成器(Generator)函数,它是一种特殊的函数,可以产生多个值的迭代器。生成器函数通过使用 yield 语句来控制函数的执行过程,可以暂停和恢复函数的执行。

以下是 Generator 函数的语法格式:

function* generatorFunction() {
  // 函数体
}
  • function* 关键字用于定义生成器函数,后跟函数名和参数列表。
  • 生成器函数内部使用 yield 关键字产生一个值,并且可以在后续调用中继续执行。

Generator 函数的特点:

  • 暂停和恢复:Generator 函数的执行可以在yield语句处暂停,并且可以通过next()方法恢复执行。每次调用next()方法,函数会从上一次yield语句的位置继续执行,直到遇到下一个yield语句或函数结束为止。
  • 可迭代性:Generator 函数返回一个迭代器对象,可以使用for...of循环或者使用next()方法逐个访问生成的值。
  • 内部状态管理:Generator 函数可以通过函数内部的变量来保存和管理内部状态,每次恢复执行时,可以保留之前的状态。

生成器函数的执行控制:

  • 调用生成器函数返回的迭代器对象的 next() 方法,会执行生成器函数并返回一个对象,这个对象包含 valuedone 两个属性。
    • value 属性包含 yield 关键字后面表达式的值。
    • done 属性指示生成器函数是否已经执行完毕。
  • 可以通过多次调用 next() 方法来实现逐步执行和暂停生成器函数的执行。

Generator 对象是通过调用 Generator 函数而创建的。它是一个迭代器对象,可以使用next()方法逐步执行 Generator 函数,并返回生成的值。

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

const generator = generatorFunction(); // 获取迭代器对象

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

14.1 iterator接口和Genrator

Generator 实现了 Iterator 接口,也就是说,Generator 函数返回的对象同时具有 IterableIterator 的特性。Generator 对象有 Symbol.iterator 方法,执行后返回它自身。

function* generatorFunction() {
  yield 'Hello';
  yield 'World';
  yield '!';
}

let generator = generatorFunction(); // 调用Generator函数获取Generator对象
let iterator = generator[Symbol.iterator]();
console.log(iterator === generator); // true

上述代码中,我们通过调用 generatorFunction 函数,返回一个 Generator 对象并赋值给 generator,然后通过调用 generator[Symbol.iterator]() 获取的迭代器对象,由于 generator 本身就是迭代器对象,所以 iterator === generator 返回的是 true。

14.2 next()方法

Generator 函数的 next() 方法可以接受一个参数,该参数会作为上一个 yield 表达式的返回值。此外,如果没有传入参数,则上一次 yield 表达式的返回值为 undefined

  • 不传参情况

    next() 方法不传入参数时,Generator 函数从上次执行被暂停的地方继续执行,直到遇到下一个 yield 关键字或函数结束。函数执行到 yield 关键字时会暂停,并将 yield 后面的表达式作为结果返回。

    function* myGenerator() {
      const x = yield 'Hello';
      const y = yield x + 5;
      return x + y;
    }
    
    const iter = myGenerator();
    console.log(iter.next()); // { value: 'Hello', done: false }
    console.log(iter.next()); // { value: NaN, done: false }
    console.log(iter.next()); // { value: undefined, done: true }
    

    在上面的示例中,我们定义了一个名为 myGenerator 的 Generator 函数。在函数体内,我们使用了两个 yield 表达式,并且在第二个 yield 表达式后面使用变量来接收传入的参数。

    通过调用 iter.next() 方法,函数开始执行并执行到第一个 yield 关键字处,暂停,并将该 yield 表达式返回的 'Hello' 作为结果返回。

    接着调用 iter.next() 方法,函数从上次暂停的地方继续执行,并执行到第二个 yield 关键字处,暂停,并将上一次 yield 表达式的返回值 undefinedx 相加得到 NaN,作为结果返回。

    最后一次调用 iter.next() 方法,函数从上次暂停的地方继续执行,并执行到函数体的最后一条语句,将返回值为 undefined,将其加上之前的 x,最终结果为 NaN + undefined,得到 undefined,作为最终结果返回。

  • 传参情况

    next() 方法传入参数时,Generator 函数从上次执行被暂停的地方继续执行,直到遇到下一个 yield 关键字或函数结束。与不传参的情况不同的是,传入的参数会作为上一个 yield 表达式的返回值。

    function* myGenerator() {
      const x = yield 'Hello';
      const y = yield x + 5;
      return x + y;
    }
    
    const iter = myGenerator();
    console.log(iter.next()); // { value: 'Hello', done: false }
    console.log(iter.next(10)); // { value: 15, done: false }
    console.log(iter.next(7)); // { value: 17, done: true }
    

    在上面的示例中,我们定义了一个名为 myGenerator 的 Generator 函数。在函数体内,我们使用了两个 yield 表达式,并且在第二个 yield 表达式后面使用变量来接收传入的参数。

    通过调用 iter.next() 方法,函数开始执行并执行到第一个 yield 关键字处,暂停,并将该 yield 表达式返回的 'Hello' 作为结果返回。

    接着调用 iter.next(10) 方法,函数从上次暂停的地方继续执行,并执行到第二个 yield 关键字处,暂停,并将参数 10x 相加得到 15,作为结果返回。

    最后一次调用 iter.next(7) 方法,函数从上次暂停的地方继续执行,并执行到函数体的最后一条语句,将参数 7 与之前的 x 相加得到 17,作为最终结果返回。

可以看到,通过调用 next() 方法并传入参数,我们可以在函数内部获取外部的值,并进行处理。这种特性使得 Generator 函数非常适合用于异步编程,能够更好地控制和管理异步操作的流程。

14.3 Generator.prototype.return()

Generator.prototype.return() 是 Generator 函数的一个方法,用于提前结束函数的执行,并返回一个给定的值。

当调用 return() 方法时,Generator 函数会立即终止执行,并将传入的参数作为函数的返回值。如果没有传入参数,则返回值为 { value: undefined, done: true }

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

const iter = myGenerator();
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.return('Finished')); // { value: 'Finished', done: true }
console.log(iter.next()); // { value: undefined, done: true }

在上面的示例中,我们定义了一个名为 myGenerator 的 Generator 函数,它会依次生成数字 1、2、3。我们通过调用 iter.next() 方法来逐步执行 Generator 函数。

在第一次调用 iter.next() 方法后,函数暂停并返回值为 { value: 1, done: false }。接着我们调用 iter.return('Finished') 方法,这会立即终止函数的执行,并将 'Finished' 作为返回值。

最后一次调用 iter.next() 方法时,函数已经被终止,因此返回值为 { value: undefined, done: true }

14.4 Generator.prototype.throw()

Generator.prototype.throw() 是 Generator 函数的一个方法,用于向函数内部抛出一个异常。当函数内部捕获到异常时,可以选择如何处理该异常。

当调用 throw() 方法时,Generator 函数会将传入的参数作为异常抛出,并在函数体内寻找最近的 try...catch 语句来处理该异常。如果没有找到匹配的 try...catch 语句,则该异常会被传递到调用栈上层的代码中。

function* myGenerator() {
  try {
    yield 1;
    yield 2;
    yield 3;
  } catch (err) {
    console.log('Caught an error:', err);
  }
}

const iter = myGenerator();
console.log(iter.next()); // { value: 1, done: false }
console.log(iter.throw('Something went wrong')); // Caught an error: Something went wrong
console.log(iter.next()); // { value: undefined, done: true }

在上面的示例中,我们定义了一个名为 myGenerator 的 Generator 函数,它会依次生成数字 1、2、3。在函数体内,我们使用了一个 try...catch 语句来捕获可能抛出的异常。

我们通过调用 iter.next() 方法来逐步执行 Generator 函数。在第一次调用 iter.next() 方法后,函数暂停并返回值为 { value: 1, done: false }

接着我们调用 iter.throw('Something went wrong') 方法,这会向函数内部抛出一个异常,并将 'Something went wrong' 作为异常信息。由于在代码中存在 try...catch 语句,因此该异常会被捕获,并输出错误信息 Caught an error: Something went wrong

最后一次调用 iter.next() 方法时,由于函数已经被中断,因此返回值为 { value: undefined, done: true }

14.5 for…of

可以使用 for...of 循环来遍历一个 Generator 函数生成的迭代器对象。

下面是一个遍历 Generator 函数的示例:

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

const iter = myGenerator();

for (const value of iter) {
  console.log(value);
}

在上面的示例中,我们定义了一个名为 myGenerator 的 Generator 函数,它会依次生成数字 1、2、3。

我们通过调用 myGenerator() 来获取一个迭代器对象,并将其赋值给变量 iter

然后,我们使用 for...of 循环遍历迭代器对象 iter。在每次循环迭代时,value 变量会依次接收迭代器的值。在本例中,我们会打印出数字 1、2、3。

注意:使用 for...of 循环遍历 Generator 函数生成的迭代器对象时,如果 Generator 函数内部没有定义终止条件,那么循环会一直进行下去,直到手动停止 Generator 函数或者迭代器对象的 return() 方法被调用。

14.6 yield*

yield* 语句是在 Generator 函数内部使用的一种语法,用于委托生成器。通过 yield* 语句,可以将执行权委托给另一个可迭代对象(如数组、字符串或另一个生成器),并将其生成的值逐个返回给调用者。

function* generator1() {
  yield 'a';
  yield 'b';
}

function* generator2() {
  yield 'x';
  yield* generator1();
  yield 'y';
}

const iter = generator2();

console.log(iter.next()); // { value: 'x', done: false }
console.log(iter.next()); // { value: 'a', done: false }
console.log(iter.next()); // { value: 'b', done: false }
console.log(iter.next()); // { value: 'y', done: false }
console.log(iter.next()); // { value: undefined, done: true }

在上面的示例中,我们定义了两个 Generator 函数:generator1generator2。其中,generator1 生成器函数会依次生成字母 'a''b',而 generator2 生成器函数在生成字母 'x' 后使用 yield* 语句将执行权委托给了 generator1,然后再生成字母 'y'

我们通过调用 generator2 函数获取一个迭代器对象,并将其赋值给变量 iter

然后,我们通过连续调用 iter.next() 方法来逐步执行生成器函数。在每次调用 next() 方法后,会返回一个包含当前生成的值和是否完成的对象。

在本例中,我们可以看到依次输出了 'x''a''b''y'。最后一次调用 next() 方法时,生成器函数已经完成,因此返回值的 done 属性为 true

14.7 作为对象的属性

Generator 函数可以作为对象的属性,可以通过对象字面量或者使用类的方式进行定义。

const myObject = {
  *generatorFunc() {
    yield 1;
    yield 2;
    yield 3;
  }
};

// 使用对象属性中的 Generator 函数
const iter = myObject.generatorFunc();

console.log(iter.next().value); // 输出: 1
console.log(iter.next().value); // 输出: 2
console.log(iter.next().value); // 输出: 3

在上述示例中,我们使用对象字面量的方式定义了一个对象 myObject,其中的 generatorFunc 属性是一个 Generator 函数。通过调用对象属性中的 Generator 函数,我们可以创建一个迭代器 iter 并逐步获取生成的值。

另一种方式是使用类的方式定义对象属性为 Generator 函数:

class MyClass {
  *generatorFunc() {
    yield 'Hello';
    yield 'World';
  }
}

const myObject = new MyClass();

// 使用对象属性中的 Generator 函数
const iter = myObject.generatorFunc();

console.log(iter.next().value); // 输出: Hello
console.log(iter.next().value); // 输出: World

在这个示例中,我们使用类的方式定义了一个名为 MyClass 的类,并在其中定义了一个 Generator 函数 generatorFunc。通过创建类的实例 myObject,我们可以调用对象属性中的 Generator 函数,并获取生成的值。

14.8 Generator 函数中的this

在 Generator 函数中使用 bind 方法来绑定 this 的值是一种常见的做法,可以确保生成器函数内部的 this 引用的是预期的对象。

function* generatorFunc () {
  this.x = 1;
  yield this.x;
  this.y = 2;
  yield this.y;
}

const myObject = {};

const generator = generatorFunc.bind(myObject);
const iter = generator();

console.log(iter.next().value); // 输出: 1
console.log(iter.next().value); // 输出: 2

console.log(myObject); // 输出: { x: 1, y: 2 }

上述代码,先创建了一个空对象 myObject,通过调用 bind 方法将 generatorFunc 函数绑定到 myObject 对象上,并通过迭代器访问生成的值。

在绑定生成器函数时,使用 bind 方法可以确保在生成器函数内部的 this 引用的是我们所预期的对象(即绑定时传入的对象)。在这种情况下,生成器函数内部对 myObject 的赋值操作会生效。

14.7 应用案例

Generator 函数是一种特殊的函数,它可以通过暂停和恢复执行来生成多个值。这种函数在某些场景下非常有用,以下是一些使用 Generator 函数的场景案例:

  1. 异步编程

    Generator 函数可以方便地实现异步编程,它可以通过 yield 表达式暂停函数执行,等待异步操作完成后再恢复执行,从而消除了回调函数嵌套的问题。

    function* asyncFunc() {
      const result1 = yield fetch('https://example.com/data1');
      const result2 = yield fetch(`https://example.com/data2/${result1}`);
      return result2;
    }
    
    const iter = asyncFunc();
    iter.next().value.then(result1 => {
      iter.next(result1).value.then(result2 => {
    	console.log(result2);
      });
    });
    

    在这个示例中,我们定义了一个名为 asyncFunc 的 Generator 函数,该函数通过 yield 表达式暂停函数执行,并使用 Promise 对象来等待异步操作完成。在外部代码中,我们创建了一个迭代器 iter 并通过调用 next 方法来逐步执行该函数,同时使用 Promise 对象来处理每个异步操作的结果。

  2. 数据流控制

    Generator 函数可以用于控制数据流的传递,在处理大量数据时非常有用。例如,以下代码展示了如何使用 Generator 函数将数据流分批处理:

    function* batchedData(data, batchSize) {
      let index = 0;
      while (index < data.length) {
    	yield data.slice(index, index + batchSize);
    	index += batchSize;
      }
    }
    
    const data = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    const batchSize = 3;
    
    for (const batch of batchedData(data, batchSize)) {
      console.log(batch);
    }
    

    在这个示例中,我们定义了一个名为 batchedData 的 Generator 函数,该函数将数据流分批处理,并在每一批处理完成后通过 yield 表达式暂停函数执行。在外部代码中,我们使用 for...of 循环来迭代处理每一批数据。

  3. 状态机

    Generator 函数可以用于实现状态机,它可以通过 yield 表达式来表示不同的状态,从而方便地管理和维护状态。

    function* stateMachine() {
      let state = 'start';
      while (true) {
    	switch (state) {
    	  case 'start':
    		console.log('Starting state');
    		state = yield 'start';
    		break;
    	  case 'middle':
    		console.log('Middle state');
    		state = yield 'middle';
    		break;
    	  case 'end':
    		console.log('Ending state');
    		state = yield 'end';
    		break;
    	}
      }
    }
    
    const machine = stateMachine();
    console.log(machine.next().value); // Starting state
    console.log(machine.next('middle').value); // Middle state
    console.log(machine.next('end').value); // Ending state
    

    在这个示例中,我们定义了一个名为 stateMachine 的 Generator 函数,该函数模拟了一个简单的状态机,它通过 yield 表达式表示不同的状态,并使用 switch...case 语句来处理状态转换逻辑。在外部代码中,我们创建了一个迭代器 machine 并通过调用 next 方法来迭代状态机的不同状态。

15-Promise

ES6 引入的 Promise 是一种用于处理异步操作的对象,它可以更优雅地处理回调函数嵌套的问题,并提供了一种更结构化和可读性强的方式来编写异步代码。

Promise 对象具有三个状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。一旦状态改变,就不会再改变。

15.1 基本用法

通过调用 Promise 构造函数来创建一个 Promise 对象。构造函数接受一个带有 resolvereject 两个参数的执行器函数作为参数,用于处理异步操作。

const promise = new Promise((resolve, reject) => {
  // 异步操作
  // 如果操作成功,调用 resolve 方法并传递结果
  // 如果操作失败,调用 reject 方法并传递错误信息
});

15.2 方法介绍

Promise 提供了一组方法来管理异步操作的状态和结果。下面是 Promise 的常用方法的详细介绍:

  1. Promise.prototype.then()

    • 用于注册 Promise 对象状态变为 fulfilled(已成功)时的回调函数。
    • 接受两个参数:onFulfilled 和 onRejected,分别表示在成功和失败时要执行的回调函数。
    • 返回一个新的 Promise 对象,表示由 onFulfilled 或 onRejected 回调函数返回的结果。
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
    	resolve("成功");
      }, 1000);
    });
    
    promise.then((result) => {
      console.log(result); // 输出: "成功"
    });
    
  2. Promise.prototype.catch()

    • 用于注册 Promise 对象状态变为 rejected(已失败)时的回调函数。
    • 是 then() 方法的一个特殊形式,只处理失败的情况。
    • 接受一个参数 onRejected,表示在失败时要执行的回调函数。
    • 返回一个新的 Promise 对象,表示由 onRejected 回调函数返回的结果。
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
    	reject(new Error("失败"));
      }, 1000);
    });
    
    promise.catch((error) => {
      console.error(error.message); // 输出: "失败"
    });
    
  3. Promise.prototype.finally()

    • 用于注册一个回调函数,不论 Promise 对象状态最终是 fulfilled 还是 rejected,都会执行该回调函数。
    • 不接受任何参数。
    • 返回一个新的 Promise 对象,该 Promise 对象在 finally 回调函数执行完毕后,会重新采用之前的解析值或拒绝原因。
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
    	resolve("成功");
      }, 1000);
    });
    
    promise
      .then((result) => {
    	console.log(result); // 输出: "成功"
      })
      .finally(() => {
    	console.log("无论成功还是失败,都会执行");
      });
    
  4. Promise.all()

    • 用于将多个 Promise 对象包装成一个新的 Promise 对象,等待所有的 Promise 对象都变为 fulfilled 或其中一个 Promise 对象变为 rejected
    • 接受一个可迭代对象作为参数,如数组或类数组对象。
    • 返回一个新的 Promise 对象,当所有 Promise 对象都变为 fulfilled 时,该 Promise 对象变为 fulfilled,并将所有 Promise 对象的解析值以数组的形式传递给回调函数;如果有任何一个 Promise 对象变为 rejected,则该 Promise 对象变为 rejected,并将第一个被拒绝的 Promise 对象的拒绝原因传递给回调函数。
    const promise1 = Promise.resolve(1);
    const promise2 = 2;
    const promise3 = new Promise((resolve, reject) => {
      setTimeout(() => {
    	resolve(3);
      }, 1000);
    });
    
    Promise.all([promise1, promise2, promise3]).then((results) => {
      console.log(results); // 输出: [1, 2, 3]
    });
    
  5. Promise.race()

    • 用于将多个 Promise 对象包装成一个新的 Promise 对象,等待其中一个 Promise 对象变为 fulfilledrejected
    • 接受一个可迭代对象作为参数,如数组或类数组对象。
    • 返回一个新的 Promise 对象,当其中一个 Promise 对象变为 fulfilledrejected 时,该 Promise 对象变为相同的状态,并将第一个完成的 Promise 对象的解析值或拒绝原因传递给回调函数。
    const promise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
    	resolve("成功");
      }, 2000);
    });
    
    const promise2 = new Promise((resolve, reject) => {
      setTimeout(() => {
    	reject(new Error("失败"));
      }, 1000);
    });
    
    Promise.race([promise1, promise2])
      .then((result) => {
    	console.log(result); // 输出: "失败"
      })
      .catch((error) => {
    	console.error(error.message); // 输出: "失败"
      });
    
  6. Promise.resolve()

    • 用于创建一个以给定值解析的已完成的 Promise 对象,或将现有对象转换为 Promise 对象。
    • 接受一个参数 value,表示要解析的值。
    • 如果参数是一个已完成的 Promise 对象,则返回该 Promise 对象;如果参数是一个 thenable 对象(具有 then() 方法),则返回一个新的 Promise 对象,并采用 thenable 对象的状态;否则,返回一个以参数值解析的已完成的 Promise 对象。
    const promise = Promise.resolve("成功");
    
    promise.then((result) => {
      console.log(result); // 输出: "成功"
    });
    
  7. Promise.reject()

    • 用于创建一个以给定原因拒绝的 Promise 对象。
    • 接受一个参数 reason,表示拒绝的原因。
    • 返回一个以给定原因拒绝的 Promise 对象。
    const promise = Promise.reject(new Error("失败"));
    
    promise.catch((error) => {
      console.error(error.message); // 输出: "失败"
    });
    

15.3 Generator函数和Promise结合

Generator 函数和 Promise 可以结合使用,以实现更强大的异步编程能力。通过将 Generator 函数和 Promise 结合,我们可以更方便地控制异步操作的流程和顺序。

function* myGenerator() {
  try {
    const result1 = yield fetch('https://api.example.com/data1');
    console.log(result1);

    const result2 = yield fetch('https://api.example.com/data2');
    console.log(result2);

    const result3 = yield fetch('https://api.example.com/data3');
    console.log(result3);
  } catch (error) {
    console.error(error);
  }
}

function runGenerator(generator) {
  const iterator = generator();

  function handle(iteratorResult) {
    if (iteratorResult.done) {
      return;
    }

    const promise = iteratorResult.value;

    promise
      .then((result) => {
        handle(iterator.next(result));
      })
      .catch((error) => {
        handle(iterator.throw(error));
      });
  }

  handle(iterator.next());
}

runGenerator(myGenerator);

在上面的示例中,myGenerator 是一个 Generator 函数,它定义了一系列需要按顺序执行的异步操作。runGenerator 函数用于运行 Generator 函数。

runGenerator 函数中,我们创建了一个迭代器 iterator,然后使用递归方式处理每个异步操作的结果。如果 Promise 对象被成功解析,我们通过调用 iterator.next(result) 继续执行 Generator 函数的下一个步骤;如果 Promise 对象被拒绝,我们通过调用 iterator.throw(error) 抛出错误,并终止 Generator 函数的执行。

16-async/await

async/await 是 ES8 中引入的一种更加简单、易用的异步编程模型。使用 async/await 可以让我们以一种类似同步代码的方式来处理异步操作,从而使代码更加清晰、简洁和易于理解。

16.1 基本语法

async/await 的基本语法非常简单,它由两个关键字组成:asyncawait

使用 async 关键字声明的函数称为异步函数,它可以包含一个或多个 await 关键字,用来等待异步操作的结果。

在异步函数中,使用 await 关键字等待异步操作的结果,并将其保存在变量中。如果异步操作成功,await 将返回操作结果;如果异步操作失败,await 将抛出错误。

下面是一个示例是如何使用 async/await 来等待异步操作的结果:

async function fetchData(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error('请求出错');
  }

  const data = await response.text();
  return data;
}

async function main() {
  try {
    const data = await fetchData('https://api.example.com/data');
    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

main();

在上面的示例中,fetchData 函数使用 async 关键字声明,表示这是一个异步函数。在函数内部,我们使用 await 关键字等待异步操作的结果。如果异步操作成功,await 返回操作结果;如果异步操作失败,await 抛出错误。

main 函数中,我们使用 try...catch 语句来处理异步操作的错误。在 try 块中,我们使用 await 关键字调用 fetchData 函数,并在成功后打印数据;在失败后,我们使用 catch 块处理错误。

16.2 async/await的注意事项

使用 async/await 是在处理异步操作时的一种简洁且直观的方式。然而,以下是一些在使用 async/await 时需要注意的事项:

  1. 异常处理:使用 try/catch 块来捕获和处理可能发生的异常。在 async 函数中,如果发生了错误并抛出了异常,可以使用 try/catch 来捕获并处理这些异常。

    async function fetchData() {
      try {
    	const data = await fetchDataFromServer();
    	// 处理数据
      } catch (error) {
    	// 处理异常
      }
    }
    
  2. 错误传递:在 async 函数中,如果发生了错误并抛出了异常,可以使用 throw 关键字将异常传递给调用方。调用方可以使用 try/catch 来捕获并处理异常。

    async function fetchData() {
      const data = await fetchDataFromServer();
      if (!data) {
    	throw new Error('Failed to fetch data');
      }
      // 处理数据
    }
    
    try {
      fetchData().catch((error) => {
    	// 处理异常
      });
    } catch (error) {
      // 处理异常
    }
    
  3. 并行执行:使用 await 关键字等待多个异步操作同时完成。可以将多个 Promise 对象包装在 Promise.all() 函数中,并使用 await 等待它们全部完成。

    async function fetchMultipleData() {
      const [data1, data2] = await Promise.all([fetchDataFromServer1(), fetchDataFromServer2()]);
      // 处理数据
    }
    
  4. 顺序执行:使用 await 关键字确保异步操作按顺序执行。在需要按顺序执行多个异步操作的情况下,可以使用连续的 await 表达式。

    async function fetchSequentialData() {
      const data1 = await fetchDataFromServer1();
      const data2 = await fetchDataFromServer2();
      // 处理数据
    }
    
  5. 避免阻塞事件循环:当使用 await 等待一个长时间运行的异步操作时,它可能会阻塞事件循环。如果有必要,可以考虑将该操作放在一个单独的 worker 线程中,以避免阻塞主线程。

    async function runHeavyTask() {
      const result = await runHeavyTaskInWorker();
      // 处理结果
    }
    
  6. 并发限制:并发执行 大量的异步操作 可能会导致资源消耗过多。为了避免这种情况,可以使用限制并发数的方法,例如使用并发控制库或手动实现一个并发队列。

    async function fetchMultipleDataWithConcurrencyLimit() {
      const promises = [fetchDataFromServer1(), fetchDataFromServer2(), fetchDataFromServer3()];
      const concurrencyLimit = 2; // 限制个数
      const results = []; // 保存执行结果返回的数据
    
      while (promises.length) {
    	const batch = promises.splice(0, concurrencyLimit); // 从0开始删除,concurrencyLimit为删除的个数,并返回被删除的异步操作任务
    	const batchResults = await Promise.all(batch);
    	results.push(...batchResults); // 添加每次执行的异步操作任务返回数据
      }
    
      // 处理结果(业务处理)
    }
    

16.3 async/await和Promise的关系

async/await 是在 Promise 基础上发展起来的一种更加简单、易用的异步编程模型。实际上,async/await 内部仍然使用了 Promise 对象来管理异步操作。

当我们在异步函数中使用 await 关键字等待 Promise 对象的结果时,实际上是使用了 Promise 的 then() 方法和 catch() 方法来处理异步操作的状态变化。使用 async/await 可以让我们以一种更加简单、易用的方式来处理异步操作,从而使代码更加清晰、简洁和易于理解。

下面是一个示例,演示了如何将 Promise 与 async/await 结合使用:

function fetchData(url) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();

    xhr.open('GET', url);
    xhr.onload = () => resolve(xhr.responseText);
    xhr.onerror = () => reject(new Error('请求出错'));
    xhr.send();
  });
}

async function main() {
  try {
    const data1 = await fetchData('https://api.example.com/data1');
    console.log(data1);

    const data2 = await fetchData('https://api.example.com/data2');
    console.log(data2);

    const data3 = await fetchData('https://api.example.com/data3');
    console.log(data3);
  } catch (error) {
    console.error(error);
  }
}

main();

16.4 async/await、Promise和Generator的比较

async/await、Promise 和 Generator 是 JavaScript 中用于处理异步操作的三种不同的方式。它们都可以用来管理异步代码,但在使用上有一些区别:

  1. Promise:Promise 是 ES6 引入的一种机制,用于处理异步操作。通过 Promise,我们可以将异步任务封装成一个 Promise 对象,然后使用链式调用的方式进行处理。Promise 对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。使用 .then() 方法可以处理 Promise 成功的情况,使用 .catch() 方法可以处理 Promise 失败的情况。Promise 提供了一种简单的方式来处理异步操作,并且可以方便地进行错误处理和链式调用。

  2. Generator:Generator 是 ES6 引入的一种特殊函数,通过使用 function* 声明一个 Generator 函数,我们可以在函数内部使用 yield 关键字来暂停函数的执行,并返回一个 Iterator 对象。通过调用 Iterator 对象的 .next() 方法,我们可以继续执行 Generator 函数并获取下一个 yield 语句的返回值。Generator 函数可以用于实现协程和异步操作的控制流程,但相对于 Promise 和 async/await,它的语法更加复杂,使用上也更加灵活。

  3. async/await:async/await 是 ES8 引入的语法糖,基于 Promise 对象的一种更加简洁易用的异步编程模型。通过使用 async 关键字声明一个异步函数,我们可以使用 await 关键字等待一个 Promise 对象的结果,并以同步的方式处理异步操作。在 async 函数内部,我们可以使用 await 关键字等待一个 Promise 对象的结果,并将其保存到变量中。如果 Promise 成功,await 返回结果;如果 Promise 失败,await 将抛出错误。async/await 可以使异步代码更加清晰、简洁和易于理解。

17-Class

ES6 中引入了 class 关键字,使得在 JavaScript 中实现面向对象编程更加方便和直观。class 可以看作是一种特殊的函数,用于定义类,通过类可以创建对象实例。

17.1 基本用法

类的定义使用 class 关键字,定义类的语法格式如下:

class 类名 {
	// 属性
	// 构造器
	// 方法
}

17.1.1 构造器

构造函数(constructor)是一个特殊的方法,用于创建和初始化对象实例时被调用。它是在类中定义的第一个方法,并且是可选的。构造函数的目的是设置对象实例的初始状态。语法格式:constructor() {}

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

在上述示例中,Person 类的构造函数接受两个参数 nameage。当使用 new 关键字创建一个新的 Person 对象实例时,会自动调用构造函数,并将传入的参数赋值给对象实例的属性 nameage

构造函数的特点和用法如下:

  1. 构造函数的名称必须是 constructor,并且没有其他名称选项。
  2. 构造函数在对象实例化的时候执行,可以使用 new 关键字来调用构造函数创建对象实例。
  3. 构造函数用于初始化对象实例的属性,可以在函数内部使用 this 关键字来引用当前对象实例。
  4. 如果没有定义构造函数,类会自动生成一个默认的空构造函数。

构造函数的主要作用是设置对象实例的初始状态。在构造函数内部,可以执行一些初始化操作,比如对属性进行赋值、调用其他方法等。构造函数也可以接受任意数量的参数,用于初始化对象实例的属性。

以下是使用构造函数创建对象实例的示例:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}

const person = new Person('Alice', 25);
console.log(person.name); // Alice
console.log(person.age); // 25

在上述示例中,使用 new 关键字创建了一个 Person 类的对象实例 person。构造函数将传入的参数 'Alice'25 分别赋值给对象实例的属性 nameage。通过访问对象实例的属性,可以获取其初始值。

注意:构造函数只会在对象实例化时执行一次。每次使用 new 关键字创建新的对象实例时,会调用构造函数来初始化新的对象,但不会再次执行已经初始化的对象的构造函数。

17.1.2 name属性

在 ES6 中,class 是一种特殊的函数,因此也有 name 属性。name 属性返回类的名称,即类名,可以用于调试和动态创建类的实例。

class Person {
  constructor(name) {
    this.name = name;
  }
}

console.log(Person.name); // Person

在上述示例中,Person 类的 name 属性返回了类名 "Person"

注意:
1.如果使用类表达式定义类,name 属性会返回表达式的名称,否则返回类名。
2.如果将类赋值给一个变量,那么该变量的 name 属性会返回变量名,而不是类的名称。
3.如果使用 bind 方法对类进行绑定,name 属性会返回被绑定后的函数名称。

javascript
// 类表达式
const PersonClass = class Person {
  constructor(name) {
    this.name = name;
  }
};

// 匿名类
// const PersonClass = class {
//   constructor(name) {
//     this.name = name;
//   }
// };

console.log(PersonClass.name); // Person

// 类赋值给变量
const PersonVar = PersonClass;
console.log(PersonVar.name); // PersonVar

// 使用 bind 方法
const BoundPerson = PersonClass.bind(null);
console.log(BoundPerson.name); // BoundPerson

17.2 继承

class 关键字提供了一种更简洁和易于理解的方式来实现面向对象编程中的继承。通过使用 extends 关键字,可以轻松地定义一个类继承自另一个类。继承的类被称为子类,被继承的类称为父类,子类可以继承父类的属性和方法。语法格式:class 子类 extends 父类

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(name, age) {
    super(name); // 调用父类的构造函数
    this.age = age;
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

在上述示例中,我们定义了一个 Animal 类作为父类,并在其构造函数中定义了一个属性 name 和一个方法 speak。然后,我们定义一个 Dog 类,使用 extends 关键字将其继承自 Animal 类。在 Dog 类的构造函数中,我们使用 super 关键字调用父类的构造函数,并传入相应的参数。此外,我们还重写了父类的 speak 方法。

通过继承,Dog 类继承了 Animal 类的属性和方法。我们可以创建 Dog 的对象实例,并调用其继承的方法:

const dog = new Dog('Buddy', 2);
console.log(dog.name); // Buddy
console.log(dog.age); // 2
dog.speak(); // Buddy barks.

在上述示例中,我们创建了一个 Dog 类的对象实例 dog,并访问了继承自父类的属性 name 和新增的属性 age。然后调用了 speak 方法,由于子类重写了父类的方法,所以执行的是子类中的方法。

注意:
1.在子类的构造函数中使用 super 关键字调用父类的构造函数,以初始化继承的属性。如果省略 super 关键字,将无法访问父类的属性。
2.子类可以重写(覆盖)父类的方法,提供自己的实现。
3.子类还可以在重写父类方法的同时通过 super 关键字调用父类的方法,以拓展父类的功能。

17.2.1 prototype属性和__proto__属性

在 class 中,每个方法都被定义在类的 prototype 属性上,而每个对象实例都有一个指向其构造函数的 __proto__ 属性。

  • prototype属性

    在 ES6 中,每个 class 都有一个 prototype 属性,它是一个对象,包含了类的所有方法。通过给 prototype 对象添加方法,可以为类添加新的方法。

    例如,我们可以给之前的 Animal 类添加一个新的方法:

    class Animal {
      constructor(name) {
    	this.name = name;
      }
    
      speak() {
    	console.log(`${this.name} makes a sound.`);
      }
    }
    
    Animal.prototype.eat = function() {
      console.log(`${this.name} is eating.`);
    };
    

    在上述示例中,我们使用 prototype 属性添加了 eat 方法到 Animal 类中。这意味着,所有 Animal 类的对象实例都可以访问 eat 方法。

  • __proto__ 属性

    在 ES6 中,每个对象实例都有一个指向其构造函数的 __proto__ 属性。这个属性指向构造函数的原型对象,即 prototype 属性所指向的对象。

    例如,在之前的 Animal 类的基础上,我们创建了一个 animal 对象实例,并访问了它的 __proto__ 属性:

    class Animal {
      constructor(name) {
    	this.name = name;
      }
    
      speak() {
    	console.log(`${this.name} makes a sound.`);
      }
    }
    
    const animal = new Animal('Cat');
    console.log(animal.__proto__ === Animal.prototype); // true
    

    在上述示例中,我们创建了一个 Animal 类的对象实例 animal,并访问了它的 __proto__ 属性。由于 __proto__ 属性指向构造函数的原型对象,即 Animal.prototype,所以该表达式返回 true

    注意:虽然可以通过 __proto__ 属性访问对象的原型对象,但是不推荐使用,因为这个属性不是标准的 JavaScript 属性,可能在一些环境中不可用或产生不可预期的行为。更好的做法是使用 Object.getPrototypeOf() 方法来获取对象的原型对象。

类继承中的 prototype 属性和 __proto__ 属性是不同的,prototype 属性用于定义类的方法,__proto__ 属性用于构建原型链关系。

子类的 __proto__ 属性表示构造函数的继承,其指向父类。子类的 prototype 属性的 __proto__ 表示方法的继承,总是指向父类的 prototype 属性。

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a sound.`);
  }
}

class Dog extends Animal {
  constructor(name, age) {
    super(name); // 调用父类的构造函数
    this.age = age;
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

const dog = new Dog('Buddy', 2);
console.log(dog.name); // Buddy

console.log(Dog.__proto__ === Animal); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true

在这个示例中,我们通过比较 Dog.__proto__Animal,以及 Dog.prototype.__proto__Animal.prototype 来验证原型链关系。结果都返回了 true,这说明子类的原型对象和父类的原型对象之间确实建立了关联,形成了正确的原型链关系。

子类的原型对象(__proto__属性)指向父类的构造函数,而子类的构造函数的原型对象(prototype 属性)指向父类的实例。

17.2.2 实例的__proto__属性

对于一个 ES6 类的实例,其 __proto__ 属性指向该类的原型对象,即该类的 prototype 属性。下面我会详细介绍 ES6 类的实例的 __proto__ 属性:

  1. 指向构造函数的原型对象:当你使用一个类来创建实例时,该实例的 __proto__ 属性会指向该类的原型对象,这意味着实例可以访问类定义的方法和属性。

  2. 继承父类的原型对象:如果类是另一个类的子类,那么子类的实例的 __proto__ 属性也会指向父类的原型对象,形成原型链。

  3. 原型链:通过实例的 __proto__ 属性,你可以沿着原型链访问到从其构造函数的原型对象中继承的方法和属性,以及从父类的原型对象中继承的方法和属性。

class Animal {
  speak() {
    console.log('Animal makes a sound.');
  }
}

class Dog extends Animal {
  bark() {
    console.log('Dog barks.');
  }
}

const dog = new Dog();
console.log(dog.__proto__ === Dog.prototype); // true
console.log(dog.__proto__.__proto__ === Animal.prototype); // true

在这个示例中,dog 实例的 __proto__ 属性指向 Dog 类的原型对象 Dog.prototype,而 Dog.prototype__proto__ 属性指向 Animal.prototype,形成了完整的原型链。

17.3 继承原生构造函数

在 ES6 中,可以使用类来继承原生构造函数。原生构造函数是 JavaScript 语言内置的一些构造函数,例如 ArrayDateRegExp 等。

下面是一个具体的示例,演示如何使用类来继承原生构造函数:

class MyArray extends Array {
  constructor(...args) {
    super(...args);
  }
  
  getFirstElement() {
    return this[0];
  }
}

const myArray = new MyArray(1, 2, 3);
console.log(myArray.length); // 3
console.log(myArray.getFirstElement()); // 1
console.log(myArray instanceof MyArray); // true
console.log(myArray instanceof Array); // true

在这个示例中,我们定义了一个名为 MyArray 的类,它继承自原生构造函数 Array。通过 extends 关键字,MyArray 类继承了 Array 的所有属性和方法。

MyArray 的构造函数中,我们使用 super(...args) 来调用父类 Array 的构造函数,确保正确地初始化实例。

然后,我们定义了一个 getFirstElement() 方法,它返回数组的第一个元素。

最后,我们创建了一个 myArray 实例,并测试了一些属性和方法。可以看到,myArray 实例继承了 Array 的特性,也可以使用自己定义的方法。

注意:继承原生构造函数时,必须调用 super(...args) 来调用父类的构造函数。这是因为原生构造函数具有内部状态和行为,需要在子类中正确初始化。

17.4 getter和setter

ES6 引入了一种更简洁和易用的属性访问方式,即 gettersetter。它们允许你在访问对象属性时执行额外的逻辑,从而控制属性的读取和设置行为。下面将详细介绍 ES6 的 getter 和 setter:

getter(获取器)

getter 是一个特殊的函数,用于获取对象的属性值。它被定义为对象的一个属性,并使用 get 关键字来声明。当访问该属性时,会自动调用 getter 函数并返回其结果。

class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  
  get diameter() {
    return this.radius * 2;
  }
}

const circle = new Circle(5);
console.log(circle.diameter); // 10

在这个示例中,我们定义了一个名为 Circle 的类,它有一个构造函数接受一个 radius 参数。然后,我们定义了一个名为 diametergetter 属性,它返回半径的两倍。通过 circle.diameter 访问该属性时,会自动调用 getter 函数并返回结果。

setter(设置器)

setter 也是一个特殊的函数,用于设置对象的属性值。它被定义为对象的一个属性,并使用 set 关键字来声明。当修改该属性时,会自动调用 setter 函数并传递新的值作为参数。

class Circle {
  constructor(radius) {
    this.radius = radius;
  }
  
  get diameter() {
    return this.radius * 2;
  }
  
  set diameter(value) {
    this.radius = value / 2;
  }
}

const circle = new Circle(5);
console.log(circle.diameter); // 10

circle.diameter = 14; // 调用 setter,修改半径
console.log(circle.radius); // 7
console.log(circle.diameter); // 14

在这个示例中,我们在 Circle 类中添加了一个名为 diametersetter 属性。当给 circle.diameter 赋值时,会自动调用 setter 函数,并将新的值作为参数传递给它。在 setter 函数中,我们更新了半径的值。

注意:如果只定义了 getter 而没有定义 setter,属性就成了只读属性。类似地,如果只定义了 setter 而没有定义 getter,属性就成了只写属性。

通过使用 gettersetter,你可以灵活地控制对象属性的访问和修改行为,并进行一些逻辑处理,例如校验输入值、计算衍生属性等。

17.5 静态属性

ES6 引入了一种新的语法糖,使我们能够更方便地创建类,并在类中定义静态属性和方法。ES6 的类静态属性是指类本身拥有的属性,而不是实例属性,因此可以在类定义中直接访问和修改。

定义静态属性

在 ES6 类中,可以使用 static 关键字来定义静态属性和方法。静态属性是指属于类本身的属性,而非实例属性。静态属性可以被类自身调用,也可以被子类继承和重写。

class Circle {
  static defaultColor = 'red'; // 定义静态属性
  
  constructor(radius) {
    this.radius = radius;
  }
  
  get diameter() {
    return this.radius * 2;
  }
  
  get color() {
    return Circle.defaultColor; // 访问静态属性
  }
  
  set color(value) {
    Circle.defaultColor = value; // 修改静态属性
  }
}

const circle1 = new Circle(5);
console.log(circle1.color); // red

const circle2 = new Circle(10);
console.log(circle2.color); // red

circle1.color = 'blue';
console.log(circle1.color); // blue
console.log(circle2.color); // blue

在这个示例中,我们在 Circle 类中定义了一个静态属性 defaultColor,它的默认值为 'red'。我们可以通过 Circle.defaultColor 来访问和修改这个静态属性。在 colorgettersetter 方法中,我们使用了 Circle.defaultColor 来访问和修改静态属性。

注意:注意静态属性和方法不属于实例,因此不能使用 this 关键字来访问它们。相反,应该使用类本身来访问或修改静态属性和方法,即使用 ClassName.propertyNameClassName.methodName() 的方式来访问或调用。

继承静态属性

子类可以继承父类的静态属性和方法,并且可以在子类中重写它们。当在子类中调用静态方法时,会自动使用子类的实现。

class ColoredCircle extends Circle {
  static defaultColor = 'green'; // 继承并重写父类的静态属性
  
  constructor(radius, color) {
    super(radius);
    this.color = color;
  }
}

const coloredCircle = new ColoredCircle(7, 'yellow');
console.log(coloredCircle.color); // yellow
console.log(coloredCircle.constructor.defaultColor); // green

在这个示例中,我们定义了一个名为 ColoredCircle 的子类,并继承了父类 Circle 的静态属性和方法。我们在子类中重写了父类的静态属性 defaultColor,它的值为 'green'。在创建子类实例时,我们传入半径和颜色,并可以直接访问子类的实例属性 color,也可以访问静态属性 defaultColor,它会自动使用子类的实现。

17.6 静态方法

ES6 类中的静态方法是指属于类本身而不是类实例的方法。使用静态方法可以在类级别上执行操作,而不需要创建类的实例。静态方法通常用于实现与类相关的功能,而不涉及特定实例的状态。

定义静态方法

在 ES6 类中,可以使用 static 关键字来定义静态方法。类的静态方法可以直接通过类本身调用,而无需创建类的实例。静态方法通常用于执行与类相关的操作,例如工具函数、工厂方法等。

class MathUtils {
  static add(a, b) {
    return a + b;
  }
  
  static subtract(a, b) {
    return a - b;
  }
}

console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.subtract(7, 2)); // 5

在这个示例中,我们定义了一个名为 MathUtils 的类,并在其中定义了两个静态方法 addsubtract。我们可以直接通过 MathUtils.addMathUtils.subtract 调用这些静态方法,而无需创建 MathUtils 的实例。

继承和重写静态方法

子类可以继承父类的静态方法,并且可以在子类中重写它们。当在子类中调用静态方法时,会自动使用子类的实现。

class MathExtension extends MathUtils {
  static multiply(a, b) {
    return a * b;
  }
  
  static add(a, b) {
    return super.add(a, b) + 1; // 调用父类的静态方法并进行扩展
  }
}

console.log(MathExtension.add(5, 3)); // 9
console.log(MathExtension.subtract(7, 2)); // 5
console.log(MathExtension.multiply(4, 6)); // 24

在这个示例中,我们定义了一个名为 MathExtension 的子类,并继承了父类 MathUtils 的静态方法。我们在子类中添加了一个新的静态方法 multiply,同时在子类中重写了父类的静态方法 add,并对其进行了扩展。在调用子类的静态方法时,会自动使用子类的实现。

通过使用静态方法,你可以在类级别上执行操作,而无需创建类的实例。静态方法通常用于实现与类相关的功能,例如工具函数、工厂方法等,使得代码更加清晰和易于维护。

17.7 Class的Generator方法

可以使用 Generator 方法来定义一个生成器函数。生成器函数是一种特殊的函数,可以生成可暂停和可恢复执行的迭代器。

class MyIterator {
  constructor(data) {
    this.data = data;
  }
  
  *[Symbol.iterator]() {
    for (let i = 0; i < this.data.length; i++) {
      yield this.data[i];
    }
  }
}

const myArray = new MyIterator(['a', 'b', 'c']);

for (let value of myArray) {
  console.log(value);
}

// Output:
// 'a'
// 'b'
// 'c'

在这个示例中,我们创建了一个名为 MyIterator 的类,并在其中定义了一个 Symbol.iterator 方法,用于返回一个默认的迭代器对象。Symbol.iterator 方法使用 * 符号声明为生成器函数,并使用 yield 关键字生成迭代器的值。

通过创建 MyIterator 类的实例 myArray,我们可以使用 for...of 循环迭代 myArray 对象。由于 myArray 对象使用 Symbol.iterator 方法定义了迭代器,所以 for...of 循环会自动调用 myArray 对象的 Symbol.iterator 方法来获取迭代器。

17.8 new.target

new.target 是一个元属性(meta-property),用于在类或构造函数中获取被实例化的对象的引用。

class MyClass {
  constructor() {
    console.log(new.target);
  }
}

const obj1 = new MyClass(); // 输出:[Function: MyClass]

在这个示例中,我们创建了一个名为 MyClass 的类,并在其构造函数中打印 new.target。当我们通过 new 关键字实例化 MyClass 类时,new.target 将会返回被实例化的类本身的引用,即 MyClass

new.target 主要用于以下场景:

  1. 在继承中判断是否通过子类直接实例化父类。通过比较 new.target 和父类的引用,可以确保子类不会直接实例化父类。

    class Parent {
      constructor() {
    	if (new.target === Parent) {
    	  throw new Error('Parent class cannot be instantiated directly.');
    	}
      }
    }
    
    class Child extends Parent {}
    
    const obj2 = new Parent(); // 抛出错误:Parent class cannot be instantiated directly.
    const obj3 = new Child(); // 正常实例化
    
  2. 在抽象基类中阻止直接实例化。抽象基类是一个不能被直接实例化的基类,只能通过派生类进行实例化。通过在抽象基类的构造函数中检查 new.target,可以抛出错误以阻止直接实例化。

    class AbstractClass {
      constructor() {
    	if (new.target === AbstractClass) {
    	  throw new Error('AbstractClass cannot be instantiated directly.');
    	}
      }
    }
    
    class ConcreteClass extends AbstractClass {}
    
    const obj4 = new AbstractClass(); // 抛出错误:AbstractClass cannot be instantiated directly.
    const obj5 = new ConcreteClass(); // 正常实例化
    

18-装饰器

ES6 中的装饰器是一种特殊的语法,用于修改类、方法、属性或参数的行为。装饰器可以方便地在不修改原始代码的情况下添加、修改或扩展功能。

在 ES6 中,装饰器是通过 @ 符号来表示,并紧跟在要修饰的目标之前。装饰器可以是一个函数,也可以是一个表达式,它返回一个函数。装饰器函数在运行时被调用,并接受不同的参数,具体取决于它所修饰目标的类型。

18.1 类装饰器

类装饰器是一种在类声明之前被声明的装饰器,用于修改或扩展类的行为。类装饰器可以接收一个参数,该参数是被装饰的类的构造函数。通过在类装饰器中修改构造函数或原型,可以实现对类的修改。

function logger(target) {
  console.log('Class being logged:', target);
}

@logger
class MyClass {
  // 类的定义
}

在这个示例中,logger 是一个类装饰器。它接收一个参数 target,代表被装饰的类的构造函数。在装饰器中,我们可以对构造函数进行修改或添加额外的逻辑。

类装饰器还可以返回一个新的构造函数,以替换原始的构造函数。这允许我们在不修改原始类定义的情况下,动态地修改类的行为。例如:

function addProperty(target) {
  return class {
    newProperty = 'new property value';
    oldProperty = target.prototype.oldProperty;
  };
}

@addProperty
class MyClass {
  oldProperty = 'old property value';
}

const obj = new MyClass();
console.log(obj.newProperty); // 输出:'new property value'
console.log(obj.oldProperty); // 输出:'old property value'

在这个示例中,addProperty 是一个类装饰器,它返回一个新的构造函数。新的构造函数添加了一个新的属性 newProperty,并保留了原始类定义中的属性 oldProperty

18.2 方法装饰器

方法装饰器是一种在类方法声明之前被声明的装饰器,用于修改或扩展方法的行为。方法装饰器可以接收三个参数:目标类的原型、被装饰的方法的名称和一个属性描述符。通过在方法装饰器中修改属性描述符,可以实现对方法的修改。

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

class MyClass {
  @readonly
  myMethod() {
    // 方法的定义
  }
}

在这个示例中,readonly 是一个方法装饰器。它接收三个参数:target 是类的原型,name 是被装饰方法的名称,descriptor 是属性描述符对象。在装饰器中,我们可以修改属性描述符对象的属性,以实现对方法的修改。在这个例子中,装饰器将 writable 属性设置为 false,使得被装饰的方法变为只读方法,不能被修改。

方法装饰器还可以返回一个新的属性描述符对象,以替换原始的属性描述符对象。这允许我们在不修改原始方法定义的情况下,动态地修改方法的行为。例如

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

function logExecutionTime(target, name, descriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args) {
    console.time(name);
    const result = originalMethod.apply(this, args);
    console.timeEnd(name);
    return result;
  };

  return descriptor;
}

class MyClass {
  @readonly
  @logExecutionTime
  myMethod() {
    // 方法的定义
  }
}

const obj = new MyClass();
obj.myMethod();

在这个示例中,logExecutionTime 是一个方法装饰器,它返回一个新的属性描述符对象。新的属性描述符对象修改了方法的行为,在方法执行前后分别记录了方法的执行时间。

18.3 属性装饰器

属性装饰器是一种在类属性声明之前被声明的装饰器,用于修改或扩展属性的行为。属性装饰器可以接收两个参数:目标类的原型和被装饰的属性的名称。通过在属性装饰器中修改原型,可以实现对属性的修改。

function uppercase(target, name) {
  const value = target[name];

  const getter = function () {
    return value.toUpperCase();
  };

  const setter = function (newValue) {
    value = newValue.toUpperCase();
  };

  Object.defineProperty(target, name, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class MyClass {
  @uppercase
  myProperty = 'hello';
}

在这个示例中,uppercase 是一个属性装饰器。它接收两个参数:target 是类的原型,name 是被装饰属性的名称。在装饰器中,我们可以通过修改原型来修改属性的行为。在这个例子中,装饰器将属性值转换为大写,并定义了新的 gettersetter 方法来访问和修改属性。

18.4 参数装饰器

参数装饰器是一种在类方法的参数声明之前被声明的装饰器,用于修改或扩展方法参数的行为。参数装饰器可以接收三个参数:目标类的原型、被装饰方法的名称和参数在函数参数列表中的索引位置。通过在参数装饰器中修改函数参数的元数据,可以实现对方法参数的修改。

function required(target, methodName, paramIndex) {
  const method = target[methodName];

  const oldFunc = method;

  method = function (...args) {
    if (args[paramIndex] === undefined || args[paramIndex] === null) {
      throw new Error(`Parameter ${paramIndex + 1} is required`);
    } else {
      return oldFunc.apply(this, args);
    }
  };

  target[methodName] = method;
}

class MyClass {
  myMethod(@required param1, param2) {
    // 方法的定义
  }
}

在这个示例中,required 是一个参数装饰器。它接收三个参数:target 是类的原型,methodName 是被装饰方法的名称,paramIndex 是被装饰参数在方法参数列表中的索引位置。在装饰器中,我们可以修改函数参数的元数据,从而实现对方法参数的定制化操作。在这个例子中,装饰器检查第一个参数是否存在,如果不存在则抛出一个错误。

18.5 装饰器的执行顺序

装饰器的执行顺序是从上往下,从内到外的顺序。当多个装饰器应用在同一个目标上时,它们的执行顺序是从上至下依次执行。

  1. 类装饰器的执行顺序

    当一个类被多个装饰器修饰时,装饰器的执行顺序是从上往下的,即从外到内的顺序。例如:

    @decorator1
    @decorator2
    class MyClass {}
    

    在这个例子中,decorator1 将先于 decorator2 执行。首先,MyClass 将被传递给 decorator1,然后再将结果传递给 decorator2,最终得到最终的装饰后的类。

  2. 方法装饰器的执行顺序

    在方法装饰器中,执行顺序也是从上往下的。对于同一个方法,先声明的装饰器会首先执行,然后依次向下执行。

    class MyClass {
      @decorator1
      @decorator2
      myMethod() {}
    }
    

    在这个例子中,decorator1 将先于 decorator2 执行,并且它们都会对 myMethod 进行修改。

  3. 参数装饰器的执行顺序:

    参数装饰器的执行顺序与方法装饰器类似,也是从上往下,即从左到右。

  4. 属性装饰器的执行顺序:

    对类的属性进行装饰时,同样遵循从上往下的执行顺序。

注意:对于同一个目标(类、方法、属性等),装饰器的执行顺序对其行为可能会产生影响。因此,在使用装饰器时,要特别考虑它们的执行顺序,以确保得到预期的结果。

19-模块

模块化是一种软件设计和开发的方法,它将大型程序拆分成小的、可管理的模块,每个模块负责完成特定的功能。模块是指将相关的代码组织在一起,形成一个独立的单元或文件。模块是一种新的组织代码的方式,它提供了更好的封装性和可重用性。模块的功能主要是由 exportimport 两个命令构成的。

ES6 模块是静态的,意味着它们的导入和导出在编译时确定,而不是运行时。这使得模块能够更好地优化和管理,同时也提高了代码的可读性和可维护性。

19.1 export

export 命令用于从模块中导出变量、函数、类等,以便其他模块可以使用这些导出的成员。export 命令可以单独导出变量、函数、类,也可以通过命名导出或默认导出来导出模块中的内容。

19.1.2 命名导出

使用 export 命令单独导出模块中的变量、函数或类。例如:

// math.js
export const PI = 3.1415926;
export function square(x) {
  return x * x;
}

在上面的例子中,math.js 模块中通过 export 命令分别导出了一个常量 PI 和一个函数 square

19.1.2 默认导出

使用 export default 命令导出模块中的内容作为默认导出。一个模块只能有一个默认导出,且在导入时不需要使用花括号包裹。例如:

// math.js
const PI = 3.1415926;
function square(x) {
  return x * x;
}
export default { PI, square };

在上面的例子中,math.js 模块通过 export default 命令默认导出了一个对象,其中包含了常量 PI 和函数 square

19.1.3 重新导出

在一个模块中可以使用 export 命令重新导出另一个模块中已经导出的变量、函数或类。例如:

// moreMath.js
export { PI, square } from './math.js';

在上面的例子中,moreMath.js 模块重新导出了 math.js 模块中已经导出的 PIsquare

export 命令的灵活性使得模块之间的交互变得非常简单和直观。通过导出和导入,我们可以更好地组织和管理代码,同时也提高了代码的可重用性和可维护性。

19.2 import

import 命令用于从其他模块中导入变量、函数、类等,以便在当前模块中使用这些导入的成员。import 命令可以导入命名导出的成员、默认导出的内容,也可以使用重命名的方式导入成员。

19.2.1 导入命名导出的成员

使用 import 命令导入其他模块中通过 export 导出的成员。导入的成员需要使用花括号包裹,并且名称必须和导出时的名称保持一致。例如:

// app.js
import { PI, square } from './math.js';
console.log(PI); // 输出 3.1415926
console.log(square(2)); // 输出 4

19.2.2 导入默认导出的内容

使用 import 命令导入其他模块中通过 export default 导出的内容。默认导出的内容在导入时不需要使用花括号包裹,直接赋值给一个变量即可。例如:

// app.js
import math from './math.js';
console.log(math.PI); // 输出 3.1415926
console.log(math.square(2)); // 输出 4

19.2.3 重命名导入的成员

在导入时可以使用 as 关键字对导入的成员进行重命名,这样就可以在当前模块中使用新的名称来代替原始的名称。例如:

// app.js
import { PI as circlePI, square as squareFn } from './math.js';
console.log(circlePI); // 输出 3.1415926
console.log(squareFn(2)); // 输出 4

import 命令使得在模块之间进行交互变得非常简单和直观。通过导入和导出,我们可以更好地组织和管理代码,同时也提高了代码的可重用性和可维护性。同时,import 命令还支持动态导入,使得我们可以在运行时根据条件导入模块,从而进一步增强了模块化开发的灵活性。

19.3 模块的整体加载

在 ES6 中,可以使用 import * as <module> 语法来整体加载一个模块的所有导出内容。这种方式会将被导入模块中所有通过命名导出和默认导出的内容都绑定到一个对象上。

19.3.1 整体加载命名导出的模块内容

使用 import * as <module> 语法来整体加载一个模块中通过命名导出的所有内容。例如:

// app.js
import * as math from './math.js';
console.log(math.PI); // 输出 3.1415926
console.log(math.square(2)); // 输出 4

在上面的例子中,import * as mathmath.js 模块中通过命名导出的所有内容都绑定到一个名为 math 的对象上,我们可以通过该对象来访问模块中导出的所有内容。

19.3.2 整体加载默认导出的模块内容

对于默认导出的内容,也可以使用整体加载的方式来导入模块中的默认导出内容。例如:

// app.js
import * as math from './math.js';
console.log(math.default.PI); // 输出 3.1415926
console.log(math.default.square(2)); // 输出 4

在这个例子中,假设 math.js 模块中通过 export default 导出了一个对象,那么我们同样可以使用整体加载的方式来访问默认导出的内容。

整体加载的方式使得我们可以一次性地将一个模块中的所有导出内容引入到当前模块中,并且通过对象的方式进行访问。这种方式在需要使用大量模块内容时非常方便,并且可以有效地避免命名冲突。

19.4 模块的继承

在 ES6 中,模块之间可以通过导入和导出来实现继承关系。通过导入父模块中的内容,子模块可以使用父模块中导出的变量、函数、类等内容,并在此基础上进行扩展。继承分为两种方式:命名导出和默认导出。

命名导出

可以在父模块中使用 export 命令将需要继承的内容导出,然后在子模块中使用 import 命令将它们导入。

例如,我们有一个名为 math.js 的模块,其中定义了两个函数:

// math.js
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

现在,如果我们想在另一个模块中继承这些函数并添加一些额外的功能,我们可以创建另一个名为 advancedMath.js 的模块,并使用 import 命令将 addsubtract 函数导入:

// advancedMath.js
import { add, subtract } from './math.js';

export function multiply(a, b) {
  return a * b;
}

export function addAndMultiply(a, b, c) {
  const sum = add(a, b);
  return multiply(sum, c);
}

export function subtractAndDivide(a, b, c) {
  const difference = subtract(a, b);
  return difference / c;
}

在上述示例中,advancedMath.js 模块导入了 math.js 模块中的 addsubtract 函数,并在此基础上定义了 multiplyaddAndMultiplysubtractAndDivide 函数。通过这种方式,advancedMath.js 模块实现了对 math.js 模块的继承。

其他模块可以导入 advancedMath.js 模块并使用其中的函数,包括继承自 math.js 模块的函数。

默认导出

除了命名导出,还可以通过默认导出来实现模块之间的继承。默认导出允许将一个值、函数或类作为模块的默认输出,其他模块可以通过导入该模块的默认输出来访问它。

例如,我们有一个名为 math.js 的模块,其中定义了一个函数:

// math.js
export default function add(a, b) {
  return a + b;
}

现在,如果我们想在另一个模块中继承该函数并添加一些额外的功能,我们可以创建另一个名为 advancedMath.js 的模块,并使用 import 命令将默认输出导入:

// advancedMath.js
import add from './math.js';

export function multiply(a, b) {
  return a * b;
}

export function addAndMultiply(a, b, c) {
  const sum = add(a, b);
  return multiply(sum, c);
}

在上述示例中,advancedMath.js 模块导入了 math.js 模块的默认输出 add 函数,并在此基础上定义了 multiplyaddAndMultiply 函数。通过这种方式,advancedMath.js 模块实现了对 math.js 模块的继承。

其他模块可以导入 advancedMath.js 模块并使用其中的函数,包括继承自 math.js 模块的默认输出函数。

通过命名导出和默认导出,可以轻松地实现模块之间的继承关系,并在子模块中扩展父模块的功能。这种模块化的继承方式提供了更好的代码组织和复用性,使得应用程序更加可维护和可扩展。

19.5 CommonJS规范

CommonJS 规范是一种在 JavaScript 中实现模块化开发的规范,它最初是为了解决服务器端 JavaScript 缺乏标准模块系统的问题而提出的。CommonJS 规范定义了一组API,包括 requireexportsmodule等,用来实现模块的定义、导入和导出等操作。

CommonJS 是 Node.js 中使用的模块系统,它采用动态加载的方式来导入和导出模块。这意味着 CommonJS 允许在运行时动态修改和加载模块。CommonJS 的导入和导出是基于对象的,通过 module.exports 导出模块,通过 require 函数加载模块。

// math.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = { add, subtract };

还可以使用 exports 导出变量、函数或类等,exportsmodule.exports 的一个引用。它是一个空对象,初始时与 module.exports 相等。

// math.js
exports.add = function(a, b) {
  return a + b;
};

exports.subtract = function(a, b) {
  return a - b;
};

注意:当你使用 module.exports 导出一个单独的函数、类或对象时,不能再使用 exports 对象来导出其他内容。因为在赋值过程中,exports 对象和 module.exports 之间的引用关系会断开。

在 CommonJS 规范中,模块的导出可以通过两种方式实现:module.exportsexports。无论是使用 module.exports 还是 exports,最终都是将要导出的内容赋值给 module.exports,以便其他模块可以通过 require 函数进行导入。

// app.js
const math = require('./math.js');

console.log(math.add(3, 4)); // 输出:7
console.log(math.subtract(5, 2)); // 输出:3

ES6 模块和 CommonJS 的差异主要在两个方面。首先,ES6 模块是基于编译时静态分析的,而 CommonJS 是基于运行时动态加载的。其次,ES6 模块的导入和导出语法与 CommonJS 不同,导致在使用上存在一些差异。因此,在开发过程中,需要根据实际情况选择合适的模块系统。如果你使用的是 ES6 语言特性,那么就应该优先考虑使用 ES6 模块;如果你在 Node.js 环境下开发,那么就应该使用 CommonJS。

19.6 循环加载

ES6 模块允许循环加载,即模块 A 中可以引入模块 B,同时模块 B 也可以引入模块 A。这种循环加载的情况在实际开发中可能会出现,但需要注意避免出现过度复杂的循环依赖关系,以免导致代码难以理解和维护。

假设有两个模块 moduleA.js 和 moduleB.js:

moduleA.js:

import { funcB } from './moduleB.js';

export function funcA() {
  console.log('Function A');
  funcB();
}

moduleB.js:

import { funcA } from './moduleA.js';

export function funcB() {
  console.log('Function B');
  funcA();
}

在这个示例中,moduleA.js 中引入了 moduleB.js 中的函数 funcB,而 moduleB.js 中也引入了 moduleA.js 中的函数 funcA。当任何一个模块中的函数被调用时,都会触发另一个模块中的函数,从而形成循环加载的关系。

在大多数情况下,JavaScript 引擎能够正确处理循环加载,并保证每个模块只会执行一次。然而,如果存在复杂的循环依赖关系,就可能会导致意想不到的问题,比如模块未完全初始化、内存泄漏等。因此,在设计模块结构时,应尽量避免复杂的循环依赖关系,或者考虑重构代码结构以消除循环依赖。