当前位置: 首页 > news >正文

ECMAScript6-学习指南-全-

ECMAScript6 学习指南(全)

原文:zh.annas-archive.org/md5/2e74de015757f2bd7f53f3ea6c57c46f

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

ECMAScript 是由 Ecma International 在 ECMA-262 规范和 ISO/IEC 16262 中标准化的脚本语言。JavaScript、JScript 和 ActionScript 等脚本语言是 ECMAScript 的超集。尽管 JavaScript、JScript 和 ActionScript 比 ECMAScript 具有更多功能,通过定义更多的对象和方法,这些语言的核心特性与 ECMAScript 相同。

ECMAScript 6 是 ECMAScript 语言的第六版和第七版。简而言之,它也被称为“ES6”。

尽管 JavaScript 非常强大且灵活,但它经常因不必要的冗余而受到批评。因此,JavaScript 开发者经常使用像 CoffeeScript 和 Typescript 这样的抽象,它们提供更简单的语法、强大的功能,并编译成 JavaScript。ES6 的引入是为了改进 JavaScript,并确保开发者不再需要使用抽象或其他技术来编写高质量的代码,这已经成为一个漫长的过程。

ES6 的特性继承自其他流行的抽象语言,如 CoffeeScript。因此,ES6 语言特性在其他语言中的行为方式相同,即使在 JavaScript 中它们是新的,在编程世界中也不算新。

本书为 ECMAScript 6 新版本的所有特性提供了带示例的解释。本书是关于 ECMAScript 6 的 JavaScript 实现的。本书中的所有特性和示例都在所有 JavaScript 环境中工作,例如浏览器、Node.js、Cordova 等。

本书涵盖的内容

第一章, 玩转语法,介绍了创建变量和函数参数的新方法。本章更深入地讨论了新的对象和函数语法。

第二章, 了解你的库,介绍了现有对象的新基于原型的方法。

第三章, 使用迭代器,展示了 ES6 中可用的不同类型的迭代器以及如何创建自定义迭代器。它还讨论了 ES6 中的尾调用优化。

第四章, 异步编程,说明了 Promise 如何使编写异步执行代码更容易。

第五章, 实现 Reflect API,提供了对 ES6 中对象反射的深入指南。

第六章, 使用代理,展示了如何使用 ES6 代理定义对对象的基本操作的自定义行为。

第七章, 带你走进类,介绍了使用 ES6 类进行面向对象编程。这里解释了诸如继承、构造函数、抽象、信息隐藏等概念。

第八章,模块化编程,解释了使用 JavaScript 创建模块的不同方法。涵盖了诸如 IIFE、CommonJS、AMD、UMD 和 ES6 模块等技术。

你需要这本书什么

如果你是在所有 JavaScript 引擎完全支持 ES6 之后阅读这本书,那么你不需要设置任何特定的测试环境。你可以在你选择的任何引擎上简单地测试示例。

如果你在所有 JavaScript 引擎完全支持 ES6 之前阅读这本书,那么请继续阅读这本书并执行可以使用 ES6 编译器转换的代码片段。如果你想在浏览器环境中运行代码示例,那么请使用这个示例网页模板,它已经附带了 Traceur 编译器,用于在每次页面加载时将 ES6 转换为 ES5:

<!doctype html>
<html>
<head>...</head>
<body>
...<script src="img/traceur.js"></script>
<script src="img/bootstrap.js"></script>
<script type="module">//Place ES6 code here
</script>
</body>
</html>

google.github.io/traceur-compiler/bin/traceur.js下载traceur.js脚本,从google.github.io/traceur-compiler/src/bootstrap.js下载bootstrap.js脚本。然后,将它们放在包含前面代码的 HTML 文件相同的目录下。

在练习文件(代码包)中,Traceur 编译器和填充程序已经附上。练习文件是为了在浏览器上测试代码示例而创建的。

对于第四章,异步编程,你将不得不使用浏览器环境进行测试,因为我们已经在示例中使用了 jQuery 和 AJAX。你还需要一个 Web 服务器来支持它。

对于第八章,模块化编程,如果你使用浏览器环境进行测试,那么你需要一个 Web 服务器。但如果你使用 Node.js 环境,那么你不需要 Web 服务器。

与 ECMAScript 6 的兼容性

这本书是在所有 JavaScript 引擎开始支持 ES6 的所有特性之前编写的。

ES6 的规范已经完成。只是并非所有 JavaScript 引擎都完成了 ES6 所有特性的实现。我相当确信到 2016 年底,所有 JavaScript 引擎都将支持 ES6。

Kangax 创建了一个 ES6 兼容性表格,你可以在这里跟踪各种 JavaScript 引擎对 ES6 各种特性的支持情况。你可以在这个表格中找到kangax.github.io/compat-table/es6/

在不兼容的引擎中运行 ECMAScript 6

如果你想在不支持 ES6 的引擎中运行 ES6,那么你可以使用 ES6 填充程序或 ES6 编译器。

Polyfill 是一段代码,它提供了开发者期望 JavaScript 引擎原生提供的功能。请记住,并非每个 ES6 功能都有 Polyfill,而且它们也不能被创建。所有可用的 Polyfill 及其下载链接可在 github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#ecmascript-6-harmony 找到。

ES6 编译器将 ES6 源代码转换为 ES5 源代码,这与所有 JavaScript 引擎兼容。编译器支持比 Polyfill 转换更多功能,但可能不支持 ES6 的所有功能。有各种编译器可用,例如 Google Traceur (github.com/google/traceur-compiler)、Google Caja (developers.google.com/caja/)、Babel (babeljs.io/)、Termi ES6 Transpiler (github.com/termi/es6-transpiler) 等。您应该在将 ES6 代码附加到网页之前始终将其转换为 ES5,而不是每次页面加载时在客户端进行转换,这样网页就不会加载得更慢。

因此,通过使用编译器和/或 Polyfill,您可以在所有引擎完全支持 ES6 以及非 ES6 引擎变得过时之前,就开始编写用于分发的 ES6 代码。

本书面向对象

本书面向任何熟悉 JavaScript 的人。您不必是 JavaScript 专家就能理解这本书。这本书将帮助您将 JavaScript 知识提升到下一个层次。

惯例

在本书中,您将找到多种文本样式,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理方式如下:“我们可以通过使用include指令来包含其他上下文。”

代码块设置如下:

var a = 12; //accessible globallyfunction myFunction()
{console.log(a);var b = 13; //accessible throughout functionif(true){var c = 14; //accessible throughout functionconsole.log(b);}console.log(c);
}myFunction();

新术语重要词汇以粗体显示。

注意

警告或重要注意事项以如下框的形式出现。

小贴士

小技巧和技巧如下所示。

读者反馈

我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中受益的标题非常重要。

要向我们发送一般反馈,只需发送电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南 www.packtpub.com/authors。

客户支持

现在您是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有勘误都可以通过从www.packtpub.com/support选择您的标题来查看。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现任何我们作品的非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们作者和我们为您提供有价值内容的能力方面的帮助。

询问

如果您在本书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们将尽力解决。

第一章. 玩转语法

JavaScript 在与声明常量变量、声明块作用域变量、从数组中提取数据、函数声明更简短的语法等各种语法形式相比时,落后于一些其他编程语言。ES6 为 JavaScript 添加了许多基于新语法的特性,这有助于开发者写出更少但功能更强大的代码。ES6 还防止程序员使用各种为了达到各种目标而采取的“黑客”手段,这些手段会对性能产生负面影响,并使代码更难以阅读。在本章中,我们将探讨 ES6 引入的新语法特性。

在本章中,我们将涵盖:

  • 使用 let 关键字创建块作用域变量

  • 使用 const 关键字创建常量变量

  • 扩展运算符和剩余参数

  • 使用解构赋值从可迭代对象和对象中提取数据

  • 箭头函数

  • 创建对象属性的新语法

使用 let 关键字

ES6 的 let 关键字用于声明一个块作用域变量,可选地初始化它为一个值。来自其他编程语言背景但新接触 JavaScript 的程序员,常常会编写出容易出错的 JavaScript 程序,认为 JavaScript 变量是块作用域的。几乎每种流行的编程语言在变量作用域方面都有相同的规则,但 JavaScript 由于缺乏块作用域变量而略有不同。由于 JavaScript 变量不是块作用域的,存在内存泄漏的风险,而且 JavaScript 程序也更难以阅读和调试。

声明函数作用域变量

使用 var 关键字声明的 JavaScript 变量被称为 函数作用域 变量。函数作用域变量在整个脚本中全局可访问,即如果在外部声明,则在整个脚本中可访问。同样,如果函数作用域变量在函数内部声明,则它们在整个函数中可访问,但在函数外部不可访问。

这里有一个示例,展示了如何创建函数作用域变量:

var a = 12; //accessible globallyfunction myFunction()
{console.log(a);var b = 13; //accessible throughout functionif(true){var c = 14; //accessible throughout functionconsole.log(b);}console.log(c);
}myFunction();

代码的输出如下:

12
13
14

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户中下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

在这里,您可以看到 c 变量在 if 语句外部是可访问的,但在其他编程语言中并非如此。因此,来自其他语言的程序员会期望 c 变量在 if 语句外部是未定义的,但事实并非如此。因此,ES6 引入了 let 关键字,它可以用来创建块作用域变量。

声明块作用域变量

使用let关键字声明的变量被称为块作用域变量。当在函数外部声明时,块作用域变量的行为与函数作用域变量相同,即它们是全局可访问的。但是,当块作用域变量在块内部声明时,它们只在其定义的块内部(以及任何子块)可访问,而不可在块外部访问。

注意

块用于组合零个或多个语句。一对花括号定义了块,即{}

让我们拿之前的示例脚本,将var替换为let关键字,看看输出结果:

let a = 12; //accessible globallyfunction myFunction()
{console.log(a);let b = 13; //accessible throughout functionif(true){let c = 14; //accessible throughout the "if" statementconsole.log(b);}console.log(c);
}myFunction();

代码的输出是:

12
13
Reference Error Exception

现在,输出结果符合习惯使用其他编程语言的程序员的预期。

重新声明变量

当你使用var关键字声明一个变量,而这个变量在同一作用域内已经使用var关键字声明时,它会被覆盖。考虑以下示例:

var a = 0;
var a = 1;console.log(a);function myFunction()
{var b = 2;var b = 3;console.log(b);
}myFunction();

代码的输出是:

1
3

输出结果符合预期。但是使用let关键字创建的变量表现方式并不相同。

当你使用let关键字声明一个变量,而这个变量在同一作用域内已经使用let关键字声明时,它会抛出一个TypeError异常。考虑以下示例:

let a = 0;
let a = 1; //TypeErrorfunction myFunction()
{let b = 2;let b = 3; //TypeErrorif(true){let c = 4;let c = 5; //TypeError}
}myFunction();

当你使用一个在函数(或内部函数)中已经可访问的名称声明一个变量,或者使用varlet关键字分别创建一个子块时,那么它是一个不同的变量。这里有一个示例来展示这种行为:

var a = 1;
let b = 2;function myFunction()
{var a = 3; //different variablelet b = 4; //different variableif(true){var a = 5; //overwrittenlet b = 6; //different variableconsole.log(a);console.log(b);}console.log(a);console.log(b);
}myFunction();console.log(a);
console.log(b);

代码的输出是:

5
6
5
4
1
2

注意

varlet,哪一个该用?

当编写 ES6 代码时,建议切换到使用let关键字,因为它使脚本更节省内存,防止作用域错误,防止意外错误,并使代码更容易阅读。但如果你已经习惯了var关键字并且使用起来很舒服,那么你仍然可以使用它。

你可能想知道为什么不直接使用var关键字来定义块作用域变量,而不是引入let关键字?之所以没有将var关键字做得足够用来定义块作用域变量,而是引入let关键字,是为了向后兼容。

const关键字

ES6 的const关键字用于声明只读变量,即值不能重新分配的变量。在 ES6 之前,程序员通常会在应该为常量的变量前加上前缀。例如,看看以下代码:

var const_pi = 3.141;
var r = 2;
console.log(const_pi * r * r); //Output "12.564"

pi的值应该始终保持不变。在这里,尽管我们已添加了前缀,但仍有可能在程序中的某个地方意外更改其值,因为它们没有对pi值的原生保护。添加前缀只是不足以追踪常量变量。

因此,引入了const关键字来为常量变量提供原生保护。所以,之前的程序在 ES6 中应该这样编写:

const pi = 3.141;
var r = 2;console.log(pi * r * r); //Output "12.564"pi = 12; //throws read-only exception

在这里,当我们尝试改变 pi 的值时,抛出了一个只读异常。

常量变量的作用域

常量变量是块作用域变量,即它们遵循与使用 let 关键字声明的变量相同的作用域规则。以下是一个示例,展示了常量变量的作用域:

const a = 12; //accessible globallyfunction myFunction()
{console.log(a);const b = 13; //accessible throughout functionif(true){const c = 14; //accessible throughout the "if" statementconsole.log(b);}console.log(c);
}myFunction();

上述代码的输出是:

12
13
ReferenceError Exception

在这里,我们可以看到,常量变量在作用域规则方面与块作用域变量表现相同。

使用常量变量引用对象

当我们将一个对象赋值给一个变量时,变量持有的是对象的引用而不是对象本身。因此,当将对象赋值给一个常量变量时,对象的引用就变成了对该变量的常量引用,而不是对对象本身的引用。因此,对象是可变的。

考虑这个例子:

const a = {"name" : "John"
};console.log(a.name);a.name = "Eden";console.log(a.name);a = {}; //throws read-only exception

上述代码的输出是:

John
Eden
a is read only: Exception

在这个例子中,a 变量存储了对象的地址(即引用)。因此,对象的地址是 a 变量的值,且不能被改变。但是对象是可变的。所以当我们尝试将另一个对象赋值给 a 变量时,我们得到了一个异常,因为我们试图改变 a 变量的值。

默认参数值

在 JavaScript 中,没有定义的方法来为未传递的函数参数分配默认值。因此,程序员通常检查具有 undefined 值的参数(因为它是缺失参数的默认值),并将默认值分配给它们。以下是一个示例,展示了如何做到这一点:

function myFunction(x, y, z)
{x = x === undefined ? 1 : x;y = y === undefined ? 2 : y;z = z === undefined ? 3 : z;console.log(x, y, z); //Output "6 7 3"
}
myFunction(6, 7);

ES6 提供了一种新的语法,可以更简单地实现这一点。以下代码演示了如何在 ES6 中实现这一点:

function myFunction(x = 1, y = 2, z = 3)
{console.log(x, y, z); // Output "6 7 3"
}myFunction(6,7);

此外,传递 undefined 被视为缺少参数。以下是一个示例来演示这一点:

function myFunction(x = 1, y = 2, z = 3)
{console.log(x, y, z); // Output "1 7 9"
}myFunction(undefined,7,9);

默认值也可以是表达式。以下是一个示例来演示这一点:

function myFunction(x = 1, y = 2, z = 3 + 5)
{console.log(x, y, z); // Output "6 7 8"
}myFunction(6,7);

扩展运算符

扩展运算符由""符号表示。扩展运算符将可迭代对象拆分为单独的值。

注意

可迭代对象是包含一组值并实现 ES6 可迭代协议的对象,使我们能够遍历其值。数组是内置的可迭代对象的一个例子。

扩展运算符可以放置在代码中期望多个函数参数或多个元素(对于数组字面量)的任何位置。

扩展运算符通常用于将可迭代对象的值扩展到函数的参数中。让我们以一个数组为例,看看如何将其拆分为函数的参数。

在 ES6 之前,为了将数组的值作为函数参数提供,程序员使用函数的 apply() 方法。以下是一个示例:

function myFunction(a, b)
{return a + b;
}var data = [1, 4];
var result = myFunction.apply(null, data);console.log(result); //Output "5"

在这里,apply 方法接受一个数组,提取其值,将它们作为单独的参数传递给函数,然后调用它。

ES6 提供了一种简单的方法来实现这一点,使用扩展运算符。以下是一个示例:

function myFunction(a, b)
{return a + b;
}let data = [1, 4];
let result = myFunction(...data);
console.log(result); //Output "5"

在运行时,在 JavaScript 解释器调用myFunction函数之前,它将…data替换为1,4表达式:

let result = myFunction(...data);

之前的代码被替换为:

let result = myFunction(1,4);

在此之后,函数被调用。

注意

扩展操作符不调用apply()方法。JavaScript 运行时引擎使用迭代协议来展开数组,这与apply()方法无关,但行为相同。

扩展操作符的其他用法

扩展操作符不仅限于将可迭代对象展开为函数参数,它还可以用于代码中期望多个元素(对于数组字面量)的任何地方。因此,它有很多用途。让我们看看扩展操作符在数组中的其他一些用例。

将数组值作为另一个数组的一部分

它也可以用来将数组值作为另一个数组的一部分。以下是一个示例代码,演示了如何在创建数组的同时将现有数组的值作为另一个数组的一部分。

let array1 = [2,3,4];
let array2 = [1, ...array1, 5, 6, 7];console.log(array2); //Output "1, 2, 3, 4, 5, 6, 7"

这里是以下行:

let array2 = [1, ...array1, 5, 6, 7];

将被替换为以下行:

let array2 = [1, 2, 3, 4, 5, 6, 7];

将数组值推入另一个数组

有时,我们可能需要将现有数组的值推入另一个现有数组的末尾。

在 ES6 之前,程序员通常这样做:

var array1 = [2,3,4];
var array2 = [1];Array.prototype.push.apply(array2, array1);console.log(array2); //Output "1, 2, 3, 4"

但在 ES6 中,我们有一个更简洁的方式来处理,如下所示:

let array1 = [2,3,4];
let array2 = [1];array2.push(...array1);console.log(array2); //Output "1, 2, 3, 4"

这里,push 方法接受一系列变量,并将它们添加到被调用数组的末尾。

这里是以下行:

array2.push(...array1);

将被替换为以下行:

array2.push(2, 3, 4);

扩展多个数组

多个数组可以被展开到一行表达式上。例如,以下代码:

let array1 = [1];
let array2 = [2];
let array3 = [...array1, ...array2, ...[3, 4]];//multi array spread
let array4 = [5];function myFunction(a, b, c, d, e)
{return a+b+c+d+e;
}let result = myFunction(...array3, ...array4); //multi array spreadconsole.log(result); //Output "15"

剩余参数

剩余参数也由“”标记表示。带有“”前缀的函数的最后一个参数被称为剩余参数。剩余参数是一个数组类型,它包含当参数数量超过命名参数数量时函数的其余参数。

剩余参数用于在函数内部捕获可变数量的函数参数。

在 ES6 之前,程序员使用函数的arguments对象来检索传递给函数的额外参数。arguments对象不是一个数组,但它提供了一些类似于数组的接口。

这里有一个代码示例,展示了如何使用arguments对象来检索额外的参数:

function myFunction(a, b)
{var args = Array.prototype.slice.call(arguments, myFunction.length);console.log(args);
}myFunction(1, 2, 3, 4, 5); //Output "3, 4, 5"

在 ES6 中,这可以通过使用剩余参数以更简单、更干净的方式完成。以下是一个使用剩余参数的示例:

function myFunction(a, b, ...args)
{console.log(args); //Output "3, 4, 5"
}myFunction(1, 2, 3, 4, 5);

arguments对象不是一个数组对象。因此,要在arguments对象上执行数组操作,您需要首先将其转换为数组。由于 ES6 的剩余参数是数组类型,与之一起工作更容易。

注意

“…”标记被称为什么?

“…”标记被称为扩展操作符或剩余参数,具体取决于其使用方式和位置。

解构赋值

解构赋值是一个表达式,允许您使用类似于数组或对象构造字面量的语法将可迭代或对象的值或属性分配给变量。

解构赋值通过提供更短的语法,使得从可迭代对象或对象中提取数据变得容易。解构赋值已经在编程语言中存在,例如PerlPython,并且它们的工作方式在所有地方都是相同的。

有两种解构赋值表达式——数组和对象解构赋值。让我们详细看看每一种。

数组解构赋值

数组解构赋值用于从可迭代对象中提取值并将它们分配给变量。它被称为数组解构赋值,因为表达式类似于数组构造字面量。

在 ES6 之前,程序员通常这样做来将数组值分配给变量:

var myArray = [1, 2, 3];
var a = myArray[0];
var b = myArray[1];
var c = myArray[2];

在这里,我们正在提取数组的值并将它们分别分配给abc变量。

在 ES6 中,我们可以通过一行语句使用数组解构赋值来完成此操作:

let myArray = [1, 2, 3];
let a, b, c;[a, b, c] = myArray; //array destructuring assignment syntax

如您所见,[a, b, c]是数组解构表达式。

在数组解构语句的左侧,我们需要放置想要分配数组值的变量,使用与数组字面量类似的语法。在右侧,我们需要放置一个数组(实际上可以是任何可迭代对象),从中提取我们想要的值。

之前的示例代码可以这样进一步缩短:

let [a, b, c] = [1, 2, 3];

在这里,我们在同一语句中创建变量,而不是提供数组变量,而是提供具有构造字面量的数组。

如果数组中的变量数量少于项目数量,则只考虑前几个项目。

注意

如果在数组解构赋值语法的右侧放置一个非可迭代对象,则会抛出TypeError异常。

忽略值

我们也可以忽略可迭代对象中的一些值。以下是一个示例代码,展示了如何做到这一点:

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

输出如下:

1
3

在数组解构赋值中使用剩余操作符

我们可以使用“”标记作为数组解构表达式最后一个变量的前缀。在这种情况下,如果其他变量的数量少于可迭代对象中的值,则该变量始终被转换为数组对象,该对象包含可迭代对象剩余的值。

考虑以下示例来理解它:

let [a, ...b] = [1, 2, 3, 4, 5, 6];console.log(a);
console.log(Array.isArray(b));
console.log(b);

输出如下:

1
true
2,3,4,5,6

在之前的示例代码中,您可以看到b变量被转换为数组,并且它包含右侧数组中所有其他值。

这里,“”标记被称为剩余操作符

我们也可以在使用剩余操作符时忽略值。以下是一个示例来演示这一点:

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

输出如下:

1
4,5,6

这里,我们忽略了2, 3值。

变量的默认值

在展开过程中,如果你提供一个数组索引是undefined,你也可以为变量提供默认值。以下是一个演示此功能的示例:

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

嵌套数组展开

我们还可以从多维数组中提取值并将它们分配给变量。以下是一个演示此功能的示例:

let [a, b, [c, d]] = [1, 2, [3, 4]];

将展开赋值用作参数

我们还可以使用数组展开表达式作为函数参数,以提取作为函数参数传递的可迭代对象的值。以下是一个演示此功能的示例:

function myFunction([a, b, c = 3])
{console.log(a, b, c); //Output "1 2 3"
}myFunction([1, 2]);

在本章前面,我们看到了如果我们将undefined作为参数传递给函数调用,那么 JavaScript 会检查默认参数值。因此,我们也可以在这里提供一个默认数组,如果参数是undefined,则将使用该数组。以下是一个演示此功能的示例:

function myFunction([a, b, c = 3] = [1, 2, 3])
{console.log(a, b, c);  //Output "1 2 3"
}myFunction(undefined);

这里,我们传递了undefined作为参数,因此使用了默认数组[1, 2, 3]来提取值。

对象展开赋值

对象展开赋值用于提取对象的属性值并将它们分配给变量。

在 ES6 之前,程序员通常以以下方式将对象的属性值赋给变量:

var object = {"name" : "John", "age" : 23};
var name = object.name;
var age = object.age;

在 ES6 中,我们可以通过一行语句来完成这个操作,使用对象展开赋值:

let object = {"name" : "John", "age" : 23};
let name, age;({name, age} = object); //object destructuring assignment syntax

在对象展开语句的左侧,我们需要放置想要将对象属性值赋给变量的变量,使用与对象字面量类似的语法。在右侧,我们需要放置一个包含我们想要提取的属性值的对象,并最终使用( )标记来关闭语句。

在这里,变量名必须与对象属性名相同。如果你想分配不同的变量名,可以这样做:

let object = {"name" : "John", "age" : 23};
let x, y;({name: x, age: y} = object);

之前的代码可以通过这种方式进一步缩短:

let {name: x, age: y} = {"name" : "John", "age" : 23};

在这里,我们是在同一行上创建变量和对象。由于我们是在同一语句上创建变量,因此不需要使用( )标记来关闭语句。

变量的默认值

如果在展开对象时对象属性是undefined,你也可以为变量提供默认值。以下是一个演示此功能的示例:

let {a, b, c = 3} = {a: "1", b: "2"};
console.log(c); //Output "3"

展开计算属性名

一些属性名是使用表达式动态构建的。在这种情况下,为了提取属性值,我们可以使用[ ]标记来提供一个属性名表达式。以下是一个示例:

let {["first"+"Name"]: x} = { firstName: "Eden" };
console.log(x); //Output "Eden"

展开嵌套对象

我们还可以从嵌套对象中提取属性值,即对象内的对象。以下是一个演示此功能的示例:

var {name, otherInfo: {age}} = {name: "Eden", otherInfo: {age: 23}};
console.log(name, age); //Eden 23

将对象展开赋值用作参数

就像数组展开赋值一样,我们也可以将对象展开赋值用作函数参数。以下是一个演示此功能的示例:

functionmyFunction({name = 'Eden', age = 23, profession = "Designer"} = {})
{console.log(name, age, profession); //Output "John 23 Designer"
}myFunction({name: "John", age: 23});

这里,我们传递了一个空对象作为默认参数值,如果将 undefined 作为函数参数传递,它将用作默认对象。

箭头函数

ES6 提供了一种使用 => 操作符创建函数的新方法。这些函数被称为 箭头 函数。这种方法具有更短的语法,箭头函数是无名函数。

这里有一个示例,展示了如何创建一个箭头函数:

let circleArea = (pi, r) => {let area = pi * r * r;return area;
}let result = circleArea(3.14, 3);console.log(result); //Output "28.26"

在这里,circleArea 是一个变量,引用了匿名箭头函数。前面的代码与以下 ES5 代码类似:

Var circleArea = function(pi, r) {var area = pi * r * r;return area;
}var result = circleArea(3.14, 3);console.log(result); //Output "28.26"

如果箭头函数只包含一个语句,那么你不需要使用 {} 括号来包裹代码。以下是一个示例:

let circleArea = (pi, r) => pi * r * r;
let result = circleArea(3.14, 3);console.log(result); //Output "28.26"

当没有使用 {} 括号时,则语句体的值会自动返回。

箭头函数中 "this" 的值

在箭头函数中,this 关键字的值与封闭作用域(全局或函数作用域,无论箭头函数在哪里定义)中 this 关键字的值相同,而不是指向上下文对象(即函数作为属性所在的那个对象),这是传统函数中 this 的值。

考虑以下示例,以了解传统函数和箭头函数的 this 值之间的差异:

var object = {f1: function(){console.log(this);var f2 = function(){ console.log(this); }f2();setTimeout(f2, 1000);}
}object.f1();

输出如下:

Object
Window
Window

在这里,f1 函数内部的 this 指向 object,因为 f1 是它的属性。f2 函数内部的 this 指向 window 对象,因为 f2window 对象的属性。

但是在箭头函数中,this 的行为有所不同。让我们将前面的代码中的传统函数替换为箭头函数,并查看 this 的值:

var object = {f1: () => {console.log(this);var f2 = () => { console.log(this); }f2();setTimeout(f2, 1000);}
}object.f1();

输出如下:

Window
Window
Window

在这里,f1 函数内部的 this 复制了全局作用域的 this 值,因为 f1 位于全局作用域。f2 函数内部的 this 复制了 f1this 值,因为 f2 位于 f1 作用域。

箭头函数与传统函数之间的其他差异

箭头函数不能用作对象构造函数,也就是说,不能对它们应用 new 操作符。

除了语法、this 值和 new 操作符之外,箭头函数与传统函数之间没有其他差异,也就是说,它们都是 Function 构造函数的实例。

增强的对象字面量

ES6 为 {} 对象字面量添加了一些基于语法的扩展,用于创建属性。让我们看看它们:

定义属性

ES6 提供了一种更短的语法来将对象属性分配给具有相同名称的变量值。

在 ES5 中,你一直是这样做:

var x = 1, y = 2;
var object = {x: x,y: y
};console.log(object.x); //output "1"

在 ES6 中,你可以这样操作:

let x = 1, y = 2;
let object = { x, y };console.log(object.x); //output "1"

定义方法

ES6 提供了一种新的语法来定义对象上的方法。以下是一个演示新语法的示例:

let object = {myFunction(){console.log("Hello World!!!"); //Output "Hello World!!!"}
}object.myFunction();

这种简洁的函数允许在其中使用 super,而对象的传统方法不允许使用 super。我们将在本书的后面了解更多关于它的内容。

计算属性名称

在运行时评估的属性名称被称为计算属性名称。通常,表达式会被解析以动态地找到属性名称。

在 ES5 中,计算属性是这样定义的:

var object = {};object["first"+"Name"] = "Eden";//"firstName" is the property name//extract
console.log(object["first"+"Name"]); //Output "Eden"

在这里,在创建对象之后,我们将属性附加到对象上。但在 ES6 中,我们可以在创建对象的同时添加具有计算名称的属性。以下是一个示例:

let object = {["first" + "Name"]: "Eden",
};//extract
console.log(object["first" + "Name"]); //Output "Eden"

摘要

在本章中,我们学习了变量的作用域、只读变量、将数组拆分为单个值、向函数传递不定参数、从对象和数组中提取数据、箭头函数以及创建对象属性的新语法。

在下一章中,我们将学习内置对象和符号,并发现 ES6 为字符串、数组和对象添加的新属性。

第二章:熟悉你的库

ES6 向内置的 JavaScript 对象添加了许多新的属性和方法,以便程序员可以轻松地完成繁琐的任务。这些新功能旨在帮助开发者摆脱使用黑客和易出错的技巧来完成与数字、字符串和数组相关的各种操作。在本章中,我们将查看 ES6 添加到原生对象的所有新功能。

在本章中,我们将介绍:

  • Number 对象的新属性和方法

  • 将数字常量表示为二进制或八进制

  • Math 对象的新属性和方法

  • 创建多行字符串和 String 对象的新方法

  • Array 对象的新属性和方法

  • 什么是 Map 和 Set?

  • 使用数组缓冲区和类型化数组

  • Object 对象的新属性和方法

与数字一起工作

ES6 增加了创建数字的新方法以及 Number 对象的新属性,以便更容易地处理数字。ES6 通过增强 Number 对象来简化创建数学丰富应用的过程,并防止导致错误的常见误解。ES6 还提供了在 ES5 中已经可能做到的事情的新方法,例如将数字常量表示为八进制。

注意

JavaScript 将数字表示为十进制。默认情况下,数字常量被解释为十进制。

二进制表示法

在 ES5 中,没有原生的方法来表示二进制的数字常量。但在 ES6 中,你可以使用 0b 符号来前缀数字常量,使 JavaScript 将它们解释为二进制。

这里有一个例子:

let a = 0b00001111;
let b = 15;console.log(a === b);
console.log(a);

输出如下:

true
15

这里,0b00001111 是 15 的二进制表示,基于十进制的十进制。

八进制表示法

在 ES5 中,要表示八进制的数字常量,我们需要使用 0 前缀。例如,看看下面的例子:

var a = 017;
var b = 15;console.log(a === b);
console.log(a);

输出如下:

true
15

但是,对于初学者来说,八进制表示法可能会造成混淆,因为他们认为以 0 开头的十进制数与 17 相同。例如,他们认为 01717 相同。因此,为了消除这种混淆,ES6 允许我们使用 0o 前缀来使 JavaScript 将这些数字常量解释为八进制。

这里有一个例子来演示这一点:

let a = 0o17;
let b = 15;console.log(a === b);
console.log(a);

输出如下:

true
15

Number.isInteger(number) 方法

JavaScript 中的数字以 64 位浮点数的形式存储。因此,JavaScript 中的整数是没有小数部分的浮点数,或者小数部分全部为 0 的浮点数。

在 ES5 中,没有内置的方法来检查一个数字是否为整数。ES6 向 Number 对象添加了一个新方法,称为 isInteger(),它接受一个数字并返回 truefalse,这取决于该数字是否为整数。

这里是一个示例代码:

let a = 17.0;
let b = 1.2;console.log(Number.isInteger(a));
console.log(Number.isInteger(b));

输出如下:

true
false

Number.isNaN(value) 方法

在 ES5 中,没有方法可以检查一个变量是否包含 NaN 值。

注意

全局 isNaN() 函数用于检查一个值是否是数字。如果值不是数字,则返回 true,否则返回 false

因此,ES6 引入了一个新的 Number 对象方法,称为 isNaN(),用于检查一个值是否是 NaN。以下是一个示例,它演示了 Number.isNaN() 并解释了它与全局 isNaN() 函数的不同之处:

let a = "NaN";
let b = NaN;
let c = "hello";
let d = 12;console.log(Number.isNaN(a));
console.log(Number.isNaN(b));
console.log(Number.isNaN(c));
console.log(Number.isNaN(d));console.log(isNaN(a));
console.log(isNaN(b));
console.log(isNaN(c));
console.log(isNaN(d));

输出如下:

false
true
false
false
true
true
true
false

这里你可以看到,Number.isNaN() 方法仅在传入的值正好是 NaN 时返回 true

提示

你可能会问,为什么不用 ===== 运算符来代替 Number.isNaN(value) 方法?NaN 值是唯一一个不等于自身的值,也就是说,表达式 NaN==NaNNaN===NaN 将返回 false

Number.isFinite(number) 方法

在 ES5 中,没有内置的方法来检查一个值是否是有限数。

注意

全局 isFinite() 函数接受一个值并检查它是否是一个有限数。但不幸的是,它也会对转换为 Number 类型的值返回 true

因此,ES6 引入了 Number.isFinite() 方法,解决了 window.isFinite() 函数的问题。以下是一个演示这个特性的例子:

console.log(isFinite(10));
console.log(isFinite(NaN));
console.log(isFinite(null));
console.log(isFinite([]));console.log(Number.isFinite(10));
console.log(Number.isFinite(NaN));
console.log(Number.isFinite(null));
console.log(Number.isFinite([]));

输出如下:

true
false
true
true
true
false
false
false

Number.isSafeInteger(number) 方法

JavaScript 数字以 64 位浮点数的形式存储,遵循国际 IEEE 754 标准。这种格式使用 64 位存储数字,其中数字(分数)存储在 0 到 51 位,指数在 52 到 62 位,符号在最后一位。

因此,在 JavaScript 中,安全的整数是那些不需要四舍五入到其他整数以适应 IEEE 754 表示的数。从数学上讲,从 -(2⁵³-1) 到 (2⁵³-1) 的数被认为是安全的整数。

下面是一个演示这个特性的例子:

console.log(Number.isSafeInteger(156));
console.log(Number.isSafeInteger('1212'));
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER));
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1));
console.log(Number.isSafeInteger(Number.MIN_SAFE_INTEGER));
console.log(Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1));

输出如下:

true
false
true
false
true
false

这里,Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER 是 ES6 中引入的常量值,分别代表 (2⁵³-1) 和 -(2⁵³-1)。

Number.EPSILON 属性

JavaScript 使用的是计算机无法准确表示的二进制浮点数表示法,例如 0.1、0.2、0.3 等数字。当你的代码执行时,像 0.1 这样的数字会被四舍五入到该格式中最接近的数字,这会导致小的舍入误差。

考虑以下示例:

console.log(0.1 + 0.2 == 0.3);
console.log(0.9 - 0.8 == 0.1);
console.log(0.1 + 0.2);
console.log(0.9 - 0.8);

输出如下:

false
false
0.30000000000000004
0.09999999999999998

Number.EPSILON 属性是在 ES6 中引入的,其值大约为 2^(-52)。这个值表示在比较浮点数时一个合理的误差范围。使用这个数字,我们可以创建一个自定义函数来比较浮点数,同时忽略最小的舍入误差。

下面是一个示例代码:

functionepsilonEqual(a, b)
{return Math.abs(a - b) <Number.EPSILON;
}console.log(epsilonEqual(0.1 + 0.2, 0.3));
console.log(epsilonEqual(0.9 - 0.8, 0.1));

输出如下:

true
true

在这里,epsilonEqual() 是我们构建的用于比较两个值是否相等的自定义函数。现在,输出结果符合预期。

注意

要了解更多关于 JavaScript 的这种行为和浮点算术的信息,请访问 floating-point-gui.de/

进行数学运算

ES6 向Math对象添加了许多新方法,涉及三角学、算术和杂项。这使得开发者可以使用原生方法而不是外部数学库。原生方法针对性能进行了优化,并且具有更好的十进制精度。

与三角学相关的操作

下面是一个示例代码,展示了添加到Math对象中的所有与三角学相关的函数:

console.log(Math.sinh(0));    //hyberbolic sine of a value
console.log(Math.cosh(0));    //hyberbolic cosine of a value
console.log(Math.tanh(0));    //hyberbolic tangent of a value
console.log(Math.asinh(0));  //inverse hyperbolic sine of a value
console.log(Math.acosh(1));  //inverse hyperbolic cosine of a value
console.log(Math.atanh(0));  //inverse hyperbolic tangent of a value
console.log(Math.hypot(2, 2, 1));//Pythagoras theorem

输出如下:

0
1
0
0
0
0
3

与算术相关的操作

下面是一个示例代码,展示了添加到Math对象中的所有与算术相关的函数:

console.log(Math.log2(16));    //log base 2
console.log(Math.log10(1000)); //log base 10
console.log(Math.log1p(0));    //same as log(1 + value)
console.log(Math.expm1(0));    //inverse of Math.log1p()
console.log(Math.cbrt(8));     //cube root of a value

输出如下:

4
3
0
0
2

杂项方法

ES6 向Math对象添加了一些杂项方法。这些方法用于转换和从数字中提取信息。

Math.imul(number1, number2) 函数

Math.imul() 函数将两个数字作为 32 位整数相乘,并返回结果的下 32 位。这是 JavaScript 中进行 32 位整数乘法的唯一原生方法。

下面有一个示例来演示这一点:

console.log(Math.imul(590, 5000000)); //32-bit integer multiplication
console.log(590 * 5000000); //64-bit floating-point multiplication

输出如下:

-1344967296
2950000000

在这里,当进行乘法运算时,产生了一个如此大的数字,以至于它无法存储在 32 位中,因此丢失了低位。

Math.clz32(number) 函数

Math.clz32() 函数返回一个数字在 32 位表示中的前导零位数。

下面是一个演示此功能的示例:

console.log(Math.clz32(7));
console.log(Math.clz32(1000));
console.log(Math.clz32(295000000));

输出如下:

29
22
3

Math.sign(number) 函数

Math.sign() 函数返回一个数字的符号,表示该数字是负数、正数还是零。

这里有一个示例来演示这一点:

console.log(Math.sign(11));
console.log(Math.sign(-11));
console.log(Math.sign(0));

输出如下:

1
-1
0

从前面的代码中,我们可以看到,如果数字是正数,Math.sign() 函数返回 1;如果是负数,返回 -1;如果是零,返回 0

Math.trunc(number) 函数

Math.trunc() 函数通过移除任何小数位来返回一个数字的整数部分。以下是一个演示此功能的示例:

console.log(Math.trunc(11.17));
console.log(Math.trunc(-1.112));

输出如下:

11
-1

Math.fround(number) 函数

Math.fround() 函数将一个数字四舍五入到 32 位浮点值。以下是一个演示此功能的示例:

console.log(Math.fround(0));
console.log(Math.fround(1));
console.log(Math.fround(1.137));
console.log(Math.fround(1.5));

输出如下:

0
1
1.1369999647140503
1.5

字符串操作

ES6 提供了创建字符串的新方法,并为全局 String 对象及其实例添加了新属性,以使字符串操作更容易。与 Python 和Ruby等编程语言相比,JavaScript 中的字符串缺乏功能和能力,因此 ES6 增强了字符串以改变这一点。

在我们深入了解新的字符串功能之前,让我们回顾一下 JavaScript 的内部字符编码和转义序列。在 Unicode 字符集中,每个字符都由一个称为代码点的十进制基数表示。代码单元是在内存中存储代码点的固定位数。编码方案决定了代码单元的长度。如果使用UTF-8编码方案,则代码单元是 8 位,如果使用UTF-16编码方案,则代码单元是 16 位。如果一个代码点不适合代码单元,它将被分割成多个代码单元,即多个字符按顺序表示一个字符。

JavaScript 解释器默认将 JavaScript 源代码解释为 UTF-16 代码单元的序列。如果源代码是用 UTF-8 编码方案编写的,那么有各种方法可以告诉 JavaScript 解释器将其解释为 UTF-8 代码单元的序列。JavaScript 字符串始终是 UTF-16 代码点的序列。

任何小于 65536 的代码点的 Unicode 字符都可以使用其代码点的十六进制值来转义,并在其前面加上\u。转义序列由六个字符组成。它们需要紧跟\u后的正好四个字符。如果十六进制字符代码只有一位、两位或三位,则需要用前导零填充。以下是一个演示此点的例子:

var \u0061 = "\u0061\u0062\u0063";
console.log(a); //Output is "abc"

转义更大的代码点

在 ES5 中,对于需要超过 16 位存储的字符,我们需要两个 Unicode 转义序列。例如,要将\u1F691添加到字符串中,我们必须这样转义:

console.log("\uD83D\uDE91");

这里\uD83D\uDE91被称为代理对。代理对是两个 Unicode 字符,当它们按顺序书写时,代表另一个字符。

在 ES6 中,我们可以不使用代理对来编写它:

console.log("\u{1F691}");

字符串将\u1F691存储为\uD83D\uDE91,因此上述字符串的长度仍然是2

codePointAt(index)方法

字符串的codePointAt()方法返回一个非负整数,表示给定索引处的字符的代码点。

以下是一个演示此点的例子:

console.log("\uD83D\uDE91".codePointAt(1));
console.log("\u{1F691}".codePointAt(1));
console.log("hello".codePointAt(2));

输出如下:

56977
56977
1080

String.fromCodePoint(number1, …, number 2)方法

String对象的fromCodePoint()方法接受一系列代码点,并返回一个字符串。以下是一个演示此点的例子:

console.log(String.fromCodePoint(0x61, 0x62, 0x63));
console.log("\u0061\u0062 " == String.fromCodePoint(0x61, 0x62));

输出如下:

abc
true

repeat(count)方法

字符串的repeat()方法构建并返回一个新的字符串,该字符串包含调用它的指定次数的副本,并将它们连接在一起。以下是一个演示此点的例子:

console.log("a".repeat(6));      //Output "aaaaaa"

includes(string, index)方法

includes()方法用于确定一个字符串是否可以在另一个字符串中找到,根据情况返回truefalse。以下是一个演示此点的例子:

var str = "Hi, I am a JS Developer";
console.log(str.includes("JS")); //Output "true"

它有一个可选的第二个参数,表示在字符串中开始搜索的位置。以下是一个演示此点的例子:

var str = "Hi, I am a JS Developer";
console.log(str.includes("JS", 13)); // Output "false"

startsWith(string, index)方法

startsWith() 方法用于确定一个字符串是否以另一个字符串的字符开头,根据情况返回 truefalse。以下是一个演示此功能的示例:

var str = "Hi, I am a JS Developer";
console.log(str.startsWith('Hi, I am')); //Output "true"

它接受一个可选的第二个参数,表示在字符串中开始搜索的位置。以下是一个演示此功能的示例:

var str = "Hi, I am a JS Developer";
console.log(str.startsWith('JS Developer', 11)); //Output "true"

endsWith(string, index) 函数

endsWith() 方法用于确定一个字符串是否以另一个字符串的字符结尾,根据情况返回 true 或 false。它还接受一个可选的第二个参数,表示假设为字符串结尾的字符串中的位置。以下是一个演示此功能的示例:

var str = "Hi, I am a JS Developer";
console.log(str.endsWith("JS Developer"));  //Output "true"
console.log(str.endsWith("JS", 13));        //Output "true"

规范化

规范化简单地说,是一个在改变字符串意义的情况下搜索和标准化码点的过程。

还有不同的规范化形式:NFC、NFD、NFKC 和 NFKD。

让我们通过一个示例用例来理解 Unicode 字符串规范化。

案例研究

有许多 Unicode 字符可以用 16 位存储,也可以使用代理对表示。例如,"é" 字符可以用两种方式转义:

console.log("\u00E9");  //output 'é'
console.log("e\u0301"); //output 'é'

问题在于当应用 == 运算符、迭代或查找长度时,你会得到一个意外的结果。以下是一个演示此功能的示例:

var a = "\u00E9";
var b = "e\u0301";console.log(a == b);
console.log(a.length);
console.log(b.length);for(let i = 0; i<a.length; i++)
{console.log(a[i]);
}for(let i = 0; i<b.length; i++)
{console.log(b[i]);
}

输出如下:

false
1
2
é
é

在这里,这两个字符串显示方式相同,但当我们对它们进行各种字符串操作时,我们会得到不同的结果。

length 属性忽略代理对,并假设每个 16 位是一个单独的字符。== 运算符匹配二进制位,因此它也忽略了代理对。[] 运算符也假设每个 16 位是一个索引,因此忽略了代理对。

在这种情况下,为了解决问题,我们需要将代理对转换为 16 位字符表示。这个过程被称为规范化。为此,ES6 提供了一个 normalize() 函数。以下是一个演示此功能的示例:

var a = "\u00E9".normalize();
var b = "e\u0301".normalize();console.log(a == b);
console.log(a.length);
console.log(b.length);for(let i = 0; i<a.length; i++)
{console.log(a[i]);
}for(let i = 0; i<b.length; i++)
{console.log(b[i]);
}

输出如下:

true
1
1
é
é

这里输出符合预期。normalize() 返回字符串的规范化版本。normalize() 默认使用 NFC 格式。

规范化不仅用于代理对的情况;还有许多其他情况。

注意

字符串的规范化版本不是用于向用户显示的;它用于字符串的比较和搜索。

要了解更多关于 Unicode 字符串规范化和规范化形式的信息,请访问 www.unicode.org/reports/tr15/.

模板字符串

模板字符串只是创建字符串的新字面量,这使得许多事情变得更容易。它们提供嵌入表达式、多行字符串、字符串插值、字符串格式化、字符串标记等功能。它们总是在运行时被处理和转换为正常的 JavaScript 字符串,因此可以在使用正常字符串的任何地方使用。

模板字符串使用反引号而不是单引号或双引号来编写。以下是一个简单模板字符串的示例:

let str1 = `hello!!!`; //template string
let str2 = "hello!!!";console.log(str1 === str2); //output "true"

表达式

在 ES5 中,要在普通字符串中嵌入表达式,你会这样做:

Var a = 20;
Var b = 10;
Var c = "JavaScript";
Var str = "My age is " + (a + b) + " and I love " + c;console.log(str);

输出是:

My age is 30 and I love JavaScript

在 ES6 中,模板字符串使得在字符串中嵌入表达式变得更加容易。模板字符串可以包含表达式。这些表达式放置在由美元符号和大括号指示的占位符中,即 ${expressions}。占位符中的表达式解析值和它们之间的文本被传递给一个函数以解析模板字符串为普通字符串。默认函数只是将部分连接成一个字符串。如果我们使用自定义函数处理字符串部分,则模板字符串被称为 标签模板字符串,自定义函数被称为 标签函数

以下是一个示例,展示了如何在模板字符串中嵌入表达式:

let a = 20;
let b = 10;
let c = "JavaScript";
letstr = `My age is ${a+b} and I love ${c}`;console.log(str);

输出是:

My age is 30 and I love JavaScript

让我们创建一个标签模板字符串,即使用标签函数处理字符串。让我们实现标签函数以执行与默认函数相同的功能。以下是一个演示此功能的示例:

let tag = function(strings, ...values)
{let result = "";for(let i = 0; i<strings.length; i++){result += strings[i];if(i<values.length){result += values[i];}}return result;
};return result;
};let a = 20;
let b = 10;
let c = "JavaScript";
let str = tag `My age is ${a+b} and I love ${c}`;console.log(str);

输出是:

My age is 30 and I love JavaScript

在这里,我们的标签函数的名称是 tag,但你可以将其命名为任何其他名称。自定义函数接受两个参数,即第一个参数是模板字符串的字符串字面量数组,第二个参数是表达式的解析值数组。第二个参数作为多个参数传递,因此我们使用剩余参数。

多行字符串

模板字符串提供了一种创建包含多行文本的新字符串的方法。

在 ES5 中,我们需要使用 \n 换行符来添加换行符。以下是一个演示此功能的示例:

console.log("1\n2\n3");

输出是:

1
2
3

在 ES6 中,使用 多行 字符串我们可以简单地写:

console.log(`1
2
3`);

输出是:

1
2
3

在上面的代码中,我们简单地包含了需要放置 \n 的换行符。在将模板字符串转换为普通字符串时,换行符被转换为 \n

原始字符串

原始字符串是一个普通的字符串,其中转义字符不会被解释。

我们可以使用模板字符串创建原始字符串。我们可以使用 String.raw 标签函数获取模板字符串的原始版本。以下是一个演示此功能的示例:

let s = String.raw `xy\n${ 1 + 1 }z`;
console.log(s);

输出是:

xy\n2z

这里 \n 不会被解释为换行符,而是其两个字符,即 \n。变量 s 的长度将是 6

如果你创建了一个标签函数并想返回原始字符串,则使用第一个参数的 raw 属性。raw 属性是一个数组,它包含第一个参数的字符串的原始版本。以下是一个演示此功能的示例:

let tag = function(strings, ...values)
{return strings.raw[0]
};letstr = tag `Hello \n World!!!`;console.log(str);

输出是:

Hello \n World!!!

数组

ES6 向全局 Array 对象及其实例添加了新属性,以使处理数组更容易。与 Python 和 Ruby 等编程语言相比,JavaScript 中的数组缺乏功能和能力,因此 ES6 增强了数组以改变这一点。

Array.from(iterable, mapFunc, this) 方法

Array.from() 方法从一个可迭代对象创建一个新的数组实例。第一个参数是可迭代对象的引用。第二个参数是可选的,是一个回调函数(称为 映射函数),它会对可迭代对象的每个元素进行调用。第三个参数也是可选的,是映射函数内部 this 的值。

这里有一个示例来演示这一点:

letstr = "0123";
letobj = {number: 1};
letarr = Array.from(str, function(value){return parseInt(value) + this.number;
}, obj);console.log(arr);

输出是:

1, 2, 3, 4

Array.of(values…) 方法

Array.of() 方法是创建数组的 Array 构造函数的替代方案。当使用 Array 构造函数时,如果我们只传递一个参数,而且这个参数是一个数字,那么 Array 构造函数将创建一个空数组,其 length 属性等于传递的数字,而不是创建一个包含该数字的单元素数组。因此,引入了 Array.of() 方法来解决这个问题。

这里有一个示例来演示这一点:

let arr1 = new Array(2);
let arr2 = new Array.of(2);console.log(arr1[0], arr1.length);
console.log(arr2[0], arr2.length);

输出是:

undefined 2
2 1

当你动态地构造一个新的数组实例时,应该使用 Array.of() 而不是 Array 构造函数,即当你不知道值的类型和元素数量时。

fill(value, startIndex, endIndex) 方法

数组的 fill() 方法使用给定的值填充数组从 startIndexendIndex(不包括 endIndex)的所有元素。记住,startIndexendIndex 参数是可选的;因此,如果它们没有提供,则整个数组将用给定的值填充。如果只提供了 startIndex,则 endIndex 默认为数组的长度减 1。

如果 startIndex 是负数,则它被视为数组长度加上 startIndex。如果 endIndex 是负数,则它被视为数组长度加上 endIndex

这里有一个示例来演示这一点:

let arr1 = [1, 2, 3, 4];
let arr2 = [1, 2, 3, 4];
let arr3 = [1, 2, 3, 4];
let arr4 = [1, 2, 3, 4];
let arr5 = [1, 2, 3, 4];arr1.fill(5);
arr2.fill(5, 1, 2);
arr3.fill(5, 1, 3);
arr4.fill(5, -3, 2);
arr5.fill(5, 0, -2);console.log(arr1);
console.log(arr2);
console.log(arr3);
console.log(arr4);
console.log(arr5);

输出是:

5,5,5,5
1,5,3,4
1,5,5,4
1,5,3,4
5,5,3,4

find(testingFunc, this) 方法

数组的 find() 方法返回一个数组元素,如果它满足提供的测试函数。否则它返回 undefined

find() 方法接受两个参数,即第一个参数是测试函数,第二个参数是测试函数中 this 的值。第二个参数是可选的。

测试函数有三个参数:第一个参数是正在处理的数组元素,第二个参数是正在处理的当前元素的索引,第三个参数是调用 find() 的数组。

测试函数需要返回 true 来满足一个值。find() 方法返回第一个满足提供的测试函数的元素。

这里有一个示例来演示 find() 方法:

var x = 12;
var arr = [11, 12, 13];
var result = arr.find(function(value, index, array){if(value == this){return true;}
}, x);console.log(result); //Output "12"

findIndex(testingFunc, this) 方法

findIndex() 方法与 find() 方法类似。findIndex() 方法返回满足条件的数组元素的索引,而不是元素本身。

let x = 12;
let arr = [11, 12, 13];
let result = arr.findIndex(function(value, index, array){if(value == this){return true;}
}, x);console.log(result); Output "1"

copyWithin(targetIndex, startIndex, endIndex) 函数

数组的 copyWithin() 方法用于将数组的值序列复制到数组中的不同位置。

copyWithin()方法接受三个参数:第一个参数表示目标索引,即要复制元素的位置,第二个参数表示开始复制的索引位置,第三个参数表示索引,即实际结束复制元素的位置。

第三个参数是可选的,如果没有提供,则默认为length-1,其中length是数组的长度。如果startIndex是负数,则计算为length+startIndex。同样,如果endIndex是负数,则计算为length+endIndex

这里有一个示例来演示这一点:

let arr1 = [1, 2, 3, 4, 5];
let arr2 = [1, 2, 3, 4, 5];
let arr3 = [1, 2, 3, 4, 5];
let arr4 = [1, 2, 3, 4, 5];arr1.copyWithin(1, 2, 4);
arr2.copyWithin(0, 1);
arr3.copyWithin(1, -2);
arr4.copyWithin(1, -2, -1);console.log(arr1);
console.log(arr2);
console.log(arr3);
console.log(arr4);

输出是:

1,3,4,4,5
2,3,4,5,5
1,4,5,4,5
1,4,3,4,5

entries()keys()values()方法

数组的entries()方法返回一个包含数组每个索引的键/值对的可迭代对象。同样,数组的keys()方法返回一个包含数组中每个索引的键的可迭代对象。同样,数组的values()方法返回一个包含数组值的可迭代对象。

entries()方法返回的可迭代对象以数组的形式存储键/值对。

这些函数返回的可迭代对象不是一个数组。

这里有一个示例来演示这一点:

let arr = ['a', 'b', 'c'];
let entries = arr.entries();
let keys = arr.keys();
let values = arr.values();console.log(...entries);
console.log(...keys);
console.log(...values);

输出是:

0,a 1,b 2,c
0 1 2
a b c

集合

集合是指任何将多个元素存储为单一单元的对象。ES6 引入了各种新的集合对象,以提供更好的存储和组织数据的方式。

在 ES5 中,数组是唯一的集合对象。ES6 引入了数组缓冲区、类型化数组、集合(Sets)和映射(Maps),这些都是内置的集合对象。

让我们看看 ES6 提供的不同集合对象。

数组缓冲区

数组的元素可以是任何类型,如字符串、数字、对象等。数组可以动态增长。数组的问题在于它们在执行时间上较慢,并且占用更多内存。这导致在开发需要大量计算和大量处理数字的应用程序时出现问题。因此,引入数组缓冲区来解决这个问题。

数组缓冲区是内存中 8 位块的集合。每个块都是一个数组缓冲区元素。数组缓冲区的大小在创建时需要确定,因此它不能动态增长。数组缓冲区只能存储数字。所有块在创建数组缓冲区时都初始化为数字 0。

使用ArrayBuffer构造函数创建数组缓冲区对象。

let buffer = new ArrayBuffer(80); //80 bytes size

可以使用DataView对象从数组缓冲区对象中读取值或写入值。不强制使用 8 位来表示数字。我们可以使用 8 位、16 位、32 位和 64 位来表示数字。以下是一个示例,展示了如何创建DataView对象并读取/写入ArrayBuffer对象:

let buffer = new ArrayBuffer(80);
let view = new DataView(buffer);view.setInt32(8,22,false);var number = view.getInt32(8,false);console.log(number); //Output "22"

在这里,我们使用DataView构造函数创建了一个DataView对象。DataView对象提供了几种方法来将数字读入或写入数组缓冲区对象。这里我们使用了setInt32()方法,它使用 32 位来存储提供的数字。

所有用于将数据写入数组缓冲区对象的 DataView 对象的方法都接受三个参数。第一个参数表示偏移量,即我们想要写入数字的字节。第二个参数是要存储的数字。第三个参数是一个布尔类型,它表示数字的端序,例如false表示大端序。

类似地,所有用于读取数组缓冲区对象数据的 DataView 对象的方法都接受两个参数。第一个参数是偏移量,第二个参数表示使用的端序。

这里是 DataView 对象提供的其他用于存储数字的函数:

  • setInt8: 使用 8 位存储一个数字。它接受一个有符号整数(负或正)。

  • setUint8: 使用 8 位存储一个数字。它接受一个无符号整数(正)。

  • setInt16: 使用 16 位存储一个数字。它接受一个有符号整数。

  • setUint16: 使用 16 位存储一个数字。它接受一个无符号整数。

  • setInt32: 使用 32 位存储一个数字。它接受一个有符号整数。

  • setUint32: 使用 32 位存储一个数字。它接受一个无符号整数。

  • setFloat32: 使用 32 位存储一个数字。它接受一个有符号十进制数。

  • setFloat64: 使用 64 位存储一个数字。它接受一个有符号十进制数。

这里是 DataView 对象提供的其他用于检索存储数字的函数:

  • getInt8: 读取 8 位。返回有符号整数。

  • getUint8: 读取 8 位。返回无符号整数。

  • getInt16: 读取 16 位。返回有符号整数。

  • getUint16: 读取 16 位。返回无符号整数。

  • getInt32: 读取 32 位。返回有符号整数。

  • getUint32: 读取 32 位。返回无符号整数。

  • getFloat32: 读取 32 位。返回有符号十进制数。

  • getFloat64: 读取 64 位。返回有符号十进制数。

类型化数组

我们看到了如何在数组缓冲区中读取和写入数字。但是方法非常繁琐,因为每次都必须调用一个函数。类型化数组允许我们像处理普通数组一样读取和写入数组缓冲区对象。

类型化数组像一个数组缓冲区对象的包装器,并将数组缓冲区对象的数据视为n-位数字的序列。n值取决于我们如何创建类型化数组。

下面是一个代码示例,演示了如何创建一个数组缓冲区对象,并使用类型化数组对其进行读写:

var buffer = new ArrayBuffer(80);
vartyped_array = new Float64Array(buffer);
typed_array[4] = 11;console.log(typed_array.length);
console.log(typed_array[4]);

输出是:

10
11

在这里,我们使用Float64Array构造函数创建了类型化数组,因此它将数组缓冲区中的数据视为 64 位有符号十进制数的序列。这里数组缓冲区对象的大小是 640 位,因此只能存储 10 个 64 位数字。

类似地,还有其他类型化数组构造函数,用于将数组缓冲区中的数据表示为不同位数的序列。以下是列表:

  • Int8Array: 将其视为 8 位有符号整数

  • Uint8Array: 将其视为 8 位无符号整数

  • Int16Array: 将其视为 16 位有符号整数

  • Uint16Array:被视为 16 位无符号整数

  • Int32Array:被视为 32 位有符号整数

  • Uint32Array:被视为 32 位无符号整数

  • Float32Array:被视为 32 位有符号十进制数

  • Float64Array:被视为 64 位有符号十进制数

类型化数组提供了正常 JavaScript 数组提供的所有方法。它们也实现了可迭代协议,因此可以用作可迭代对象。

Set

Set 是任何数据类型唯一值的集合。集合中的值按插入顺序排列。集合是通过 Set 构造函数创建的。以下是一个示例:

let set1 = new Set();
let set2 = new Set("Hello!!!");

在这里 set1 是一个空集合。而 set2 是使用可迭代对象的值创建的,即字符串的字符,并且字符串不为空,因此 set2 不是空的。

这里是示例代码,它演示了可以在集合上执行的各种操作:

let set = new Set("Hello!!!");set.add(12); //add 12console.log(set.has("!")); //check if value exists
console.log(set.size);set.delete(12); //delete 12console.log(...set);set.clear(); //delete all values

输出是:

true
6
H e l o !

我们向 set 对象中添加了九个项,但大小只有六,因为集合会自动删除重复的值。字符 l! 被重复多次。

集合对象也实现了可迭代协议,因此它们可以用作可迭代对象。

当你想维护一个值的集合并检查一个值是否存在而不是检索一个值时,会使用集合。例如:如果你在代码中只使用数组的 indexOf() 方法来检查值是否存在,那么集合可以用作数组的替代。

WeakSet

这里是 Set 和 WeakSet 对象之间的区别:

  • 集合可以存储原始类型和对象引用,而 WeakSet 对象只能存储对象引用。

  • WeakSet 对象的一个重要特性是,如果没有其他引用指向存储在 WeakSet 对象中的对象,则它们会被垃圾回收。

  • 最后,WeakSet 对象是不可枚举的,也就是说,你不能找到它的大小;它也没有实现可迭代协议。

除了这三个区别之外,它的行为与 Set 完全相同。除了这三个区别之外,Set 和 WeakSet 对象之间的一切都是相同的。

WeakSet 对象是通过 WeakSet 构造函数创建的。你不能将可迭代对象作为参数传递给 WeakSet 对象。

这里是一个演示 WeakSet 的示例:

letweakset = new WeakSet();(function(){let a = {};weakset.add(a);
})()//here 'a' is garbage collected from weakset
console.log(weakset.size); //output "undefined"
console.log(...weakset); //Exception is thrownweakset.clear(); //Exception, no such function

Map

Map 是键/值对的集合。Map 的键和值可以是任何数据类型。键/值对按插入顺序排列。Map 对象是通过 Map 构造函数创建的。

这里是一个示例,演示了如何创建 Map 对象并在其上执行各种操作:

let map = new Map();
let o = {n: 1};map.set(o, "A"); //add
map.set("2", 9);console.log(map.has("2")); //check if key exists
console.log(map.get(o)); //retrieve value associated with key
console.log(...map);map.delete("2"); //delete key and associated value
map.clear(); //delete everything//create a map from iterable object
let map_1 = new Map([[1, 2], [4, 5]]);console.log(map_1.size); //number of keys

输出是:

true
A
[object Object],A 2,9
2

在从可迭代对象创建 Map 对象时,我们需要确保可迭代对象返回的值是长度为 2 的数组,即索引 0 是键,索引 1 是值。

如果我们尝试添加一个已经存在的键,那么它将被覆盖。Map 对象也实现了可迭代协议,因此也可以用作可迭代对象。在通过可迭代协议迭代 Map 时,它们返回包含键/值对的数组,正如前一个示例所示。

WeakMap

以下是 Map 和 WeakMap 对象之间的差异:

  • Map 对象的键可以是原始类型或对象引用,但 WeakMap 对象中的键只能是对象引用

  • WeakMap对象的一个重要特性是,如果没有其他引用指向由键引用的对象,则该键将被垃圾回收。

  • 最后,WeakMap 对象不是可枚举的,也就是说,你不能找到它的大小,它也不实现可迭代协议。

除了这三个差异之外,Map 和 WeakMap 对象之间的一切都是相似的。

使用WeakMap构造函数创建WeakMap。以下是一个演示其使用的示例:

let weakmap = new WeakMap();(function(){let o = {n: 1};weakmap.set(o, "A");
})()//here 'o' key is garbage collected
let s = {m: 1};weakmap.set(s, "B");console.log(weakmap.get(s));
console.log(...weakmap); //exception thrownweakmap.delete(s);
weakmap.clear(); //Exception, no such functionlet weakmap_1 = new WeakMap([[{}, 2], [{}, 5]]);   //this worksconsole.log(weakmap_1.size); //undefined

Object

ES6 标准化了对象的__proto__属性,并向全局Object对象添加了新属性。

__proto__属性

JavaScript 对象有一个内部[[prototype]]属性,它引用对象的原型,即它继承的对象。为了读取属性,我们必须使用Object.getPrototypeOf(),为了创建具有给定原型的新的对象,我们必须使用Object.create()方法。[[prototype]]属性不能直接读取或修改。

由于[[prototype]]属性的性质,继承变得繁琐,因此一些浏览器在对象中添加了一个特殊的__proto__属性,这是一个访问器属性,它公开了内部的[[prototype]]属性,使得与原型的工作更加容易。__proto__属性在 ES5 中并未标准化,但由于其流行,它在 ES6 中被标准化。

以下是一个演示此点的示例:

//In ES5
var x = {x: 12};
var y = Object.create(x, {y: {value: 13}});console.log(y.x); //Output "12"
console.log(y.y); //Output "13"//In ES6
let a = {a: 12, __proto__: {b: 13}};
console.log(a.a); //Output "12"
console.log(a.b); //Output "13"

Object.is(value1, value2)方法

Object.is()方法确定两个值是否相等。它与===运算符类似,但Object.is()方法有一些特殊情况。以下是一个演示这些特殊情况的示例:

console.log(Object.is(0, -0));
console.log(0 === -0);
console.log(Object.is(NaN, 0/0));
console.log(NaN === 0/0);
console.log(Object.is(NaN, NaN));
console.log(NaN ===NaN);

输出是:

false
true
true
false
true
false

Object.setPrototypeOf(object, prototype)方法

Object.setPrototypeOf()方法只是分配对象[[prototype]]属性的另一种方式。以下是一个演示此点的示例:

let x = {x: 12};
let y = {y: 13};Object.setPrototypeOf(y, x)console.log(y.x); //Output "12"
console.log(y.y); //Output "13"

Object.assign(targetObj, sourceObjs...)方法

Object.assign()方法用于将一个或多个源对象的所有可枚举自有属性的值复制到目标对象。此方法将返回targetObj

以下是一个演示此点的示例:

let x = {x: 12};
let y = {y: 13, __proto__: x};
let z = {z: 14, get b() {return 2;}, q: {}};Object.defineProperty(z, "z", {enumerable: false});let m = {};Object.assign(m, y, z);console.log(m.y);
console.log(m.z);
console.log(m.b);
console.log(m.x);
console.log(m.q == z.q);

输出是:

13
undefined
2
undefined
true

在使用Object.assign()方法时,以下是一些需要记住的重要事项:

  • 它在来源上调用 getter,在目标上调用 setter。

  • 它只是将源属性的值分配给目标的新或现有属性。

  • 它不会复制来源的[[prototype]]属性。

  • JavaScript 属性名可以是字符串或符号。Object.assign() 会复制两者。

  • 属性定义不会从源中复制,因此你需要使用 Object.getOwnPropertyDescriptor()Object.defineProperty()

  • 它会忽略带有 nullundefined 值的键的复制。

摘要

在本章中,我们学习了 ES6 为处理数字、字符串、数组和对象添加的新特性。我们看到了数组如何在数学密集型应用中影响性能,以及如何使用数组缓冲区来替代。我们还了解了 ES6 提供的新集合对象。

在下一章中,我们将学习关于符号和迭代协议的内容,并且我们会发现 yield 关键字和生成器。

第三章。使用迭代器

ES6 引入了新的对象接口和循环用于迭代。新迭代协议的添加为 JavaScript 打开了算法和能力的新世界。我们将从介绍符号和Symbol对象的各个属性开始本章。我们还将学习嵌套函数调用的执行栈是如何创建的,它们的影响,以及如何优化它们的性能和内存使用。

尽管符号是迭代器的一个独立主题,但我们仍将在本章中介绍符号,因为要实现迭代协议,你需要使用符号。

在本章中,我们将涵盖:

  • 使用符号作为对象属性键

  • 在对象中实现迭代协议

  • 创建和使用生成器对象

  • 使用for...of循环进行迭代

  • 尾调用优化

ES6 符号

ES6 符号是 ES6 中引入的新原始类型。符号是一个唯一且不可变的值。以下是一个示例代码,展示了如何创建一个符号:

var s = Symbol();

符号没有字面形式;因此,我们需要使用Symbol()函数来创建一个符号。每次调用Symbol()函数时,它都会返回一个唯一的符号。

Symbol()函数接受一个可选的字符串参数,表示符号的描述。符号的描述可用于调试,但不能用于访问符号本身。具有相同描述的两个符号完全不等于彼此。以下是一个示例来演示这一点:

let s1 = window.Symbol("My Symbol");
let s2 = window.Symbol("My Symbol");console.log(s1 === s2); //Output is "false"

从前面的示例中,我们也可以说,符号是一个类似字符串的值,它不会与其他任何值冲突。

"typeof"运算符

当应用于包含符号的变量时,typeof运算符输出"symbol"。以下是一个示例来演示这一点:

var s = Symbol();
console.log(typeof s); //Output "symbol"

使用typeof运算符是唯一识别变量是否包含符号的方法。

"new"运算符

你不能在Symbol()函数上应用new运算符。Symbol()函数会检测它是否被用作构造函数,如果是,则抛出异常。以下是一个示例来演示这一点:

try
{let s = new Symbol(); //"TypeError" exception
}
catch(e)
{console.log(e.message); //Output "Symbol is not a constructor"
}

但 JavaScript 引擎可以内部使用Symbol()函数作为构造函数来包装一个符号。因此,"s"将等于Object(s)

注意

从 ES6 开始引入的所有原始类型都不会允许手动调用它们的构造函数。

使用符号作为属性键

直到 ES5,JavaScript 对象属性键必须是字符串类型。但在 ES6 中,JavaScript 对象属性键可以是字符串或符号。以下是一个示例,演示如何使用符号作为对象属性键:

let obj = null;
let s1 = null;(function(){let s2 = Symbol();s1 = s2;obj = {[s2]: "mySymbol"}console.log(obj[s2]);console.log(obj[s2] == obj[s1]);
})();console.log(obj[s1]);

输出是:

mySymbol
true
mySymbol

从前面的代码中,你可以看到,为了使用符号创建或检索属性键,你需要使用[]标记。我们在讨论第二章中的计算属性名称时看到了[]标记,即了解你的库

要访问符号属性键,我们需要符号。在前面示例中,s1s2 都持有相同的符号值。

注意

在 ES6 中引入符号的主要原因是使其可以作为对象属性的键使用,并防止属性键的意外冲突。

Object.getOwnPropertySymbols() 方法

Object.getOwnPropertyNames() 方法不能检索符号属性。因此,ES6 引入了 Object.getOwnPropertySymbols() 来检索对象符号属性数组。以下是一个示例来演示这一点:

let obj = {a: 12};
let s1 = Symbol("mySymbol");
let s2 = Symbol("mySymbol");Object.defineProperty(obj, s1, {enumerable: false
});obj[s2] = "";console.log(Object.getOwnPropertySymbols(obj));

输出如下:

Symbol(mySymbol),Symbol(mySymbol)

从前面的示例中,你可以看到 Object.getOwnPropertySymbols() 方法也可以检索不可枚举的符号属性。

注意

in 操作符可以在对象中找到符号属性,而 for…in 循环和 Object.getOwnPropertyNames() 由于向后兼容性原因不能在对象中找到符号属性。

Symbol.for(string) 方法

Symbol 对象维护一个键/值对的注册表,其中键是符号描述,值是符号。每次我们使用 Symbol.for() 方法创建符号时,它都会被添加到注册表中,并且该方法返回符号。如果我们尝试使用已存在的描述创建符号,那么将检索现有的符号。

使用 Symbol.for() 方法而不是 Symbol() 方法创建符号的优势在于,在使用 Symbol.for() 方法时,你不必担心使符号在全局范围内可用,因为它始终在全局范围内可用。以下是一个示例来演示这一点:

let obj = {};(function(){let s1 = Symbol("name");obj[s1] = "Eden";
})();//obj[s1] cannot be accessed here(function(){let s2 = Symbol.for("age");obj[s2] = 27;
})();console.log(obj[Symbol.for("age")]); //Output "27"

已知符号

除了你自己的符号外,ES6 还提供了一套内置的符号,称为已知符号。以下是一个属性列表,引用了一些重要的内置符号:

  • Symbol.iterator

  • Symbol.match

  • Symbol.search

  • Symbol.replace

  • Symbol.split

  • Symbol.hasInstance

  • Symbol.species

  • Symbol.unscopables

  • Symbol.isContcatSpreadable

  • Symbol.toPrimitive

  • Symbol.toStringTag

你将在本书的各个章节中遇到这些符号的使用。

注意

在引用文本中的已知符号时,我们通常使用 @@ 符号作为前缀。例如,Symbol.iterator 符号被称为 @@iterator 方法。这样做是为了使在文本中引用已知符号更容易。

迭代协议

迭代协议是一组规则,对象需要遵循这些规则以实现接口,当使用这些规则时,循环或构造可以遍历对象的一组值。

ES6 引入了两种新的迭代协议,称为可迭代协议和迭代器协议。

迭代器协议

任何实现迭代器协议的对象都称为迭代器。根据迭代器协议,一个对象需要提供一个 next() 方法,该方法返回一组项目序列中的下一个项目。

以下是一个演示此点的例子:

let obj = {array: [1, 2, 3, 4, 5],nextIndex: 0,next: function(){return this.nextIndex < this.array.length ?{value: this.array[this.nextIndex++], done: false} :{done: true};}
};console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().value);
console.log(obj.next().done);

输出如下:

1
2
3
4
5
true

每次调用 next() 方法时,它返回一个具有两个属性的对象:valuedone。让我们看看这两个属性代表什么:

  • done 属性:如果迭代器已经遍历完值集合,则返回 true。否则,返回 false

  • value 属性:持有集合中当前项的值。当 done 属性为 true 时,该值被省略。

可迭代协议

任何实现可迭代协议的对象都称为可迭代对象。根据可迭代协议,一个对象需要提供 @@iterator 方法;也就是说,它必须具有 Symbol.iterator 符号作为属性键。@@iterator 方法必须返回一个迭代器对象。

以下是一个演示此点的例子:

let obj = {array: [1, 2, 3, 4, 5],nextIndex: 0,[Symbol.iterator]: function(){return {array: this.array,nextIndex: this.nextIndex,next: function(){return this.nextIndex < this.array.length ?{value: this.array[this.nextIndex++], done: false} :{done: true};}}}
};let iterable = obj[Symbol.iterator]()console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().done);

输出如下:

1
2
3
4
5
true

生成器

生成器函数就像一个普通函数一样,但它不是返回单个值,而是逐个返回多个值。调用生成器函数不会立即执行其主体,而是返回一个生成器对象的新实例(即实现迭代器和可迭代协议的对象)。

每个生成器对象都持有生成器函数的新执行上下文。当我们执行生成器对象的 next() 方法时,它会执行生成器函数的主体,直到遇到 yield 关键字。它返回产生值,并暂停函数。当再次调用 next() 方法时,它继续执行,然后返回下一个产生值。当生成器函数不再产生任何值时,done 属性为 true

生成器函数使用 function* 表达式编写。以下是一个演示此点的例子:

function* generator_function()
{yield 1;yield 2;yield 3;yield 4;yield 5;
}let generator = generator_function();console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().done);generator = generator_function();let iterable = generator[Symbol.iterator]();console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().done);

输出如下:

1
2
3
4
5
true
1
2
3
4
5
true

yield 关键字之后有一个表达式。该表达式的值是通过可迭代协议由生成器函数返回的。如果我们省略该表达式,则返回 undefined。该表达式的值就是我们所说的产生值。

我们还可以向 next() 方法传递一个可选参数。这个参数成为生成器函数暂停时 yield 语句返回的值。以下是一个演示此点的例子:

function* generator_function()
{var a = yield 12;var b = yield a + 1;var c = yield b + 2;yield c + 3;
}var generator = generator_function();console.log(generator.next().value);
console.log(generator.next(5).value);
console.log(generator.next(11).value);
console.log(generator.next(78).value);
console.log(generator.next().done);

输出如下:

12
6
13
81
true

return(value) 方法

您可以使用生成器对象的 return() 方法在任何时候结束生成器函数,即使它还没有产生所有值。return() 方法接受一个可选参数,表示要返回的最终值。

以下是一个演示此点的例子:

function* generator_function()
{yield 1;yield 2;yield 3;
}var generator = generator_function();console.log(generator.next().value);
console.log(generator.return(22).value);
console.log(generator.next().done);

输出如下:

1
22
true

throw(exception) 方法

您可以使用生成器对象的 throw() 方法在生成器函数内部手动触发异常。您必须向 throw() 方法传递您想要抛出的异常。以下是一个演示此点的例子:

function* generator_function()
{try{yield 1;}catch(e){console.log("1st Exception");}try{yield 2;}catch(e){console.log("2nd Exception");}}var generator = generator_function();console.log(generator.next().value);
console.log(generator.throw("exception string").value);
console.log(generator.throw("exception string").done);

输出如下:

1
1st Exception
2
2nd Exception
true

在前面的示例中,你可以看到异常是在函数上次暂停的地方抛出的。在异常被处理后,throw()方法继续执行,并返回下一个产生的值。

“yield*”关键字

生成器函数内部的yield*关键字将一个可迭代对象作为表达式并迭代它以产生其值。以下是一个演示此功能的示例:

function* generator_function_1()
{yield 2;yield 3;
}function* generator_function_2()
{yield 1;yield* generator_function_1();yield* [4, 5];
}var generator = generator_function_2();console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().done);

输出如下:

1
2
3
4
5
true

“for…of”循环

到目前为止,我们一直在使用next()方法迭代可迭代对象,这是一个繁琐的任务。ES6 引入了for…of循环来简化这个任务。

for…of循环被引入来迭代可迭代对象的值。以下是一个演示此功能的示例:

function* generator_function()
{yield 1;yield 2;yield 3;yield 4;yield 5;
}let arr = [1, 2, 3];for(let value of generator_function())
{console.log(value);
}for(let value of arr)
{console.log(value);
}

输出如下:

1
2
3
4
5
1
2
3

尾调用优化

每当进行函数调用时,都会在栈内存中创建一个执行栈来存储函数的变量。

当在另一个函数调用内部进行函数调用时,会为内部函数调用创建一个新的执行栈。但问题是,内部函数执行栈会占用一些额外的内存,即它存储了一个额外的地址,表示当此函数执行完毕时如何恢复执行。切换和创建执行栈也会消耗一些额外的 CPU 时间。当有少量或几百层嵌套调用时,这个问题并不明显,但当有数千或更多层嵌套调用时,这个问题就明显了,即 JavaScript 引擎会抛出RangeError: Maximum call stack size exceeded异常。你可能在创建递归函数时遇到过RangeError异常。

尾调用是一种函数调用,它可选地出现在函数的return语句的末尾。如果一个尾调用反复调用同一个函数,那么它被称为尾递归,这是递归的一个特例。尾调用的特别之处在于,有一种方法可以在进行尾调用时实际上防止额外的 CPU 时间和内存使用,那就是重用外函数的栈,而不是创建一个新的执行栈,从而节省 CPU 时间和额外的内存使用。在执行尾调用时重用执行栈被称为尾调用优化

ES6 在脚本以"use strict"模式编写时增加了对尾调用优化的支持。让我们看看一个尾调用的例子:

"use strict";function _add(x, y)
{return x + y;
}function add1(x, y)
{x = parseInt(x);y = parseInt(y);//tail callreturn _add(x, y);
}function add2(x, y)
{x = parseInt(x);y = parseInt(y);//not tail callreturn 0 + _add(x, y);
}console.log(add1(1, '1')); //2
console.log(add2(1, '2')); //3

在这里,add1()函数中的_add()调用是一个尾调用,因为它是add1()函数的最终操作。但add2()函数中的_add()调用不是尾调用,因为它不是最终操作,将0添加到_add()的结果才是最终操作。

add1()函数中的_add()调用不会创建一个新的执行栈。相反,它重用了add1()函数的执行栈;换句话说,发生了尾调用优化。

将非尾调用转换为尾调用

由于尾调用得到了优化,因此你必须在可能的情况下使用尾调用,而不是非尾调用。你可以通过将非尾调用转换为尾调用来优化你的代码。

让我们看看将非尾调用转换为尾调用的一个例子,这与之前的例子类似:

"use strict";function _add(x, y)
{return x + y;
}function add(x, y)
{x = parseInt(x);y = parseInt(y);var result = _add(x, y);return result;
}console.log(add(1, '1'));

在之前的代码中,_add()调用不是一个尾调用,因此创建了两个执行栈。我们可以这样将其转换为尾调用:

function add(x, y)
{x = parseInt(x);y = parseInt(y);return _add(x, y);
}

在这里,我们省略了result变量的使用,而是将函数调用与return语句并排排列。同样,还有许多其他策略可以将非尾调用转换为尾调用。

摘要

在本章中,我们学习了一种使用符号创建对象属性键的新方法。我们看到了迭代器和可迭代协议,并学习了如何在自定义对象中实现这些协议。然后,我们学习了如何使用for…of循环遍历可迭代对象。最后,我们通过学习尾调用是什么以及它们在 ES6 中的优化来结束本章。

在下一章中,我们将学习什么是 Promise,以及如何使用 Promise 编写更好的异步代码。

第四章:异步编程

ES6 引入了对知名编程模式的原生支持。其中一种模式是 Promise 模式,它使得异步代码的读写更加容易。在本章中,我们将学习如何使用 ES6 Promise API 编写异步代码。新的 JavaScript 和 HTML5 异步 API 现在正通过 Promise 实现,以简化代码的编写。因此,深入学习 Promise 非常重要。我们还将看到一些使用 Promise 公开的示例 API,例如Web Cryptography APIBattery Status API

在本章中,我们将涵盖:

  • JavaScript 执行模型

  • 编写异步代码时遇到的困难

  • 创建 Promise 及其工作原理

  • Promise 如何简化异步代码的编写

  • Promise 的不同状态

  • Promise 对象的各种方法。

  • 使用 Promise 的 JavaScript 和 HTML5 API

JavaScript 执行模型

JavaScript 代码是在单线程中执行的,也就是说,两段脚本不能同时运行。浏览器中打开的每个网站都会获得一个用于下载、解析和执行网站的单独线程,称为主线程。

主线程还维护一个队列,该队列包含排队等待依次执行的任务。这些排队任务可以是事件处理器、回调函数或任何其他类型的任务。当发生 AJAX 请求/响应、事件发生、注册计时器等情况时,新任务会被添加到队列中。一个长时间运行的队列任务可能会停止所有其他队列任务和主脚本的执行。主线程会在可能的情况下执行队列中的任务。

注意

HTML5 引入了Web Workers,它们是与主线程并行运行的线程。当 Web Worker 完成执行或需要通知主线程时,它只需将一个新的事件项添加到队列中。

这个队列使得异步执行代码成为可能。

编写异步代码

ES5 原生支持两种编写异步代码的模式,即事件模式和回调模式。在编写异步代码时,我们通常启动一个异步操作并注册事件处理器或传递回调函数,这些函数将在操作完成后执行。

根据特定的异步 API 设计,使用事件处理器或回调函数。为事件模式设计的 API 可以通过一些自定义代码包装成回调模式,反之亦然。例如,AJAX 是为事件模式设计的,但jQuery AJAX 将其暴露为回调模式。

让我们考虑一些涉及事件和回调的异步代码编写示例及其困难。

涉及事件的异步代码

对于涉及事件的异步 JavaScript API,你需要注册成功和错误事件处理器,它们将根据操作是否成功而分别执行。

例如,在发起 AJAX 请求时,我们注册的事件处理器将根据 AJAX 请求是否成功而执行。考虑以下发起 AJAX 请求并记录检索信息的代码片段:

function displayName(json)
{try{//we usally display it using DOMconsole.log(json.Name);}catch(e){console.log("Exception: " + e.message);}
}
function displayProfession(json)
{try{console.log(json.Profession);}catch(e){console.log("Exception: " + e.message);}
}function displayAge(json)
{try{console.log(json.Age);}catch(e){console.log("Exception: " + e.message);}
}function displayData(data)
{try{var json = JSON.parse(data);displayName(json);displayProfession(json);displayAge(json);}catch(e){console.log("Exception: " + e.message);}
}var request = new XMLHttpRequest();
var url = "data.json";request.open("GET", url);
request.addEventListener("load", function(){if(request.status === 200){displayData(request.responseText);}else{console.log("Server Error: " + request.status);}
}, false);request.addEventListener("error", function(){console.log("Cannot Make AJAX Request");
}, false);request.send();

在这里,我们假设 data.json 文件包含以下内容:

{"Name": "Eden","Profession": "Developer","Age": "25"
}

XMLHttpRequest 对象的 send() 方法是异步执行的,它检索 data.json 文件并调用 loaderror 事件处理器,具体取决于请求是否成功。

对于 AJAX 的工作方式,绝对没有任何问题,但问题在于我们编写涉及事件处理的代码。以下是我们在编写前一段代码时遇到的问题:

  • 我们不得不为每个将要异步执行的代码块添加异常处理器。我们不能只用一个 try…catch 语句来包裹整个代码。这使得捕获异常变得困难。

  • 代码的可读性较差,因为嵌套的函数调用使得代码流程难以追踪。

  • 如果程序的其他部分想要知道异步操作是否已完成、挂起或正在执行,那么我们必须维护用于此目的的自定义变量。因此,我们可以说找到异步操作的状态是困难的。

如果你在嵌套多个 AJAX 或其他任何异步操作,这段代码可能会变得更加复杂和难以阅读。例如,在显示数据后,你可能希望让用户验证数据是否正确,然后将布尔值发送回服务器。以下是一个代码示例,演示了这一过程:

function verify()
{try{var result = confirm("Is the data correct?");if (result == true){//make AJAX request to send data to server}else{//make AJAX request to send data to server}}catch(e){console.log("Exception: " + e.message);}
}function displayData(data)
{try{var json = JSON.parse(data);displayName(json);displayProfession(json);displayAge(json);verify();}catch(e){console.log("Exception: " + e.message);}
}

涉及回调的异步代码

对于涉及回调的异步 JavaScript API,你需要传递成功和错误回调,它们将根据操作是否成功或失败而分别被调用。

例如,在使用 jQuery 进行 AJAX 请求时,我们需要传递回调函数,这些函数将根据 AJAX 请求是否成功而执行。考虑以下使用 jQuery 进行 AJAX 请求并记录检索信息的代码片段:

function displayName(json)
{try{console.log(json.Name);}catch(e){console.log("Exception: " + e.message);}
}function displayProfession(json)
{try{console.log(json.Profession);}catch(e){console.log("Exception: " + e.message);}
}function displayAge(json)
{try{console.log(json.Age);}catch(e){console.log("Exception: " + e.message);}
}function displayData(data)
{try{var json = JSON.parse(data);displayName(json);displayProfession(json);displayAge(json);}catch(e){console.log("Exception: " + e.message);}
}$.ajax({url: "data.json", success: function(result, status, responseObject){displayData(responseObject.responseText);
}, error: function(xhr,status,error){console.log("Cannot Make AJAX Request. Error is: " + error);
}});

即使在这里,jQuery AJAX 的工作方式绝对没有任何问题,但问题在于我们编写涉及回调的代码。以下是我们在编写前一段代码时遇到的问题:

  • 捕获异常很困难,因为我们不得不使用多个 trycatch 语句。

  • 代码的可读性较差,因为嵌套的函数调用使得代码流程难以追踪。

  • 维护异步操作的状态很困难。

即使这样,如果我们在嵌套多个 jQuery AJAX 或其他任何异步操作,代码也会变得更加复杂。

承诺拯救

ES6 引入了一种新的本地模式来编写异步代码,称为 Promise 模式。

这种新的模式消除了事件和回调模式中常见的代码问题。它还使代码看起来更像同步代码。

Promise(或 Promise 对象)表示一个异步操作。现有的异步 JavaScript API 通常用 Promise 包装,新的 JavaScript API 正在纯使用 Promise 实现。

Promise 在 JavaScript 中是新的,但已经在许多其他编程语言中存在。例如,支持 Promise 的编程语言有 C# 5、C++ 11、Swift、Scala 等。

ES6 提供了 Promise API,我们可以使用它创建 Promise 并使用它。让我们探索 ES6 的 Promise API。

Promise 构造函数

使用Promise构造函数来创建新的 Promise 实例。Promise 对象表示一个异步操作。

我们需要将一个回调函数传递给Promise构造函数,该函数执行异步操作。这个回调函数被称为执行器。执行器应该接受两个参数,即resolvereject回调。如果异步操作成功,则应执行resolve回调,如果操作失败,则应执行reject回调。如果异步操作成功并且有一个结果,则我们可以将异步操作的结果传递给resolve回调。如果异步操作失败,则我们可以将失败原因传递给reject回调。

这里有一个代码示例,演示了如何创建一个 Promise 并使用它包装一个 AJAX 请求:

var promise = new Promise(function(resolve, reject){var request = new XMLHttpRequest();var url = "data.json";request.open("GET", url);request.addEventListener("load", function(){if(request.status === 200){resolve(request.responseText);}else{reject("Server Error: " + request.status);}}, false);request.addEventListener("error", function(){reject("Cannot Make AJAX Request");}, false);request.send();});

执行器是同步执行的。但是执行器正在执行一个异步操作,因此,执行器可以在异步操作完成之前返回。

Promise 始终处于以下状态之一:

  • 已履行:如果resolve回调以非 Promise 对象作为参数调用或没有参数,那么我们说 Promise 已履行

  • 已拒绝:如果调用reject回调或执行器作用域中发生异常,那么我们说 Promise 已拒绝

  • 挂起:如果resolvereject回调尚未被调用,那么我们说 Promise 是挂起的

  • 已解决:如果 Promise 要么已履行要么已拒绝,但不是挂起,则称 Promise 已解决

一旦 Promise 被履行或拒绝,它就不能再回退。尝试转换它将没有效果。

注意

如果resolve回调以 Promise 对象作为参数调用,那么 Promise 对象要么已履行,要么已拒绝,具体取决于传递的 Promise 对象是已履行还是已拒绝。

履行值

已履行 Promise 的履行值表示成功异步操作的结果。

如果我们传递给resolve回调函数的参数不是另一个 Promise 对象,那么这个参数本身就被视为 Promise 对象的完成值。

如果我们没有向resolve回调函数传递任何内容,那么完成值被视为undefined,并且 Promise 被认为是完成的。

要了解当我们将 Promise 对象作为参数传递给resolve回调函数时会发生什么,请考虑以下示例——假设我们有一个名为 A 的 Promise。当通过传递 Promise B 作为参数调用 Promise A 的resolve回调函数时,如果 Promise B 被完成,那么 Promise A 也被认为是完成的,并且 Promise A 的完成值现在与 Promise B 的完成值相同。

考虑以下代码示例:

var a = new Promise(function(resolve, reject){var b = new Promise(function(res, rej){rej("Reason");});resolve(b);
});var c = new Promise(function(resolve, reject){var d = new Promise(function(res, rej){res("Result");});resolve(d);
});

在前面的例子中,由于 Promise B 被拒绝,因此 Promise A 也被拒绝。两个 Promise 拒绝的原因是字符串"Reason"。同样,如果 D 被完成,那么 C 也会被完成。C 和 D 的完成值是字符串"Result"

注意

当我们说“Promise 以某个值解决,或者被某个值解决”时,这意味着 Promise 的执行者调用了或已经调用了resolve回调函数,并传递了该值。

then(onFulfilled, onRejected)方法

Promise 对象的then()方法允许我们在 Promise 完成或拒绝后执行一些任务。这个任务也可以是另一个事件驱动或基于回调的异步操作。

Promise 对象的then()方法接受两个参数,即onFulfilledonRejected回调函数。如果 Promise 对象被完成,则执行onFulfilled回调函数;如果 Promise 被拒绝,则执行onRejected回调函数。

如果在执行者作用域中抛出异常,则也会执行onRejected回调函数。因此,它表现得像一个异常处理器,即它捕获异常。

onFulfilled回调函数接受一个参数,即 Promise 的完成值。同样,onRejected回调函数接受一个参数,即拒绝的原因。

传递给then()方法的回调函数是异步执行的。

下面是演示then()方法的代码示例:

var promise = new Promise(function(resolve, reject){var request = new XMLHttpRequest();var url = "data.json";request.open("GET", url);request.addEventListener("load", function(){if(request.status === 200){resolve(request.responseText);}else{reject("Server Error: " + request.status);}}, false);request.addEventListener("error", function(){reject("Cannot Make AJAX Request");}, false);request.send();
});promise.then(function(value){value = JSON.parse(value);return value;
}, function(reason){console.log(reason);
});

在这里,如果 AJAX 请求成功(即 Promise 被完成),则通过传递响应文本作为参数执行onFulfilled回调函数。onFulfilled回调函数将 JSON 字符串转换为 JavaScript 对象。onFulfilled回调函数返回 JavaScript 对象。

许多程序员移除了 Promise 对象变量,并这样编写前面的代码:

function ajax()
{return new Promise(function(resolve, reject){var request = new XMLHttpRequest();var url = "data.json";request.open("GET", url);request.addEventListener("load", function(){if(request.status === 200){resolve(request.responseText);}else{reject("Server Error: " + request.status);}}, false);request.addEventListener("error", function(){reject("Cannot Make AJAX Request");}, false);request.send();});
}ajax().then(function(value){value = JSON.parse(value);return value;
}, function(reason){console.log(reason);
});

这种风格使代码更容易阅读。所有使用 Promises 实现的新的 JavaScript API 都采用这种模式。

then()方法始终返回一个新的promise对象,该对象解决调用回调函数的返回值。以下是then()方法返回新的promise对象的方式:

  • 如果onFulfilled回调被调用,并且其中没有返回语句,那么内部会创建一个新的已解决的Promise并返回。

  • 如果onFulfilled回调被调用,并且我们返回一个自定义Promise,那么它会内部创建并返回一个新的promise对象。新Promise对象解析为自定义Promise对象。

  • 如果onFulfilled回调被调用,并且我们返回的不是自定义Promise,那么也会内部创建一个新的Promise对象并返回。新Promise对象解析为返回值。

  • 如果我们传递null而不是onFulfilled回调,那么内部会创建一个回调并替换为null。内部创建的onFulfilled返回一个新的已解决的promise对象。新promise对象的解决值与父Promise的解决值相同。

  • 如果onRejected回调被调用,并且其中没有返回语句,那么内部会创建一个新的被拒绝的Promise并返回。

  • 如果onRejected回调被调用,并且我们返回一个自定义Promise,那么它会内部创建并返回一个新的promise对象。新promise对象解析为自定义Promise对象。

  • 如果onRejected回调被调用,并且我们返回的不是自定义Promise,那么内部也会创建一个新的promise对象并返回。新promise对象解析为返回的值。

  • 如果我们传递null而不是onRejected回调,或者省略它,那么内部会创建一个回调并使用它。内部创建的onRejected回调返回一个新的被拒绝的promise对象。新promise对象被拒绝的原因与父Promise被拒绝的原因相同。

在之前的代码示例中,我们还没有将检索到的数据记录到控制台。我们可以通过链式连接Promise来实现这一点。此外,在之前的代码中,我们没有处理onFulfilled回调中可能发生的异常。以下是我们可以如何扩展代码来记录数据和处理所有链式onFulfilled回调的异常:

function ajax()
{return new Promise(function(resolve, reject){var request = new XMLHttpRequest();var url = "data.json";request.open("GET", url);request.addEventListener("load", function(){if(request.status === 200){resolve(request.responseText);}else{reject("Server Error: " + request.status);}}, false);request.addEventListener("error", function(){reject("Cannot Make AJAX Request");}, false);request.send();});
}ajax().then(function(value){value = JSON.parse(value);return value;
}).then(function(value){console.log(value.Name);return value;
}).then(function(value){console.log(value.Profession);return value;
}).then(function(value){console.log(value.Age);return value;
}).then(null, function(reason){console.log(reason);
});

在这个代码示例中,我们使用then()方法链式连接多个Promise,以解析和记录链中第一个Promise的执行者接收到的响应。在这里,最后一个then()方法被用作所有onFulfilled方法和执行者的异常或错误处理器。

这里有一张图展示了多个链式Promise的执行方式:

then(onFulfilled, onRejected)方法

图片由 MDN 提供

让我们继续向链中添加一个事件驱动的异步操作,即验证显示的数据是否正确。以下是我们可以如何扩展代码来完成此操作:

function ajax()
{return new Promise(function(resolve, reject){var request = new XMLHttpRequest();var url = "http://localhost:8888/data.json";request.open("GET", url);request.addEventListener("load", function(){if(request.status === 200){resolve(request.responseText);}else{reject("Server Error: " + request.status);}}, false);request.addEventListener("error", function(){reject("Cannot Make AJAX Request");}, false);request.send();});
}function verify(value)
{return new Promise(function(resolve, reject){if(value == true){//make AJAX request to send data to server}else{//make AJAX request to send data to server}});
}ajax().then(function(value){value = JSON.parse(value);return value;
}).then(function(value){console.log(value.Name);return value;
}).then(function(value){console.log(value.Profession);return value;
}).then(function(value){console.log(value.Age);return value;
}).then(function(value){var result = confirm("Is the data correct?");return result;
}).then(verify).then(null, function(reason){console.log(reason);
});

现在我们可以看到,将 AJAX 操作用Promise包装使代码更容易阅读和编写。现在代码一开始看起来就更容易理解。

catch(onRejected)方法

当我们只使用then()方法来处理错误和异常时,使用promise对象的catch()方法代替then()方法。catch()方法的工作方式并没有什么特别之处。它只是使代码更容易阅读,因为“catch”这个词使其更有意义。

catch()方法只接受一个参数,即onRejected回调。catch()方法的onRejected回调以与then()方法的onRejected回调相同的方式被调用。

catch()方法始终返回一个Promise。以下是catch()方法如何返回一个新的promise对象:

  • 如果onRejected回调中没有返回语句,那么内部会创建一个新的实现Promise并返回。

  • 如果我们返回一个自定义Promise,那么它内部创建并返回一个新的promise对象。新promise对象解析自定义promise对象。

  • 如果在onRejected回调中返回除自定义Promise之外的其他内容,那么也会内部创建一个新的promise对象并返回。新promise对象解析返回的值。

  • 如果我们传递null而不是onRejected回调,或者省略它,那么内部会创建一个回调并使用它代替。内部创建的onRejected回调返回一个拒绝的promise对象。新promise对象拒绝的原因与父promise对象拒绝的原因相同。

  • 如果被catch()调用的promise对象得到实现,那么catch()方法简单地返回一个新的实现promise对象,并忽略onRejected回调。新promise对象的成功值与父Promise的成功值相同。

要理解catch()方法,考虑以下代码:

promise.then(null, function(reason){
});

这段代码可以使用catch()方法重写为以下方式:

promise.catch(function(reason){
});

这两个代码片段工作方式完全相同。

让我们通过用catch()方法替换最后一个链式then()方法来重写 AJAX 代码示例:

function ajax()
{return new Promise(function(resolve, reject){var request = new XMLHttpRequest();var url = "data.json";request.open("GET", url);request.addEventListener("load", function(){if(request.status === 200){resolve(request.responseText);}else{reject("Server Error: " + request.status);}}, false);request.addEventListener("error", function(){reject("Cannot Make AJAX Request");}, false);request.send();});
}function verify(value)
{return new Promise(function(resolve, reject){if(value == true){//make AJAX request to send data to server}else{//make AJAX request to send data to server}});
}ajax().then(function(value){value = JSON.parse(value);return value;
}).then(function(value){console.log(value.Name);return value;
}).then(function(value){console.log(value.Profession);return value;
}).then(function(value){console.log(value.Age);return value;
}).then(function(value){var result = confirm("Is the data correct?");return result;
}).then(verify)
.catch(function(reason){console.log(reason);
});

现在代码在第一眼看起来就更容易阅读了。

Promise.resolve(value)方法

Promise对象的resolve()方法接受一个值,并返回一个解析传递值的promise对象。

resolve()方法基本上用于将一个值转换为promise对象。当你发现自己有一个可能或可能不是Promise的值,但你想将其用作Promise时,它很有用。例如,jQuery Promises 与 ES6 Promises 有不同的接口。因此,你可以使用resolve()方法将 jQuery Promises 转换为 ES6 Promises。

这里是一个代码示例,演示了如何使用resolve()方法:

var p1 = Promise.resolve(4);
p1.then(function(value){console.log(value);
});//passed a promise object
Promise.resolve(p1).then(function(value){console.log(value);
});Promise.resolve({name: "Eden"}).then(function(value){console.log(value.name);
});

输出如下:

4
4
Eden

Promise.reject(value)方法

Promise对象的reject()方法接受一个值,并返回一个带有传递值的拒绝promise对象。

Promise.resolve() 方法不同,reject() 方法用于调试目的,而不是将值转换为 Promises。

下面是一个代码示例,演示如何使用 reject() 方法:

var p1 = Promise.reject(4);
p1.then(null, function(value){console.log(value);
});Promise.reject({name: "Eden"}).then(null, function(value){console.log(value.name);
});

输出如下:

4
Eden

Promise.all(iterable) 方法

Promise 对象的 all() 方法接受一个可迭代对象作为参数,并在可迭代对象中的所有 Promise 都被实现时返回一个实现的 Promise。

这在我们在一些异步操作完成后想要执行一些任务时非常有用。

下面是一个代码示例,演示如何使用 Promise.all() 方法:

var p1 = new Promise(function(resolve, reject){setTimeout(function(){resolve();}, 1000);
});var p2 = new Promise(function(resolve, reject){setTimeout(function(){resolve();}, 2000);
});var arr = [p1, p2];Promise.all(arr).then(function(){console.log("Done"); //"Done" is logged after 2 seconds
});

如果可迭代对象包含一个不是 promise 对象的值,那么它将使用 Promise.resolve() 方法转换为 Promise 对象。

如果传递的任何一个 Promise 被拒绝,那么 Promise.all() 方法会立即返回一个新的被拒绝的 Promise,其拒绝原因与被拒绝的传递的 Promise 相同。以下是一个演示此功能的示例:

var p1 = new Promise(function(resolve, reject){setTimeout(function(){reject("Error");}, 1000);
});var p2 = new Promise(function(resolve, reject){setTimeout(function(){resolve();}, 2000);
});var arr = [p1, p2];Promise.all(arr).then(null, function(reason){console.log(reason); //"Error" is logged after 1 second
});

Promise.race(iterable) 方法

Promise 对象的 race() 方法接受一个可迭代对象作为参数,并返回一个 Promise,该 Promise 在可迭代对象中的任何一个 Promise 被实现或拒绝时立即实现或拒绝,其实现值或原因来自那个 Promise。

如其名所示,race() 方法用于在多个 Promise 之间进行竞争,以查看哪个先完成。

下面是一个代码示例,展示如何使用 race() 方法:

var p1 = new Promise(function(resolve, reject){setTimeout(function(){resolve("Fulfillment Value 1");}, 1000);
});var p2 = new Promise(function(resolve, reject){setTimeout(function(){resolve("fulfillment Value 2");}, 2000);
});var arr = [p1, p2];Promise.race(arr).then(function(value){console.log(value); //Output "Fulfillment value 1"
}, function(reason){console.log(reason);
});

基于 Promises 的 JavaScript API

新的异步 JavaScript API 现在基于 Promise 模式,而不是事件和回调。旧 JavaScript API 的新版本现在也基于 Promises。

例如,旧版本的电池状态 API 和 Web 加密 API 基于事件,但这些 API 的新版本完全使用 Promises 实现。让我们看看这些 API 的概述。

电池状态 API

电池状态 API 提供了电池的当前充电水平和充电状态。以下是一个新电池状态 API 的代码示例:

navigator.getBattery().then(function(value){console.log("Batter Level: " + (value.level * 100));
}, function(reason){console.log("Error: " + reason);
});

navigator 对象的 getBattery() 方法在成功检索电池信息时返回一个实现的 Promise。否则,它返回一个被拒绝的 Promise。

如果 Promise 被实现,那么实现值是一个包含电池信息的对象。实现值的 level 属性表示剩余的充电水平。

Web 加密 API

Web 加密 API 允许我们进行哈希、签名生成和验证、加密和解密。

下面是新 Web 加密 API 的代码示例:

function convertStringToArrayBufferView(str)
{var bytes = new Uint8Array(str.length);for (var iii = 0; iii < str.length; iii++){bytes[iii] = str.charCodeAt(iii);}return bytes;
}function convertArrayBufferToHexaDecimal(buffer)
{var data_view = new DataView(buffer)var iii, len, hex = '', c;for(iii = 0, len = data_view.byteLength; iii < len; iii++){c = data_view.getUint8(iii).toString(16);if(c.length < 2){c = '0' + c;}hex += c;}return hex;
}window.crypto.subtle.digest({name: "SHA-256"}, convertStringToArrayBufferView("ECMAScript 6")).then(function(result){var hash_value = convertArrayBufferToHexaDecimal(result);console.log(hash_value);
});

在此代码示例中,我们将找到一个字符串的 SHA-256 哈希值。

window.crypto.subtle.digest 方法接收一个字符串的数组缓冲区和哈希算法名称,并返回一个 Promise 对象。如果成功生成了哈希值,则返回一个已解决的 Promise,其解决值是一个表示哈希值的数组缓冲区。

摘要

在本章中,我们学习了 JavaScript 如何执行异步代码。我们了解了编写异步代码的不同模式。我们看到了 Promises 如何使异步代码的读写更加容易,以及如何使用 ES6 Promise API。我们还看到了一些基于 Promises 的 JavaScript API。总的来说,本章旨在解释 Promises、它们的优点以及如何使用基于它们的 API。

在下一章中,我们将学习 ES6 Reflect API 及其用法。

第五章:实现 Reflect API

ES6 引入了一个新的 API,即 Reflect API,用于对象反射(即检查和操作对象的属性)。尽管 ES5 已经有了对象反射的 API,但这些 API 组织得并不好,并且在失败时,它们通常会抛出异常。ES6 的 Reflect API 组织得很好,使得代码的阅读和编写更加容易,因为它在失败时不会抛出异常。相反,它返回一个布尔值,表示操作是否成功。由于开发者正在适应 Reflect API 进行对象反射,因此深入了解这个 API 非常重要。

在本章中,我们将介绍:

  • 使用给定的 this 值调用函数

  • 使用另一个构造函数的 prototype 属性调用构造函数

  • 定义或修改对象属性的属性

  • 使用迭代器对象遍历对象的属性

  • 获取和设置对象的内部 [[prototype]] 属性

  • 以及许多与检查和操作对象的方法和属性相关的其他操作。

Reflect 对象

ES6 全局 Reflect 对象公开了所有新的对象反射方法。Reflect 不是一个函数对象,因此你不能调用 Reflect 对象。此外,你不能使用 new 操作符来使用它。

ES6 Reflect API 的所有方法都被封装在 Reflect 对象中,使其看起来组织得很好。

Reflect 对象提供了许多方法,在功能上与全局 Object 对象的方法重叠。

让我们看看 Reflect 对象提供的各种对象反射方法。

Reflect.apply(function, this, args) 方法

Reflect.apply() 方法用于使用给定的 this 值调用函数。通过 Reflect.apply() 调用的函数被称为目标函数。它与函数对象的 apply() 方法相同。

Reflect.apply() 方法接受三个参数:

  • 第一个参数表示目标函数。

  • 第二个参数表示目标函数内部的 this 值。此参数是可选的。

  • 第三个参数是一个数组对象,指定目标函数的参数。此参数是可选的。

Reflect.apply() 方法返回目标函数返回的任何内容。

这里有一个代码示例,演示如何使用 Reflect.apply() 方法:

function function_name(a, b, c)
{return this.value + a + b + c;
}var returned_value = Reflect.apply(function_name, {value: 100}, [10, 20, 30]);console.log(returned_value); //Output "160"

Reflect.construct(constructor, args, prototype) 方法

Reflect.construct() 方法用于将函数作为构造函数调用。它与 new 操作符类似。将要作为构造函数调用的函数被称为 目标构造函数

有一个特殊的原因,你可能想要使用 Reflect.construct() 方法而不是 new 操作符,那就是当你想要目标构造函数的 prototype 与另一个构造函数的 prototype 匹配时。

Reflect.construct() 方法接受三个参数:

  • 第一个参数是目标构造函数。

  • 第二个参数是一个数组,指定目标构造函数的参数。此参数是可选的。

  • 第三个参数是另一个构造函数,其 prototype 将用作目标构造函数的 prototype。此参数是可选的。

Reflect.construct() 方法返回由目标构造函数创建的新实例。

这里是代码示例,演示如何使用 Reflect.constructor() 方法:

function constructor1(a, b)
{this.a = a;this.b = b;this.f = function(){return this.a + this.b + this.c;}
}function constructor2(){}
constructor2.prototype.c = 100;var myObject = Reflect.construct(constructor1, [1,2], constructor2);console.log(myObject.f()); //Output "103"

在前面的例子中,我们在调用 constructor1 时使用了 consturctor2prototype 作为 constructor1prototype

Reflect.defineProperty(object, property, descriptor) 方法

Reflect.defineProperty() 方法直接在对象上定义新属性或修改对象上的现有属性。它返回一个布尔值,指示操作是否成功。

它类似于 Object.defineProperty() 方法。区别在于 Reflect.defineProperty() 方法返回一个布尔值,而 Object.defineProperty() 返回修改后的对象。如果 Object.defineProperty() 方法无法修改或定义对象属性,则抛出异常,而 Reflect.defineProperty() 方法返回 false 结果。

Reflect.defineProperty() 方法接受三个参数:

  • 第一个参数是用于定义或修改属性的对象

  • 第二个参数是要定义或修改的属性的符号或名称

  • 第三个参数是要定义或修改的属性的描述符

理解数据属性和访问器属性

自从 ES5 以来,每个对象属性要么是数据属性,要么是访问器属性。数据属性有一个值,这个值可能是可写的,也可能不可写,而访问器属性有一对用于设置和检索属性值的函数。

数据属性的特性包括值、可写、可枚举和可配置。另一方面,访问器属性的特性包括 setgetenumerableconfigurable

描述符是一个描述属性特性的对象。当使用 Reflect.defineProperty() 方法、Object.defineProperty() 方法、Object.defineProperties() 方法或 Object.create() 方法创建属性时,我们需要传递属性描述符。

数据属性的描述符对象具有以下属性:

  • :这是与属性关联的值。默认值是 undefined

  • 可写性:如果这是 true,则可以使用赋值运算符更改属性值。默认值是 false

  • 可配置性:如果这是 true,则可以更改属性属性,并且可以删除属性。默认值是 false。记住,当可配置属性为 false 且可写为 true 时,可以更改值和可写属性。

  • 可枚举:如果这是 true,则该属性会在 for…in 循环和 Object.keys() 方法中出现。默认值是 false

访问器属性的描述符具有以下属性:

  • 获取:这是一个返回属性值的函数。该函数没有参数,默认值是未定义。

  • 设置:这是一个设置属性值的函数。该函数将接收被分配给属性的新值。

  • 可配置:如果这是 true,则属性描述符可以被更改,并且属性可以被删除。默认值是 false

  • 可枚举:如果这是 true,则该属性会在 for…in 循环和 Object.keys() 方法中出现。默认值是 false

根据描述符对象的属性,JavaScript 决定属性是数据属性还是访问器属性。

如果你没有使用 Reflect.defineProperty() 方法、Object.defineProperty() 方法、Object.defineProperties() 方法或 Object.create() 方法添加属性,则该属性是一个数据属性,并且 writableenumerableconfigurable 属性都被设置为 true。属性添加后,你可以修改其属性。

当调用 Reflect.defineProperty() 方法、Object.defineProperty() 方法或 Object.defineProperties() 方法时,如果对象已经存在具有指定名称的属性,则该属性会被修改。描述符中未指定的属性保持不变。

你可以将数据属性更改为访问器属性,反之亦然。如果你这样做,描述符中未指定的 configurableenumerable 属性将在属性中保留。其他未在描述符中指定的属性将被设置为它们的默认值。

这里是一个示例代码,展示了如何使用 Reflect.defineProperty() 方法创建一个数据属性:

var obj = {}Reflect.defineProperty(obj, "name", {value: "Eden",writable: true,configurable: true,enumerable: true
});console.log(obj.name); //Output "Eden"

这里是另一个示例代码,展示了如何使用 Reflect.defineProperty() 方法创建一个访问器属性:

var obj = {__name__: "Eden"
}Reflect.defineProperty(obj, "name", {get: function(){return this.__name__;},set: function(newName){this.__name__ = newName;},configurable: true,enumerable: true
});obj.name = "John";
console.log(obj.name);      //Output "John"

Reflect.deleteProperty(object, property) 方法

Reflect.deleteProperty() 方法用于删除对象的一个属性。它与 delete 操作符相同。

此方法接受两个参数,即第一个参数是对象的引用,第二个参数是要删除的属性的名称。如果 Reflect.deleteProperty() 方法成功删除了属性,则返回 true。否则,它返回 false

这里是一个示例代码,展示了如何使用 Reflect.deleteProperty() 方法删除一个属性:

var obj = {name: "Eden"
}console.log(obj.name);      //Output "Eden"Reflect.deleteProperty(obj, "name");console.log(obj.name);      //Output "undefined"

Reflect.enumerate(object) 方法

Reflect.enumerate() 方法接受一个对象作为参数,并返回一个迭代器对象,该对象表示对象的可枚举属性。它还返回对象的继承的可枚举属性。

Reflect.enumerate() 方法类似于 for…in 循环。Reflect.enumerate() 方法返回一个迭代器,而 for…in 循环遍历可枚举属性。

这里有一个示例来演示如何使用 Reflect.enumerate() 方法:

var obj = {a: 1,b: 2,c: 3
};var iterator = Reflect.enumerate(obj);console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().done);

输出如下:

a
b
c
true

Reflect.get(object, property, this) 方法

Reflect.get() 方法用于检索对象的属性值。第一个参数是对象,第二个参数是属性名。如果属性是访问器属性,则我们可以提供一个第三个参数,它将是 get 函数内部的 this 的值。

这里是一个代码示例,展示了如何使用 Reflect.get() 方法:

var obj = {__name__: "Eden"
};Reflect.defineProperty(obj, "name", {get: function(){return this.__name__;}
});console.log(obj.name);      //Output "Eden"var name = Reflect.get(obj, "name", {__name__: "John"});console.log(name);      //Output "John"

Reflect.set(object, property, value, this) 方法

Reflect.set() 方法用于设置对象的属性值。第一个参数是对象,第二个参数是属性名,第三个参数是属性值。如果属性是访问器属性,则我们可以提供一个第四个参数,它将是 set 函数内部的 this 的值。

Reflect.set() 方法如果成功设置属性值,则返回 true。否则,它返回 false

这里是一个代码示例,展示了如何使用 Reflect.set() 方法:

var obj1 = {__name__: "Eden"
};Reflect.defineProperty(obj1, "name", {set: function(newName){this.__name__ = newName;},get: function(){return this.__name__;}
});var obj2 = {__name__: "John"
};Reflect.set(obj1, "name", "Eden", obj2);console.log(obj1.name); //Output "Eden"
console.log(obj2.__name__); //Output "Eden"

Reflect.getOwnPropertyDescriptor(object, property) 方法

Reflect.getOwnPropertyDescriptor() 方法用于检索对象的属性描述符。

Reflect.getOwnPropertyDescriptor() 方法与 Object.getOwnPropertyDescriptor() 方法相同。Reflect.getOwnPropertyDescriptor() 方法接受两个参数。第一个参数是对象,第二个参数是属性名。

这里有一个示例来演示 Reflect.getOwnPropertyDescriptor() 方法:

var obj = {name: "Eden"
};var descriptor = Reflect.getOwnPropertyDescriptor(obj, "name");console.log(descriptor.value);
console.log(descriptor.writable);
console.log(descriptor.enumerable);
console.log(descriptor.configurable);

输出如下:

Eden
true
true
true

Reflect.getPrototypeOf(object) 方法

Reflect.getPrototypeOf() 方法用于检索对象的原型,即对象的内部 [[prototype]] 属性的值。

Reflect.getPrototypeOf() 方法与 Object.getPrototypeOf() 方法相同。

这里是展示如何使用 Reflect.getPrototypeOf() 方法的代码示例:

var obj1 = {__proto__: {name: "Eden"}
};var obj2 = Reflect.getPrototypeOf(obj1);console.log(obj2.name); //Output "Eden"

Reflect.setPrototypeOf(object, prototype) 方法

Reflect.setPrototypeOf() 用于设置对象的内部 [[prototype]] 属性的值。如果成功设置内部 [[prototype]] 属性的值,Reflect.setPrototypeOf() 方法将返回 true。否则,它将返回 false

这里是一个代码示例,它展示了如何使用它:

var obj = {};Reflect.setPrototypeOf(obj, {name: "Eden"
});console.log(obj.name); //Output "Eden"

Reflect.has(object, property) 方法

Reflect.has() 用于检查属性是否存在于对象中。它也会检查继承属性。如果属性存在,则返回 true。否则,它将返回 false

它与 in 操作符相同。

这里是一个代码示例,展示了如何使用 Reflect.has() 方法:

var obj = {__proto__: {name: "Eden"},age: 12
};console.log(Reflect.has(obj, "name")); //Output "true"
console.log(Reflect.has(obj, "age")); //Output "true"

Reflect.isExtensible(object) 方法

Reflect.isExtensible() 方法用于检查一个对象是否可扩展,也就是说,我们是否可以向对象添加新的属性。

可以使用 Object.preventExtensions()Object.freeze()Object.seal() 方法将对象标记为不可扩展。

Reflect.isExtensible() 方法与 Object.isExtensible() 方法相同。

这里是代码示例,演示了如何使用 Reflect.isExtensible() 方法:

var obj = {name: "Eden"
};console.log(Reflect.isExtensible(obj)); //Output "true"Object.preventExtensions(obj);console.log(Reflect.isExtensible(obj)); //Output "false"

Reflect.preventExtensions(object) 方法

Reflect.preventExtensions() 用于将对象标记为不可扩展。它返回一个布尔值,指示操作是否成功。

它与 Object.preventExtensions() 方法相同:

var obj = {name: "Eden"
};console.log(Reflect.isExtensible(obj)); //Output "true"console.log(Reflect.preventExtensions(obj)); //Output "true"console.log(Reflect.isExtensible(obj)); //Output "false"

Reflect.ownKeys(object) 方法

Reflect.ownKeys() 方法返回一个数组,其值表示提供对象属性的键。它忽略了继承的属性。

这里是演示此方法的示例代码:

var obj = {a: 1,b: 2,__proto__: {c: 3}
};var keys = Reflect.ownKeys(obj);console.log(keys.length); //Output "2"
console.log(keys[0]); //Output "a"
console.log(keys[1]); //Output "b"

摘要

在本章中,我们学习了什么是对象反射,以及如何使用 ES6 Reflect API 进行对象反射。我们通过示例看到了 Reflect 对象的各种方法。总的来说,本章介绍了 ES6 Reflect API,用于检查和操作对象的属性。

在下一章中,我们将学习关于 ES6 代理及其用途的内容。

第六章:使用代理

代理用于定义对象基本操作的自定义行为。代理已经在编程语言如 C#、C++和 Java 中可用,但 JavaScript 从未有过代理。ES6 引入了 Proxy API,它允许我们创建代理。在本章中,我们将探讨代理、它们的用法和代理陷阱。由于代理的好处,开发者越来越多地使用代理,因此深入了解代理并举例说明非常重要,我们将在本章中这样做。

在本章中,我们将涵盖:

  • 使用 Proxy API 创建代理

  • 理解代理是什么以及如何使用它们

  • 使用陷阱拦截对象上的各种操作

  • 可用的不同类型的陷阱

  • 代理的一些用例

代理概述

代理就像对象的包装器,并定义了对象基本操作的自定义行为。对象的一些基本操作包括属性查找、属性赋值、构造函数调用、枚举等。

一旦使用代理包装了一个对象,所有应该在对象上执行的操作现在都应该在代理对象上执行,以便执行自定义行为。

术语

在学习代理时,这里有一些重要的术语:

  • 目标: 这是被代理器包装的对象。

  • 陷阱: 这些是拦截目标对象上各种操作并定义这些操作自定义行为的函数。

  • 处理器: 这是一个持有陷阱的对象。处理器附加到代理对象上。

Proxy API

ES6 Proxy API 提供了Proxy构造函数来创建代理。Proxy构造函数接受两个参数,它们是:

  • 目标: 这是将被代理器包装的对象

  • 处理器: 这是一个包含目标对象陷阱的对象

可以为目标对象上的每个可能的操作定义一个陷阱。如果没有定义陷阱,则默认行为将在目标对象上执行。

这里有一个代码示例,展示了如何创建代理,并在目标对象上执行各种操作。在这个例子中,我们没有定义任何陷阱:

var target = {age: 12
};
var handler = {};
var proxy = new Proxy(target, handler);proxy.name = "Eden";console.log(target.name);
console.log(proxy.name);
console.log(target.age);
console.log(proxy.age);

输出如下:

Eden
Eden
12
12

在这里,我们可以看到target对象的age属性可以通过proxy对象访问。当我们向proxy对象添加name属性时,它实际上被添加到了target对象上。

由于没有为属性赋值附加陷阱,因此proxy.name赋值导致默认行为,即简单地将值赋给属性。

因此,我们可以说proxy只是target对象的包装器,并且可以定义陷阱以更改操作的默认行为。

许多开发者没有为目标对象保留引用变量以使用代理强制访问对象。只有在你需要为多个代理重用处理器时才保留处理器的引用。以下是他们重写之前代码的方式:

var proxy = new Proxy({age: 12
}, {});proxy.name = "Eden";

陷阱

对于可以在对象上执行的不同操作,有不同的陷阱。其中一些陷阱需要返回值。在返回值时,它们需要遵循一些规则。返回的值被代理拦截以进行过滤,以及/或检查返回的值是否遵守规则。如果一个陷阱在返回值时违反规则,则代理抛出 TypeError 异常。

在陷阱内部,this 的值始终是处理器的引用。

让我们来看看各种陷阱。

get(target, property, receiver) 方法

当我们使用点或方括号表示法检索属性值,或使用 Reflect.get() 方法时,会执行 get 陷阱。它接受三个参数,即目标对象、属性名和代理。

它必须返回表示属性值的值。

这里是一个代码示例,展示了如何使用 get 陷阱:

var proxy = new Proxy({age: 12}, {get: function(target, property, receiver){if(property in target){return target[property];}else{return "Not Found";}}}
);console.log(Reflect.get(proxy, "age"));
console.log(Reflect.get(proxy, "name"));

输出如下:

12
Not found

在这里,我们可以看到 get 陷阱在 target 对象中查找属性,如果找到,则返回属性值。否则,它返回一个字符串,表示未找到。

receiver 参数是我们打算访问其属性的对象的引用。考虑以下示例以更好地理解 receiver 参数的值:

var proxy = new Proxy({age: 13}, {get: function(target, property, receiver){console.log(receiver);if(property in target){console.log(receiver);return target[property];}else{return "Not Found";}}}
);var temp = proxy.name;var obj = {age: 12,__proto__: proxy
}temp = obj.name;

输出如下:

{age: 13}
{age: 12}

这里 obj 继承了 proxy 对象。因此,当在 obj 对象中找不到 name 属性时,它会在 proxy 对象中查找。由于 proxy 对象有一个 get 陷阱,它提供了一个值。

因此,当我们通过 obj.name 表达式访问 name 属性时,receiver 参数的值是 obj,当我们通过 proxy.name 表达式访问 name 属性时,是 proxy

对于所有其他陷阱,receiver 参数的值也是以相同的方式决定的。

规则

在使用 get 陷阱时,不应违反以下规则:

  • 如果目标对象的属性是一个不可写、不可配置的数据属性,则返回该属性的值必须与目标对象属性的值相同。

  • 如果目标对象的属性是一个不可配置的访问器属性,并且其 [[Get]] 属性为 undefined,则返回该属性的值必须是 undefined

set(target, property, value, receiver) 方法

当我们使用赋值运算符或 Reflect.set() 方法设置属性值时,会调用 set 陷阱。它接受四个参数,即目标对象、属性名、新属性值和接收者。

如果赋值成功,set 陷阱必须返回 true。否则,它将返回 false

这里是一个代码示例,展示了如何使用 set 陷阱:

var proxy = new Proxy({}, {set: function(target, property, value, receiver){target[property] = value;return true;}
});Reflect.set(proxy, "name", "Eden");
console.log(proxy.name); //Output "Eden"

规则

在使用 set 陷阱时,不应违反以下规则:

  • 如果目标对象属性是一个不可写、不可配置的数据属性,那么它将返回 false,也就是说,你不能更改属性值

  • 如果目标对象属性是一个不可配置的访问器属性,并且其 [[Set]] 属性为 undefined,那么它将返回 false,也就是说,你不能更改属性值

has(target, property) 方法

当我们使用 in 操作符检查属性是否存在时,将执行 has 陷阱。它接受两个参数,即目标对象和属性名。它必须返回一个布尔值,表示属性是否存在。

这里有一个代码示例,演示了如何使用 has 陷阱:

var proxy = new Proxy({age: 12}, {has: function(target, property){if(property in target){return true;}else{return false;}}
});console.log(Reflect.has(proxy, "name"));
console.log(Reflect.has(proxy, "age"));

输出如下:

false
true

规则

在使用 has 陷阱时,不应违反以下规则:

  • 如果属性作为 target 对象的非配置性自身属性存在,你不能返回 false

  • 如果属性作为 target 对象的自身属性存在,并且 target 对象不可扩展,你不能返回 false

isExtensible(target) 方法

当我们使用 Object.isExtensible() 方法检查对象是否可扩展时,将执行 isExtensible 陷阱。它只接受一个参数,即 target 对象。它必须返回一个布尔值,表示对象是否可扩展。

这里有一个代码示例,演示了如何使用 isExtensible 陷阱:

var proxy = new Proxy({age: 12}, {isExtensible: function(target){return Object.isExtensible(target);}
});console.log(Reflect.isExtensible(proxy)); //Output "true"

规则

在使用 isExtensible 陷阱时,不应违反以下规则:

  • 如果目标可扩展,你不能返回 false。同样,如果目标不可扩展,你不能返回 true

getPrototypeOf(target) 方法

当我们使用 Object.getPrototypeOf() 方法或 __proto__ 属性检索内部 [[prototype]] 属性的值时,将执行 getPrototypeOf 陷阱。它只接受一个参数,即 target 对象。

它必须返回一个对象或 null 值。null 值表示对象没有继承其他内容,并且是继承链的末端。

这里有一个代码示例,演示了如何使用 getPrototypeOf 陷阱:

var proxy = new Proxy({age: 12, __proto__: {name: "Eden"}}, {getPrototypeOf: function(target){return Object.getPrototypeOf(target);}
});console.log(Reflect.getPrototypeOf(proxy).name); //Output "Eden"

规则

在使用 getPrototypeOf 陷阱时,不应违反以下规则:

  • 它必须返回一个对象或返回 null 值。

  • 如果目标不可扩展,那么这个陷阱必须返回实际的原型

setPrototypeOf(target, prototype) 方法

当我们使用 Object.setPrototypeOf() 方法或 __proto__ 属性设置内部 [[prototype]] 属性的值时,将执行 setPrototypeOf 陷阱。它接受两个参数,即目标对象和要分配的属性值。

这个陷阱将返回一个布尔值,表示是否已成功设置原型。

这里有一个代码示例,演示了如何使用 setPrototypeOf 陷阱:

var proxy = new Proxy({}, {setPrototypeOf: function(target, value){Reflect.setPrototypeOf(target, value);return true;}
});Reflect.setPrototypeOf(proxy, {name: "Eden"});console.log(Reflect.getPrototypeOf(proxy).name); //Output "Eden"

规则

在使用 setPrototypeOf 陷阱时,不应违反以下规则:

  • 如果目标不可扩展,你必须返回false

preventExtensions(target)方法

当我们使用Object.preventExtensions()方法防止添加新属性时,会执行preventExtensions陷阱。它只接受一个参数,即target对象。

它必须返回一个布尔值,指示是否已成功防止对象的扩展。

这里是一个代码示例,演示了如何使用preventExtensions陷阱:

var proxy = new Proxy({}, {preventExtensions: function(target){Object.preventExtensions(target);return true;}
});Reflect.preventExtensions(proxy);proxy.a = 12;
console.log(proxy.a); //Output "undefined"

规则

在使用preventExtensions陷阱时,不应违反以下规则:

  • 此陷阱只有在目标不可扩展或已使目标不可扩展的情况下才能返回true

getOwnPropertyDescriptor(target, property)方法

当我们使用Object.getOwnPropertyDescriptor()方法检索属性的描述符时,会执行getOwnPropertyDescriptor陷阱。它接受两个参数,即目标对象和属性名称。

此陷阱必须返回一个描述符对象或undefined。如果属性不存在,则返回undefined值。

这里是一个代码示例,演示了如何使用getOwnPropertyDescriptor陷阱:

var proxy = new Proxy({age: 12}, {getOwnPropertyDescriptor: function(target, property){return Object.getOwnPropertyDescriptor(target, property);}
});var descriptor = Reflect.getOwnPropertyDescriptor(proxy, "age");console.log("Enumerable: " + descriptor.enumerable);
console.log("Writable: " + descriptor.writable);
console.log("Configurable: " + descriptor.configurable);
console.log("Value: " + descriptor.value);

输出如下:

Enumerable: true
Writable: true
Configurable: true
Value: 12

规则

在使用getOwnPropertyDescriptor陷阱时,不应违反以下规则:

  • 此陷阱必须返回一个对象或返回一个undefined属性。

  • 如果属性作为target对象的非可配置自有属性存在,则不能返回undefined值。

  • 如果属性作为target对象的非可配置自有属性存在,并且target对象不可扩展,则不能返回undefined值。

  • 如果属性不是target对象的自有属性,并且target对象不可扩展,你必须返回undefined

  • 如果属性作为target对象的自有属性存在,或者作为可配置的自有属性存在,则不能将返回的描述符对象的configurable属性设置为false

defineProperty(target, property, descriptor)方法

当我们使用Object.defineProperty()方法定义属性时,会执行defineProperty陷阱。它接受三个参数,即target对象、属性名称和描述符对象。

此陷阱应返回一个布尔值,指示是否已成功定义属性。

这里是一个代码示例,演示了如何使用defineProperty陷阱:

var proxy = new Proxy({}, {defineProperty: function(target, property, descriptor){Object.defineProperty(target, property, descriptor);return true;}
});Reflect.defineProperty(proxy, "name", {value: "Eden"});console.log(proxy.name); //Output "Eden"

规则

在使用defineProperty陷阱时,不应违反以下规则:

  • 如果target对象不可扩展,并且属性尚未存在,则必须返回false

deleteProperty(target, property)方法

当我们使用delete运算符或Reflect.deleteProperty()方法删除属性时,会执行deleteProperty陷阱。它接受两个参数,即target对象和property名称。

此陷阱必须返回一个布尔值,指示属性是否成功删除。

下面是一个代码示例,演示了如何使用 deleteProperty 陷阱:

var proxy = new Proxy({age: 12}, {deleteProperty: function(target, property){return delete target[property];}
});Reflect.deleteProperty(proxy, "age");
console.log(proxy.age); //Output "undefined"

规则

在使用 deleteProperty 陷阱时,不应违反此规则:

  • 如果属性作为 target 对象的非配置性自身属性存在,则此陷阱必须返回 false

enumerate(target) 方法

当我们使用 for...in 循环或 Reflect.enumerate() 方法遍历属性键时,会执行 enumerate 陷阱。它接受一个参数,即 target 对象。

这个陷阱必须返回一个迭代器对象,表示对象的可枚举键。

下面是一个代码示例,演示了如何使用 enumerate 陷阱:

var proxy = new Proxy({age: 12, name: "Eden"}, {enumerate: function(target){var arr = [];for(var p in target){arr[arr.length] = p;}return arr[Symbol.iterator]();}
});var iterator = Reflect.enumerate(proxy);console.log(iterator.next().value);
console.log(iterator.next().value);
console.log(iterator.next().done);

输出如下:

age
name
true

规则

在使用 enumerate 陷阱时,不应违反此规则:

  • 此陷阱必须返回一个对象

ownKeys(target) 方法

当我们使用 Reflect.ownKeys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.keys() 方法检索自身属性键时,会执行 ownKeys 陷阱。它只接受一个参数,即 target 对象。

Reflect.ownKeys() 方法类似于 Object.getOwnPropertyNames() 方法,即它们返回对象的枚举和非枚举属性键。它们忽略继承的属性。唯一的区别是 Reflect.ownKeys() 方法返回的是符号键和字符串键,而 Object.getOwnPropertyNames() 方法只返回字符串键。

Object.getOwnPropertySymbols() 方法返回键为符号的可枚举和非枚举属性。它忽略继承的属性。

Object.keys() 方法类似于 Object.getOwnPropertyNames() 方法,但唯一的不同是 Objecy.keys() 方法只返回可枚举属性。

ownKeys 陷阱必须返回一个数组,表示自身的属性键。

下面是一个代码示例,演示了如何使用 ownKeys 陷阱:

var s = Symbol();var object = {age: 12, __proto__: {name: "Eden"}, [s]: "Symbol"};Object.defineProperty(object, "profession", {enumerable: false,configurable: false,writable: false,value: "Developer"
})var proxy = new Proxy(object, {ownKeys: function(target){return Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target));}
});console.log(Reflect.ownKeys(proxy));
console.log(Object.getOwnPropertyNames(proxy));
console.log(Object.keys(proxy));
console.log(Object.getOwnPropertySymbols(proxy));

输出如下:

["age", "profession", Symbol()]
["age", "profession"]
["age"]
[Symbol()]

在这里,我们可以看到 ownKeys 陷阱返回的数组值被代理根据调用者期望的值进行过滤。例如,Object.getOwnPropertySymbols() 调用者期望一个包含符号的数组。因此,代理从返回的数组中移除了字符串。

规则

在使用 ownKeys 陷阱时,不应违反这些规则:

  • 返回数组的元素必须是字符串或符号

  • 返回的数组必须包含 target 对象所有非配置性自身属性的键。

  • 如果 target 对象不可扩展,则返回的数组必须包含 target 对象所有自身属性的键,且不包含其他值。

apply(target, thisValue, arguments) 方法

如果目标是函数,则调用代理将执行 apply 陷阱。apply 陷阱也会在函数的 apply()call() 方法以及 Reflect.apply() 方法中执行。

apply 陷阱接受三个参数。第一个参数是 target 对象,第三个参数是一个数组,表示函数调用时的参数。第二个参数与目标函数的 this 值相同,即它等同于目标函数在没有代理的情况下被调用时的 this 值。

下面是一个代码示例,演示了如何使用 apply 陷阱:

var proxy = new Proxy(function(){}, {apply: function(target, thisValue, arguments){console.log(thisValue.name);return arguments[0] + arguments[1] + arguments[2];}
});var obj = {name: "Eden",f: proxy
}var sum = obj.f(1,2,3);console.log(sum);

输出如下:

Eden
6

construct(target, arguments) 方法

如果目标是函数,则使用 new 操作符或 Reflect.construct() 方法将目标作为构造函数调用时,将执行 construct 陷阱。

construct 陷阱接受两个参数。第一个参数是 target 对象,第二个参数是一个数组,表示构造函数调用时的参数。

construct 陷阱必须返回一个对象,表示新创建的实例。

下面是一个代码示例,演示了如何使用 construct 陷阱:

var proxy = new Proxy(function(){}, {construct: function(target, arguments){return {name: arguments[0]};}
});var obj = new proxy("Eden");
console.log(obj.name); //Output "Eden"

Proxy.revocable(target, handler) 方法

可撤销代理是一种可以被撤销(即关闭)的代理。

要创建可撤销的代理,我们必须使用 Proxy.revocable() 方法。Proxy.revocable() 方法不是一个构造函数。此方法也接受与 Proxy 构造函数相同的参数,但它不会直接返回一个可撤销的代理实例,而是返回一个具有两个属性的对象,具体如下:

  • proxy: 这是一个可撤销的代理对象

  • revoke: 当此函数被调用时,它将撤销 proxy

一旦撤销了可撤销代理,任何尝试使用它的操作都将抛出 TypeError 异常。

下面是一个演示如何创建可撤销代理并撤销它的示例:

var revocableProxy = Proxy.revocable({age: 12}, {get: function(target, property, receiver){if(property in target){return target[property];}else{return "Not Found";}}}
);console.log(revocableProxy.proxy.age);revocableProxy.revoke();console.log(revocableProxy.proxy.name);

输出如下:

12
TypeError: proxy is revoked

用例

你可以使用可撤销代理代替常规代理。当你将代理传递给一个异步运行或并行运行的函数时,你可以使用它,这样你就可以随时撤销它,以防你不想让该函数再使用该代理。

代理的使用

代理有多种用途。以下是一些主要用例:

  • 创建虚拟对象,例如远程对象、持久对象等

  • 对象的懒加载创建

  • 透明的日志记录、跟踪、分析等

  • 嵌入特定领域的语言

  • 通过泛化插入抽象来强制执行访问控制

摘要

在本章中,我们学习了什么是代理以及如何使用它们。我们通过示例看到了可用的各种陷阱。我们还看到了不同陷阱需要遵循的不同规则。本章深入解释了 ES6 Proxy API 的所有内容。最后,我们看到了一些代理的使用案例。

在下一章中,我们将介绍面向对象编程和 ES6 类。

第七章. 带你走进类

ES6 引入了类,它提供了创建构造函数和处理继承的更简单、更清晰的语法。尽管 JavaScript 是一种面向对象的编程语言,但它从未有过类的概念。来自其他编程语言背景的程序员往往由于缺乏类而难以理解 JavaScript 的面向对象模型和继承。在本章中,我们将使用 ES6 类来学习面向对象的 JavaScript:

  • JavaScript 数据类型

  • 以传统方式创建对象

  • 原始类型构造函数

  • ES6 中的类是什么

  • 使用类创建对象

  • 类中的继承

  • 类的特性

理解面向对象的 JavaScript

在我们继续学习 ES6 类之前,让我们回顾一下 JavaScript 数据类型、构造函数和继承的知识。在学习类时,我们将比较构造函数和基于原型的继承的语法与类的语法。因此,对这些主题有良好的掌握是非常重要的。

JavaScript 数据类型

JavaScript 变量持有(或存储)数据(或值)。变量所持有的数据类型称为 数据类型。在 JavaScript 中,有七种不同的数据类型:numberstringBooleannullundefinedsymbolobject

当涉及到持有对象时,变量持有对象引用(即内存地址)而不是对象本身。

除了对象之外的所有数据类型都称为 原始数据类型

注意

数组和函数实际上是 JavaScript 对象。

创建对象

在 JavaScript 中创建对象有两种方式,即使用对象字面量或使用构造函数。当我们需要创建固定对象时使用对象字面量,而当我们在运行时动态创建对象时使用构造函数。

让我们考虑一个可能需要使用构造函数而不是对象字面量的情况。以下是一个代码示例:

var student = {name: "Eden",printName: function(){console.log(this.name);}
}student.printName(); //Output "Eden"

在这里,我们使用对象字面量创建了 student 对象,即 {} 符号。当你只想创建单个 student 对象时,这很有效。

但是,当你想要创建多个 student 对象时,问题就出现了。显然,你不想多次编写之前的代码来创建多个 student 对象。这就是构造函数发挥作用的地方。

当使用 new 关键字调用时,函数表现得像一个构造函数。构造函数创建并返回一个对象。在函数内部,当作为构造函数调用时,this 关键字指向新的对象实例,一旦构造函数执行完成,新对象将自动返回。考虑以下示例:

function Student(name)
{this.name = name;
}Student.prototype.printName = function(){console.log(this.name);
}var student1 = new Student("Eden");
var student2 = new Student("John");student1.printName(); //Output "Eden"
student2.printName(); //Output "John"

在这里,为了创建多个学生对象,我们多次调用了构造函数,而不是使用对象字面量创建多个学生对象。

要向构造函数的实例添加方法,我们没有使用this关键字,而是使用了构造函数的prototype属性。我们将在下一节中了解更多关于为什么这样做以及prototype属性是什么。

实际上,每个对象都必须属于一个构造函数。每个对象都有一个名为constructor的继承属性,指向该对象的构造函数。当我们使用对象字面量创建对象时,constructor属性指向全局的Object构造函数。考虑以下示例来理解这种行为:

var student = {}console.log(student.constructor == Object); //Output "true"

理解继承

每个 JavaScript 对象都有一个内部名为[[prototype]]的属性,它指向另一个称为其原型的对象。这个原型对象有自己的原型,以此类推,直到找到一个其原型为null的对象。null没有原型,它作为原型链中的最后一个链接。

当尝试访问一个对象的属性时,如果该属性在对象中找不到,那么该属性将在对象的原型中搜索。如果仍然找不到,那么将在原型对象的原型中搜索。这个过程会一直持续到原型链中遇到null。这就是 JavaScript 中继承的工作方式。

由于 JavaScript 对象只能有一个原型,JavaScript 只支持单继承。

在使用对象字面量创建对象时,我们可以使用特殊的__proto__属性或Object.setPrototypeOf()方法来指定对象的原型。JavaScript 还提供了一个Object.create()方法,我们可以用它来创建一个新的对象,其__proto__被指定为原型。由于浏览器不支持__proto__,而Object.setPrototypeOf()方法看起来有些奇怪。以下是一个代码示例,展示了在创建对象时使用对象字面量设置对象原型的不同方法:

var object1 = {name: "Eden",__proto__: {age: 24}
}var object2 = {name: "Eden"}
Object.setPrototypeOf(object2, {age: 24});var object3 = Object.create({age: 24}, {name: {value: "Eden"}});console.log(object1.name + " " + object1.age);
console.log(object2.name + " " + object2.age);
console.log(object3.name + " " + object3.age);

输出如下:

Eden 24
Eden 24
Eden 24

在这里,{age:24}对象被称为基对象超对象父对象,因为它正在继承。而{name:"Eden"}对象被称为派生对象子对象子对象,因为它继承了另一个对象。

如果你在使用对象字面量创建对象时没有为其指定原型,那么原型将指向Object.prototype属性。由于Object.prototype的原型是null,因此导致原型链的结束。以下是一个示例来演示这一点:

var obj = {name: "Eden"
}console.log(obj.__proto__ == Object.prototype); //Output "true"

在使用构造函数创建对象时,新对象的原型始终指向函数对象的prototype属性。默认情况下,prototype属性是一个具有一个名为constructor的属性的对象。constructor属性指向函数本身。考虑以下示例来理解这个模型:

function Student()
{this.name = "Eden";
}var obj = new Student();console.log(obj.__proto__.constructor == Student); //Output "true"
console.log(obj.__proto__ == Student.prototype); //Output "true"

要向构造函数的实例添加新方法,我们应该将它们添加到构造函数的 prototype 属性中,就像我们之前做的那样。我们不应该在构造函数体中使用 this 关键字来添加方法,因为构造函数的每个实例都将有一个方法副本,这并不非常内存高效。通过将方法附加到构造函数的 prototype 属性,每个函数只有一个副本,所有实例共享。为了理解这一点,考虑以下示例:

function Student(name)
{this.name = name;
}Student.prototype.printName = function(){console.log(this.name);
}var s1 = new Student("Eden");
var s2 = new Student("John");function School(name)
{this.name = name;this.printName = function(){console.log(this.name);}
}var s3 = new School("ABC");
var s4 = new School("XYZ");console.log(s1.printName == s2.printName);
console.log(s3.printName == s4.printName);

输出如下:

true
false

在这里,s1s2 共享相同的 printName 函数,这减少了内存的使用,而 s3s4 包含两个不同的名为 printName 的函数,这使程序使用更多的内存。这是不必要的,因为这两个函数执行相同的事情。因此,我们将实例的方法添加到构造函数的 prototype 属性中。

在构造函数中实现继承层次结构并不像我们对对象字面量所做的那样直接。因为子构造函数需要调用父构造函数以执行父构造函数的初始化逻辑,并且我们需要将父构造函数的 prototype 属性的方法添加到子构造函数的 prototype 属性中,以便我们可以使用它们与子构造函数的对象一起使用。没有预定义的方式来完成所有这些。开发人员和 JavaScript 库有自己的实现方式。我将向您展示最常见的方法。

这里有一个示例,演示如何在创建对象时使用构造函数实现继承:

function School(schoolName)
{this.schoolName = schoolName;
}
School.prototype.printSchoolName = function(){console.log(this.schoolName);
}function Student(studentName, schoolName)
{this.studentName = studentName;School.call(this, schoolName);
}
Student.prototype = new School();
Student.prototype.printStudentName = function(){console.log(this.studentName);
}var s = new Student("Eden", "ABC School");
s.printStudentName();
s.printSchoolName();

输出如下:

Eden
ABC School

在这里,我们使用函数对象的 call 方法调用了父构造函数。为了继承方法,我们创建了一个父构造函数的实例,并将其分配给子构造函数的 prototype 属性。

这不是在构造函数中实现继承的万无一失的方法,因为存在许多潜在问题。例如——如果父构造函数执行的操作不仅仅是初始化属性,例如 DOM 操作,那么将父构造函数的新实例分配给子构造函数的 prototype 属性可能会引起问题。

因此,ES6 类提供了更好的、更简单的方式来继承现有的构造函数和类。我们将在本章后面进一步了解这一点。

原始数据类型的构造函数

原始数据类型,如布尔值、字符串和数字,都有它们的构造函数对应物。这些对应构造函数的行为类似于这些原始类型的包装器。例如,String 构造函数用于创建包含内部 [[PrimitiveValue]] 属性的字符串对象,该属性持有实际的原始值。

在运行时,在必要时,原始值会被它们的构造函数对应物包装,同时对应对象也会被当作原始值处理,这样代码才能按预期工作。考虑以下示例代码来理解它是如何工作的:

var s1 = "String";
var s2 = new String("String");console.log(typeof s1);
console.log(typeof s2);console.log(s1 == s2);
console.log(s1.length);

输出如下:

string
object
true
6

在这里,s1是一个原始类型,而s2是一个对象,尽管对它们应用==运算符会得到true的结果。s1是一个原始类型,但我们仍然能够访问长度属性,然而原始类型不应该有任何属性。

所有这些都是在运行时将之前的代码转换成以下形式发生的:

var s1 = "String";
var s2 = new String("String");console.log(typeof s1);
console.log(typeof s2);console.log(s1 == s2.valueOf());
console.log((new String(s1)).length);

这里,我们可以看到原始值是如何被其构造函数对应物包装的,以及当需要时对象对应物是如何被当作原始值处理的。因此,代码按预期工作。

从 ES6 开始引入的原始类型不会允许它们的对应函数作为构造函数被调用,也就是说,我们不能显式地使用它们的对象对应物来包装它们。我们在学习符号时看到了这种行为。

原始类型nullundefined没有对应的构造函数。

使用类

我们看到 JavaScript 的面向对象模型基于构造函数和基于原型的继承。嗯,ES6 类只是现有模型的新语法。类并没有为 JavaScript 引入新的面向对象模型。

ES6 类的目的是为了提供一个更简单、更清晰的语法来处理构造函数和继承。

实际上,类是函数。类只是创建用作构造函数的函数的新语法。使用类创建不作为构造函数使用的函数没有任何意义,也不会带来任何好处。相反,它会使得你的代码难以阅读,因为它变得混乱。因此,只有当你想用它来构造对象时才使用类。让我们详细看看类。

定义类

正如定义函数有两种方式,函数声明和函数表达式,定义类也有两种方式:使用类声明和类表达式。

类声明

要使用类声明来定义一个类,你需要使用class关键字和类的名称。

这里有一个代码示例,演示了如何使用类声明来定义一个类:

class Student
{constructor(name){this.name = name;}
}var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

在这里,我们创建了一个名为Student的类。然后,我们在其中定义了一个constructor方法。最后,我们创建了该类的一个新实例——一个对象,并记录了该对象的name属性。

类的主体位于花括号内,即{}。这是我们定义方法的地方。方法定义时不使用function关键字,并且在方法之间不使用逗号。

类被当作函数处理,并且内部类名被当作函数名,constructor方法的主体被当作函数的主体。

在一个类中只能有一个构造函数方法。定义多个构造函数将抛出SyntaxError异常。

默认情况下,类体内的所有代码都在strict模式下执行。

当使用函数编写时,前面的代码与这段代码相同:

function Student(name)
{this.name = name;
}var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

要证明类是一个函数,请考虑以下代码:

class Student
{constructor(name){this.name = name;}
}function School(name)
{this.name = name;
}console.log(typeof Student);
console.log(typeof School == typeof Student);

输出如下:

function
true

这里,我们可以看到类是一个函数。它只是创建函数的新语法。

类表达式

类表达式与类声明的语法类似。然而,使用类表达式时,您可以省略类名。类体和行为在这两种方式中保持相同。

下面是一个使用类表达式定义类的代码示例:

var Student = class {constructor(name){this.name = name;}
}var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

这里,我们将类的引用存储在一个变量中,并使用它来构造对象。

当使用函数编写时,前面的代码与这段代码相同:

var Student = function(name) {this.name = name;
}var s1 = new Student("Eden");
console.log(s1.name); //Output "Eden"

原型方法

类体中的所有方法都被添加到类的prototype属性中。prototype属性是使用类创建的对象的原型。

下面是一个示例,展示如何向类的prototype属性中添加方法:

class Person
{constructor(name, age){this.name = name;this.age = age;}printProfile(){console.log("Name is: " + this.name + " and Age is: " + this.age);}
}var p = new Person("Eden", 12)
p.printProfile();console.log("printProfile" in p.__proto__);
console.log("printProfile" in Person.prototype);

输出如下:

Name is: Eden and Age is: 12
true
true

这里,我们可以看到printProfile方法被添加到了类的prototype属性中。

当使用函数编写时,前面的代码与这段代码相同:

function Person(name, age)
{this.name = name;this.age = age;
}Person.prototype.printProfile = function()
{console.log("Name is: " + this.name + " and Age is: " + this.age);
}var p = new Person("Eden", 12)
p.printProfile();console.log("printProfile" in p.__proto__);
console.log("printProfile" in Person.prototype);

输出如下:

Name is: Eden and Age is: 12
true
true

获取和设置方法

在 ES5 中,为了向对象添加访问器属性,我们必须使用Object.defineProperty()方法。ES6 引入了方法的getset前缀。这些方法可以添加到对象字面量和类中,以定义访问器属性的getset属性。

当在类体中使用getset方法时,它们被添加到类的prototype属性中。

下面是一个示例,演示如何在类中定义getset方法:

class Person
{constructor(name){this._name_ = name;}get name(){return this._name_;}set name(name){this._name_ = name;}
}var p = new Person("Eden");
console.log(p.name);
p.name = "John";
console.log(p.name);console.log("name" in p.__proto__);
console.log("name" in Person.prototype);
console.log(Object.getOwnPropertyDescriptor(p.__proto__, "name").set);
console.log(Object.getOwnPropertyDescriptor(Person.prototype, "name").get);
console.log(Object.getOwnPropertyDescriptor(p, "_name_").value);

输出如下:

Eden
John
true
true
function name(name) { this._name_ = name; }
function name() { return this._name_; }
John

这里,我们创建了一个访问器属性来封装_name_属性。我们还记录了一些其他信息来证明name是一个访问器属性,它被添加到了类的prototype属性中。

生成器方法

要将对象字面量的简洁方法视为生成器方法,或将类的方法视为生成器方法,我们只需在它前面加上*字符。

类的生成器方法被添加到类的prototype属性中。

下面是一个示例,演示如何在类中定义一个生成器方法:

class myClass
{* generator_function(){yield 1;yield 2;yield 3;yield 4;yield 5;}}var obj = new myClass();let generator = obj.generator_function();console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().done);console.log("generator_function" in myClass.prototype);

输出如下:

1
2
3
4
5
true
true

静态方法

使用static前缀添加到类体中的方法被称为静态方法。静态方法是类的自有方法,也就是说,它们不是添加到类的prototype属性中,而是直接添加到类本身。例如,String.fromCharCode()方法是String构造函数的静态方法,即fromCharCodeString函数本身的自有属性。

静态方法通常用于为应用程序创建实用函数。

这里有一个示例来展示如何在类中定义和使用静态方法:

class Student
{constructor(name){this.name = name;}static findName(student){return student.name;}
}var s = new Student("Eden");
var name = Student.findName(s);console.log(name); //Output "Eden"

在这里,findNameStudent类的静态方法。

之前的代码与以下使用函数编写的代码相同:

function Student(name)
{this.name = name;
}Student.findName = function(student){return student.name;
}var s = new Student("Eden");
var name = Student.findName(s);console.log(name); //Output "Eden"

在类中实现继承

在本章的早期部分,我们看到了在函数中实现继承层次结构的难度。因此,ES6 通过引入extends子句和类的super关键字来简化这一过程。

通过使用extends子句,一个类可以继承另一个构造函数的静态和非静态属性(这些属性可能或可能不是使用类定义的)。

super关键字有两种用法:

  • 它在类constructor方法中用于调用父构造函数

  • 当在类的内部方法中使用时,它引用父构造函数的静态和非静态方法

以下是一个示例,展示如何使用extends子句和super关键字在构造函数中实现继承层次结构:

function A(a)
{this.a = a;
}A.prototype.printA = function(){console.log(this.a);
}class B extends A
{constructor(a, b){super(a);this.b = b;}printB(){console.log(this.b);}static sayHello(){console.log("Hello");}
}class C extends B
{constructor(a, b, c){super(a, b);this.c = c;}printC(){console.log(this.c);}printAll(){this.printC();super.printB();super.printA();}
}var obj = new C(1, 2, 3);
obj.printAll();C.sayHello();

输出如下:

3
2
1
Hello

在这里,A是一个函数构造函数;B是一个继承自A的类;C是一个继承自B的类;由于B继承了A,因此C也继承了A

由于类可以继承函数构造函数,我们也可以继承预构建的函数构造函数,例如StringArray,以及使用类而不是我们以前使用的其他笨拙方法来自定义函数构造函数。

之前的示例也展示了如何以及在哪里使用super关键字。记住,在constructor方法内部,在使用this关键字之前需要使用super。否则,会抛出异常。

注意

如果子类没有constructor方法,则默认行为将调用父类的constructor方法。

计算方法名

你还可以在运行时决定类的静态和非静态方法以及对象字面量的简洁方法的名字,也就是说,你可以通过表达式定义方法的名字。以下是一个示例来展示这一点:

class myClass
{static ["my" + "Method"](){console.log("Hello");}
}myClass["my" + "Method"](); //Output "Hello"

计算属性名还允许你使用符号作为方法的键。以下是一个示例来展示这一点:

var s = Symbol("Sample");class myClass
{static [s](){console.log("Hello");}
}myClass[s](); //Output "Hello"

属性的属性

当使用类时,构造函数的静态和非静态属性的属性与使用函数声明时不同:

  • 静态方法是可写和可配置的,但不是可枚举的

  • 类的 prototype 属性和 prototype.constructor 属性是不可写、不可枚举或不可配置的

  • prototype 属性的属性是可写和可配置的,但不是可枚举的

类不会被提升!

你可以在定义函数之前调用它,也就是说,函数调用可以在函数定义之上进行。但在类的情况下,你不能在定义之前使用类。在类中尝试这样做将会抛出 ReferenceError 异常。

以下是一个演示此点的示例:

myFunc();
function myFunc(){}var obj = new myClass(); //throws ReferenceError exception
class myClass{}

覆盖构造函数方法的返回结果

默认情况下,constructor 方法如果没有 return 语句,则返回新实例。如果有 return 语句,则返回 return 语句中的值。

以下是一个演示此点的示例:

class myClass
{constructor(){return Object.create(null);}
}console.log(new myClass() instanceof myClass); //Output "false"

“Symbol.species” 静态访问器属性

@@species 静态访问器属性可选地添加到子构造函数中,以便向父构造函数的方法发出信号,告知构造函数在父构造函数的方法返回新实例时应使用什么。如果子构造函数上没有定义 @@species 静态访问器属性,则父构造函数的方法可以使用默认构造函数。

考虑以下示例以了解 @@species 的用法——数组对象的 map() 方法返回一个新的 Array 实例。如果我们调用继承数组对象的对象的 map() 方法,那么 map() 方法将返回子构造函数的新实例而不是 Array 构造函数,这通常不是我们想要的。因此,ES6 引入了 @@species 属性,它提供了一种向此类函数发出信号的方式,使其使用不同的构造函数而不是默认构造函数。

以下是一个代码示例,演示如何使用 @@species 静态访问器属性:

class myCustomArray1 extends Array
{static get [Symbol.species](){return Array;}
}class myCustomArray2 extends Array{}var arr1 = new myCustomArray1(0, 1, 2, 3, 4);
var arr2 = new myCustomArray2(0, 1, 2, 3, 4);console.log(arr1 instanceof myCustomArray1);
console.log(arr2 instanceof myCustomArray2);arr1 = arr1.map(function(value){ return value + 1; })
arr2 = arr2.map(function(value){ return value + 1; })console.log(arr1 instanceof myCustomArray1);
console.log(arr2 instanceof myCustomArray2);console.log(arr1 instanceof Array);
console.log(arr2 instanceof Array);

输出如下:

true
true
false
true
true
false

如果你正在创建一个 JavaScript 库,建议你的库中的构造函数方法在返回新实例时始终查找 @@species 属性。以下是一个演示此点的示例:

//Assume myArray1 is part of library
class myArray1
{//default @@species. Child class will inherit this propertystatic get [Symbol.species](){//default constructorreturn this;}mapping(){return new this.constructor[Symbol.species]();}
}class myArray2 extends myArray1
{static get [Symbol.species](){return myArray1;}
}var arr = new myArray2();console.log(arr instanceof myArray2); //Output "true"arr = arr.mapping();console.log(arr instanceof myArray1); //Output "true"

如果你不希望在父构造函数中定义默认的 @@species 属性,那么你可以使用 if…else 条件语句来检查 @@species 属性是否已定义。但首选的做法是使用之前的模式。内置的 map() 方法也使用之前的模式。

ES6 中 JavaScript 构造函数的所有内置方法在返回新实例时都会查找 @@species 属性。例如,ArrayMapArrayBufferPromise 等构造函数的方法在返回新实例时会查找 @@species 属性。

“new.target” 隐式参数

ES6 为所有函数添加了一个名为 new.target 的参数。参数名中的点号是参数名的一部分。

new.target 的默认值是 undefined。但当函数被作为构造函数调用时,new.target 参数的值取决于以下条件:

  • 如果使用 new 操作符调用构造函数,则 new.target 指向此构造函数

  • 如果通过 super 关键字调用构造函数,则其中的 new.target 的值与被调用 super 的构造函数的 new.target 的值相同。

在箭头函数内部,new.target 的值与周围的非箭头函数的 new.target 的值相同。

这里有一个示例代码来演示这一点:

function myConstructor()
{console.log(new.target.name);
}class myClass extends myConstructor
{constructor(){super();}
}var obj1 = new myClass();
var obj2 = new myConstructor();

输出如下:

myClass
myConstructor

在对象字面量中使用 "super"

super 关键字也可以用于对象字面量的简洁方法。对象字面量简洁方法中的 super 关键字,其值与由对象字面量定义的对象的 [[prototype]] 属性相同。

在对象字面量中,super 用于通过子对象访问被覆盖的属性。

这里有一个示例来展示如何在对象字面量中使用 super

var obj1 = {print(){console.log("Hello");}
}var obj2 = {print(){super.print();}
}Object.setPrototypeOf(obj2, obj1);
obj2.print(); //Output "Hello"

摘要

在本章中,我们首先使用 ES5 学习了面向对象编程的基础。然后,我们跳入了 ES6 类,并学习了它如何使我们更容易阅读和编写面向对象的 JavaScript 代码。我们还学习了其他特性,例如 new.target 和存取器方法。

在下一章中,我们将学习如何创建和使用 ES6 模块。

第八章:模块化编程

模块化编程是软件设计中最重要且最常用的技术之一。不幸的是,JavaScript 没有原生支持模块,这导致 JavaScript 程序员使用替代技术来实现 JavaScript 中的模块化编程。但现在,ES6 正式将模块引入 JavaScript。

本章全部关于如何创建和导入 JavaScript 模块。在本章中,我们将首先学习模块是如何在早期创建的,然后我们将跳转到 ES6 中引入的新内置模块系统,称为 ES6 模块。

在本章中,我们将涵盖:

  • 什么是模块化编程?

  • 模块化编程的好处

  • IIFE 模块、AMD、UMD 和 CommonJS 的基本原理

  • 创建和导入 ES6 模块

  • 模块加载器的基本原理

  • 使用模块创建基本的 JavaScript 库

简要介绍 JavaScript 模块

将程序和库分解为模块的实践称为模块化编程。

在 JavaScript 中,模块是一组相关的对象、函数和其他程序或库的组件,它们被封装在一起,并从程序或库的其余部分或库的作用域中隔离出来。

模块将一些变量导出到外部程序,以便它访问模块封装的组件。要使用模块,程序需要导入模块及其导出的变量。

模块也可以进一步拆分为称为其子模块的模块,从而创建模块层次结构。

模块化编程有许多好处。其中一些好处包括:

  • 通过将代码拆分为多个模块,它既保持了代码的清晰分离,又进行了组织。

  • 模块化编程导致全局变量更少,也就是说,它消除了全局变量的问题,因为模块不通过全局作用域进行接口,每个模块都有自己的作用域

  • 使得代码的可重用性更容易,因为在不同项目中导入和使用相同的模块更容易。

  • 它允许许多程序员在同一程序或库上协作,通过让每个程序员针对具有特定功能的特定模块进行工作。

  • 应用程序中的错误可以很容易地识别,因为它们被局部化到特定的模块中

实现模块——旧的方式

在 ES6 之前,JavaScript 从未支持过原生的模块。开发者使用其他技术和第三方库在 JavaScript 中实现模块。

使用立即执行函数表达式IIFE)、异步模块定义AMD)、CommonJS通用模块定义UMD)是 ES5 中实现模块的多种流行方式。由于这些方式并非 JavaScript 的本地特性,它们存在一些问题。让我们来概述一下这些旧的模块实现方式。

立即执行函数表达式

立即执行函数表达式(IIFE)用于创建一个自调用的匿名函数。使用 IIFE 创建模块是创建模块最流行的方式。

让我们看看如何使用 IIFE 创建模块的示例:

//Module Starts(function(window){var sum = function(x, y){return x + y;}var sub = function(x, y){return x - y;}var math = {findSum: function(a, b){return sum(a,b);},findSub: function(a, b){return sub(a, b);}}window.math = math;
})(window)//Module Endsconsole.log(math.findSum(1, 2)); //Output "3"
console.log(math.findSub(1, 2)); //Output "-1"

在这里,我们使用 IIFE 创建了一个模块。sumsub变量在模块内部是全局的,但在模块外部不可见。math变量由模块导出到主程序,以公开它提供的功能。

此模块完全独立于程序,可以通过将其复制到源代码中或作为单独的文件导入,由任何其他程序导入。

注意

使用立即执行函数表达式(IIFE)的库,例如 jQuery,将其所有 API 封装在一个单独的 IIFE 模块中。当程序使用 jQuery 库时,它会自动导入该模块。

异步模块定义

AMD 是浏览器中实现模块化的规范。AMD 在设计时考虑到浏览器的限制,即异步导入模块以防止阻塞网页的加载。由于 AMD 不是浏览器的原生规范,我们需要使用 AMD 库。RequireJS是最流行的 AMD 库。

让我们看看如何使用 RequireJS 创建和导入模块的示例。根据 AMD 规范,每个模块都需要由一个单独的文件表示。因此,首先创建一个名为math.js的文件,表示一个模块。以下是模块中的示例代码:

define(function(){var sum = function(x, y){return x + y;}var sub = function(x, y){return x - y;}var math = {findSum: function(a, b){return sum(a,b);},findSub: function(a, b){return sub(a, b);}}return math;
});

在这里,模块导出math变量以公开其功能。

现在,让我们创建一个名为index.js的文件,它充当主程序,导入模块和导出的变量。以下是index.js文件中的代码:

require(["math"], function(math){console.log(math.findSum(1, 2)); //Output "3"console.log(math.findSub(1, 2)); //Output "-1"
})

在这里,第一个参数中的math变量是作为 AMD 模块处理的文件名。文件名.js扩展名由 RequireJS 自动添加。

第二个参数中的math变量引用了导出的变量。

在这里,模块是异步导入的,回调函数也是异步执行的。

CommonJS

CommonJS 是 Node.js 中实现模块化的规范。根据 CommonJS 规范,每个模块都需要由一个单独的文件表示。CommonJS 模块是同步导入的。

让我们看看如何使用 CommonJS 创建和导入模块的示例。首先,我们将创建一个名为math.js的文件,表示一个模块。以下是模块中的示例代码:

var sum = function(x, y){return x + y;
}var sub = function(x, y){return x - y;
}var math = {findSum: function(a, b){return sum(a,b);},findSub: function(a, b){return sub(a, b);}
}exports.math = math;

在这里,模块导出math变量以公开其功能。

现在,让我们创建一个名为index.js的文件,它充当主程序,导入模块。以下是index.js文件中的代码:

var math = require("./math").math;console.log(math.findSum(1, 2)); //Output "3"
console.log(math.findSub(1, 2)); //Output "-1"

在这里,math变量是作为模块处理的文件名。CommonJS 会自动为文件名添加.js扩展名。

通用模块定义

我们看到了实现模块的三个不同规范。这三个规范都有它们各自创建和导入模块的方式。如果我们可以创建可以以 IIFE、AMD 或 CommonJS 模块导入的模块,那岂不是很好?

UMD 是一组技术,用于创建可以作为 IIFE、CommonJS 或 AMD 模块导入的模块。因此,现在程序可以导入第三方模块,无论它使用的是哪种模块规范。

最流行的 UMD 技术是 returnExports。根据 returnExports 技术规范,每个模块都需要由一个单独的文件表示。所以,让我们创建一个名为 math.js 的文件来表示一个模块。以下是模块内部的示例代码:

(function (root, factory) {//Environment Detectionif (typeof define === 'function' && define.amd) {define([], factory);} else if (typeof exports === 'object') {module.exports = factory();} else {root.returnExports = factory();}
}(this, function () {//Module Definitionvar sum = function(x, y){return x + y;}var sub = function(x, y){return x - y;}var math = {findSum: function(a, b){return sum(a,b);},findSub: function(a, b){return sub(a, b);}}return math;
}));

现在,你可以根据需要成功导入 math.js 模块,例如,使用 CommonJS、RequireJS 或 IIFE。

实现模块 – 新方法

ES6 引入了一种新的模块系统,称为 ES6 模块。ES6 模块是原生支持的,因此它们可以被称为标准 JavaScript 模块。

你应该考虑使用 ES6 模块而不是旧方法,因为它们有更简洁的语法、更好的性能,以及许多可能作为 ES6 模块打包的新 API。

让我们详细看看 ES6 模块。

创建 ES6 模块

每个 ES6 模块都需要由一个单独的 .js 文件表示。ES6 模块可以包含任何 JavaScript 代码,并且可以导出任意数量的变量。

一个模块可以导出一个变量、函数、类或任何其他实体。

我们需要在模块中使用 export 语句来导出变量。export 语句有多种不同的格式。以下是这些格式:

export {variableName};
export {variableName1, variableName2, variableName3};
export {variableName as myVariableName};
export {variableName1 as myVariableName1, variableName2 as myVariableName2};
export {variableName as default};
export {variableName as default, variableName1 as myVariableName1, variableName2};
export default function(){};
export {variableName1, variableName2} from "myAnotherModule";
export * from "myAnotherModule";

这些格式之间的区别如下:

  • 第一种格式导出一个变量。

  • 第二种格式用于导出多个变量。

  • 第三种格式用于使用另一个名称导出一个变量,即别名。

  • 第四种格式用于导出具有不同名称的多个变量。

  • 第五种格式使用 default 作为别名。我们将在本章后面了解这种用法。

  • 第六种格式与第四种格式类似,但它也具有 default 别名。

  • 第七种格式与第五种格式类似,但在这里你可以放置一个表达式而不是变量名。

  • 第八种格式用于导出子模块的导出变量。

  • 第九种格式用于导出子模块的所有导出变量。

关于 export 语句,以下是一些你需要了解的重要事项:

  • 导出语句可以在模块的任何位置使用。不需要在模块的末尾使用它。

  • 模块中可以有任意数量的 export 语句。

  • 你不能按需导出变量。例如,将 export 语句放在 if…else 条件中会抛出错误。因此,我们可以说模块结构需要是静态的,即导出可以在编译时确定。

  • 您不能多次导出相同的变量名或别名。但您可以多次导出变量,使用不同的别名。

  • 默认情况下,模块内的所有代码都在 strict 模式下执行。

  • 导出变量的值可以在导出它们的模块内部更改。

导入 ES6 模块

要导入一个模块,我们需要使用 import 语句。import 语句有多种不同的格式。以下是格式:

import x from "module-relative-path";
import {x} from "module-relative-path";
import {x1 as x2} from "module-relative-path";
import {x1, x2} from "module-relative-path";
import {x1, x2 as x3} from "module-relative-path";
import x, {x1, x2} from "module-relative-path";
import "module-relative-path";
import * as x from "module-relative-path";
import x1, * as x2 from "module-relative-path";

一个 import 语句由两部分组成:我们想要导入的变量名和模块的相对路径。

这里是这些格式之间的差异:

  • 在第一种格式中,导入的是 default 别名。xdefault 别名的别名。

  • 在第二种格式中,导入的是 x 变量。

  • 第三种格式与第二种格式相同。只是 x2x1 的别名。

  • 在第四种格式中,我们导入 x1x2 变量。

  • 在第五种格式中,我们导入 x1x2 变量。x3x2 变量的别名。

  • 在第六种格式中,我们导入 x1x2 变量以及 default 别名。xdefault 别名的别名。

  • 在第七种格式中,我们只导入模块。我们不导入模块导出的任何变量。

  • 在第八种格式中,我们导入所有变量,并将它们包装在一个名为 x 的对象中。即使是 default 别名也被导入。

  • 第九种格式与第八种格式相同。在这里,我们给 default 别名提供了另一个别名。

这里有一些关于 import 语句的重要事项,您需要了解:

  • 在导入变量时,如果我们用别名导入它,那么要引用该变量,我们必须使用别名而不是实际变量名,也就是说,实际变量名将不可见,只有别名将是可见的。

  • import 语句不会导入导出变量的副本;相反,它使变量在导入它的程序的作用域中可用。因此,如果您在模块内部更改导出的变量,则更改对导入它的程序是可见的。

  • 导入的变量是只读的,也就是说,您不能在导出它们的模块作用域之外将它们重新分配给其他内容。

  • 在 JavaScript 引擎的单个实例中,一个模块只能导入一次。如果我们再次尝试导入,则将使用已导入的模块实例。

  • 我们不能按需导入模块。例如,将 import 语句放在 if…else 条件中会抛出错误。因此,我们可以说导入应该在编译时确定。

  • ES6 导入比 AMD 和 CommonJS 导入更快,因为 ES6 导入是原生支持的,并且导入模块和导出变量不是按需决定的。因此,这使得 JavaScript 引擎更容易优化性能。

模块加载器

模块加载器是 JavaScript 引擎的一个组件,负责导入模块。

import 语句使用内置模块加载器来导入模块。

不同 JavaScript 环境的内置模块加载器使用不同的模块加载机制。例如,当我们导入在浏览器中运行的 JavaScript 模块时,模块是从服务器加载的。另一方面,当我们导入 Node.js 中的模块时,模块是从文件系统中加载的。

模块加载器以不同的方式在不同的环境中加载模块,以优化性能。例如,在浏览器中,模块加载器异步加载和执行模块,以防止导入的模块阻塞网页的加载。

您可以使用模块加载器 API 以编程方式与内置模块加载器交互,以自定义其行为,拦截模块加载,并按需获取模块。

我们还可以使用此 API 创建我们自己的自定义模块加载器。

ES6 中没有指定模块加载器的规范。它是一个独立的标准,由WHATWG浏览器标准小组控制。您可以在whatwg.github.io/loader/找到模块加载器的规范。

ES6 规范仅指定了 importexport 语句。

在浏览器中使用模块

<script> 标签内的代码不支持 import 语句,因为标签的同步性质与浏览器中模块的异步性不兼容。相反,您需要使用新的 <module> 标签来导入模块。

使用新的 <module> 标签,我们可以将脚本定义为模块。现在,这个模块可以使用 import 语句导入其他模块。

如果您想使用 <script> 标签导入模块,那么您必须使用模块加载器 API

<module> 标签的规范没有在 ES6 中指定。

在 eval() 函数中使用模块

您不能在 eval() 函数中使用 importexport 语句。要在 eval() 函数中导入模块,您需要使用模块加载器 API。

默认导出与命名导出的比较

当我们使用 default 别名导出一个变量时,它被称为默认导出。显然,一个模块中只能有一个默认导出,因为别名只能使用一次。

除了默认导出之外的所有导出都称为命名导出

建议模块要么使用默认导出,要么使用命名导出。同时使用两者不是一个好的做法。

当我们只想导出一个变量时,使用默认导出。另一方面,当我们要导出多个变量时,使用命名导出。

深入一个例子

让我们使用 ES6 模块创建一个基本的 JavaScript 库。这将帮助我们了解如何使用 importexport 语句。我们还将学习一个模块如何导入其他模块。

我们将要创建的库将是一个数学库,它提供基本的对数和三角函数。让我们开始创建我们的库:

  • 创建一个名为 math.js 的文件,以及一个名为 math_modules 的目录。在 math_modules 目录内,分别创建两个名为 logarithm.jstrigonometry.js 的文件。

    在这里,math.js 文件是根模块,而 logarithm.jstrigonometry.js 文件是其子模块。

  • 将此代码放置在 logarithm.js 文件中:

    var LN2 = Math.LN2;
    var N10 = Math.LN10;function getLN2()
    {return LN2;
    }function getLN10()
    {return LN10;
    }export {getLN2, getLN10};
    

    在这里,模块导出的是名为 exports 的函数。

在模块层次结构中,低级模块应该分别导出所有变量是首选的,因为可能程序只需要库中的一个导出变量。在这种情况下,程序可以直接导入此模块和特定函数。当你只需要一个模块时加载所有模块在性能方面是不好的。

同样,将此代码放置在 trigonometry.js 文件中:

var cos = Math.cos;
var sin = Math.sin;function getSin(value)
{return sin(value);
}function getCos(value)
{return cos(value);
}export {getCos, getSin};

这里我们做类似的事情。将此代码放置在 math.js 文件中,该文件作为根模块:

import * as logarithm from "math_modules/logarithm";
import * as trigonometry from "math_modules/trigonometry";export default {logarithm: logarithm,trigonometry: trigonometry
}

它不包含任何库函数。相反,它使得程序能够轻松导入整个库。它导入其子模块,然后将它们导出的变量导出到主程序中。

在这里,如果 logarithm.jstrigonometry.js 脚本依赖于其他子模块,那么 math.js 模块不应该导入这些子模块,因为 logarithm.jstrigonometry.js 已经导入了它们。

这里是程序可以用来导入整个库的代码:

import math from "math";console.log(math.trigonometry.getSin(3));
console.log(math.logarithm.getLN2(3));

摘要

在本章中,我们了解了模块化编程是什么,学习了不同的模块化编程规范。最后,我们使用模块化编程设计技术创建了一个基本的库。现在,你应该有足够的信心使用 ES6 模块构建 JavaScript 应用程序。

http://www.hskmm.com/?act=detail&tid=18369

相关文章:

  • JSP征婚信息实用的系统3kx16代码+源码+数据库+调试部署+开发环境
  • CSS属性
  • 基于大数据的水产品安全信息可视化分析框架【Hadoop、spark、可视化大屏、课程毕设、毕业选题、数据分析、资料爬取、数据可视化】
  • CSS值
  • 2025_Polar秋季赛_web全解
  • QT:如何初始化窗体尺寸大小
  • 题2
  • linux命令-rm
  • 2025.9.26
  • 基于Amazon S3设置AWS Transfer Family Web 应用程序 - 实践
  • 作为 PHP 开发者,我第一次用 Go 写了个桌面应用
  • JBoltAI智能出题助手:助力高效学习与知识巩固 - 那年-冬季
  • JBoltAI设备智能检测:为设备管理维护提供高效辅助 - 那年-冬季
  • JBoltAI:Java与AI的完美融合,赋能技术团队新未来 - 那年-冬季
  • AIGS与AIGC:人工智能时代的范式跃迁与价值重构 - 那年-冬季
  • 5
  • ?模拟赛(3) 赛后总结
  • 用鼠标滚轮缩放原理图界面的小工具
  • 实验任务1
  • OI界的梗(继 @CCCsuper 2.0 版本)
  • 9/26
  • Python 私有属性深度解析
  • 菜鸟记录:c语言实现洛谷P1219 ———八皇后
  • 当危机爆发时,所有网络安全都是本地的
  • crc校验原理是什么?
  • CF1385D a-Good String
  • 9月23日(日记里有)
  • 9月25日(日记里有)
  • Git 提交代码前,一定要做的两件事
  • 本地调试接口时遇到的跨域问题,十分钟解决