小言_互联网的博客

前端的葵花宝典 - 红宝书《JavaScript高级程序设计(第4版)》学习笔记

443人阅读  评论(0)

目录


前言

1998 年,国际标准化组织(ISO)和国际电工委员会(IEC)将ECMAScript 采纳为标准(ISO/IEC-16262)。自此以后,各家浏览器均以ECMAScript 作为自己JavaScript 实现的依据,虽然具体实现各有不同。

第四版全面深入地介绍了JavaScript 开发者必须掌握的前端开发技术,涉及JavaScript 的基础特性和高级特性。

本笔记中的各小节序号,未依照原书中的章节序号。



关于电子版PDF的【分享说明1



1、第1章 什么是JavaScript

完整的JavaScript 实现包含以下几个部分:

  • 核心(ECMAScript);
  • 文档对象模型(DOM);
  • 浏览器对象模型(BOM)



1.1 ECMAScript

ECMAScript,即ECMA-262 定义的语言,并不局限于Web 浏览器。这门语言没有输入和输出之类的方法。Web 浏览器是ECMAScript 实现的一种宿主环境(host environment)

JavaScript 实现了 ECMAScript。

1.2 DOM

DOM 文档对象模型(DOM,Document Object Model)是一个应用编程接口(API),用于在HTML 中使用扩展的XML。DOM 将整个页面抽象为一组分层节点。HTML 或XML 页面的每个组成部分都是一种节点,包含不同的数据。

比如下面的HTML 页面:

<html>
	<head>
		<title>Sample Page</title>
	</head>
	<body>
		<p> Hello World!</p>
	</body>
</html>

这些代码通过DOM 可以表示为一组分层节点:


DOM 通过创建表示文档的树,让开发者可以随心所欲地控制网页的内容和结构。使用DOM API,可以轻松地删除、添加、替换、修改节点。

注意:DOM 并非只能通过JavaScript 访问,而且确实被其他很多语言实现了。不过对于浏览器来说,DOM 就是使用 ECMAScript 实现的,如今已经成为 JavaScript 语言的一大组成部分。


1.3 BOM

IE3 和 Netscape Navigator 3 提供了浏览器对象模型(BOM) API,用于支持访问和操作浏览器的窗口。使用BOM,开发者可以操控浏览器显示页面之外的部分。

总体来说,BOM 主要针对浏览器窗口和子窗口(frame),不过人们通常会把任何特定于浏览器的扩展都归在BOM 的范畴内。


小结

JavaScript 是一门用来与网页交互的脚本语言,包含以下 三个组成部分。

  1. ECMAScript:由ECMA-262 定义并提供核心功能;
  2. 文档对象模型(DOM):提供与网页内容交互的方法和接口;
  3. 浏览器对象模型(BOM):提供与浏览器交互的方法和接口。


第2章 HTML 中的JavaScript


本章内容:

  • 使用< script>元素;
  • 行内脚本与外部脚本的比较;
  • 文档模式对JavaScript 有什么影响;
  • 确保JavaScript 不可用时的用户体验

2.1 < script >元素

<script>元素有下列8 个属性:

属性 描述
async 可选。表示应该立即开始下载脚本,但不能阻止其他页面动作,比如下载资源或等待其他脚本加载(异步执行脚本)。只对外部脚本文件有效。
charset 可选。使用 src 属性指定的代码字符集(规定在外部脚本文件中使用的字符编码。)。这个属性很少使用,因为大多数浏览器不在乎它的值。
crossorigin 可选。配置相关请求的CORS(跨源资源共享)设置。默认不使用CORS。
crossorigin="anonymous"配置文件请求不必设置凭据标志;
crossorigin="use-credentials"设置凭据标志,意味着出站请求会包含凭据。
defer 可选。表示立即下载,但延迟执行(直到文档完全被解析和显示为止)。只对外部脚本文件有效。在IE7 及更早的版本中,对行内脚本也可以指定这个属性。
integrity 可选。允许比对接收到的资源和指定的加密签名以验证子资源完整性(SRI,Subresource Integrity)。如果接收到的资源的签名与这个属性指定的签名不匹配,则页面会报错,脚本不会执行。这个属性可以用于确保内容分发网络(CDN)不会提供恶意内容。
language 废弃。最初用于表示代码块中的脚本语言(如"JavaScript"、“JavaScript 1.2"或"VBScript”)。大多数浏览器都会忽略这个属性,不应该再使用它。
src 可选。表示包含要执行的代码的外部文件。
type 可选。代替language,表示代码块中脚本语言的内容类型。这个值始终都是"text/javascript",尽管"text/javascript"和"text/ecmascript"都已经废弃了。JavaScript 文件的MIME 类型通常是"application/x-javascript",不过给type 属性这个值有可能导致脚本被忽略。在非IE 的浏览器中有效的其他值还有"application/javascript"和"application/ecmascript"。如果这个值是module,则代码会被当成ES6 模块,而且只有这时候代码中才能出现import 和export 关键字。

使用<script>的两种方式:

  1. 直接在网页中嵌入 JavaScript 代码;
  2. 在网页中导入外部 JavaScript 文件。

要嵌入行内 JavaScript 代码,直接把代码放在<script>元素中就行:

<script>
	function sayHi() {
   
		console.log("Hi!");
	}
</script>

包含在<script>内的代码会被从上到下解释执行。在上面的例子中,被解释的是一个函数定义,并且该函数会被保存在解释器环境中。在<script>元素中的代码被计算执行完成之前,页面的其余内容不会被加载,也不会被显示。

在使用行内JavaScript 代码时,要注意代码中不能出现字符串</script>。比如,下面的代码会导致浏览器报错

<script>
	function sayScript() {
   
		console.log("</script>");
	}
</script>

只需在标签内添加转义字符”\“即可解决

console.log("<\/script>");

这样修改之后,代码就可以被浏览器完全解释,不会导致任何错误。

此处的转义字符指在 JavaScript 中使用反斜杠“\”来向文本字符串添加特殊字符。


要包含外部文件中的JavaScript,就必须使用`src`属性。这个属性的值是一个URL,指向包含 JavaScript 代码的文件,比如:
<script src="example.js"></script>

这个例子在页面中加载了一个名为 example.js 的外部文件。与解释行内JavaScript 一样,在解释外部 JavaScript 文件时,页面也会阻塞(阻塞时间也包含下载文件的时间。)

注意:
按照惯例,外部 JavaScript 文件的扩展名是.js。但这不是必须的,因为浏览器不会检查所包含JavaScript 文件的扩展名。这就为使用服务器端脚本语言动态生成 JavaScript 代码,或者在浏览器中将JavaScript 扩展语言(如TypeScript,或React 的JSX)转译为JavaScript提供了可能性。但请注意,服务器经常会根据文件扩展来确定响应的正确MIME 类型。如果不打算使用.js 扩展名,一定要确保服务器能返回正确的MIME 类型。

另外,使用了src 属性的<script>元素不应该再在<script>和</script>标签中再包含其他 JavaScript 代码。

如果两者都提供的话,则浏览器只会下载并执行脚本文件,从而忽略行内代码。

<script>元素的一个最为强大、同时也备受争议的特性是,它可以包含来自外部的JavaScript 文件。跟<img>元素很像,<script>元素的 src 属性可以是一个完整的URL,而且这个URL 指向的资源可以跟包含它的HTML 页面不在同一个域中,比如:

<script src="http://www.somewhere.com/afile.js"></script>

浏览器在解析这个资源时,会向src 属性指定的路径发送一个GET 请求,以取得相应资源,假定是一个JavaScript 文件。这个初始的请求不受浏览器同源策略限制,但返回并被执行的JavaScript 则受限制。当然,这个请求仍然受父页面HTTP/HTTPS 协议的限制。

如果是引用别人服务器上的JavaScript 文件时要格外小心,因为恶意的程序员随时可能替换这个文件。

要确保该域是自己所有的,或者该域是一个可信的来源。也可用。<script>标签的 integrity 属性来防范这种问题(但此属性于不同浏览器存在兼容性问题)。

2.1.1 标签位置

过去,所有<script>元素都被放在页面的标签内,如下所示:

<!DOCTYPE html>
<html>
	<head>
		<title>Example HTML Page</title>
		<script src="example1.js"></script>
		<script src="example2.js"></script>
	</head>
	<body>
		<!-- 这里是页面内容 -->
	</body>
</html>



但是,为了解决js 代码下载、解析、解释期间导致的页面渲染延迟问题(打开网页时一段时间的浏览器窗口空白),现代Web 应用程序通常将所有JavaScript 引用放在<body>元素中的页面内容后面,如下所示:

<!DOCTYPE html>
<html>
	<head>
		<title>Example HTML Page</title>
	</head>
	<body>
		<!-- 这里是页面内容 -->
		<script src="example1.js"></script>
		<script src="example2.js"></script>
	</body>
</html>

2.1.2 推迟执行脚本

HTML 4.01 为<script>元素定义了一个叫 defer 的属性。添加了这个属性,脚本会被延迟到整个页面都解析完毕后再运行。在<script>元素上设置defer 属性,相当于告诉浏览器立即下载,但延迟执行。

<!DOCTYPE html>
<html>
	<head>
		<title>Example HTML Page</title>
		<script defer src="example1.js"></script>
		<script defer src="example2.js"></script>
	</head>
	<body>
		<!-- 这里是页面内容 -->
	</body>
</html>

本示例中,由于指定了 defer 属性,即使<script>元素被放在了页面的<head>标签中,但它们仍会在浏览器解析到结束的</html>标签后才会执行。

defer 属性只对外部脚本文件才有效。这是HTML5 中明确规定的,因此支持 HTML5 的浏览器会忽略行内脚本的 defer 属性。


2.1.3 异步执行脚本

HTML5 为<script>元素定义了async 属性。从改变脚本处理方式上看,async 属性与defer 类似。且都只适用于外部脚本,都会告诉浏览器立即开始下载。不过,与defer 不同的是,标记为 async 的脚本并不保证能按照它们出现的次序执行,比如:

<!DOCTYPE html>
<html>
	<head>
		<title>Example HTML Page</title>
		<script async src="example1.js"></script>
		<script async src="example2.js"></script>
	</head>
	<body>
		<!-- 这里是页面内容 -->
	</body>
</html>

本例中,第二个脚本可能先于第一个脚本执行。因此,重点在于它们之间没有依赖关系。

给脚本添加 async 属性的目的是告诉浏览器,不必等脚本下载和执行完后再加载页面,同样也不必等到该异步脚本下载和执行后再加载其他脚本。

不过好的 Web 开发实践根本就不推荐使用这个方法。

2.2 行内代码与外部文件

虽然可以直接在HTML 文件中嵌入JavaScript 代码,但通常认为最佳实践尽可能将JavaScript 代码放在外部文件中。推荐使用外部文件的理由如下:

  • 可维护性;
  • 缓存;
  • 适应未来。


小结

JavaScript 是通过<script>元素插入到HTML 页面中的。这个元素可用于把JavaScript 代码嵌入到 HTML 页面中,跟其他标记混合在一起,也可用于引入保存在外部文件中的JavaScript。小结如下:

  • 要包含外部JavaScript 文件,必须将src 属性设置为要包含文件的URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。
  • 所有<script>元素会依照它们在网页中出现的次序被解释。在不使用 deferasync 属性的情况下,包含在<script>元素中的代码必须严格按次序解释。
  • 对不推迟执行的脚本,浏览器必须解释完位于<script>元素中的代码,才能继续渲染页面的剩余部分。应把<script>元素放到页面末尾,介于主内容之后及</body>标签之前。
  • 可以使用defer 属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。
  • 可使用async 属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。
  • 通过使用<noscript>元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启用脚本,则<noscript>元素中的任何内容都不会被渲染。


第3章 语言基础


3.1 语法

ECMAScript 的语法很大程度上借鉴了C 语言和其他类C语言,如Java 和 Perl。熟悉这些语言的开发者,应该很容易理解ECMAScript 宽松的语法。

3.1.1 区分大小写

ECMAScript 中一切都区分大小写。无论是变量、函数名还是操作符,都区分大小写。

3.1.2标识符

标识符:变量、函数、属性或函数参数的名称。
标识符可以由一或多个下列字符组成:

  • 第一个字符必须是一个字母、下划线(_)或美元符号($);
  • 剩下的其他字符可以是字母、下划线、美元符号或数字。

推荐驼峰命名法,首字母小写,后面每个单词的首字母大写。

3.1.3 严格模式

严格模式是一种不同的 JavaScript 解析和执行模型,不规范写法在这种模式下会被处理,对于不安全的活动将抛出错误。如果要对整个脚本启用严格模式,在脚本开头加上这一行:

"use strict";

它其实是一个预处理指令。任何支持的 JavaScript 引擎看到它都会切换到严格模式。选择这种语法形式的目的是不破坏ECMAScript 3 语法。

也可以单独指定一个函数在严格模式下执行,只要把这个预处理指令放到函数体开头即可:

function doSomething() {
   
	"use strict";
	// 函数体
}

严格模式会影响JavaScript 执行的很多方面,因此本书在用到它时会明确指出来(所有现代浏览器都支持严格模式)。

3.1.4 语句

ECMAScript 中的语句以分号结尾。省略分号意味着由解析器确定语句在哪里结尾,如下面的例子所示:

let sum = a + b   // 没有分号也有效 —— 不推荐
let diff = a - b; // 加分号有效    —— 推荐!

即使语句末尾的分号不是必需的,也应该加上。如果没有结尾的分号,删除空行会导致语法错误。

加分号也有助于在某些情况下提升性能,因为解析器会尝试在合适的位置补上分号以纠正语法错误。


3.2 变量

CMAScript 变量是松散类型的,意思是变量可以用于保存任何类型的数据,仅相当于一个点位符,可以保存任何类型的值。

有3 个关键字可以声明变量:varconstlet。其中,var 在 ECMAScript 的所有版本中都可以使用,而 constlet 只能在ECMAScript 6 及更晚的版本中使用。

3.2.1 var关键字

使用var 操作符定义的变量会成为包含它的函数的局部变量。比如,使用var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:

function test() {
   
	var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!

但是,如果在 函数内 定义变量时 省略var 操作符,可以创建一个全局变量

function test() {
   
	message = "hi"; // 全局变量
}
test();
console.log(message); // 输出"hi"

这样,只要调用一次函数test(),就会定义这个变量,并且可以在函数外部访问到。

~~注意~~
虽然可以通过省略 var 操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略var 是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError


在一条语句中,连续声明多个变量(用逗号分隔,换行、空格、缩进非必须):

var message = "hi",
	found = false,
	age = 29;

var 存在变量提升,重复声明也不会报错。
严格模式下,不能定义名为evalarguments 的变量,否则会导致语法错误。


3.2.2 let关键字

letvar 的作用差不多,但有着非常重要的区别。最明显的区别是,let 声明的范围是块作用域,而var 声明的范围是函数作用域块作用域函数作用域子集

if (true) {
   
	var name = 'Matt';
	console.log(name); // Matt
}
	console.log(name); // Matt

if (true) {
   
	let age = 26;
	console.log(age); // 26
}
	console.log(age); // ReferenceError: age 没有定义

1、let 没有变量提升,重复声明会报错。
2、let 在全局作用域中声明的变量不会成为window 对象的属性。不过,let 声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续(因此,需确保同一页面不会重复声明)

for 循环中的let声明

在let 出现之前,for 循环定义的迭代变量会渗透到循环体外部:

for (var i = 0; i < 5; ++i) {
   
	// 循环逻辑
}
	console.log(i); // 5

改成使用let 之后,这个问题就消失了,因为迭代变量的作用域仅限于for 循环块内部:

for (let i = 0; i < 5; ++i) {
   
	// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义

3.3.3 const 声明

const 的行为与let 基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const 声明的变量会导致运行时错误。

它也不允许重复声明,声明的作用域也是块。

const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反const 的限制。

const age = 26;
age = 36; // TypeError: 给常量赋值
// const 也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
// const 声明的作用域也是块
const name = 'Matt';

if (true) {
   
	const name = 'Nicholas';
}
	console.log(name); // Matt
	const person = {
   };
	person.name = 'Matt'; // ok

不使用 var :限制自己只使用 let 和 const 有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。
const 优先,let 次之


3.4 数据类型

ECMAScript 有 6 种简单数据类型和 1 种复杂数据类型(Object)。

数据类型 类型描述
Undefined Undefined 值未定义( 是一个假值)
null null 表示一个空对象指针,这也是给 typeof 传一个null 会返回"object"的原因
boolean true
false
布尔值 (区分大、小写,True 和False是标识符而不是布尔值)
string 字符串
number 不同数值类型有不同的数值字面量 数值
object 表示值为对象(而非函数)或null
function 函数
symbol 符号

虽然布尔值只有两个,但所有其他 ECMAScript 类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的 Boolean() 转型函数

let message = "Hello world!";
let messageAsBoolean = Boolean(message);

Boolean()转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值。什么值能转换为 truefalse 的规则取决于数据类型和实际的值。下表总结了不同类型与布尔值之间的转换规则。

数据类型 转换为 true 的值 转换为 false 的值
boolean true false
String 非空字符串 “”(空字符串)
Number 非零数值(包括无穷值) 0、NaN(参见后面的相关内容)
Object 任意对象 null
Undefined N/A(不存在) undefined

理解以上转换非常重要,因为像if 等流控制语句会自动执行其他类型值到布尔值的转换,例如:

let message = "Hello world!";
if (message) {
   
	console.log("Value is true");
}

在这个例子中,console.log 会输出字符串"Value is true",因为字符串message 会被自动转换为等价的布尔值true。

由于存在这种自动转换,理解流控制语句中使用的是什么变量就非常重要。错误地使用对象而不是布尔值会明显改变应用程序的执行流。


3.4.1 typeof操作符

对任一个使用typeof 操作符会返回上表中所列类型之一(字符串)。

let message = "some string";
console.log(typeof message);  // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95);       // "number"

typeof 是一个操作符而不是函数,不需要参数(但可以使用参数)。

严格来讲,函数在ECMAScript 中被认为是对象,并不代表一种数据类型。可是,
函数也有自己特殊的属性。为此,就有必要通过typeof 操作符来区分函数和其他对象。


var 或let 声明了变量但没有初始化时,就相当于给变量赋予了undefined 值:

let message;
console.log(message == undefined); // true

即使未初始化的变量会被自动赋予 undefined 值,但我们仍然建议在声明变量的同时进行初始化。这样,当 typeof 返回"undefined"时,你就会知道那是因为给定的变 量尚未声明,而不是声明了但未初始化。


3.4.2 Number类型

有一个特殊的数值叫NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。

console.log(0/0); // NaN
console.log(-0/+0); // NaN

如果分子是非0 值,分母是有符号0 或无符号0,则会返回Infinity 或-Infinity:

console.log(5/0); // Infinity
console.log(5/-0); // -Infinity

1. isNaN()函数

该函数会尝试把它转换为数值。某些非数值的值可以直接转换成数值,如字符串 “10” 或布尔值。任何不能转换为数值的值都会导致这个函数返回 true。举例如下:

console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN("10")); // false,可以转换为数值10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值1

虽然不常见,但 isNaN() 可以用于测试对象。此时,首先会调用对象的valueOf() 方法,然后再确定返回的值是否可以转换为数值。如果不能,再调用toString()方法,并测试其返回值

2. 数值转换

有 3 个函数可以将非数值转换为数值:

  • Number()
  • parseInt()
  • parseFloat()

Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。

let num1 = Number("Hello world!"); // NaN
let num2 = Number("");             // 0
let num3 = Number("000011");       // 11
let num4 = Number(true);           // 1

通常在需要得到整数时可以优先使用parseInt()函数。parseInt()函数更专注于字符串是否包含数值模式。

let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt("");         // NaN
let num3 = parseInt("0xA");      // 10,解释为十六进制整数
let num4 = parseInt(22.5);       // 22
let num5 = parseInt("70");       // 70,解释为十进制值
let num6 = parseInt("0xf");      // 15,解释为十六进制整数

不同的数值格式很容易混淆,因此parseInt()也接收第二个参数,用于指定底数(进制数)。如果知道要解析的值是十六进制,那么可以传入16 作为第二个参数,以便正确解析:

let num = parseInt("0xAF", 16); // 175

事实上,如果提供了十六进制参数,那么字符串前面的"0x"可以省掉:

let num1 = parseInt("AF", 16); // 175
let num1 = parseInt("AF");    // 没有传入进制数,出错了

因为不传底数参数相当于让parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给它第二个参数。

多数情况下解析的应该都是十进制数,此时第二个参数就要传入10。

parseFloat()

parseFloat()只解析十进制值,因此不能指定底数。如果字符串表示整数(没有小数点或者小数点后面只有一个零),则parseFloat()返回整数。

let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000

3.4.3 String 类型

字符串可以使用双引号(")、单引号(')或反引号(`)标示,因此下面的代码都是合法的:

let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`

ECMAScript 中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量,如下所示:

let lang = "Java";
lang = lang + "Script";

后台过程
首先会分配一个足够容纳10 个字符的空间,然后填充上 “Java” 和 “Script”。最后销毁原始的字符串 “Java” 和字符串 “Script”


1. 把一个值转换为字符串的两种方式:

  • toString()方法:可见于数值、布尔值、对象和字符串值

    • 如果值有toString()方法,则调用该方法(不传参数)并返回结果;
    • 如果值是null,返回"null";
    • 如果值是undefined,返回"undefined
  • 用加号操作符+给一个值加上空字符串""


2. 模板字面量

模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。

技术上讲,模板字面量不是字符串,而是一种特殊的JavaScript 句法表达式。


字符串插值通过在${}中使用一个 JavaScript 表达式 实现:

let interpolatedTemplateLiteral =
`${
      value } to the ${
      exponent } power is ${
      value * value }`;

所有插入的值都会使用toString()强制转型为字符串,而且任何JavaScript 表达式都可以用于插值。嵌套的模板字符串无须转义:

console.log(`Hello, ${
      `World` }!`); // Hello, World!

3.4.4 相等操作符

如果有任一操作数是NaN,则相等操作符返回false,不相等操作符返回true。

记住:即使两个操作数都是NaN,相等操作符也返回false,因为按照规则,NaN 不等于NaN。

let a = NaN == NaN;
console.log(a);   // false

3.5 语句

3.5.1 if 语句

求值结果不一定是boolean值。ECMAScript 会自动调用Boolean()函数将这个表达式的值转换为boolean值。


3.5.2 do-while 语句

后测试循环语句

1)do-while 语法示例:

// 只要i 小于10,循环就会重复执行。i 从 0 开始,每次循环递增 2。
let i = 0;
do {
   
	i += 2;
} while (i < 10);

2)使用场景:

需要循环体内的代码在退出前至少执行 1 次。

3.5.3 while 语句

先测试循环语句

while循环语法示例:

// 变量i 从0 开始,每次循环递增2。只要i 小于10,循环就会继续
let i = 0;
while (i < 10) {
   
	i += 2;
}

3.5.4 for 语句

也是先测试语句,只不过增加了进入循环之前的初始化代码,以及循环执行后要执行的表达式

for 语句示例:

let count = 10;
for (let i = 0; i < count; i++) {
   
	console.log(i);
}

与下面 while 效果相同:

let count = 10;
let i = 0;
while (i < count) {
   
	console.log(i);
	i++;
}

无法通过while 循环实现的逻辑,同样也无法使用for 循环实现。因此for 循环只是将循环相关的代码封装在了一起而已。

在for 循环的初始化代码中,其实是可以不使用变量声明关键字的。不过,初始化定义的迭代器变量在循环执行完成后几乎不可能再用到了。因此,最清晰的写法是使用let 声明迭代器变量,这样就可以将这个变量的作用域限定在循环中。

初始化、条件表达式和循环后表达式都不是必需的。因此,下面这种写法可以创建一个无穷循环:

for (;;) {
    // 无穷循环
	doSomething();
}

如果只包含条件表达式,那么for 循环实际上就变成了while 循环:

let count = 10;
let i = 0;
for (; i < count; ) {
   
	console.log(i);
	i++;
}

这种多功能性使得for 语句在这门语言中使用非常广泛。

3.5.5 for-in 语句

for-in 语句是一种严格的迭代语句,用于枚举对象中的非符号键属性。

for (const propName in window) {
   
	// 循环显示 BOM 对象 window 的所有属性
	document.write(propName);
}

ECMAScript 中对象的属性是无序的,因此 for-in 语句不能保证返回对象属性的顺序,返回的顺序可能会因浏览器而异。


3.5.6 for-of 语句

for-of 语句是一种严格的迭代语句,用于遍历可迭代对象的元素。

示例:

for (const el of [2,4,6,8]) {
   
	document.write(el);
}

ES2018 对for-of 语句进行了扩展,增加了for-await-of 循环,以支持生成期约(promise)的异步可迭代对象


3.5.7 标签语句

标签语句用于给语句加标签。

示例:

start: for (let i = 0; i < count; i++) {
   
	console.log(i);
}

在这个例子中,start 是一个标签,可以在后面通过breakcontinue 语句引用。标签语句的典型应用场景嵌套循环

3.5.8 break 和continue 语句

breakcontinue语句为执行循环代码提供了更严格的控制手段。其中,

  • break 语句用于立即退出循环,强制执行循环后的下一条语句
  • continue 语句也用于立即退出循环,但会再次从循环顶部开始执行。

示例:

let num = 0;
for (let i = 1; i < 10; i++) {
   
	if (i % 5 == 0) {
   
		break;
	}
	num++;
}
console.log(num);   // 4

之所以循环执行了4 次,是因为当i 等于5 时,break 语句会导致循环退出,该次循环不会执行递增num 的代码。

如果将 break 换成 continue,则会出现不同的效果:

let num = 0;
for (let i = 1; i < 10; i++) {
   
	if (i % 5 == 0) {
   
		continue;
	}
	num++;
}
console.log(num);  //  8

当 i =5 时,循环会在递增 num 之前退出,但会执行下一次迭代,此时 i 是6。然后,循环会一直执行到自然结束,即 i 等于10。最终num 的值是8 而不是9,是因为continue 语句导致它少递增了一次。

在嵌套循环中,breakcontinue都可以与标签语句一起使用,返回代码中特定的位置。示例如下:

let num = 0;
outermost:
for (let i = 0; i < 10; i++) {
   
	for (let j = 0; j < 10; j++) {
   
		if (i == 5 && j == 5) {
   
			break outermost;
		}
		num++;
	}
}
console.log(num); // 55

本例中,outermost标签标识的是第一个 for 语句。组合使用 标签语句breakcontinue 能实现复杂的逻辑,但也容易出错。注意标签要使用描述性强的文本,而嵌套也不要太深。

3.5.9 with 语句

用途:将代码作用域设置为特定的对象。

使用的主要场景:是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利,如下所示:

let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;

上述代码中每一行都用到了location 对象。如果使用with 语句,就可以少写一些代码:

with(location) {
   
	let qs = search.substring(1);
	let hostName = hostname;
	let url = href;
}

with 语句用于连接 location 对象。在该语句内部,每个变量首先会被认为是一个局部变量。如果没有找到该局部变量,则会搜索location对象,看它是否有一个同名的属性。如果有,则该变量会被求值为location对象的属性。

⚠️ 警告!
1、严格模式不允许使用with语句,否则会抛出错误。
2、由于with 语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用with语句。


3.5.10 switch 语句

switch语句是与 if 语句紧密相关的一种流控制语句,从其他语言借鉴而来。ECMAScript 中 switch 语句跟 C 语言中 switch 语句的语法非常相似,如下所示:

switch (expression) {
   
	case value1:
		statement
		break;
	case value2:
		statement
		break;
	case value3:
		statement
		break;
	case value4:
		statement
		break;
	default:
		statement
}

每个case(条件/分支)相当于:“如果表达式等于后面的值,则执行下面的语句。”,其中:

  • break关键字:跳出switch 语句。如果没有break,则代码会继续匹配下一个条件。
  • default关键字:不满足任何条件时,指定默认执行的语句(相当于else 语句)。

如果确实需要连续匹配几个条件,那么写个注释表明是有意忽略break,如下所示:

switch (i) {
   
	case 25:
		/*跳过*/
	case 35:
		console.log("25 or 35");
		break;
	case 45:
		console.log("45");
		break;
	default:
		console.log("Other");
}

虽然switch 语句是从其他语言借鉴过来的,但ECMAScript 为它赋予了一些独有的特性

  • switch 语句可以用于所有数据类型(在一些语言中,它只能用于数值);
  • 条件的值不需要是常量,也可以是变量或表达式。

示例如下:

switch ("hello world") {
   
	case "hello" + " world":
		console.log("Greeting was found.");
		break;
	case "goodbye":
		console.log("Closing was found.");
		break;
	default:
		console.log("Unexpected message was found.");
}

第1个条件使用的是表达式,求值为两个字符串拼接后的结果。因为拼接后的结果等于switch 的参数,所以会输出"Greeting wasfound."。

既然能够在条件判断中使用表达式,那么就可以在判断中加入更多逻辑:

let num = 25;
switch (true) {
   
	case num < 0:
		console.log("Less than 0.");
		break;
	case num >= 0 && num <= 10:
		console.log("Between 0 and 10.");
		break;
	case num > 10 && num <= 20:
		console.log("Between 10 and 20.");
		break;
	default:
		console.log("More than 20.");
}

switch 语句在比较每个条件的值时,会使用全等操作符,因此不会强制转换数据类型(比如,字符串"10"不等于数值10)。


3.6 函数

语法示例:

// 定义函数sayHi()
function sayHi(name, message) {
   
	console.log("Hello " + name + ", " + message);
}

// 调用函数sayHi()
sayHi("Nicholas", "how are you today?");

1)ECMAScript 中的函数不需要指定是否返回值

任何函数在任何时间都可以使用return 语句来返回函数的值,用法是后跟要返回的值。例如:

function sum(num1, num2) {
   
	return num1 + num2;
}

// 调用 sum()
const result = sum(5, 10);

2)只要碰到 return 语句,函数就会立即停止执行并退出

示例:

function sum(num1, num2) {
   
	return num1 + num2;
	console.log("Hello world"); // 不会执行
}

3)一个函数里也可以有多个return 语句

示例代码:

function diff(num1, num2) {
   
	if (num1 < num2) {
   
		return num2 - num1;
	} else {
   
		return num1 - num2;
	}
}

3)return 语句不带返回值。

函数会立即停止执行并返回undefined

这种用法常用于提前终止函数执行,并不是为了返回值。

示例:

function sayHi(name, message) {
   
	return;
	
	// 不会执行
	console.log("Hello " + name + ", " + message); 
}

最佳实践:
函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。


4)严格模式对函数的一些限制

  • 函数不能以evalarguments 作为名称;
  • 函数的参数不能叫evalarguments
  • 两个命名参数不能拥有同一个名称。

如果违反上述规则,则会导致语法错误,代码也不会执行。


小结

JavaScript 的核心语言特性在ECMA-262 中以伪语言ECMAScript 的形式来定义。

ECMAScript 包含所有基本语法、操作符、数据类型和对象,能完成基本的计算任务,但没有提供获得输入和产生输出的机制。理解ECMAScript 及其复杂的细节是完全理解浏览器中JavaScript 的关键。

1)ECMAScript 中的基本元素。

  • ECMAScript 中的基本数据类型包括UndefinedNullBooleanNumberStringSymbol
  • 与其他语言不同,ECMAScript 不区分整数浮点值,只有Number 一种数值数据类型。
  • Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
  • 严格模式为这门语言中某些容易出错的部分施加了限制。
  • ECMAScript 提供了C 语言和类C 语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
  • 流控制语句大多是从其他语言中借鉴而来(如if 语句、for 语句和switch语句等)。

2) ECMAScript 函数与其他语言中的函数的区别

  • 不需指定返回值,函数可以随时返回任何值。
  • 不指定返回值的函数,实际上会返回特殊值undefined



第4章 变量、作用域与内存


主要内容:

  • 通过变量使用原始值与引用值;
  • 理解执行上下文;
  • 理解垃圾回收。


4.1 原始值与引用值

ECMAScript 变量可以包含两种不同类型的数据:原始值引用值

  • 原始值(primitive value):最简单的数据;
  • 引用值(reference value):由多个值构成的对象。

在给一个变量赋值时,JavaScript 引擎必须确定这个值是原始值还是引用值

  • 保存原始值的变量按值访问的,因为我们操作的就是存储在变量中的实际值;
  • 保存引用值的变量按引用访问的。

JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是对该对象的引用而非实际的对象本身


4.1.1 动态属性

原始值和引用值的定义方式类似。但是,在变量保存这个值之后,可以对这个值做什么,则大有不同。对于引用值而言,可以随时添加、修改和删除其属性和方法。

如下所示:

let person = new Object();
person.name = "Nicholas";
console.log(person.name);     // "Nicholas"

此代码中,首先创建了一个对象,并把它保存在变量person 中。然后,给这个对象添加了一个名为name 的属性,并给这个属性赋值字符串 “Nicholas”。就可以访问这个新属性,直到对象被销毁或属性被显式地删除。

原始值不能有属性,尽管尝试给原始值添加属性不会报错。比如:

let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined

🔔 记住:只有引用值可以动态添加后面可以使用的属性。


4.1.2 传递参数

ECMAScript 中所有函数的参数都是 按值传递 的。这意味着函数外的值会被复制到函数内部的参数中。

如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。

在按值传递参数时,值会被复制到一个局部变量(即一个命名参数)。在按引用传递参数时,值在内存中的位置会被保存在局部变量中,对本地变量的修改会反映到函数外部。

示例:

function addTen(num) {
   
	num += 10;
	return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20,没有变化
console.log(result); // 30

这里,参数num其实是一个局部变量。调用时,变量count作为参数传入,count 的值是20,这个值被复制到参数num以便在函数addTen()内部使用;

function setName(obj) {
   
	obj.name = "Nicholas";
	obj = new Object();
	obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"

函数中参数的值改变之后,原始的引用仍然没变。当obj 在函数内部被重写时,它变成了一个指向本地对象的指针。而那个本地对象在函数执行结束时就被销毁了

4.1.3 确定类型

typeof 操作符适合用来判断一个变量是否为原始类型。或者说,它是判断一个变量是否为字符串、数值、布尔值或undefined 的最好方式。

如果值是对象null,那么typeof 返回"object",如下所示:

let s = "Nicholas";
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();
console.log(typeof s); // string
console.log(typeof i); // number
console.log(typeof b); // boolean
console.log(typeof u); // undefined
console.log(typeof n); // object
console.log(typeof o); // object

如要返回对象的类型,可用ECMAScript 提供的instanceof 操作符来检测,语法如下:

result = variable instanceof constructor

如果变量是给定引用类型(由其原型链决定)的实例,则instanceof操作符返回true。如下所示:

console.log(person instanceof Object);  // 变量person 是Object 吗?
console.log(colors instanceof Array);   // 变量colors 是Array 吗?
console.log(pattern instanceof RegExp); // 变量pattern 是RegExp 吗?

按照定义,所有引用值都是Object 的实例,因此通过instanceof 操作符检测任何引用值和 Object 构造函数都会返回true。类似地,如果用instanceof检测原始值,则始终会返回false,因为原始值不是对象。

typeof 操作符在用于检测函数时也会返回"function"。当在Safari(直到Safari 5)和Chrome(直到Chrome 7)中用于检测正则表达式时,由于实现细节的原因,typeof也会返回"function"。
ECMA-262 规定,任何实现内部[[Call]]方法的对象都应该在typeof 检测时返回"function"。因为上述浏览器中的正则表达式实现了这个方法,所以typeof 对正则表达式也返回"function"。在IE 和Firefox 中,typeof 对正则表达式返回"object"。


4.2 执行上下文与作用域

执行上下文(以下简称“上下文”)。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。

每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上。虽然无法通过代码访问变量对象,但后台处理数据会用到它。

全局上下文最外层的上下文。根据ECMAScript 实现的宿主环境,表示全局上下文的对象可能不一样。

在浏览器中,全局上下文就是我们常说的window 对象,因此,

  • 所有通过var 定义的全局变量和函数都会成为window 对象的属性和方法。
  • 使用letconst 的顶级声明不会定义在全局上下文中,但在作用域链解析上效果是一样的。

上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数(全局上下文在应用程序退出前才会被销毁,比如关闭网页或退出浏览器)。

每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文。ECMAScript 程序的执行流就是通过这个上下文栈进行控制的。

上下文中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。代码正在执行的上下文的变量对象 始终位于作用域链的最前端

如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments。(全局上下文中没有这个变量。)作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象。

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)

如下所示:

var color = "blue";
function changeColor() {
   
	if (color === "blue") {
   
		color = "red";
	} else {
   
		color = "blue";
	}
}
changeColor();

对这个例子而言,函数changeColor()的作用域链包含两个对象:

  1. 它自己的变量对象(就是定义arguments对象的那个);
  2. 另一个是全局上下文的变量对象。

这个函数内部之所以能够访问变量color,就是因为可以在作用域链中找到它。

局部作用域中定义的变量可用于在局部上下文中替换全局变量。如下所示:

var color = "blue";
function changeColor() {
   
	let anotherColor = "red";
	function swapColors() {
   
		let tempColor = anotherColor;
		anotherColor = color;
		color = tempColor;
		// 这里可以访问color、anotherColor 和tempColor
	}
	// 这里可以访问color 和anotherColor,但访问不到tempColor
	swapColors();
}
// 这里只能访问color
changeColor();

以上代码涉及3 个上下文:

  1. 全局上下文;
  2. changeColor()的局部上下文;
  3. swapColors()的局部上下文。

全局上下文中有一个变量color 和一个函数changeColor()changeColor()的局部上下文中有一个变量anotherColor 和一个函数swapColors(),但在这里可以访问全局上下文中的变量color

swapColors()的局部上下文中有一个变量tempColor,只能在这个上下文中访问到。全局上下文和changeColor()的局部上下文都无法访问到tempColor。而在swapColors()中则可以访问另外两个上下文中的变量,因为它们都是父上下文。


作用域链示意图

上图中,矩形表示不同的上下文。内部上下文可以通过作用域链访问外部上下文中的一切,但外部上下文无法访问内部上下文中的任何东西。

上下文之间的连接是线性的、有序的。每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。

swapColors()局部上下文的作用域链中有3 个对象:swapColors()的变量对象、changeColor()的变量对象和全局变量对象。swapColors()的局部上下文首先从自己的变量对象开始搜索变量和函数,搜不到就去搜索上一级变量对象。changeColor()上下文的作用域链中只有 2 个对象:它自己的变量对象和全局变量对象。因此,它不能访问swapColors()的上下文。


函数参数被认为是当前上下文中的变量,因此也跟上下文中的其他变量遵循相同的访问规则。


4.2.1 作用域链增强

虽然执行上下文 主要全局上下文函数上下文 两种(eval()调用内部存在第三种上下文),但有其他方式来增强作用域链。某些语句会导致在作用域链前端临时添加一个上下文,这个上下文在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:

  • try/catch 语句的catch 块;
  • with 语句

这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加 指定的对象;对catch 语句而言,则会创建一个新的 变量对象,这个变量对象会包含要抛出的错误对象的声明。看下面的例子:

function buildUrl() {
   
	let qs = "?debug=true";
	with(location){
   
		let url = href + qs;
	}
	return url;
}

【释义】这里,with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。buildUrl()函数中定义了一个变量qs。当with 语句中的代码引用变量 href 时,实际上引用的是location.href,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在buildUrl()中的那个变量,它定义在函数上下文的变量对象上。而在with 语句中使用 var 声明的变量 url 会成为函数上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作用域(稍后介绍),所以在with 块之外没有定义。

IE 的实现在IE8 之前是有偏差的,即它们会将catch 语句中捕获的错误添加到执行上下文的变量对象上,而不是catch 语句的变量对象上,导致在catch 块外部都可以访问到错误。IE9 纠正了这个问题。


4.2.2 变量声明

直到 ECMAScript 5.1,var 都是声明变量的唯一关键字。到 ES6 不仅增加了letconst 两个关键字,而且还让这两个关键字压倒性地超越 var 成为首选。

  1. 使用var 声明变量时,变量会被自动添加到最接近的上下文
    在函数中,最接近的上下文就是函数的局部上下文。在with 语句中,最接近的上下文也是函数上下文。如果变量未经声明就被初始化,那么它就会自动被添加到全局上下文,如下所示:

    function add(num1, num2) {
         
    	var sum = num1 + num2;
    	return sum;
    }
    let result = add(10, 20); // 30
    console.log(sum);         // 报错:sum 在这里不是有效变量
    

    如果省略变量 sum 前面的关键字var,那么sum 在add()被调用之后就能被访问(在调用add()之后,sum被添加到了全局上下文,并且在函数退出之后依然存在)

    未经声明而初始化变量是JavaScript 编程中一个非常常见的错误,会导致很多问题。为此,读者在初始化变量之前一定要先声明变量。在严格模式下,未经声明就初始化变量会报错。

  2. let关键字的作用域是块级的
    这是JavaScript中的新概念。块级作用域由最近的一对花括号{}界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是let声明变量的作用域。

    if (true) {
         
    	let a;
    }
    console.log(a); // ReferenceError: a 没有定义
    
    while (true) {
         
    	let b;
    }
    console.log(b); // ReferenceError: b 没有定义
    
    function foo() {
         
    	let c;
    }
    console.log(c); // ReferenceError: c 没有定义
    // 这没什么可奇怪的
    // var 声明也会导致报错
    // 这不是对象字面量,而是一个独立的块
    // JavaScript 解释器会根据其中内容识别出它来
    {
         
    	let d;
    }
    console.log(d); // ReferenceError: d 没有定义
    

    let 与var 的另一个不同之处是在同一作用域内不能声明两次。重复的var 声明会被忽略,而重复的let 声明会抛出SyntaxError

    严格来讲,let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,从写JavaScript 代码的角度说,let 的提升跟var是不一样的。

  3. 使用const的常量声明
    使用const 声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。

    const a; // SyntaxError: 常量声明时没有初始化
    const b = 3;
    console.log(b); // 3
    b = 4; // TypeError: 给常量赋值
    

    const 除了要遵循以上规则,其他方面与 let 声明是一样的:

    if (true) {
         
    	const a = 0;
    }
    console.log(a); // ReferenceError: a 没有定义
    

    const 声明只应用到顶级原语或者对象。换句话说,赋值为对象的 const 变量不能再被重新赋值为其他引用值,但对象的键则不受限制

    const o1 = {
         };
    o1 = {
         }; // TypeError: 给常量赋值
    const o2 = {
         };
    o2.name = 'Jake';
    console.log(o2.name); // 'Jake'
    

    开发实践表明,如果开发流程并不会因此而受很大影响,就应该尽可能地多使用const 声明,除非确实需要一个将来会重新赋值的变量。这样可以从根本上保证提前发现重新赋值导致的bug。


4.3 垃圾回收

JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存

在C 和C++等语言中,跟踪内存使用对开发者来说是个很大的负担,也是很多问题的来源。JavaScript 为开发者卸下了这个负担,通过自动内存管理实现内存分配和闲置资源回收

基本思路很简单:确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。垃圾回收程序必须跟踪记录哪个变量还会使用,。如何标记未使用的变量也许有不同的实现方式。浏览器发展史上,用到过两种主要的标记策略:标记清理引用计数


4.3.1 标记清理

JavaScript 最常用的垃圾回收策略是标记清理(mark-and-sweep)。当变量进入上下文,比如在函数内部声明一个变量时,这个变量会被加上存在于上下文中的标记。

垃圾回收程序运行的时候,会标记内存中存储的所有变量(标记方法有很多种)。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。

至2008 年,IE、Firefox、Opera、Chrome 和Safari 都在自己的 JavaScript 实现中采用标记清理(或其变体),只是在运行垃圾回收的频率上有所差异。


4.3.2 引用计数

另一种不常用的垃圾回收策略是引用计数(reference counting)。其思路是对每个值都记录它被引用的次数。

声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0 的值的内存。

【缺点】:循环引用时会遇到严重问题。所谓循环引用,就是对象A 有一个指针指向对象B,而对象B 也引用了对象A。比如:

function problem() {
   
	let objectA = new Object();
	let objectB = new Object();
	objectA.someOtherObject = objectB;
	objectB.anotherObject = objectA;
}

objectA 和 objectB 通过各自的属性相互引用,意味着它们的引用数都是2。它们在函数结束后还会存在,因为它们的引用数永远不会变成0。如果该函数被多次调用,则会导致大量内存永远不会被释放。为此,Netscape 在4.0 版放弃了引用计数,转而采用标记清理


⚠️警告:
  某些浏览器能主动触发垃圾回收(但不推荐)。IE 中,调用window.CollectGarbage()方法会立即触发垃圾回收。Opera 7 及更高版本中,调用window.opera.collect()也会启动垃圾回收程序。



4.3.3 内存管理

在使用垃圾回收的编程环境中,开发者通常无须关心内存管理。不过,JavaScript 运行在一个内存管理与垃圾回收都很特殊的环境。

🔔【建议】:尽可能将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为null,从而释放其引用(或者叫解除引用)。

此建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用,如下所示:

function createPerson(name){
   
	let localPerson = new Object();
	localPerson.name = name;
	return localPerson;
}
let globalPerson = createPerson("Nicholas");
globalPerson = null;  // 手动解除globalPerson 对值的引用

localPerson 在createPerson()执行完成超出上下文后会自动被解除引用,无需显式处理。但 globalPerson 是一个全局变量,在不再需要时手动解除其引用。

解除对一个值的引用,并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值不再存在于上下文中,以使它在下次垃圾回收时被回收。

  1. 通过constlet 声明提升性能
    ES6 增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为constlet 都以块(而非函数)为作用域,所以相比于使用var在块作用域比函数作用域更早终止的情况下,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存

    2.隐藏类和删除操作
    截至2017 年,Chrome 是最流行的浏览器,使用V8 JavaScript 引擎。V8 在将解释后的JavaScript代码编译为实际的机器码时会利用“隐藏类”。

    运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到。如下所示:

    function Article() {
         
    	this.title = 'Inauguration Ceremony Features Kazoo Band';
    }
    let a1 = new Article();
    let a2 = new Article();
    

    V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原 型。假设之后又添加了下面这行代码:

    a2.author = 'Jake';
    

    此时两个Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响。当然,解决方案就是避免JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性,如下所示:

    function Article(opt_author) {
         
    	this.title = 'Inauguration Ceremony Features Kazoo Band';
    	this.author = opt_author;
    }
    let a1 = new Article();
    let a2 = new Article('Jake');
    


第 5 章 基本引用类型

5.1 Date

对象被认为是某个特定引用类型的实例。新对象通过使用new操作符后跟一个构造函数(constructor)来创建。构造函数就是用来创建新对象的函数,比如下面这行代码:

let now = new Date();

Date()在这里就是构造函数,ECMAScript提供了很多像Date这样的原生引用类型。

ECMAScript还提供了Date.now()方法,返回表示方法执行时日期和时间的毫秒数。如下所示:

// 起始时间
let start = Date.now();

// 调用函数
doSomething();

// 结束时间
let stop = Date.now(),
result = stop - start;

Date.parse()方法接收一个表示日期的字符串参数,尝试将这个字符串转换为表示该日期的毫秒数。

let someDate = new Date(Date.parse("May 23, 2019"));

如果直接把表示日期的字符串传给Date构造函数,那么Date会在后台调用Date.parse(),因此,上述代码等价于:

let someDate = new Date("May 23, 2019");

Date.UTC()也会被Date构造函数隐式调用,但有一个区别:这种情况下创建的是本地日期,不是GMT日期。

5.2 RegExp

ECMAScript通过RegExp类型支持正则表达式。正则表达式使用类似Perl的简洁语法来创建:

let expression = /pattern/flags;

每个正则表达式可以带零个或多个flags(标记),用于控制正则表达式的行为。下面给出了表示匹配模式的标记。

标记 描述
g 全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束
i 不区分大小写,表示在查找匹配时忽略pattern和字符串的大小写。
m 多行模式,表示查找到一行文本末尾时会继续查找。
y 粘附模式,表示只查找从lastIndex开始及之后的字符串。
u Unicode模式,启用Unicode匹配。
s dotAll模式,表示元字符.匹配任何字符(包括\n\r)。

示例:

// 匹配字符串中的所有"at"
let pattern1 = /at/g;

// 匹配第一个"bat"或"cat",忽略大小写
let pattern2 = /[bc]at/i;

// 匹配所有以"at"结尾的三字符组合,忽略大小写
let pattern3 = /.at/gi;

RegExp实例的主要方法是exec(),主要用于配合捕获组使用。这个方法只接收一个参数,即要应用模式的字符串。如果找到了匹配项,则返回包含第一个匹配信息的数组;如果没找到匹配项,则返回null

正则表达式的另一个方法是test(),接收一个字符串参数。如果输入的文本与模式匹配,则参数返回true,否则返回false。这个方法适用于只想测试模式是否匹配,而不需要实际匹配内容的情况。test()经常用在if语句中:

let text = "000-00-0000";
let pattern = /\d{3}-\d{2}-\d{4}/;

if (pattern.test(text)) {
   
	  console.log("The pattern was matched.");
}

常用于验证用户输入,此时我们只在乎输入是否有效,不关心为什么无效。

无论正则表达式是怎么创建的,继承的方法toLocaleString()toString()都返回正则表达式的字面量表示。例如:

let pattern = new RegExp("\\[bc\\]at", "gi");
console.log(pattern.toString());       // /\[bc\]at/gi
console.log(pattern.toLocaleString()); // /\[bc\]at/gi

正则表达式的valueOf()方法返回正则表达式本身。


5.3 原始值包装类型

为了方便操作原始值,ECMAScript提供了 3 种特殊的引用类型BooleanNumberString

每当用到某个原始值的方法或属性时,后台都会创建一个相应原始包装类型的对象,来暴露操作原始值的各种方法。如下所示:

let s1 = "some text";
let s2 = s1.substring(2);  // substring(start,stop) 提取字符串中介于两个指定下标之间的字符

s1是一个包含字符串的变量,它是一个原始值。第二行紧接着在s1上调用了substring()方法,并把结果保存在s2中。

我们知道,原始值本身不是对象,因此逻辑上不应该有方法。而实际上这个例子又确实按照预期运行了。这是因为后台进行了很多处理,从而实现了上述操作。

具体来说,当第二行访问s1时,是以只读访问的,也就是要从内存中读取变量保存的值。在只以读模式访问字符串值时,后台会执行以下 3 步:

(1) 创建一个 String 类型的实例;
(2) 调用实例上的特定方法;
(3) 销毁实例。

可以把这3步想象成执行了如下3行ECMAScript代码:

let s1 = new String("some text");
let s2 = s1.substring(2);
s1 = null;

引用类型与原始值包装类型的区别

引用类型原始值包装类型主要区别 在于对象的 生命周期

  • 在通过new实例化引用类型后,得到的实例会在离开作用域时被销毁;
  • 自动创建的原始值包装对象只存在于访问它的那行 代码执行期间

示例:

let s1 = "some text";
s1.color = "red";
console.log(s1.color);  // undefined

第二行代码给字符串s1添加了一个color属性。但是,当第三行代码访问color属性时,它却不见了。原因就是第二行代码运行时会临时创建一个String对象,而当第三行代码执行时,这个对象已经被销毁了。实际上,第三行代码在这里创建了自己的String对象,但这个对象没有color属性。

——— 因此,这意味着 不能在运行时给 原始值 添加 属性方法

构造函数作为一个工厂方法,能够根据传入值的类型返回相应原始值包装类型的实例。比如:

let obj = new Object("some text");
console.log(obj instanceof String);  // true

如果传给Object的是字符串,则会创建一个String的实例。如果是数值,则会创建Number的实例。如果是布尔值则会得到Boolean的实例。

🔔 ,使用new调用原始值包装类型的构造函数,与调用同名的转型函数并不一样。例如:

let value = "25";
let number = Number(value);    // 转型函数
console.log(typeof number);    // "number"
let obj = new Number(value);   // 构造函数
console.log(typeof obj);       // "object"

上例中,变量number中保存的是一个值为25的原始数值,而变量obj中保存的是一个Number的实例。

5.3.1 Number

Number是对应数值的引用类型。要创建一个Number对象,就使用Number构造函数并传入一个数值,如下所示:

let numberObject = new Number(10);

Number类型的实例会重写valueOf()toLocaleString()toString()方法。

valueOf()方法返回Number对象表示的原始数值,另外两个方法返回数值字符串。toString()方法可选地接收一个表示基数的参数,并返回相应基数形式的数值字符串,如下所示:

let num = 10;
console.log(num.toString());   // "10"
console.log(num.toString(2));  // "1010"
console.log(num.toString(8));  // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"

Number类型还提供了几个用于将数值 格式化为字符串 的方法:

  1. toFixed()方法
    返回包含指定小数点位数的数值字符串(接收一个参数)

    let num = 10;
    console.log(num.toFixed(2)); // "10.00"
    

    示例中接收了参数2,表示返回的数值字符串要包含两位小数,当本身小数超出指定小数位数时,四舍五入到最近小数位。

  2. toExponential()方法
    返回以科学记数法(也称为指数记数法)表示的数值字符串

    let num = 10;
    console.log(num.toExponential(1));  // "1.0e+1"
    

  1. toPrecision()方法
    会根据情况返回最合理的输出结果,可能是固定长度,也可能是科学记数法形式。这个方法接收一个参数,表示结果中数字的总位数(不包含指数)。如下所示:

    let num = 99;
    console.log(num.toPrecision(1)); // "1e+2"
    console.log(num.toPrecision(2)); // "99"
    console.log(num.toPrecision(3)); // "99.0"
    

本质上,toPrecision()方法会根据数值和精度来决定调用toFixed()还是toExponential()

为了以正确的小数位精确表示数值,以上 3 个方法都会 向上或向下舍入

5.3.2 String

要创建一个 String 对象,使用 String 构造函数并传入一个数值,如下所示:

let stringObject = new String("hello world");

String 对象的方法可以在所有字符串原始值上调用。3 个继承的方法valueOf()toLocaleString()toString()都返回对象的 原始字符串值

每个String对象都有一个length属性,表示字符串中字符的数量。示例如下:

let stringValue = "hello world";
console.log(stringValue.length); // "11"

🔔 ,即使字符串中包含双字节字符(而不是单字节的ASCII字符),也仍然会按单字符来计数。

String 类型解析和操作字符串的方法

JavaScript字符串由16位码元(code unit)组成。对多数字符来说,每16位码元对应一个字符。换句话说,字符串的length属性表示字符串包含多少16位码元

let msg = "abcde";

console.log(msg.length); // 5
  1. charAt()方法
    方法返回给定索引位置的字符,由传给方法的整数参数指定。

    let message = "abcde";
    
    console.log(message.charAt(2)); // "c"
    

    这个方法查找指定索引位置的16位码元,并返回该码元对应的字符。

    JavaScript字符串使用了两种Unicode编码混合的策略:UCS-2UTF-16。对于可以采用16位编码的字符(U+0000~U+FFFF),这两种编码实际上是一样的。

  2. charCodeAt()方法
    可以查看指定码元的字符编码。这个方法返回指定索引位置的码元值,索引以整数指定。

    let message = "abcde";
    
    // Unicode "Latin small letter C"的编码是U+0063
    console.log(message.charCodeAt(2));  // 99
    
    // 十进制99等于十六进制63
    console.log(99 === 0x63);            // true
    

  1. fromCharCode()方法
    用于根据给定的UTF-16码元创建字符串中的字符。这个方法可以接受任意多个数值,并返回将所有数值对应的字符拼接起来的字符串:

    // Unicode "Latin small letter A"的编码是U+0061
    // Unicode "Latin small letter B"的编码是U+0062
    // Unicode "Latin small letter C"的编码是U+0063
    // Unicode "Latin small letter D"的编码是U+0064
    // Unicode "Latin small letter E"的编码是U+0065
    
    console.log(String.fromCharCode(0x61, 0x62, 0x63, 0x64, 0x65));  // "abcde"
    
    // 0x0061 === 97
    // 0x0062 === 98
    // 0x0063 === 99
    // 0x0064 === 100
    // 0x0065 === 101
    
    console.log(String.fromCharCode(97, 98, 99, 100, 101));          // "abcde"
    

只要字符编码大小与码元大小一 一对应,对于U+0000~U+FFFF范围内的字符,lengthcharAt()charCodeAt()fromCharCode()返回的结果都跟预期是一样的。这是因为在这个范围内,每个字符都是用16位表示的

这个对应关系在扩展到Unicode增补字符平面时就不成立了。问题很简单,即16位只能唯一表示65 536个字符。这对于大多数语言字符集是足够了,在Unicode中称为基本多语言平面(BMP)。为了表示更多的字符,Unicode采用了一个策略,即每个字符使用另外16位去选择一个增补平面。这种每个字符使用两个16位码元的策略称为代理对


在涉及增补平面的字符时,前面的字符串方法就会出问题。比如,下面的例子中使用了一个笑脸表情符号,也就是一个使用代理对编码的字符:

// "smiling face with smiling eyes" 表情符号的编码是U+1F60A
// 0x1F60A === 128522
let message = "ab☺de";

console.log(message.length);          // 6
console.log(message.charAt(1));       // b
console.log(message.charAt(2));       // <?>
console.log(message.charAt(3));       // <?>
console.log(message.charAt(4));       // d

console.log(message.charCodeAt(1));   // 98
console.log(message.charCodeAt(2));   // 55357
console.log(message.charCodeAt(3));   // 56842
console.log(message.charCodeAt(4));   // 100

console.log(String.fromCodePoint(0x1F60A)); // ☺

console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101)); // ab☺de

这些方法仍然将16位码元当作一个字符,事实上索引2和索引3对应的码元应该被看成一个代理对,只对应一个字符。fromCharCode()方法仍然返回正确的结果,因为它实际上是基于提供的二进制表示直接组合成字符串。浏览器可以正确解析代理对(由两个码元构成),并正确地将其识别为一个Unicode笑脸字符。

  1. codePointAt()方法
    codePointAt()接收16位码元的索引并返回该索引位置上的码点(code point)。码点是Unicode中一个字符的完整标识。比如,"c"的码点是0x0063,而"☺"的码点是0x1F60A。码点可能是16位,也可能是32位,而codePointAt()方法可以从指定码元位置识别完整的码点。

    let message = "ab☺de";
    
    console.log(message.codePointAt(1)); // 98
    console.log(message.codePointAt(2)); // 128522
    console.log(message.codePointAt(3)); // 56842
    console.log(message.codePointAt(4)); // 100
    

🔔 :如果传入的码元索引并非代理对的开头,就会返回错误的码点。这种错误只有检测单个字符的时候才会出现,可以通过从左到右按正确的码元数遍历字符串来规避。迭代字符串可以智能地识别代理对的码点:

console.log([..."ab☺de"]); // ["a", "b", "☺", "d", "e"]

  1. normalize()方法
    某些Unicode字符可以有多种编码方式。有的字符既可以通过一个BMP字符表示,也可以通过一个代理对表示。比如:

    // U+00C5:上面带圆圈的大写拉丁字母A
    console.log(String.fromCharCode(0x00C5));          // Å
    
    // U+212B:长度单位“埃”
    console.log(String.fromCharCode(0x212B));          // Å
    
    // U+004:大写拉丁字母A
    // U+030A:上面加个圆圈
    console.log(String.fromCharCode(0x0041, 0x030A));  // Å
    

    比较操作符不在乎字符看起来是什么样的,因此这3个字符互不相等。

    let a1 = String.fromCharCode(0x00C5),
        a2 = String.fromCharCode(0x212B),
        a3 = String.fromCharCode(0x0041, 0x030A);
    
    console.log(a1, a2, a3); // Å, Å, Å
    
    console.log(a1 === a2);  // false
    console.log(a1 === a3);  // false
    console.log(a2 === a3);  // false
    

    为解决这个问题,Unicode 提供了 4 种规范化形式,可以将类似上面的字符规范化为一致的格式,无论底层字符的代码是什么。

    这 4 种规范化形式是:

    • NFD(Normalization Form D);
    • NFC(Normalization Form C);
    • NFKD(Normalization Form KD);
    • NFKC(Normalization Form KC)。

    可以使用normalize()方法对字符串应用上述规范化形式,使用时传入表示哪种形式的字符串:"NFD"、"NFC"、"NFKD“或”NFKC"

    通过比较字符串与其调用normalize()的返回值,就可以知道该字符串是否已经规范化:

    let a1 = String.fromCharCode(0x00C5),
        a2 = String.fromCharCode(0x212B),
        a3 = String.fromCharCode(0x0041, 0x030A);
    
    // U+00C5是对0+212B进行NFC/NFKC规范化之后的结果
    console.log(a1 === a1.normalize("NFD"));  // false
    console.log(a1 === a1.normalize("NFC"));  // true
    console.log(a1 === a1.normalize("NFKD")); // false
    console.log(a1 === a1.normalize("NFKC")); // true
    
    // U+212B是未规范化的
    console.log(a2 === a2.normalize("NFD"));  // false
    console.log(a2 === a2.normalize("NFC"));  // false
    console.log(a2 === a2.normalize("NFKD")); // false
    console.log(a2 === a2.normalize("NFKC")); // false
    
    // U+0041/U+030A是对0+212B进行NFD/NFKD规范化之后的结果
    console.log(a3 === a3.normalize("NFD"));  // true
    console.log(a3 === a3.normalize("NFC"));  // false
    console.log(a3 === a3.normalize("NFKD")); // true
    console.log(a3 === a3.normalize("NFKC")); // false
    

    选择同一种规范化形式可以让比较操作符返回正确的结果:

    let a1 = String.fromCharCode(0x00C5),
        a2 = String.fromCharCode(0x212B),
        a3 = String.fromCharCode(0x0041, 0x030A);
    
    console.log(a1.normalize("NFD") === a2.normalize("NFD"));    // true
    console.log(a2.normalize("NFKC") === a3.normalize("NFKC"));  // true
    console.log(a1.normalize("NFC") === a3.normalize("NFC"));    // true
    


字符串操作方法

  1. concat()方法
    用于将一个或多个字符串拼接成一个新字符串。

    let stringValue = "hello ";
    let result = stringValue.concat("world");
    
    console.log(result);      // "hello world"
    console.log(stringValue); // "hello"
    

    本例中,对stringValue调用concat()方法的结果是得到 “hello world”,但stringValue的值保持不变。concat()方法可以接收任意多个参数,因此可以一次性拼接多个字符串,如下所示:

    let stringValue = "hello ";
    let result = stringValue.concat("world", "!");
    
    console.log(result);      // "hello world!"
    console.log(stringValue); // "hello"
    

    这个修改后的例子将字符串"world"和"!"追加到了"hello "后面。虽然concat()方法可以拼接字符串,但更常用的方式是使用加号操作符(+)。而且多数情况下,对于拼接多个字符串来说,使用加号更方便。


ECMAScript提供了3个从字符串中提取子字符串的方法:slice()substr()substring()

这3个方法都返回调用它们的字符串的一个子字符串,而且都接收 1 或 2 个参数:

  • 参数1:表示子字符串 开始 的位置;
  • 参数2:表示子字符串 结束 的位置。

slice()substring()而言,第二个参数是提取结束的位置(即该位置之前的字符会被提取出来)。对substr()而言,第二个参数表示返回的子字符串数量。任何情况下,省略第二个参数都意味着提取到字符串末尾。

concat()方法一样,slice()substr()substring()也不会修改调用它们的字符串,而只会返回提取到的原始新字符串值。来看下面的例子:

let stringValue = "hello world";
console.log(stringValue.slice(3));       // "lo world"
console.log(stringValue.substring(3));   // "lo world"
console.log(stringValue.substr(3));      // "lo world"
console.log(stringValue.slice(3, 7));    // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7));   // "lo worl"


字符串位置方法

  1. indexOf() 方法;
  2. lastIndexOf()方法
相同点 不同点
从字符串中搜索传入的字符串,并返回位置(如果没找到,则返回 -1 1)indexOf()方法:从字符串开头开始查找子字符串;
2) lastIndexOf()方法:从字符串末尾开始查找子字符串。

示例如下:

let stringValue = "hello world";
console.log(stringValue.indexOf("o"));     // 4
console.log(stringValue.lastIndexOf("o")); // 7

最后一个"o"的位置是7,即world中的 “o”。

两个方法都可以接收 可选的 第二个参数,表示开始搜索的位置:

  • indexOf()会从这个参数指定的位置开始向字符串末尾搜索,忽略该位置之前的字符;
  • lastIndexOf()则会从这个参数指定的位置开始向字符串开头搜索,忽略该位置之后直到字符串末尾的字符。
let stringValue = "hello world";
console.log(stringValue.indexOf("o", 6));     // 7
console.log(stringValue.lastIndexOf("o", 6)); // 4

运用实例】:在字符串中找到所有的目标子字符串

let stringValue = "Lorem ipsum dolor sit amet, consectetur adipisicing elit";
let positions = new Array();
let pos = stringValue.indexOf("e");

while(pos > -1) {
   
  positions.push(pos);
  pos = stringValue.indexOf("e", pos + 1);
}

console.log(positions); // [3,24,32,35,52]

代码解释:通过indexOf()遍历整个字符串。首先取得第一个"e"的位置,然后进入循环,将上一次的位置加1再传给indexOf(),确保搜索到最后一个子字符串实例之后。每个位置都保存在positions数组中,可供以后使用。



字符串包含方法

ECMAScript 6 增加了 3 个用于判断字符串中是否包含另一个字符串的方法:

  • startsWith()
  • endsWith()
  • includes()

相同点 不同点
1)都会从字符串中搜索传入的字符串,并返回一个表示是否包含的 布尔值
2)可接收第 2 个参数。
1)startsWith() 方法:检查开始于索引0的匹配项;
2) endsWith() 方法:检查开始于索引(string.length - substring.length)的匹配项。
3)includes() 方法:检查整个字符串。

如下所示:

let message = "foobarbaz";

console.log(message.startsWith("foo"));  // true
console.log(message.startsWith("bar"));  // false

console.log(message.endsWith("baz"));    // true
console.log(message.endsWith("bar"));    // false

console.log(message.includes("bar"));    // true
console.log(message.includes("qux"));    // false

startsWith()includes()方法接收 可选的 2个参数,表示开始搜索的位置。

如果传入第 2 个参数,则这两个方法会从指定位置向字符串末尾搜索,忽略该位置之前的所有字符。如下所示:

let message = "foobarbaz";

console.log(message.startsWith("foo"));     // true
console.log(message.startsWith("foo", 1));  // false

console.log(message.includes("bar"));       // true
console.log(message.includes("bar", 4));    // false

trim()方法

ECMAScript 在所有字符串上都提供了trim()方法。该方法会创建字符串的一个副本,删除前、后所有空格符,再返回结果。

let stringValue = "  hello world  ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue);         // "  hello world "
console.log(trimmedStringValue);  // "hello world"

原始字符串不受影响,即原本的前、后空格符都会保留。

trimLeft()trimRight()方法分别用于清理字符串开始和末尾的空格符。


repeat()方法

ECMAScript在所有字符串上都提供了repeat()方法。该方法接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果。

let stringValue = "na ";
console.log(stringValue.repeat(16) + "batman");
// na na na na na na na na na na na na na na na na batman

padStart()padEnd()方法

padStart()padEnd()方法会复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件。

这两个方法的参数1长度参数 2 是可选的 填充字符串,默认为空(U+0020)。

let stringValue = "foo";

console.log(stringValue.padStart(6));       // "   foo"
console.log(stringValue.padStart(9, "."));  // "......foo"

console.log(stringValue.padEnd(6));         // "foo   "
console.log(stringValue.padEnd(9, "."));    // "foo......"

🔔 注: 第 2 个参数并不限于 1 个字符。如果提供了多个字符的字符串,则会将其拼接并截断以匹配指定长度(如果长度小于或等于字符串长度,则会返回原始字符串)。如下所示:

let stringValue = "foo";

console.log(stringValue.padStart(8, "bar")); // "barbafoo"
console.log(stringValue.padStart(2));        // "foo"

console.log(stringValue.padEnd(8, "bar"));   // "foobarba"
console.log(stringValue.padEnd(2));          // "foo"

字符串迭代与解构

字符串的原型上暴露了@@iterator方法,可以迭代字符串的每个字符。如:

let message = "abc";
let stringIterator = message[Symbol.iterator]();

console.log(stringIterator.next());  // {value: "a", done: false}
console.log(stringIterator.next());  // {value: "b", done: false}
console.log(stringIterator.next());  // {value: "c", done: false}
console.log(stringIterator.next());  // {value: undefined, done: true}

for-of循环中,可通过迭代器按序访问每个字符:

for (const c of "abcde") {
   
  console.log(c);
}

利用此迭代器,字符串就可通过解构操作符来解构。比如,把字符串分割为字符数组:

let message = "abcde";
console.log([...message]); // ["a", "b", "c", "d", "e"]

字符串大小写转换

包括 4 个方法:

  • toLowerCase()
  • toLocaleLowerCase()
  • toUpperCase()
  • toLocaleUpperCase()
let stringValue = "hello world";
console.log(stringValue.toLocaleUpperCase());  // "HELLO WORLD"
console.log(stringValue.toUpperCase());        // "HELLO WORLD"
console.log(stringValue.toLocaleLowerCase());  // "hello world"
console.log(stringValue.toLowerCase());        // "hello world"

字符串模式匹配方法

  1. match() 方法
    match()方法接收一个参数(正则表达式字符串,或者RegExp对象)。

    此方法本质上跟RegExp对象的exec()方法相同。

    let text = "cat, bat, sat, fat";
    let pattern = /.at/;
    
    // 等价于pattern.exec(text)
    let matches = text.match(pattern);
    console.log(matches.index);      // 0
    console.log(matches[0]);         // "cat"
    console.log(pattern.lastIndex);  // 0
    

    match()方法返回的数组与RegExp对象的exec()方法返回的数组相同:第一个元素是与整个模式匹配的字符串,其余元素则是与表达式中的捕获组匹配的字符串(如果有)。

  2. search()方法
    match()方法一样,唯一的参数是正则表达式字符串或RegExp对象。

    该方法 返回 第1个匹配的位置 索引,如果没找到则返回-1(始终从字符串开头向后匹配)。示例如下:

    let text = "cat, bat, sat, fat";
    let pos = text.search(/at/);
    console.log(pos);  // 1
    

    这里返回1,即"at"的第一个字符在字符串中的位置。

  3. replace()方法
    简化子字符串替换操作,ECMAScript提供了replace()方法;

    该方法接收两个参数:

    参数 说明
    参数1 可以是RegExp对象或字符串(该字符串不会转换为正则表达式)
    参数2 可以是字符串或函数

    🔔注:
    1)
    如果第一个参数是字符串,那么只会替换第一个子字符串。要想替换所有子字符串,第一个参数须为正则表达式且全局匹配(/g),如下所示:

    let text = "cat, bat, sat, fat";
    let result = text.replace("at", "ond");
    console.log(result);  // "cond, bat, sat, fat"
    
    result = text.replace(/at/g, "ond");
    console.log(result);  // "cond, bond, sond, fond"
    

    2)如第二个参数是字符串,有几个特殊的字符序列,可用来插入正则表达式操作的值。ECMA-262中规定了下表中的值:

    符序列 替换文本
    $$ $
    $& 匹配所有的子字符串。与RegExp.lastMatch相同
    $' 匹配的子字符串之前的字符串。与RegExp.rightContext相同
    $` 匹配的子字符串之后的字符串。与RegExp.leftContext相同
    $n 匹配第n个捕获组的字符串,其中n是0~9。比如,$1是匹配第一个捕获组的字符串,$2是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串
    $nn 匹配第nn个捕获组字符串,其中nn是01~99。比如,$01是匹配第一个捕获组的字符串,$02是匹配第二个捕获组的字符串,以此类推。如果没有捕获组,则值为空字符串

    使用这些特殊的序列,可以在替换文本时使用之前匹配的内容,如下所示:

    let text = "cat, bat, sat, fat";
    result = text.replace(/(.at)/g, "word ($1)");
    console.log(result);  // word (cat), word (bat), word (sat), word (fat)
    

    这里,每个以"at"结尾的词都会被替换成"word"后跟一对小括号,其中包含捕获组匹配的内容$1

    3) replace()的第二个参数可以是一个函数。
    在只有一个匹配项时,此函数会收到3个参数:

    • 与整个模式匹配的字符串;
    • 匹配项在字符串中的开始位置;
    • 整个字符串。
    function htmlEscape(text) {
         
      return text.replace(/[<>"&]/g, function(match, pos, originalText) {
         
        switch(match) {
         
          case "<":
            return "&lt;";
          case ">":
            return "&gt;";
          case "&":
            return "&amp;";
          case "\"":
            return "&quot;";
        }
      });
    }
    
    console.log(htmlEscape("<p class=\"greeting\">Hello world!</p>"));
    // "&lt;p class=&quot;greeting&quot;&gt;Hello world!</p>"
    

    这里,函数htmlEscape()用于将一段HTML中的4个字符替换成对应的实体:<>&,还有""(都必须经过转义)。实现这个任务最简单的办法就是用一个正则表达式查找这些字符,然后定义一个函数,根据匹配的每个字符分别返回特定的HTML实体。

  4. split() 方法
    该方法会根据传入的分隔符将字符串拆分成数组

    作为分隔符的参数可以是字符串,也可以是RegExp对象。也可以传入第二个参数,即数组大小,确保返回的数组不会超过指定大小。

    let colorText = "red,blue,green,yellow";
    let colors1 = colorText.split(",");       // ["red", "blue", "green", "yellow"]
    let colors2 = colorText.split(",", 2);    // ["red", "blue"]
    let colors3 = colorText.split(/[^,]+/);   // ["", ",", ",", ",", ""]
    

    在这里,字符串colorText是一个逗号分隔的颜色名称符串。调用split(",")会得到包含这些颜色名的数组,基于逗号进行拆分。要把数组元素限制为2个,传入第二个参数2即可

    使用正则表达式可以得到一个包含逗号的数组。注意在最后一次调用split()时,返回的数组前后包含两个空字符串。这是因为正则表达式指定的分隔符出现在了字符串开头("red")和末尾("yellow")。

  5. localeCompare()方法
    该方法用于比较两个字符串,返回如下 3 个值中的一个:

    • 负值(通常是-1):依字母表顺序,字符串排在字符串参数前头,返回负值;
    • 0值:符串与字符串参数相等,则返回0
    • 正值(通常是1):依字母表顺序,字符串排在字符串参数后面。
      如下所示:
    let stringValue = "yellow";
    console.log(stringValue.localeCompare("brick"));  // 1
    console.log(stringValue.localeCompare("yellow")); // 0
    console.log(stringValue.localeCompare("zoo"));    // -1
    

    上面示例中,"brick“按字母表顺序应在”yellow“前,因此localeCompare()返回1;”yellow“等于”yellow",因此"localeCompare()“返回0;”zoo“在”yellow"后面,因此localeCompare()返回-1

    注: 因为返回的具体值可能因具体实现而异,所以最好像下面的示例中一样,使用localeCompare()

    function determineOrder(value) {
         
    	let result = stringValue.localeCompare(value);
    	if (result < 0) {
         
    		console.log(`字符串'yellow'在字符串'${
           value}'之前.`);
    	} else if (result > 0) {
         
    		console.log(`字符串'yellow'在字符串'${
           value}'之后.`);
    	} else {
         
    		console.log(`字符串'yellow'等于字符串'${
           value}'.`);
    	}
    }
             
    determineOrder("brick");
    determineOrder("yellow");
    determineOrder("zoo");
    

    这样,即可保证在所有实现中都能正确判断字符串的顺序。

    localeCompare()的独特之处在于,实现所在的地区(国家和语言)决定了这个方法如何比较字符串。在美国,英语是ECMAScript实现的标准语言,localeCompare()区分大小写,大写字母排在小写字母前面。但其他地区未必是这种情况。

5.4 单例内置对象

内置对象 是 “ 任何由 ECMAScript 实现提供、与宿主环境无关,并在 ECMAScript 程序开始执行时就存在的对象 ”—— 意即不用显式实例化内置对象,因为它们已经实例化好了。

常见的内置对象包括ObjectArrayString,这里主要介绍另外两个单例内置对象:GlobalMath

5.4.1 Global

Global对象不会被代码显式访问(这是比较特点的地方),ECMA-262规定Global对象为一种兜底对象,它所 针对 的是 不属于任何对象的属性和方法

事实上,不存在 全局变量全局函数 这种东西。在全局作用域中定义的变量和函数都会变成Global对象的属性 。

包括isNaN()isFinite()parseInt()parseFloat(),实际上都是 Global 对象的方法。除了这些,Global 对象上还有另外一些方法。

URL编码方法

用于编码统一资源标识符URI),以便传给浏览器。有效的URI不能包含某些字符(比如空格)。使用URI编码方法来编码URI可以让浏览器能够理解它们,同时又以特殊的UTF-8编码替换掉所有无效字符。

编码】:

  1. ecnodeURI()方法:用于对整个URI进行编码;
    比如 “www.wrox.com/illegal value.js”。

  2. encodeURIComponent()方法:用于编码URI中单独的组件,比如前面URL中的 " illegal value.js "

    主要区别: encodeURI()不会编码属于URL组件的特殊字符(比如冒号、斜杠、问号、井号),而encodeURIComponent()会编码它发现的所有非标准字符。

    如下所示:

    let uri = "http://www.wrox.com/illegal value.js#start";
    
    // "http://www.wrox.com/illegal%20value.js#start"
    console.log(encodeURI(uri)); // 空格被替换为%20
    
    // "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.js%23start"
    console.log(encodeURIComponent(uri));
    

    注: 一般来说,使用 encodeURIComponent() 比使用 encodeURI() 的频率更高,这是因为编码查询字符串参数比编码基准URI的次数更多。


encodeURI()encodeURIComponent()相对的是:

解码】:

  1. decodeURI()方法:只对使用encodeURI()编码过的字符解码;
    例如,%20会被替换为空格,但%23不会被替换为井号(#),因为井号不是由encodeURI()替换的。

  2. decodeURIComponent()方法:解码所有被encodeURIComponent()编码的字符,基本上就是解码所有特殊值。

    注:
    1、URI方法encodeURI()、encodeURIComponent()、decodeURI()和decodeURIComponent() 取代了 escape()和unescape()方法,后者在ECMA-262第3版中已经废弃。URI方法始终是首选方法,因为它们对所有Unicode字符进行编码,而原来的方法只能正确编码ASCII字符。
    2、不要在生产环境中使用escape()和unescape()。




eval()方法

eval()可能是整个ECMAScript语言中最强大的了,它就是一个完整的 ECMAScript 解释器

eval("console.log('hi')");

等价于下面这行代码:

console.log("hi");

通过eval()执行的代码属于该调用所在上下文,被执行的代码与该上下文拥有相同的作用域链。定义在包含上下文中的变量可以在eval()调用内部被引用,如下所示:

let msg = "hello world";
eval("console.log(msg)");  // "hello world"

变量msg是在eval()调用的外部上下文中定义的,而console.log()显示了文本"hello world"。这是因为第二行代码会被替换成一行真正的函数调用代码。同样,可以在eval()内部定义一个函数或变量,然后在外部代码中引用,如下所示:

eval("function sayHi() { console.log('hi'); }");
sayHi();

通过eval()定义的任何变量和函数都不会被提升,这是因为在解析代码的时候,它们是被包含在一个字符串中的。只是在eval()执行的时候才会被创建。

严格模式下,在eval()内部创建的变量和函数无法被外部访问。同样,在严格模式下,赋值给eval也会导致错误:

"use strict";
eval = "hi";  // 导致错误

注: 解释代码字符串的能力虽然非常强大,但也很危险。在使用eval()时必须慎重,特别是解释用户输入内容时。因为该方法会对XSS利用暴露出很大的攻击面。恶意用户可能插入会导致你网站或应用崩溃的代码。


Global 对象属性

Global 对象有很多属性(比如undefinedNaNInfinity等特殊值)。此外,所有 原生引用类型构造函数(比如ObjectFunction)也是 Global 对象的属性。

Global属性表

属性 说明
undefined 特殊值 undefined
NaN 特殊值 NaN
Infinity 特殊值 Infinity
Object Object的构造函数
Array Array的构造函数
Function Function的构造函数
Boolean Boolean的构造函数
String String的构造函数
Number Number的构造函数
Date Date的构造函数
RegExp RegExp的构造函数
Symbol Symbol的伪构造函数
Error Error的构造函数
EvalError EvalError的构造函数
RangeError RangeError的构造函数
ReferenceError ReferenceError的构造函数
SyntaxError SyntaxError的构造函数
TypeError TypeError的构造函数
URIError URIError的构造函数


window 对象

ECMA-262没有规定直接访问 Global 对象的方式,但浏览器将 window 对象实现为 Global 对象的代理。因此,所有全局作用域中声明的变量和函数都变成了 window 的属性

示例代码】:

var color = "red";

function sayColor() {
   
	console.log(window.color);
}

window.sayColor(); // "red"

示例中,定义了color这个全局变量和一个sayColor()全局函数。在sayColor()内部,通过window.color访问了color变量,说明全局变量变成了 window 的属性。接着,又通过 window 对象调用window.sayColor()函数,输出了字符串。

注: window 对象在 JavaScript 中远不止实现了 ECMAScript 的 Global 对象那么简单。


另一种获取 Global 对象的方式,如下所示:

let global = function() {
   
	return this;
}();

本段代码创建了立即函数,返回了this的值。

当一个函数在没有明确(通过成为某个对象的方法,或者通过call()/apply())指定this值的情况下执行时,this值等于 Global 对象。—— 因此,调用一个简单返回this的函数是在任何执行上下文中获取 Global 对象的通用方式。



5.4.2 Math

Math对象用于保存数学公式、信息和计算,并提供了一些辅助计算的属性和方法。

Math对象上提供的计算要比直接在 JavaScript 实现的快得多,因为Math对象上的计算使用了JavaScript 引擎中更高效的实现和处理器指令。

注意: 使用 Math 计算的精度会因浏览器、操作系统、指令集和硬件而异


1)Math对象属性

Math对象有一些属性,主要用于保存数学中的一些特殊值。如下所示:

属性 说明
Math.E 自然对数的基数e的值
Math.LN10 10为底的自然对数
Math.LN2 2为底的自然对数
Math.LOG2E 2为底e的对数
Math.LOG10E 10为底e的对数
Math.PI π的值
Math.SQRT1_2 1/2的平方根
Math.SQRT2 2的平方根

2)min() 和 max() 方法

用于确定一组数值中的最小值和最大值,都接收任意多个参数。

let max = Math.max(3, 54, 32, 16);
console.log(max);  // 54

let min = Math.min(3, 54, 32, 16);
console.log(min);  // 3

这两个方法可以避免使用额外的循环和if语句来确定一组数值的最大、最小值。

数组中的最大值和最小值,可以像下面这样使用扩展操作符(...):

let values = [1, 2, 3, 4, 5, 6, 7, 8];
let max = Math.max(...values);

3)Math 舍入方法

把小数值舍入为整数的4个方法:

  • Math.ceil()方法:舍入 为最接近的整数;
  • Math.floor()方法:舍入 为最接近的整数。;
  • Math.round()方法:执行 四舍五入
  • Math.fround()方法:返回数值最接近的 单精度(32位)浮点值

示例代码】:

console.log(Math.ceil(25.9));   // 26
console.log(Math.ceil(25.5));   // 26
console.log(Math.ceil(25.1));   // 26

console.log(Math.round(25.9));  // 26
console.log(Math.round(25.5));  // 26
console.log(Math.round(25.1));  // 25

console.log(Math.fround(0.4));  // 0.4000000059604645
console.log(Math.fround(0.5));  // 0.5
console.log(Math.fround(25.9)); // 25.899999618530273

console.log(Math.floor(25.9));  // 25
console.log(Math.floor(25.5));  // 25
console.log(Math.floor(25.1));  // 25

4)Math.random()方法

Math.random()方法返回一个0~1范围内的 随机数,其中包含0但不包含1。可应用于显示随机名言或随机新闻的网页。

  • 应用场景:

    从一组整数中随机选择一个数:

    1)公式:

    number = Math.floor(Math.random() * total_number_of_choices + first_possible_value)
    

    公式中使用了Math.floor()方法,因为Math.random()始终返回小数,即便乘以一个数再加上一个数也是小数。

    2)代码:

    1~10范围内随机选择一个数,代码如下:

    let num = Math.floor(Math.random() * 10 + 1);
    

    这样就有 10 个可能的值(1~10),其中最小的值为1

    如果想选择一个2~10范围内的值,则代码如下:

    let num = Math.floor(Math.random() * 9 + 2);
    

    2~10只有9个数,所以可选总数total_number_of_choices)是9,而最小可能的值(first_possible_value)为2

  • 随机数函数 - 封装

    可以通过函数算出可选总数和最小可能的值,如下所示:

    function selectFrom(lowerValue, upperValue) {
         
    	let choices = upperValue - lowerValue + 1;
    	return Math.floor(Math.random() * choices + lowerValue);
    }
    
    let num = selectFrom(2,10);
    console.log(num);  // 2~10范围内的值,其中包含2和10
    

  • 随机数函数 - 调用

    这样,从一个数组中随机选择一个元素就很容易:

    let colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"];
    let color = colors[selectFrom(0, colors.length-1)];
    

    注: Math.random()方法在这里出于演示目的是没有问题的。如果是为了加密而需要生成随机数,建议使用window.crypto.getRandomValues()



小节

  • JavaScript 中的 对象称为引用值,几种内置的引用类型可用于创建特定类型的对象。

  • 引用值与传统面向对象编程语言中的相似,但实现不同

  • Date类型提供关于日期和时间的信息(包括当前日期、时间及相关计算)。

  • RegExp类型是 ECMAScript 支持正则表达式的接口,提供了大多数基础的和部分高级的正则表达式功能。

  • JavaScript 比较独特的一点是,函数 实际上是Function类型的实例,也就是说函数也是对象。因为函数也是对象,所以函数也有方法,可以用于增强其能力。

  • 由于原始值包装类型的存在,JavaScript 中的原始值可以被当成对象来使用。有3种原始值包装类型:BooleanNumberString。它们都具备如下特点:

    • 每种包装类型都映射到同名的原始类型;
    • 以只读模式访问原始值时,后台会实例化一个原始值包装类型的对象,借助这个对象可以操作相应的数据;
    • 涉及原始值的语句执行完毕后,包装对象就会被销毁。

  • 当代码开始执行时,全局上下文中会存在 2 个内置对象GlobalMath。其中,Global 对象在大多数 ECMAScript 实现中无法直接访问。不过,浏览器将其实现为 window 对象。所有全局变量函数都是Global对象的属性。Math对象包含辅助完成复杂计算的属性和方法。



第6章 集合引用类型


6.1 Object

Object 是 ECMAScript 中最常用的类型之一,很适合存储和在应用程序间交换数据。

显式创建 Object 的实例的两种方式:

  1. 使用new操作符和Object构造函数:

    示例如下:

    let person = new Object();
    person.name = "Nicholas";
    person.age = 29;
    

    等价于下面(用对象字面量表示法来定义的只有默认属性和方法的person对象):

    let person = {
         }; // 与new Object()相同
    person.name = "Nicholas";
    person.age = 29;
    

  1. 对象字面量object literal表示法

    对象字面量是对象定义的简写形式,目的是为了简化包含大量属性的对象的创建。

    对象字面量表示法通常只在为了让属性一目了然时才使用

    let person = {
         
    	name: "Nicholas",
    	age: 29
    };
    

    本段代码中定义了与前面示例相同的person对象,但使用的是对象字面量表示法。

    在对象字面量表示法中,属性名 可以是 字符串 数值,比如:

    let person = {
         
    	"name": "Nicholas",
    	"age": 29,
    	5: true
    };
    

    本例可得到一个带有属性nameage5的对象。注意,数值属性会自动转换为字符串

    注意:在使用对象字面量表示法定义对象时,并不会实际调用Object构造函数。


开发者更倾向于使用对象字面量表示法。这是因为对象字面量代码更少,对象字面量已经成为给函数传递大量可选参数的主要方式

属性一般通过点语法.)来存取的,虽然这是面向对象语言的惯例,但也可以使用 中括号 存取。在使用中括号时,要在括号内使用 属性名的字符串形式,比如:

console.log(person["name"]); // "Nicholas"
console.log(person.name);    // "Nicholas"

从功能上讲,这 2 种存取属性的方式没有区别。

使用 中括号主要优势 就是 可以通过变量访问属性,如下所示:

let propertyName = "name";
console.log(person[propertyName]); // "Nicholas"

另外,如果属性名中包含可能会导致语法错误的字符,或者包含关键字/保留字时,也可以使用中括号语法。比如:

person["first name"] = "Nicholas";

因为"first name"中包含一个空格,所以不能使用点语法来访问。属性名是可以包含非字母数字字符的,此时,用中括号语法存取可。


点语法首选的属性存取方式,除非访问属性时必须使用变量。


6.2 Array

但跟其他语言不同的是,ECMAScript 数组中每个槽位可以存储 任意类型 的数据。

这意味着一个数组:它的第一个元素是字符串,第二个元素可以是数值,第三个可以是对象。ECMAScript数组也是动态大小的,会随着数据添加而自动增长。


6.2.1 创建数组

创建数组的基本方式:

  • 使用Array构造函数;
  • 数组字面量(array literal)表示法

1)使用Array构造函数

let colors = new Array();
let colors = new Array(20); // 创建一个初始length为20
// 传入要保存的元素,创建一个包含3个字符串值的数组
let colors = new Array("red", "blue", "green");

使用 Array 构造函数时,可 省略new操作符,结果一样,比如:

// 创建一个包含3个元素的数组
let colors = Array(3);

// 创建一个只包含一个元素,即字符串"Greg"的数组
let names = Array("Greg");

2)数组字面量(array literal)表示法

数组字面量是在中括号中包含 以逗号分隔 的元素列表,如下所示:

let colors = ["red", "blue", "green"];  // 创建一个包含3个元素的数组
let names = [];                         // 创建一个空数组
let values = [1,2,];                    // 创建一个包含2个元素的数组

第1行:创建一个包含3个字符串的数组。
第2行:用一对空中括号创建了一个空数组。
第3行:在数组最后一个值后面加逗号:values是一个包含两个值(12)的数组。

与对象一样,在使用数组字面量表示法创建数组不会调用Array构造函数。


Array 构造函数 ES6 新增(创建数组的)静态方法:

  • from():用于将类数组结构转换为数组实例;

  • of():用于将一组参数转换为数组实例。

    Array.from()的第 1 个参数是类数组对象(即任何可迭代的结构,或者有一个length属性和可索引元素的结构)。如下所示:

    // 字符串会被拆分为单字符数组
    console.log(Array.from("Matt")); // ["M", "a", "t", "t"]
    
    // 可以使用from()将集合和映射转换为一个新数组
    const m = new Map().set(1, 2)
                       .set(3, 4);
    const s = new Set().add(1)
                       .add(2)
                       .add(3)
                       .add(4);
    
    console.log(Array.from(m)); // [[1, 2], [3, 4]]
    console.log(Array.from(s)); // [1, 2, 3, 4]
    
    // Array.from()对现有数组执行浅复制
    const a1 = [1, 2, 3, 4];
    const a2 = Array.from(a1);
    
    console.log(a1);        // [1, 2, 3, 4]
    alert(a1 === a2); // false
    
    
    // 可以使用任何可迭代对象
    const iter = {
         
    	*[Symbol.iterator]() {
         
    		yield 1;
    		yield 2;
    		yield 3;
    		yield 4;
    	}
    };
    console.log(Array.from(iter)); // [1, 2, 3, 4]
    
    // arguments对象可以被轻松地转换为数组
    function getArgsArray() {
         
    	return Array.from(arguments);
    }
    console.log(getArgsArray(1, 2, 3, 4)); // [1, 2, 3, 4]
    
    // from()也能转换带有必要属性的自定义对象
    const arrayLikeObject = {
         
    	0: 1,
    	1: 2,
    	2: 3,
    	3: 4,
    	length: 4
    };
    console.log(Array.from(arrayLikeObject)); // [1, 2, 3, 4]
    

Array.from()还接收第 2 个可选的映射函数参数。这个函数可以直接增强新数组的值,而无须像调用Array.from().map()那样先创建一个中间数组。还可以接收第三个可选参数,用于指定映射函数中this的值。但这个重写的this在箭头函数中不适用

const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1, x => x**2);
const a3 = Array.from(a1, function(x) {
   return x**this.exponent}, {
   exponent: 2});
console.log(a2);  // [1, 4, 9, 16]
console.log(a3);  // [1, 4, 9, 16]

Array.of()可把一组参数转换为数组。这个方法用于替代在ES6之前常用的Array.prototype.slice.call(arguments),一种异常笨拙的将arguments对象转换为数组的写法:

console.log(Array.of(1, 2, 3, 4)); // [1, 2, 3, 4]
console.log(Array.of(undefined));  // [undefined]

6.2.2 数组空位

使用数组字面量初始化数组时,可以使用一串逗号(,)来创建空位(hole)。ECMAScript 会将逗号之间相应索引位置的值当成空位,ES6规范重新定义了该如何处理这些空位。

创建空位数组:

const options = [,,,,,];       // 创建包含5个元素的数组
console.log(options.length);   // 5
console.log(options);          // [,,,,,]

ES6 新增方法普遍将这些空位当成存在的元素,只不过值为undefined

const options = [1,,,,5];

for (const option of options) {
   
	console.log(option === undefined);
}
// false
// true
// true
// true
// false

const a = Array.from([,,,]); // 使用ES6的Array.from()创建的包含3个空位的数组
for (const val of a) {
   
	alert(val === undefined);
}
// true
// true
// true

alert(Array.of(...[,,,])); // [undefined, undefined, undefined]

for (const [index, value] of options.entries()) {
   
	alert(value);
}
// 1
// undefined
// undefined
// undefined
// 5

ES6之前的方法则会忽略这个空位,但具体的行为也会因方法而异:

const options = [1,,,,5];

// map()会跳过空位置
console.log(options.map(() => 6));  // [6, undefined, undefined, undefined, 6]

// join()视空位置为空字符串
console.log(options.join('-'));     // "1----5"

注意 由于行为不一致和存在性能隐患,因此实践中要避免使用数组空位。如果确实需要空位,则可以显式地用undefined值代替


6.2.3 数组索引

要取得或设置数组的值,需要使用中括号并提供相应值的数字索引,如下所示:

let colors = ["red", "blue", "green"];  // 定义一个字符串数组
alert(colors[0]);                       // 显示第一项
colors[2] = "black";                    // 修改第三项
colors[3] = "brown";                    // 添加第四项

当把一个值设置给超过数组最大索引的索引,示例中的colors[3],则数组长度会自动扩展到该索引值加1(示例中设置的索引3,所以数组长度变成了4)。

数组length属性的独特之处在于,它不是只读的。通过修改length属性,可以从数组末尾删除或添加元素:

let colors = ["red", "blue", "green"]; // 创建一个包含3个字符串的数组
colors.length = 2;
alert(colors[2]);  // undefined

如果将length设置为大于数组元素数的值,则新添加的元素都将以undefined填充:

let colors = ["red", "blue", "green"];  // 创建一个包含3个字符串的数组
colors.length = 4;
alert(colors[3]);  // undefined

使用length属性可以方便地向 数组末尾 添加元素,如下所示:

let colors = ["red", "blue", "green"];  // 创建一个包含3个字符串的数组
colors[colors.length] = "black";        // 添加一种颜色(位置3)
colors[colors.length] = "brown";        // 再添加一种颜色(位置4)

数组中最后一个元素的索引始终是length - 1,因此下一个新增槽位的索引就是length

注: 数组最多可以包含4 294 967 295个元素,这对于大多数编程任务足够了。如果尝试添加更多项,则会导致抛出错误。如以这个最大值作为初始值创建数组,可能导致脚本运行时间过长的错误。


6.2.4 检测数组

一个经典的 ECMAScript 问题:判断一个对象是不是数组。

在只有一个网页(因而只有一个全局作用域)的情况下,使用instanceof操作符即可:

if (value instanceof Array){
   
  // 操作数组
}

使用instanceof的前提是假定 只有一个全局执行上下文

如果网页里有多个框架,则可能涉及 2 个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。

为解决此问题,ECMAScript 提供了Array.isArray()方法。该方法的目的就是 确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。来看下面的例子:

if (Array.isArray(value)){
   
  // 操作数组
}

6.2.5 迭代器方法

ES6中,Array的原型上暴露了 3 个检索数组内容的方法:keys()values()entries()

  • keys():返回数组索引的迭代器;
  • values():返回数组元素的迭代器;
  • entries():返回 索引 / 值对 的迭代器。
const a = ["foo", "bar", "baz", "qux"];

// 因为这些方法都返回迭代器,所以可以将它们的内容
// 通过Array.from()直接转换为数组实例
const aKeys = Array.from(a.keys());
const aValues = Array.from(a.values());
const aEntries = Array.from(a.entries());

console.log(aKeys);     // [0, 1, 2, 3]
console.log(aValues);   // ["foo", "bar", "baz", "qux"]
console.log(aEntries);  // [[0, "foo"], [1, "bar"], [2, "baz"], [3, "qux"]]

使用 ES6 的 解构 可以轻松地在循环中拆分 键/值 对:

const a = ["foo", "bar", "baz", "qux"];

for (const [idx, element] of a.entries()) {
   
	alert(idx);
	alert(element);
}
// 0
// foo
// 1
// bar
// 2
// baz
// 3
// qux

注: 虽然这些方法是ES6 规范定义的,但在2017年底的时候仍有浏览器没有实现它们。


6.2.6 复制和填充方法

ES6新增 2 种方法:

  • copyWithin() :批量复制方法;
  • fill():填充数组方法。

这两种方法都需要指定既有数组实例上的一个范围,包含开始索引,不包含结束索引。使用这个方法不会改变数组的大小

  • 1)fill()方法

    可以向一个已有的数组中插入全部或部分相同的值。开始索引用于指定开始填充的位置(可选),如果不提供结束索引,则一直填充到数组末尾。负值索引从数组末尾开始计算。也可以将负索引想象成数组长度加上它得到的一个正索引:

    const zeroes = [0, 0, 0, 0, 0];
    
    // 用5填充整个数组
    zeroes.fill(5);
    console.log(zeroes);  // [5, 5, 5, 5, 5]
    zeroes.fill(0);       // 重置
    
    // 用6填充索引大于等于3的元素
    zeroes.fill(6, 3);
    console.log(zeroes);  // [0, 0, 0, 6, 6]
    zeroes.fill(0);       // 重置
    
    // 用7填充索引大于等于1且小于3的元素
    zeroes.fill(7, 1, 3);
    console.log(zeroes);  // [0, 7, 7, 0, 0];
    zeroes.fill(0);       // 重置
    
    // 用8填充索引大于等于1且小于4的元素
    // (-4 + zeroes.length = 1)
    // (-1 + zeroes.length = 4)
    zeroes.fill(8, -4, -1);
    console.log(zeroes);  // [0, 8, 8, 8, 0];
    

    fill()静默忽略超出数组边界、零长度及方向相反的索引范围:

    const zeroes = [0, 0, 0, 0, 0];
    
    // 索引过低,忽略
    zeroes.fill(1, -10, -6);
    console.log(zeroes);  // [0, 0, 0, 0, 0]
    
    // 索引过高,忽略
    zeroes.fill(1, 10, 15);
    console.log(zeroes);  // [0, 0, 0, 0, 0]
    
    // 索引反向,忽略
    zeroes.fill(2, 4, 2);
    console.log(zeroes);  // [0, 0, 0, 0, 0]
    
    // 索引部分可用,填充可用部分
    zeroes.fill(4, 3, 10)
    console.log(zeroes);  // [0, 0, 0, 4, 4]
    
  • 2)copyWithin()方法:

    copyWithin()会按照指定范围浅复制数组中的部分内容,然后将它们插入到指定索引开始的位置。开始索引和结束索引则与fill()使用同样的计算方法:

    let ints,
        reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    reset();
    
    // 从ints中复制索引0开始的内容,插入到索引5开始的位置
    // 在源索引或目标索引到达数组边界时停止
    ints.copyWithin(5);
    console.log(ints);  // [0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
    reset();
    
    // 从ints中复制索引5开始的内容,插入到索引0开始的位置
    ints.copyWithin(0, 5);
    console.log(ints);  // [5, 6, 7, 8, 9, 5, 6, 7, 8, 9]
    reset();
    
    // 从ints中复制索引0开始到索引3结束的内容
    // 插入到索引4开始的位置
    ints.copyWithin(4, 0, 3);
    alert(ints);  // [0, 1, 2, 3, 0, 1, 2, 7, 8, 9]
    reset();
    
    // JavaScript引擎在插值前会完整复制范围内的值
    // 因此复制期间不存在重写的风险
    ints.copyWithin(2, 0, 6);
    alert(ints);  // [0, 1, 0, 1, 2, 3, 4, 5, 8, 9]
    reset();
    
    // 支持负索引值,与fill()相对于数组末尾计算正向索引的过程是一样的
    ints.copyWithin(-4, -7, -3);
    alert(ints);  // [0, 1, 2, 3, 4, 5, 3, 4, 5, 6]
    

    copyWithin()静默忽略超出数组边界、零长度及方向相反的索引范围:

    let ints,
    reset = () => ints = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    reset();
    
    // 索引过低,忽略
    ints.copyWithin(1, -15, -12);
    alert(ints);  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    reset()
    
    // 索引过高,忽略
    ints.copyWithin(1, 12, 15);
    alert(ints);  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    reset();
    
    // 索引反向,忽略
    ints.copyWithin(2, 4, 2);
    alert(ints);  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
    reset();
    
    // 索引部分可用,复制、填充可用部分
    ints.copyWithin(4, 7, 10)
    alert(ints);  // [0, 1, 2, 3, 7, 8, 9, 7, 8, 9];
    


6.2.7 转换方法

所有对象都有toLocaleString()toString()valueOf()方法。其中,

  • valueOf()返回 的是 数组本身

  • toString()返回 由数组中每个值的等效字符串拼接而成的一个 逗号分隔的字符串。也就是说,对数组的每个值都会调用其toString()方法,以得到最终的字符串。如下所示:

    let colors = ["red", "blue", "green"]; // 创建一个包含3个字符串的数组
    alert(colors.toString());   // red,blue,green
    alert(colors.valueOf());    // red,blue,green
    alert(colors);              // red,blue,green
    
  • toLocaleString()方法:也可能返回跟toString()valueOf()相同的结果(但也不一定)。在调用数组的toLocaleString()方法时,会得到一个逗号分隔的数组值的字符串。

    它与toString()valueOf()方法唯一的区别:为了得到最终的字符串,会调用数组每个值的toLocaleString()方法,而不是toString()方法。如下所示:

    let person1 = {
         
    	toLocaleString() {
         
    		return "Nikolaos";
    	},
    
    	toString() {
         
    		return "Nicholas";
    	}
    };
    
    let person2 = {
         
    	toLocaleString() {
         
    		return "Grigorios";
    	},
    
    	toString() {
         
    		return "Greg";
    	}
    };
    
    let people = [person1, person2];
    alert(people);                   // Nicholas,Greg
    alert(people.toString());        // Nicholas,Greg
    alert(people.toLocaleString());  // Nikolaos,Grigorios
    

    本例中,定义了两个对象person1person2,都定义了toString()toLocaleString()方法,而且返回不同的值。然后又创建了一个包含这两个对象的数组people。在将数组传给alert()时,输出的是"Nicholas,Greg",这是因为会在数组每一项上调用toString()方法(与下一行显式调用toString()方法结果一样)。而在调用数组的toLocaleString()方法时,结果变成了"Nikolaos, Grigorios",这是因为调用了数组每一项的toLocaleString()方法。


继承的方法toLocaleString()以及toString()都返回数组值的逗号分隔的字符串。如果想使用不同的分隔符,则可以使用join()方法。join()方法接收一个参数,即字符串分隔符,返回包含所有项的字符串。如下所示:

let colors = ["red", "green", "blue"];
alert(colors.join(","));     // red,green,blue
alert(colors.join("||"));    // red||green||blue

注: 如果数组中某一项是nullundefined,则在join()toLocaleString()toString()valueOf()返回的结果中会以空字符串表示。



6.2.8 栈方法

ECMAScript 给数组提供几个方法,让它看起来像是另外一种数据结构。数组对象 可以像 一样,也就是一种限制插入和删除项的数据结构。

是一种 后进先出(LIFO,Last-In-First-Out)的结构,也就是最近添加的项先被删除。数据项的 插入(称为推入,push)和 删除(称为弹出,pop)只在栈的一个地方发生,即栈顶。ECMAScript 数组提供了push()pop()方法,以实现类似栈的行为。

  • push()方法:接收任意数量的参数,并将它们添加到数组 末尾返回 数组的 最新长度

  • pop()方法:用于 删除 数组的 最后一项,同时减少数组的length值,返回被删除的项。如下所示:

    let colors = new Array();                 // 创建数组
    let count = colors.push("red", "green");  // 推入两项
    alert(count);                             // 2
    
    count = colors.push("black");  // 再推入一项
    alert(count);                  // 3
    
    let item = colors.pop();       // 删除最后一项
    alert(item);                   // black
    alert(colors.length);          // 2
    

    这里创建了一个当作栈来使用的数组(push()pop()是数组的默认方法)。首先,使用push()方法把两个字符串推入数组末尾,将结果保存在变量count中(结果为2)。

    然后,再推入另一个值,再把结果保存在count中。因为现在数组中有3个元素,所以push()返回3。在调用pop()时,会返回数组的最后一项,即字符串"black"。此时数组还有两个元素。

栈方法可以与数组的其他任何方法一起使用,如下所示:

let colors = ["red", "blue"];
colors.push("brown");        // 再添加一项
colors[3] = "black";         // 添加一项
alert(colors.length);        // 4

let item = colors.pop();     // 删除最后一项
alert(item);                 // black

这里先初始化了包含两个字符串的数组,然后通过push()添加了第三个值,第四个值是通过直接在位置3上赋值添加的。调用pop()时,返回了字符串"black",也就是最后添加到数组的字符串。



6.2.9 队列方法

就像栈是以 LIFO 形式限制访问的数据结构一样,队列先进先出(FIFO,First-In-First-Out)形式 限制访问 队列在列表末尾添加数据,但从列表开头获取数据

  • shift()方法

    因为有了在数据末尾添加数据的push()方法,所以要模拟队列就要有一个从数组开头取得数据的方法。该数组方法就是shift(),它会 删除 数组的 第一项 并返回它,然后数组长度减1。使用shift()push(),可以把数组当成队列来使用,如下所示:

    let colors = new Array();                 // 创建数组
    let count = colors.push("red", "green");  // 推入两项
    alert(count);                             // 2
    
    count = colors.push("black"); // 再推入一项
    alert(count);                 // 3
    
    let item = colors.shift();  // 删除第一项
    alert(item);                // red
    alert(colors.length);       // 2
    

  • unshift()方法

    执行跟shift()相反的操作:在数组开头添加任意多个值,然后返回新的数组长度。通过使用unshift()pop(),可以在相反方向上模拟队列,即 数组开头添加新数据,在数组末尾取得数据,如下所示:

    let colors = new Array();                    // 创建数组
    let count = colors.unshift("red", "green");  // 数组开头推入两项
    alert(count);                                // 2
    
    count = colors.unshift("black");  // 再推入一项
    alert(count);                     // 3
    
    let item = colors.pop();  // 删除最后一项
    alert(item);              // green
    alert(colors.length);     // 2
    

    这里先创建了一个数组,再通过unshift()填充数组。首先,给数组添加"red""green",再添加"black",得到["black","red","green"]。调用pop()时,删除最后一项"green"并返回它。



6.2.10 排序方法

数组有两个方法可以用来对元素重新排序:

  • reverse()方法:将数组元素 反向排列。如下所示:

    let values = [1, 2, 3, 4, 5];
    values.reverse();
    alert(values);  // 5,4,3,2,1
    

    由于此方法虽然直观,但不够灵活,所以才有了sort()方法。

  • sort()方法:按照升序(即从小到大)重新排列数组元素。

    sort()会在每一项上调用String()转型函数,然后比较字符串来决定顺序(即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序)。示例如下:

    let values = [0, 1, 5, 10, 15];
    values.sort();
    alert(values);  // 0,1,10,15,5
    

    由于sort()会按照数值的字符串形式重新排序,因此,即使5小于10,但字符串"10“在字符串”5"的前头,所以10还是会排到5前面。

    很明显,这在多数情况下都不是最合适的。为此,sort()方法可以接收一个比较函数,用于判断哪个值应该排在前面。

    比较函数 接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相等,就返回0;如果第一个参数应该排在第二个参数后面,就返回正值。

    示例(简单比较函数):

    function compare(value1, value2) {
         
    	if (value1 < value2) {
         
    		return -1;
    	} else if (value1 > value2) {
         
    		return 1;
    	} else {
         
    		return 0;
    	}
    }
    

    这个比较函数可以适用于大多数数据类型,可以把它 当作参数 传给sort()方法,如下所示:

    let values = [0, 1, 5, 10, 15];
    values.sort(compare);
    alert(values);  // 0,1,5,10,15
    

    在给sort()方法传入比较函数后,数组中的数值在排序后保持了正确的顺序。当然,比较函数也可以产生 降序效果,只要把返回值 交换 一下即可:

    function compare(value1, value2) {
         
    	if (value1 < value2) {
         
    		return 1;
    	} else if (value1 > value2) {
         
    		return -1;
    	} else {
         
    		return 0;
    	}
    }
    
    let values = [0, 1, 5, 10, 15];
    values.sort(compare);
    alert(values);  // 15,10,5,1,0
    

    此外,这个比较函数还可简写为一个箭头函数:

    let values = [0, 1, 5, 10, 15];
    values.sort((a, b) => a < b ? 1 : a > b ? -1 : 0);
    alert(values); // 15,10,5,1,0
    

    在这个修改版函数中,如果第一个值应该排在第二个值后面则返回1,如果第一个值应该排在第二个值前面则返回-1。交换这两个返回值之后,较大的值就会排在前头,数组就会按照降序排序。当然,如果只是想反转数组的顺序,reverse()更简单也更快。

注: reverse()sort()都返回调用它们的数组的引用。


如果数组的元素是数值,或者是其valueOf()方法返回数值的对象(如Date对象),这个比较函数还可以写得更简单,因为这时可以直接用第二个值减去第一个值:

function compare(value1, value2){
   
	return value2 - value1;
}

比较函数就是要返回小于00和大于0的数值,因此减法操作完全可以满足要求。


6.2.11 数组方法

数组中的操作元素的方法,

  • concat()方法:

    可在现有数组全部元素基础上创建一个新数组(它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组)。

    如果传入一个或多个数组,则concat()会把这些数组的每一项都添加到结果数组。如果参数不是数组,则直接把它们添加到结果数组末尾。示例如下:

    let colors = ["red", "green", "blue"];
    let colors2 = colors.concat("yellow", ["black", "brown"]);
    
    console.log(colors);   // ["red", "green","blue"]
    console.log(colors2);  // ["red", "green", "blue", "yellow", "black", "brown"]
    

    这里先创建一个包含3个值的数组colors。然后colors调用concat()方法,传入字符串"yellow"和一个包含"black""brown"的数组。保存在colors2中的结果就是["red", "green", "blue", "yellow", "black", "brown"]。原始数组colors保持不变。

    打平数组参数的行为可以重写,方法是在参数数组上指定一个特殊的符号:Symbol.isConcatSpreadable。这个符号能够阻止concat()打平参数数组。相反,把这个值设置为true可以强制打平类数组对象,如下所示:

    let colors = ["red", "green", "blue"];
    let newColors = ["black", "brown"];
    let moreNewColors = {
         
    	[Symbol.isConcatSpreadable]: true,
    	length: 2,
    	0: "pink",
    	1: "cyan"
    };
    
    newColors[Symbol.isConcatSpreadable] = false;
    
    // 强制不打平数组
    let colors2 = colors.concat("yellow", newColors);
    
    // 强制打平类数组对象
    let colors3 = colors.concat(moreNewColors);
    
    console.log(colors);   // ["red", "green", "blue"]
    console.log(colors2);  // ["red", "green", "blue", "yellow", ["black", "brown"]]
    console.log(colors3);  // ["red", "green", "blue", "pink", "cyan"]
    

  • slice()方法

    slice()方法用于 创建 一个包含原有数组中一个或多个元素的 新数组slice()可接收一个或两个参数:返回元素的 开始索引结束索引

    如果只有一个参数,则slice()会返回该索引到数组末尾的所有元素。如果有两个参数,则slice()返回从开始索引到结束索引对应的所有元素(其中不包含结束索引对应的元素)。注意,该操作不影响原始数组。示例如下:

    let colors = ["red", "green", "blue", "yellow", "purple"];
    let colors2 = colors.slice(1);
    let colors3 = colors.slice(1, 4);
    
    alert(colors2);  // green,blue,yellow,purple
    alert(colors3);  // green,blue,yellow
    

    这里,colors数组一开始有 5 个元素。调用slice()传入1会得到包含4个元素的新数组。其中不包括"red",这是因为拆分操作要从位置1开始,即从"green"开始。得到的colors2数组包含"green""blue""yellow""purple"colors3数组是通过调用slice()并传入1和4得到的,即从位置1开始复制到位置3。因此colors3包含"green""blue""yellow"

    注: 如果slice()的参数有负值,那么就以数值长度加上这个负值的结果确定位置。比如,在包含5个元素的数组上调用slice(-2,-1),就相当于调用slice(3,4)。如果结束位置小于开始位置,则返回空数组。


  • splice()方法

    splice()方法的主要目的,是 在数组中间插入元素,但有 3 种不同的方式使用该方法:

    • 删除
      需要给splice()传 2 个参数:要删除的第一个元素的位置和要删除的元素数量。可以从数组中删除任意多个元素,比如splice(0, 2)会删除前两个元素;

    • 插入
      需要给splice()传 3 个参数:开始位置、0(要删除的元素数量)和要插入的元素,可以在数组中指定的位置插入元素。第三个参数之后还可以传第四个、第五个参数,乃至 任意多个 要插入的元素。比如,splice(2, 0, "red", "green")会从数组位置 2 开始插入字符串"red""green"

    • 替换
      splice()在删除元素的同时可以在指定位置插入新元素,同样要传入3个参数: 开始位置要删除元素的数量要插入的任意多个元素。要插入的元素数量不一定跟删除的元素数量一致。比如,splice(2, 1, "red", "green")会在位置 2 删除一个元素,然后从该位置开始向数组中插入"red""green"

    splice()方法始终返回这样一个数组,它包含从数组中被删除的元素(如果没有删除元素,则返回空数组)。

    示例如下(展示了上述 3 种使用方式):

    let colors = ["red", "green", "blue"];
    let removed = colors.splice(0,1);  // 删除第一项
    alert(colors);                     // green,blue
    alert(removed);                    // red,只有一个元素的数组
    
    removed = colors.splice(1, 0, "yellow", "orange");   // 在位置1插入两个元素
    alert(colors);                                       // green,yellow,orange,blue
    alert(removed);                                      // 空数组
    
    removed = colors.splice(1, 1, "red", "purple");  // 插入两个值,删除一个元素
    alert(colors);                                   // green,red,purple,orange,blue
    alert(removed);                                  // yellow,只有一个元素的数组
    

    本例中,colors数组一开始包含3个元素。第一次调用splice()时,只删除了第一项,colors中还有"green""blue"。第二次调用slice()时,在位置1插入两项,然后colors包含"green""yellow""orange""blue"。这次没删除任何项,因此返回空数组。最后一次调用splice()时删除了位置1上的一项,同时又插入了"red""purple"。最后,colors数组包含"green""red""purple""orange""blue"



6.2.12 搜索和位置方法

ECMAScript 提供两类搜索数组的方法:

  • 严格相等
  • 断言函数

6.2.12.1 严格相等

ECMAScript提供了 3 个严格相等的搜索方法:

  1. indexOf()(所有版本中可用):从(数组)前(第一项)往后搜索;
  2. lastIndexOf()(所有版本中可用):从数组末尾(最后一项)开始向前搜索;
  3. includes()(ES7新增):从(数组)前(第一项)往后搜索。

indexOf()lastIndexOf()都返回要查找的元素在数组中的位置,如果没找到则返回-1

includes()返回布尔值,表示是否至少找到一个与指定元素匹配的项。在比较第一个参数跟数组每一项时,会使用全等(===)比较,也就是说两项必须严格相等。

示例如下:

let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];

alert(numbers.indexOf(4));          // 3
alert(numbers.lastIndexOf(4));      // 5
alert(numbers.includes(4));         // true

alert(numbers.indexOf(4, 4));       // 5
alert(numbers.lastIndexOf(4, 4));   // 3
alert(numbers.includes(4, 7));      // false

let person = {
    name: "Nicholas" };
let people = [{
    name: "Nicholas" }];
let morePeople = [person];

alert(people.indexOf(person));      // -1
alert(morePeople.indexOf(person));  // 0
alert(people.includes(person));     // false
alert(morePeople.includes(person)); // true

6.2.12.2 断言函数

ECMAScript 也允许按照定义的断言函数搜索数组,每个索引都会调用此函数。断言函数的返回值决定了相应索引的元素是否被认为匹配。

断言函数接收 3 个 参数元素索引数组本身。其中元素是数组中当前搜索的元素,索引是当前元素的索引,数组是正在搜索的数组。断言函数返回真值,表示是否匹配。

  1. find()方法 返回 第一个匹配的 元素
  2. findIndex()方法返回 第一个匹配元素的 索引
  • 共同点】:

    • 使用了断言函数;
    • 两个方法都从数组的最小索引开始;
    • 也都接收第 2 个可选参数,用于指定断言函数内部this的值。

  • 示例代码】:

    const people = [
    	{
         
    		name: "Matt",
    		age: 27
    	},
    	{
         
    		name: "Nicholas",
    		age: 29
    	}
    ];
    
    alert(people.find((element, index, array) => element.age < 28));
    // {name: "Matt", age: 27}
    
    alert(people.findIndex((element, index, array) => element.age < 28));
    // 0
    

    找到匹配项后,这两个方法都不再继续搜索:

    const evens = [2, 4, 6];
    
    // 找到匹配后,永远不会检查数组的最后一个元素
    evens.find((element, index, array) => {
         
    	console.log(element);
    	console.log(index);
    	console.log(array);
    	return element === 4;
    });
    // 2
    // 0
    // [2, 4, 6]
    // 4
    // 1
    // [2, 4, 6]
    


6.2.13 迭代方法

ECMAScript 为数组定义了 5 个 迭代方法。每个方法接收两个参数:以每一项为参数运行的函数,以及可选的作为函数运行上下文的作用域对象(影响函数中this的值)。传给每个方法的函数接收 3 个 参数数组元素元素索引数组本身

  • 数组的 5 个迭代方法

    • every():对数组每一项都运行传入的函数,如果对每一项函数都返回true,则该方法返回true
    • filter():对数组每一项都运行传入的函数,函数返回true的项会组成数组之后返回;
    • forEach():对数组每一项都运行传入的函数,没有返回值
    • map():对数组每一项都运行传入的函数,返回 由每次函数调用的结果构成的 数组
    • some():对数组每一项都运行传入的函数,如果有一项函数返回true,则该方法返回true

    注: 这 5 个方法都不会改变调用它们的数组。


  • 迭代方法比较

    • every()some()方法:这些方法中,这两个是最相似的,都是从数组中搜索符合某个条件的元素。

      1)every()来说,传入的函数必须对 每一项 都返回true,它才会返回true;否则,它就返回false

      2)some()来说,只要有一项 让传入的函数返回true,它就会返回true

      示例】:

      let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
      
      let everyResult = numbers.every((item, index, array) => item > 2);
      alert(everyResult);  // false
      
      let someResult = numbers.some((item, index, array) => item > 2);
      alert(someResult);   // true
      

      以上代码调用了every()some(),传入的函数都是在给定项>2时返回trueevery()返回false是因为并不是每一项都能达到要求。而some()返回true是因为至少有一项满足条件。

    • filter()方法:该方法基于 给定的函数 来决定某一项是否应该包含在它返回的数组中。

      比如,要返回一个所有数值都大于2 的数组,可以使用如下代码:

      let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
      
      let filterResult = numbers.filter((item, index, array) => item > 2);
      alert(filterResult);  // 3,4,5,4,3
      

      这个方法非常适合 从数组中筛选满足给定条件的元素

    • map()方法: 也会返回一个数组。这个数组的每一项都是对原始数组中同样位置的元素运行传入函数而返回的结果。

      例如,可以将一个数组中的每一项都 ×2,并返回包含所有结果的数组,如下所示:

      let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
      
      let mapResult = numbers.map((item, index, array) => item * 2);
      
      alert(mapResult);  // 2,4,6,8,10,8,6,4,2
      

      此方法非常适合 创建一个与原始数组元素一 一对应的新数组

    • forEach()方法:没有返回值。该方法本质上相当于使用for循环遍历数组。

      如下所示:

      let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
      
      numbers.forEach((item, index, array) => {
             
        // 执行某些操作
      });
      


6.2.14 归并方法

ECMAScript 为 数组 提供了两个 归并方法

  • reduce():从数组第一项开始 遍历 到最后一项;
  • reduceRight():从最后一项开始 遍历 至第一项。
  1. 两个方法都会迭代数组的所有项,并在此基础上构建一个最终返回值。

  2. 这两个方法都接收 两个 参数:对每一项都会运行的 归并函数,以及 可选的 以之为归并起点的 初始值

  3. 传给reduce()reduceRight()的函数接收4个 参数上一个归并值当前项当前项的索引数组本身。该函数返回的任何值都会作为下一次调用同一个函数的第一个参数。

    如果没有给这两个方法传入可选的第二个参数(作为归并起点值),则第一次迭代将从数组的第二项开始,因此传给归并函数的第一个参数是数组的第一项,第二个参数是数组的第二项。

  4. 使用reduce()函数执行累加数组中所有数值的操作,比如:

    let values = [1, 2, 3, 4, 5];
    let sum = values.reduce((prev, cur, index, array) => prev + cur);
    
    alert(sum);  // 15
    

    上面的代码,第一次执行归并函数时,prev是1,cur是2。第二次执行时,prev是3(1 + 2),cur是3(数组第三项)。如此递进,直到把所有项都遍历一次,最后返回归并结果。

  5. reduceRight()方法与之类似,只是方向相反。示例如下:

    let values = [1, 2, 3, 4, 5];
    let sum = values.reduceRight(function(prev, cur, index, array){
         
    	return prev + cur;
    });
    alert(sum); // 15
    

    在这里,第一次调用归并函数时prev是5,而cur是4。当然,最终结果相同,因为归并操作都是简单的加法。

  • 方法的选择

    究竟是使用reduce()还是reduceRight()

    这只取决于遍历数组元素的方向。除此之外,这两个方法没什么区别。



6.3 定型数组

定型数组(typed array)是 ECMAScript 新增的结构,目的是 提升向原生库传输数据的效率 。实际上,JavaScript 并没有“TypedArray”类型,它所指的其实是一种特殊的包含数值类型的数组。为理解如何使用定型数组,就要先了解它的用途。

Mozilla 实现了CanvasFloatArray。这是一个提供 JavaScript 接口的、C语言风格的浮点值数组。JavaScript 运行时使用这个类型可以分配、读取和写入数组。这个数组可以直接传给底层图形驱动程序API,也可以直接从底层获取到。最终,CanvasFloatArray变成了Float32Array,也就是今天定型数组中可用的第一个“类型”。

6.3.1 ArrayBuffer

Float32Array实际上是一种“视图”,可以允许JavaScript 运行时访问一块名为ArrayBuffer的预分配内存。ArrayBuffer是所有 定型数组 及视图引用的 基本单位

注: SharedArrayBufferArrayBuffer的一个变体,可以无须复制就在执行上下文间传递它。


ArrayBuffer()是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节空间。

const buf = new ArrayBuffer(16);  // 在内存中分配16字节
alert(buf.byteLength);            // 16

ArrayBuffer一经创建就不能再调整大小。不过,可以使用slice()复制其全部或部分到一个新实例中:

const buf1 = new ArrayBuffer(16);
const buf2 = buf1.slice(4, 12);
alert(buf2.byteLength);  // 8

ArrayBuffer某种程度上类似于C++malloc(),但也有几个明显的区别。
















~~待续


  1. 不能分享 pdf 的说明:
     鉴于《JavaScript高级程序设计(第4版)》pdf 电子版扉页有明确《数字版权声明》:“仅供个人使用,未经授权,不得进行传播”。并且此电子版页脚有本人图灵社区会员账号水印因此不便分享,敬请谅解↩︎


转载:https://blog.csdn.net/zglibk/article/details/108842337
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场