内存管理的速成课程

关于ECMAScript2017特性SharedArrayBuffer和atomics的背景知识:

本文分为三部分介绍

内存管理的速成课程

要理解为什么将ArrayBuffer和SharedArrayBuffer添加到JavaScript中,您需要了解一些有关内存管理的知识。

您可以将机器中的内存视为一堆盒子。我认为这些就像你在办公室里的邮箱,或学龄前儿童必须存放东西的小房间。

如果您需要为其他孩子留下一些东西,可以将它放在一个盒子里。

在这些框的每一个旁边,您都有一个数字,即内存地址。这就是你如何告诉别人在哪里找到你留给他们的东西。

这些盒子中的每一个都是相同的大小,可以容纳一定数量的信息。盒子的大小是机器特有的。该大小称为字大小。它通常类似于32位或64位。但是为了更容易显示,我将使用8位的字大小。

如果我们想将数字2放在其中一个框中,我们可以轻松完成。数字很容易用二进制表示。

如果我们想要一些不是数字的东西呢?喜欢字母H?

我们需要有办法将其表示为数字。为此,我们需要一种编码,如UTF-8。我们需要一些东西把它变成那个数字……就像一个编码器环。然后我们可以存储它。

当我们想要将它从包装盒中取出时,我们必须通过解码器将其转换回H.

自动内存管理
当你在JavaScript中工作时,你实际上并不需要考虑这个记忆。它被抽象出来了。这意味着您不要直接触摸内存。

相反,JS引擎充当中介。它为您管理内存。

所以让我们说一些JS代码,比如React,想要创建一个变量。

JS引擎所做的是通过编码器运行该值以获取值的二进制表示。

它会在内存中找到可以将二进制表示放入的空间。此过程称为分配内存。

然后,引擎将跟踪该变量是否仍可从程序中的任何位置访问。如果无法再访问该变量,则将回收内存,以便JS引擎可以在其中放置新值。

这个监视变量字符串,对象和其他类型值的过程在内存中进行,并在无法再访问它们时将其清除,称为垃圾回收。

JavaScript之类的语言(代码不直接处理内存)称为内存管理语言。

这种自动内存管理可以使开发人员更轻松。但它也增加了一些开销。而这种开销有时会使性能无法预测。

手动内存管理
手动管理内存的语言不同。例如,让我们看看如果React是用C语言编写的,React将如何处理内存(现在可以 使用WebAssembly)。

C没有JavaScript在内存上做的那个抽象层。相反,你直接在内存上运行。您可以从内存加载内容,并可以将内容存储到内存中。

当您将C或其他语言编译为WebAssembly时,您使用的工具会在WebAssembly中添加一些帮助程序代码。例如,它将添加执行编码和解码字节的代码。此代码称为运行时环境。运行时环境将帮助处理JS引擎为JS所做的一些事情。

但对于手动管理的语言,该运行时将不包括垃圾收集。

这并不意味着你完全依靠自己。即使在具有手动内存管理的语言中,您通常也会从语言运行时获得一些帮助。例如,在C中,运行时将跟踪在称为空闲列表的内容中打开的内存地址。

您可以使用该函数malloc(内存分配的简称)来要求运行时查找一些适合您数据的内存地址。这将从免费列表中取出这些地址。当您完成该数据后,您必须调用free以释放内存。然后这些地址将被添加回空闲列表。

你必须弄清楚何时调用这些功能。这就是为什么它被称为手动内存管理 - 你自己管理内存。

作为开发人员,弄清楚何时清除内存的不同部分可能很困难。如果你在错误的时间进行,它可能会导致错误,甚至导致安全漏洞。如果你不这样做,你的内存就会耗尽。

这就是许多现代语言使用自动内存管理的原因 - 以避免人为错误。但这是以牺牲性能为代价的。我将在下一部分中详细解释这一点。

ES7,8,9,10新特性介绍

ES7新特性(ECMAScript 2016)


ES7在ES6的基础上主要添加了两项内容:

  • Array.prototype.includes()方法
  • 求幂运算符(**)

Array.prototype.includes()方法

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

1
2
3
4
5
6
7
8
9
10
11
12
var array = [1, 2, 3];

console.log(array.includes(2));
// expected output: true

var pets = ['cat', 'dog', 'bat'];

console.log(pets.includes('cat'));
// expected output: true

console.log(pets.includes('at'));
// expected output: false

Array.prototype.includes()方法接收两个参数:

  • 要搜索的值
  • 搜索的开始索引。
    当第二个参数被传入时,该方法会从索引处开始往后搜索(默认索引值为0)。若搜索值在数组中存在则返回true,否则返回false。 且看下面示例:
    1
    2
    3
    ['a', 'b', 'c', 'd'].includes('b')         // true
    ['a', 'b', 'c', 'd'].includes('b', 1) // true
    ['a', 'b', 'c', 'd'].includes('b', 2) // false
    乍一看,includes的作用跟数组的indexOf重叠,为什么要特意增加这么一个api呢?主要区别有以下几点:
  • 返回值。看一个函数,先看他们的返回值。indexOf的返回数是值型的,includes的返回值是布尔型,所以在if条件判断的时候includes要简单得多,而indexOf 需要多写一个条件进行判断。
    1
    2
    3
    4
    5
    6
    7
    var ary = [1];
    if (ary.indexOf(1) !== -1) {
    console.log("数组存在1")
    }
    if (ary.includes(1)) {
    console.log("数组存在1")
    }
  • NaN的判断。如果数组中有NaN,你又正好需要判断数组是否有存在NaN,这时你使用indexOf是无法判断的,你必须使用includes这个方法。
    1
    2
    3
    var ary1 = [NaN];
    console.log(ary1.indexOf(NaN))//-1
    console.log(ary1.includes(NaN))//true
  • 当数组的有空的值的时候,includes会认为空的值是undefined,而indexOf不会。
    1
    2
    3
    var ary1 = new Array(3);
    console.log(ary1.indexOf(undefined));//-1
    console.log(ary1.includes(undefined))//true

    求幂运算符(**)

    加/减法我们通常都是用其中缀形式,直观易懂。在ECMAScript2016中,我们可以使用**来替代Math.pow。
    1
    4 ** 3           // 64
    效果等同于
    1
    Math.pow(4,3)
    值得一提的是,作为中缀运算符,**还支持以下操作
    1
    2
    3
    let n = 4;
    n **= 3;
    // 64

    ES8新特性(ECMAScript 2017)


    ECMAScript 2017特性一览

主要新功能:

  • 异步函数 Async Functions(Brian Terlson)
  • 共享内存和Atomics(Lars T. Hansen)

次要新功能:

  • Object.values / Object.entries(Jordan Harband)
  • String padding(Jordan Harband,Rick Waldron)
  • Object.getOwnPropertyDescriptors() (Jordan Harband,Andrea Giammarchi)
  • 函数参数列表和调用中的尾逗号(Jeff Morrison)

异步函数 Async Functions(Brian Terlson)

Async Functions也就是我们常说的Async/Await,相信大家对于这个概念都已经不陌生了。Async/Await是一种用于处理JS异步操作的语法糖,可以帮助我们摆脱回调地狱,编写更加优雅的代码。

通俗的理解,async关键字的作用是告诉编译器对于标定的函数要区别对待。当编译器遇到标定的函数中的await关键字时,要暂时停止运行,带到await标定的函数处理完毕后,再进行相应操作。如果该函数fulfiled了,则返回值是fulfillment value,否则得到的就是reject value。

下面通过拿普通的promise写法来对比,就很好理解了:

1
2
3
4
5
6
7
8
9
10
11
12
async function asyncFunc() {
const result = await otherAsyncFunc();
console.log(result);
}

// Equivalent to:
function asyncFunc() {
return otherAsyncFunc()
.then(result => {
console.log(result);
});
}

按顺序处理多个异步函数的时候优势更为明显:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function asyncFunc() {
const result1 = await otherAsyncFunc1();
console.log(result1);
const result2 = await otherAsyncFunc2();
console.log(result2);
}

// Equivalent to:
function asyncFunc() {
return otherAsyncFunc1()
.then(result1 => {
console.log(result1);
return otherAsyncFunc2();
})
.then(result2 => {
console.log(result2);
});
}

并行处理多个异步函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function asyncFunc() {
const [result1, result2] = await Promise.all([
otherAsyncFunc1(),
otherAsyncFunc2(),
]);
console.log(result1, result2);
}

// Equivalent to:
function asyncFunc() {
return Promise.all([
otherAsyncFunc1(),
otherAsyncFunc2(),
])
.then([result1, result2] => {
console.log(result1, result2);
});
}

处理错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function asyncFunc() {
try {
await otherAsyncFunc();
} catch (err) {
console.error(err);
}
}

// Equivalent to:
function asyncFunc() {
return otherAsyncFunc()
.catch(err => {
console.error(err);
});
}

Async Functions若是要展开去讲,可以占用很大段的篇幅。鉴于本文是一篇介绍性文章,再次不再进行深入。

SharedArrayBuffer和Atomics

注,如果之前您没有接触过ArrayBuffer相关知识的话,建议您移步浏览下内存管理系列文章
ECMAScript 2017 特性 SharedArrayBuffer 和 atomics”,由Lars T. Hansen设计。它引入了一个新的构造函数 SharedArrayBuffer 和 具有辅助函数的命名空间对象 Atomics。

在我们开始之前,让我们澄清两个相似但截然不同的术语:并行(Parallelism) 和 并发(Concurrency) 。他们存在许多定义,我使用的定义如下

  • 并行(Parallelism) (parallel 并行 vs. serial 串行):同时执行多个任务;
  • 并发(Concurrency) (concurrent 并发 vs. sequential 连续):在重叠的时间段内(而不是一个接一个)执行几个任务。

JS并行的历史

  • JavaScript 在单线程中执行。某些任务可以异步执行:浏览器通常会在单线程中运行这些任务,然后通过回调将结果重新加入到单线程中。
  • Web workers 将任务并行引入了 JavaScript :这些是相对重量级的进程。每个 workers 都有自己的全局环境。默认情况下,不共享任何内容。 workers 之间的通信(或在 workers 和主线程之间的通信)发展:
    起初,你只能发送和接收字符串。
    然后,引入结构化克隆:可以发送和接收数据副本。结构化克隆适用于大多数数据(JSON 数据,TypedArray,正则表达式,Blob对象,ImageData对象等)。它甚至可以正确处理对象之间的循环引用。但是,不能克隆 error 对象,function 对象和 DOM 节点。
    可在 workers 之间的转移数据:当接收方获得数据时,发送方失去访问权限。
  • 通过 WebGL 使用 GPU 计算(它倾向于数据并行处理)

共享数组缓冲区(Shared Array Buffers)
共享阵列缓冲区是更高并发抽象的基本构建块。它们允许您在多个 workers 和主线程之间共享 SharedArrayBuffer 对象的字节(该缓冲区是共享的,用于访问字节,将其封装在一个 TypedArray 中)这种共享有两个好处:

你可以更快地在 workers 之间共享数据。
workers 之间的协调变得更简单和更快(与 postMessage() 相比)。

1
2
3
4
5
6
7
8
9
10
11
12
// main.js
const worker = new Worker('worker.js');

// 要分享的buffer
const sharedBuffer = new SharedArrayBuffer( // (A)
10 * Int32Array.BYTES_PER_ELEMENT); // 10 elements

// 使用Worker共用sharedBuffer
worker.postMessage({sharedBuffer}); // clone

// 仅限本地使用
const sharedArray = new Int32Array(sharedBuffer); // (B)

创建一个共享数组缓冲区(Shared Array Buffers)的方法与创建普通的数组缓冲区(Array Buffer)类似:通过调用构造函数,并以字节的形式指定缓冲区的大小(行A)。你与 workers 共享的是 缓冲区(buffer) 。对于你自己的本地使用,你通常将共享数组缓冲区封装在 TypedArray 中(行B)。

workers的实现如下所列。

1
2
3
4
5
6
// worker.js
self.addEventListener('message', function (event) {
const {sharedBuffer} = event.data;
const sharedArray = new Int32Array(sharedBuffer); // (A)
// ···
});

sharedArrayBuffer 的 API
构造函数:

  • new SharedArrayBuffer(length)
    创建一个 length 字节的 buffer(缓冲区)。
    静态属性:

  • get SharedArrayBuffer[Symbol.species]
    默认情况下返回 this。 覆盖以控制 slice() 的返回。
    实例属性:

  • get SharedArrayBuffer.prototype.byteLength()
    返回 buffer(缓冲区) 的字节长度。

  • SharedArrayBuffer.prototype.slice(start, end)
    创建一个新的 this.constructor[Symbol.species] 实例,并用字节填充从(包括)开始到(不包括)结束的索引。

Atomics: 安全访问共享数据
举一个例子

1
2
3
// main.js
sharedArray[1] = 11;
sharedArray[2] = 22;

在单线程中,您可以重新排列这些写入操作,因为在中间没有读到任何内容。 对于多线程,当你期望以特定顺序执行写入操作时,就会遇到麻烦:

1
2
3
// worker.js
while (sharedArray[2] !== 22) ;
console.log(sharedArray[1]); // 0 or 11

Atomics 方法可以用来与其他 workers 进行同步。例如,以下两个操作可以让你读取和写入数据,并且不会被编译器重新排列:

  • Atomics.load(ta : TypedArray, index)
  • Atomics.store(ta : TypedArray, index, value : T)

这个想法是使用常规操作读取和写入大多数数据,而 Atomics 操作(load ,store 和其他操作)可确保读取和写入安全。通常,您将使用自定义同步机制,例如锁,其实现基于Atomics。

这是一个非常简单的例子,它总是有效的:

1
2
3
4
5
6
7
// main.js
console.log('notifying...');
Atomics.store(sharedArray, 0, 123);

// worker.js
while (Atomics.load(sharedArray, 0) !== 123) ;
console.log('notified');

Atomics 的 API
Atomic 函数的主要操作数必须是 Int8Array ,Uint8Array ,Int16Array ,Uint16Array ,Int32Array 或 Uint32Array 的一个实例。它必须包裹一个 SharedArrayBuffer 。

所有函数都以 atomically 方式进行操作。存储操作的顺序是固定的并且不能由编译器或 CPU 重新排序。

加载和存储

  • Atomics.load(ta : TypedArray, index) : T
    读取和返回 ta[index] 上的元素,返回数组指定位置上的值。
  • Atomics.store(ta : TypedArray, index, value : T) : T
    在 ta[index] 上写入 value,并且返回 value。
  • Atomics.exchange(ta : TypedArray, index, value : T) : T
    将 ta[index] 上的元素设置为 value ,并且返回索引 index 原先的值。
  • Atomics.compareExchange(ta : TypedArray, index, expectedValue, replacementValue) : T
    如果 ta[index] 上的当前元素为 expectedValue , 那么使用 replacementValue 替换。并且返回索引 index 原先(或者未改变)的值。
    简单修改 TypeArray 元素

以下每个函数都会在给定索引处更改 TypeArray 元素:它将一个操作符应用于元素和参数,并将结果写回元素。它返回元素的原始值。

  • Atomics.add(ta : TypedArray, index, value) : T
    执行 ta[index] += value 并返回 ta[index] 的原始值。

  • Atomics.sub(ta : TypedArray, index, value) : T
    执行 ta[index] -= value 并返回 ta[index] 的原始值。

  • Atomics.and(ta : TypedArray, index, value) : T
    执行 ta[index] &= value 并返回 ta[index] 的原始值。

  • Atomics.or(ta : TypedArray, index, value) : T
    执行 ta[index] |= value 并返回 ta[index] 的原始值。

  • Atomics.xor(ta : TypedArray, index, value) : T
    执行 ta[index] ^= value 并返回 ta[index] 的原始值。
    等待和唤醒

  • Atomics.wait(ta: Int32Array, index, value, timeout=Number.POSITIVE_INFINITY) : (‘not-equal’ | ‘ok’ | ‘timed-out’)
    如果 ta[index] 的当前值不是 value ,则返回 ‘not-equal’。否则继续等待,直到我们通过 Atomics.wake() 唤醒或直到等待超时。 在前一种情况下,返回 ‘ok’。在后一种情况下,返回’timed-out’。timeout 以毫秒为单位。记住此函数执行的操作:“如果 ta[index] 为 value,那么继续等待” 。

  • Atomics.wake(ta : Int32Array, index, count)
    唤醒等待在 ta[index] 上的 count workers。

Object.values and Object.entries

Object.values() 方法返回一个给定对象自己的所有可枚举属性值的数组,值的顺序与使用for…in循环的顺序相同 ( 区别在于for-in循环枚举原型链中的属性 )。

obj参数是需要待操作的对象。可以是一个对象,或者一个数组(是一个带有数字下标的对象,[10,20,30] -> {0: 10,1: 20,2: 30})。

1
2
3
4
5
6
7
8
9
10
11
12
const obj = { x: 'xxx', y: 1 };
Object.values(obj); // ['xxx', 1]

const obj = ['e', 's', '8']; // 相当于 { 0: 'e', 1: 's', 2: '8' };
Object.values(obj); // ['e', 's', '8']

// 当我们使用数字键值时,返回的是数字排序
// 根据键值排序
const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' };
Object.values(obj); // ['yyy', 'zzz', 'xxx']

Object.values('es8'); // ['e', 's', '8']

Object.entries 方法返回一个给定对象自身可遍历属性 [key, value] 的数组, 排序规则和 Object.values 一样。这个方法的声明比较琐碎:

1
2
3
4
5
6
7
8
9
10
const obj = { x: 'xxx', y: 1 };
Object.entries(obj); // [['x', 'xxx'], ['y', 1]]

const obj = ['e', 's', '8'];
Object.entries(obj); // [['0', 'e'], ['1', 's'], ['2', '8']]

const obj = { 10: 'xxx', 1: 'yyy', 3: 'zzz' };
Object.entries(obj); // [['1', 'yyy'], ['3', 'zzz'], ['10': 'xxx']]

Object.entries('es8'); // [['0', 'e'], ['1', 's'], ['2', '8']]

String padding

为 String 对象增加了 2 个函数:padStart 和 padEnd。

像它们名字那样,这几个函数的主要目的就是填补字符串的首部和尾部,为了使得到的结果字符串的长度能达到给定的长度。你可以通过特定的字符,或者字符串,或者默认的空格填充它。下面是函数的声明:

1
2
str.padStart(targetLength [, padString])
str.padEnd(targetLength [, padString])

这些函数的第一个参数是 targetLength(目标长度),这个是结果字符串的长度。第二个参数是可选的 padString(填充字符),一个用于填充到源字符串的字符串。默认值是空格。

1
2
3
4
5
6
7
8
9
10
11
'es8'.padStart(2);          // 'es8'
'es8'.padStart(5); // ' es8'
'es8'.padStart(6, 'woof'); // 'wooes8'
'es8'.padStart(14, 'wow'); // 'wowwowwowwoes8'
'es8'.padStart(7, '0'); // '0000es8'

'es8'.padEnd(2); // 'es8'
'es8'.padEnd(5); // 'es8 '
'es8'.padEnd(6, 'woof'); // 'es8woo'
'es8'.padEnd(14, 'wow'); // 'es8wowwowwowwo'
'es8'.padEnd(7, '6'); // 'es86666'

Object.getOwnPropertyDescriptors

getOwnPropertyDescriptors 方法返回指定对象所有自身属性的描述对象。属性描述对象是直接在对象上定义的,而不是继承于对象的原型。ES2017加入这个函数的主要动机在于方便将一个对象深度拷贝给另一个对象,同时可以将getter/setter拷贝。声明如下:

1
Object.getOwnPropertyDescriptors(obj)

obj 是待操作对象。返回的描述对象键值有:configurable, enumerable, writable, get, set and value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = { 
get es7() { return 777; },
get es8() { return 888; }
};
Object.getOwnPropertyDescriptor(obj);
// {
// es7: {
// configurable: true,
// enumerable: true,
// get: function es7(){}, //the getter function
// set: undefined
// },
// es8: {
// configurable: true,
// enumerable: true,
// get: function es8(){}, //the getter function
// set: undefined
// }
// }

结尾逗号

结尾逗号用代码展示非常明了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 参数定义时
function foo(
param1,
param2,
) {}

// 函数调用时
foo(
'abc',
'def',
);

// 对象中
let obj = {
first: 'Jane',
last: 'Doe',
};

// 数组中
let arr = [
'red',
'green',
'blue',
];

这个改动有什么好处呢?

首先,重新排列项目更简单,因为如果最后一项更改其位置,则不必添加和删除逗号。
其次,它可以帮助版本控制系统跟踪实际发生的变化。例如,从:

1
2
3
[
'foo'
]

修改为

1
2
3
4
[
'foo',
'bar'
]

导致线条’foo’和线条’bar’被标记为已更改,即使唯一真正的变化是后一条线被添加。

ES9新特性(ECMAScript 2018)


ES9的新特性索引如下:

主要新功能:

  • 异步迭代(Domenic Denicola,Kevin Smith)

  • Rest/Spread 属性(SebastianMarkbåge)
    新的正则表达式功能:

  • RegExp named capture groups(Gorkem Yakin,Daniel Ehrenberg)

  • RegExp Unicode Property Escapes(Mathias Bynens)

  • RegExp Lookbehind Assertions(Gorkem Yakin,NozomuKatō,Daniel Ehrenberg)

  • s (dotAll) flag for regular expressions(Mathias Bynens)
    其他新功能:

  • Promise.prototype.finally() (Jordan Harband)

  • 模板字符串修改(Tim Disney)

##异步迭代
首先来回顾一下同步迭代器:
ES6引入了同步迭代器,其工作原理如下:

  • Iterable:一个对象,表示可以通过Symbol.iterator方法进行迭代。
  • Iterator:通过调用iterable [Symbol.iterator] ()返回的对象。它将每个迭代元素包装在一个对象中,并通过其next()方法一次返回一个。
  • IteratorResult:返回的对象next()。属性value包含一个迭代的元素,属性done是true 后最后一个元素。
    示例:
    1
    2
    3
    4
    5
    6
    7
    8
    const iterable = ['a', 'b'];
    const iterator = iterable[Symbol.iterator]();
    iterator.next()
    // { value: 'a', done: false }
    iterator.next()
    // { value: 'b', done: false }
    iterator.next()
    // { value: undefined, done: true }

异步迭代器
先前的迭代方式是同步的,并不适用于异步数据源。例如,在以下代码中,readLinesFromFile()无法通过同步迭代传递其异步数据:

1
2
3
for (const line of readLinesFromFile(fileName)) {
console.log(line);
}

异步迭代器和常规迭代器的工作方式非常相似,但是异步迭代器涉及promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function example() {
// 普通迭代器:
const iterator = createNumberIterator();
iterator.next(); // Object {value: 1, done: false}
iterator.next(); // Object {value: 2, done: false}
iterator.next(); // Object {value: 3, done: false}
iterator.next(); // Object {value: undefined, done: true}

// 异步迭代器:
const asyncIterator = createAsyncNumberIterator();
const p = asyncIterator.next(); // Promise
await p;// Object {value: 1, done: false}
await asyncIterator.next(); // Object {value: 2, done: false}
await asyncIterator.next(); // Object {value: 3, done: false}
await asyncIterator.next(); // Object {value: undefined, done: true}
}

异步迭代器对象的next()方法返回了一个Promise,解析后的值跟普通的迭代器类似。
用法:iterator.next().then(({ value, done })=> {//{value: ‘some val’, done: false}}

1
2
3
4
5
6
7
8
9
10
11
12
const promises = [
new Promise(resolve => resolve(1)),
new Promise(resolve => resolve(2)),
new Promise(resolve => resolve(3)),
];

async function test() {
for await (const p of promises) {
console.log(p);
}
}
test(); //1 ,2 3

Rest/Spread 属性

这个就是我们通常所说的rest参数和扩展运算符,这项特性在ES6中已经引入,但是ES6中的作用对象仅限于数组:

1
2
3
4
5
6
7
8
9
10
restParam(1, 2, 3, 4, 5);

function restParam(p1, p2, ...p3) {
// p1 = 1
// p2 = 2
// p3 = [3, 4, 5]
}

const values = [99, 100, -1, 48, 16];
console.log( Math.max(...values) ); // 100

在ES9中,为对象提供了像数组一样的rest参数和扩展运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
a: 1,
b: 2,
c: 3
}
const { a, ...param } = obj;
console.log(a) //1
console.log(param) //{b: 2, c: 3}

function foo({a, ...param}) {
console.log(a); //1
console.log(param) //{b: 2, c: 3}
}

正则表达式命名捕获组

编号的捕获组

1
2
3
4
5
6
7
8
7
//正则表达式命名捕获组
const RE_DATE = /([0-9]{4})-([0-9]{2})-([0-9]{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31

通过数字引用捕获组有几个缺点:

  • 找到捕获组的数量是一件麻烦事:必须使用括号。
  • 如果要了解组的用途,则需要查看正则表达式。
  • 如果更改捕获组的顺序,则还必须更改匹配代码。
    命名的捕获组
    ES9中可以通过名称来识别捕获组:(?[0-9]{4})

在这里,我们用名称标记了前一个捕获组year。该名称必须是合法的JavaScript标识符(认为变量名称或属性名称)。匹配后,您可以通过访问捕获的字符串matchObj.groups.year来访问。

让我们重写前面的代码:

1
2
3
4
5
6
7
8
9
10
11
const RE_DATE = /(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})/;

const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

// 使用解构语法更为简便
const {groups: {day, year}} = RE_DATE.exec('1999-12-31');
console.log(year); // 1999
console.log(day); // 31

可以发现,命名捕获组有以下优点:

  • 找到捕获组的“ID”更容易。
  • 匹配代码变为自描述性的,因为捕获组的ID描述了正在捕获的内容。
  • 如果更改捕获组的顺序,则无需更改匹配代码。
  • 捕获组的名称也使正则表达式更容易理解,因为您可以直接看到每个组的用途。

正则表达式 Unicode 转义

该特性允许您使用\p{}通过提及大括号内的Unicode字符属性来匹配字符,在正则表达式中使用标记 u (unicode) 设置。

1
2
3
4
/^\p{White_Space}+$/u.test('\t \n\r')
// true
/^\p{Script=Greek}+$/u.test('μετά')
// true

新方法匹配中文字符
由于在Unicode里面,中文字符对应的Unicode Script是Han,于是我们就可以用这个reg来匹配中文:

1
/\p{Script=Han}/u

这样我们就可以不用记忆繁琐又不好记的/[\u4e00-\u9fa5]/了,况且这个表达式已经有些年头了,说实话,后来又新增的属性为Han的字符并不在这个范围内,因此这个有年头reg并不一定好使。

我随便从网上找了一个Unicode8.0添加的中文字符“𬬭”,我测了一下两种reg的兼容性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
oldReg=/[\u4e00-\u9fa5]/
newReg=/\p{Script=Han}/u

oldReg.test('abc')
// false
newReg.test('abc')
// false

oldReg.test('地平线')
// true
newReg.test('地平线')
// true

oldReg.test('𬬭')
// false
newReg.test('𬬭')
// true

http://www.unicode.org/charts/PDF/U4E00.pdf

可以参考一下这个PDF,是Unicode的汉字全集,从524页9FA6至526页(最后一页)用旧匹配方式都无法生效。

一些对于Unicode的科普

  • Name:唯一名称,由大写字母,数字,连字符和空格组成。例如:

    A: Name = LATIN CAPITAL LETTER A
    😀: Name = GRINNING FACE

  • General_Category:对字符进行分类。例如:

    X: General_Category = Lowercase_Letter
    $: General_Category = Currency_Symbol

  • White_Space:用于标记不可见的间距字符,例如空格,制表符和换行符。例如:

    \ T: White_Space = True
    π: White_Space = False

  • Age:引入字符的Unicode标准版本。例如:欧元符号€在Unicode标准的2.1版中添加。

    €: Age = 2.1

  • Script:是一个或多个书写系统使用的字符集合。

    有些脚本支持多种写入系统。例如,拉丁文脚本支持英语,法语,德语,拉丁语等书写系统。
    某些语言可以用多个脚本支持的多个备用写入系统编写。例如,土耳其语在20世纪初转换为拉丁文字之前使用了阿拉伯文字。
    例子:

       α: Script = Greek
      Д: Script = Cyrillic
    

正则表达式的Unicode属性转义

  • 匹配其属性prop具有值的所有字符value:
    1
    \p{prop=value}
  • 匹配所有没有属性prop值的字符value:
    1
    \P{prop=value}
  • 匹配二进制属性bin_prop为True的所有字符:
    1
    \p{bin_prop}
  • 匹配二进制属性bin_prop为False的所有字符:
    1
    \P{bin_prop}
  • 匹配空格:
    1
    2
    /^\p{White_Space}+$/u.test('\t \n\r')
    //true
  • 匹配字母:
    1
    2
    /^\p{Letter}+$/u.test('πüé')
    //true
  • 匹配希腊字母:
    1
    2
    /^\p{Script=Greek}+$/u.test('μετά')
    //true
  • 匹配拉丁字母:
    1
    2
    /^\p{Script=Latin}+$/u.test('Grüße')
    //true

正则表达式反向断言

先来看下正则表达式先行断言是什么:

如获取货币的符号

1
2
3
4
5
6
const noReLookahead = /\D(\d+)/,
reLookahead = /\D(?=\d+)/,
match1 = noReLookahead.exec('$123.45'),
match2 = reLookahead.exec('$123.45');
console.log(match1[0]); // $123
console.log(match2[0]); // $

在ES9中可以允许反向断言:

1
2
3
const reLookahead = /(?<=\D)[\d\.]+/;
match = reLookahead.exec('$123.45');
console.log(match[0]); // 123.45

使用?<=进行反向断言,可以使用反向断言获取货币的价格,而忽略货币符号。

正则表达式dotAll模式

正则表达式中点.匹配除回车外的任何单字符,标记s改变这种行为,允许行终止符的出现,例如:

1
2
3
/hello.world/.test('hello\nworld');  // false
/hello.world/s.test('hello\nworld'); // true
Promise.prototype.finally()

这个基本没什么好讲的,看名字就能看懂了。其用法如下:

1
2
3
4
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

finally的回调总会被执行。

模板字符串修改

ES2018 移除对 ECMAScript 在带标签的模版字符串中转义序列的语法限制。
之前,\u开始一个 unicode 转义,\x开始一个十六进制转义,\后跟一个数字开始一个八进制转义。这使得创建特定的字符串变得不可能,例如Windows文件路径 C:\uuu\xxx\111。

要取消转义序列的语法限制,可在模板字符串之前使用标记函数String.raw:

1
2
3
4
`\u{54}`
// "T"
String.raw`\u{54}`
// "\u{54}"

ES10新特性(ECMAScript 2019)


ES10的新特性索引如下:

  • BigInt
  • string.prototype.matchAll()
  • 动态导入
  • Array.flat()
  • Array.flatMap()
  • Object.fromEntries()
  • String.trimStart() 与 String.trimEnd()
  • 格式良好的 JSON.stringify()
  • 稳定的 Array.prototype.sort()
  • 新的F unction.toString()
  • 可选的 Catch Binding
  • 标准化 globalThis 对象
  • Symbol.description
  • Hashbang 语法
  • ES10类:private、static 和 公共成员

BigInt 任意精度整数。

BigInt 是第七种 原始类型。
BigInt 是一个任意精度的整数。这意味着变量现在可以 表示²⁵³ 数字,而不仅仅是9007199254740992。

1
const b = 1n;  // 追加 n 以创建 BigInt

在过去,不支持大于 9007199254740992 的整数值。如果超过,该值将锁定为 MAX_SAFE_INTEGER + 1:

1
2
3
4
5
6
7
8
9
10
11
12
const limit = Number.MAX_SAFE_INTEGER;
⇨ 9007199254740991
limit + 1;
⇨ 9007199254740992
limit + 2;
⇨ 9007199254740992 <--- MAX_SAFE_INTEGER + 1 exceeded
const larger = 9007199254740991n;
⇨ 9007199254740991n
const integer = BigInt(9007199254740991); // initialize with number
⇨ 9007199254740991n
const same = BigInt("9007199254740991"); // initialize with "string"
⇨ 9007199254740991n

typeof

1
2
3
4
typeof 10;
⇨ 'number'
typeof 10n;
⇨ 'bigint'

等于运算符可用于两种类型之间比较:

1
2
3
4
10n === BigInt(10);
⇨ true
10n == 10;
⇨ true

数学运算符只能在自己的类型中工作:

1
2
3
4
5
200n / 10n
⇨ 20n
200n / 20
⇨ Uncaught TypeError:
Cannot mix BigInt and other types, use explicit conversions <

-运算符可以操作, + 不可用

1
2
3
4
5
-100n
⇨ -100n
+100n
⇨ Uncaught TypeError:
Cannot convert a BigInt value to a number

string.prototype.matchAll()

当你读到这篇文章的时候,matchAll 可能已经在 Chrome C73 中正式实现了——如果不是,它仍然值得一看。特别是如果你是一个正则表达式(regex)爱好者。

如果您运行谷歌搜索JavaScript string match all,第一个结果将是这样的:如何编写正则表达式“match all”?

最佳结果将建议 String.match 与正则表达式和 /g 一起使用或者带有 /g 的 RegExp.exec 或者带有 /g 的 RegExp.test 。

首先,让我们看看旧规范是如何工作的。

带字符串参数的 String.match 仅返回第一个匹配:

1
2
3
let string = 'Hello';
let matches = string.match('l');
console.log(matches[0]); // "l"

结果是单个 “l”(注意:匹配存储在 matches[0] 中而不是 matches)

在“hello”中搜索 “l” 只返回 “l”。

将 string.match 与 regex 参数一起使用也是如此:

让我们使用正则表达式 /l/ 找到字符 串“hello” 中的 “l” 字符:

1
2
3
let string = "Hello";
let matches = string.match(/l/);
console.log(matches[0]); // "l"

添加 /g 混合

1
2
let string = "Hello";
let ret = string.match(/l/g); // (2) [“l”, “l”];

很好,我们使用 < ES10 方式得到了多个匹配,它一直起作用。

那么为什么要使用全新的 matchAll 方法呢? 在我们更详细地回答这个问题之前,让我们先来看看 捕获组。如果不出意外,你可能会学到一些关于正则表达式的新知识。

正则表达式捕获组
在 regex 中捕获组只是从 () 括号中提取一个模式,可以使用 /regex/.exec(string) 和string.match 捕捉组。

常规捕获组是通过将模式包装在 (pattern) 中创建的,但是要在结果对象上创建 groups 属性,它是: (?pattern)。

要创建一个新的组名,只需在括号内附加 ?,结果中,分组 (pattern) 匹配将成为 group.name,并附加到 match 对象,以下是一个实例:

字符串标本匹配:blackraven limeparrot white*seagull

这里创建了 match.groups.color 和 match.groups.bird :

1
2
3
4
5
6
7
8
9
10
11
const string = 'black*raven lime*parrot white*seagull';
const regex = /(?<color>.*?)\*(?<bird>[a-z0-9]+)/g;
while (match = regex.exec(string))
{
let value = match[0];
let index = match.index;
let input = match.input;
console.log(`${value} at ${index} with '${input}'`);
console.log(match.groups.color);
console.log(match.groups.bird);
}

需要多次调用 regex.exec 方法来遍历整个搜索结果集。 在每次迭代期间调用.exec 时,将显示下一个结果(它不会立即返回所有匹配项。),因此使用 while 循环。

输出如下:

1
2
3
4
5
6
7
8
9
black*raven at 0 with 'black*raven lime*parrot white*seagull'
black
raven
lime*parrot at 11 with 'black*raven lime*parrot white*seagull'
lime
parrot
white*seagull at 23 with 'black*raven lime*parrot white*seagull'
white
seagull

但奇怪的是:

如果你从这个正则表达式中删除 /g,你将永远在第一个结果上创建一个无限循环。这在过去是一个巨大的痛苦。想象一下,从某个数据库接收正则表达式时,你不确定它的末尾是否有 /g,你得先检查一下。

使用 .matchAll() 的好理由

  • 在与捕获组一起使用时,它可以更加优雅,捕获组只是使用 () 提取模式的正则表达式的一部分。
  • 它返回一个迭代器而不是一个数组,迭代器本身是有用的。
  • 迭代器可以使用扩展运算符 (…) 转换为数组。
  • 它避免了带有 /g 标志的正则表达式,当从数据库或外部源检索未知正则表达式并与陈旧的RegEx 对象一起使用时,它非常有用。
  • 使用 RegEx 对象创建的正则表达式不能使用点 (.) 操作符链接。
  • 高级: RegEx 对象更改跟踪最后匹配位置的内部 .lastindex 属性,这在复杂的情况下会造成严重破坏。

.matchAll() 是如何工作的?
让我们尝试匹配单词 hello 中字母 e 和 l 的所有实例, 因为返回了迭代器,所以可以使用 for…of 循环遍历它:

1
2
3
4
// Match all occurrences of the letters: "e" or "l" 
let iterator = "hello".matchAll(/[el]/);
for (const match of iterator)
console.log(match);

这一次你可以跳过 /g, .matchall 方法不需要它,结果如下:

1
2
3
[ 'e', index: 1, input: 'hello' ] // Iteration 1
[ 'l', index: 2, input: 'hello' ] // Iteration 2
[ 'l', index: 3, input: 'hello' ] // Iteration 3

使用 .matchAll() 捕获组示例:
.matchAll 具有上面列出的所有好处。它是一个迭代器,可以用 for…of 循环遍历它,这就是整个语法的不同。

1
2
3
4
5
6
7
8
9
10
const string = 'black*raven lime*parrot white*seagull';
const regex = /(?<color>.*?)\*(?<bird>[a-z0-9]+)/;
for (const match of string.matchAll(regex)) {
let value = match[0];
let index = match.index;
let input = match.input;
console.log(`${value} at ${index} with '${input}'`);
console.log(match.groups.color);
console.log(match.groups.bird);
}

请注意已经没有 /g 标志,因为 .matchAll() 已经包含了它,打印如下:

1
2
3
4
5
6
7
8
9
black*raven at 0 with 'black*raven lime*parrot white*seagull'
black
raven
lime*parrot at 11 with 'black*raven lime*parrot white*seagull'
lime
parrot
white*seagull at 23 with 'black*raven lime*parrot white*seagull'
white
seagull

也许在美学上它与原始正则表达式非常相似,执行while循环实现。但是如前所述,由于上面提到的许多原因,这是更好的方法,移除 /g 不会导致无限循环。

动态导入

现在可以将导入分配给变量:

1
2
3
4
element.addEventListener('click', async() => {
const module = await import(`./api-scripts/button-click.js`);
module.clickEvent();
})

Array.flat()

扁平化多维数组:

1
2
3
4
5
let multi = [1,2,3,[4,5,6,[7,8,9,[10,11,12]]]];
multi.flat(); // [1,2,3,4,5,6,Array(4)]
multi.flat().flat(); // [1,2,3,4,5,6,7,8,9,Array(3)]
multi.flat().flat().flat(); // [1,2,3,4,5,6,7,8,9,10,11,12]
multi.flat(Infinity); // [1,2,3,4,5,6,7,8,9,10,11,12]

Array.flatMap()

1
2
3
4
5
6
let array = [1, 2, 3, 4, 5];
array.map(x => [x, x * 2]);


let array = [1, 2, 3, 4, 5];
array.map(x => [x, x * 2]);

结果:

1
2
3
4
5
6
[Array(2), Array(2), Array(2), Array(2), Array(2)]
0: (2) [1, 2]
1: (2) [2, 4]
2: (2) [3, 6]
3: (2) [4, 8]
4: (2) [5, 10]

使用 flatMap 方法:

1
2
array.flatMap(v => [v, v * 2]);
[1, 2, 2, 4, 3, 6, 4, 8, 5, 10]

Object.fromEntries()

将键值对列表转换为对象:

1
2
3
4
5
6
7
8
9
let obj = { apple : 10, orange : 20, banana : 30 };
let entries = Object.entries(obj);
entries;
(3) [Array(2), Array(2), Array(2)]
0: (2) ["apple", 10]
1: (2) ["orange", 20]
2: (2) ["banana", 30]
let fromEntries = Object.fromEntries(entries);
{ apple: 10, orange: 20, banana: 30 }

String.trimStart() 与 String.trimEnd()

1
2
3
let greeting = "     Space around     ";
greeting.trimEnd(); // " Space around";
greeting.trimStart(); // "Space around ";

格式良好的 JSON.stringify()

此更新修复了字符 U+D800 到 U+DFFF 的处理,有时可以进入 JSON 字符串。 这可能是一个问题,因为 JSON.stringify 可能会将这些数字格式化为没有等效 UTF-8 字符的值, 但 JSON 格式需要 UTF-8 编码。

解析方法使用格式良好的JSON字符串,如:

1
'{ “prop1” : 1, "prop2" : 2 }'; // A well-formed JSON format string

注意,要创建正确 JSON 格式的字符串,绝对需要在属性名周围加上双引号。缺少或任何其他类型的引号都不会生成格式良好的JSON。

1
'{ “prop1” : 1, "meth" : () => {}}'; // Not JSON format string

JSON 字符串格式与 Object Literal 不同,后者看起来几乎一样,但可以使用任何类型的引号括住属性名,也可以包含方法(JSON格式不允许使用方法):

1
let object_literal = { property: 1, meth: () => {} };

不管怎样,一切似乎都很好。第一个示例看起来是兼容的。但它们也是简单的例子,大多数情况下都能顺利地工作!

U+2028 和 U+2029 字符
问题是, ES10 之前的 EcmaScript 实际上并不完全支持 JSON 格式。前 ES10 时代不接受未转义行分隔符 U+2028 和段落分隔符 U+2029 字符:
对于 U+D800 - U+DFFF 之间的所有字符也是如此
如果这些字符潜入 JSON 格式的字符串(假设来自数据库记录),你可能会花费数小时试图弄清楚为什么程序的其余部分会产生解析错误。

因此,如果你传递 eval 这样的字符串 “console.log(‘ hello ‘)”,它将执行 JavaScript语句 (通过尝试将字符串转换为实际代码),也类似于 JSON.parse 将处理你的 JSON 字符串的方式。

稳定的 Array.prototype.sort()

V8 之前的实现对包含10个以上项的数组使用了一种不稳定的快速排序算法。

一个稳定的排序算法是当两个键值相等的对象在排序后的输出中出现的顺序与在未排序的输入中出现的顺序相同时。
但情况不再是这样了,ES10 提供了一个稳定的数组排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fruit = [
{ name: "Apple", count: 13, },
{ name: "Pear", count: 12, },
{ name: "Banana", count: 12, },
{ name: "Strawberry", count: 11, },
{ name: "Cherry", count: 11, },
{ name: "Blackberry", count: 10, },
{ name: "Pineapple", count: 10, }
];
// 创建排序函数:
let my_sort = (a, b) => a.count - b.count;
// 执行稳定的ES10排序:
let sorted = fruit.sort(my_sort);
console.log(sorted);

控制台输出(项目会以相反的顺序出现):

新的Function.toString()

函数是对象,并且每个对象都有一个 .toString() 方法,因为它最初存在于Object.prototype.toString() 上。 所有对象(包括函数)都是通过基于原型的类继承从它继承的。

这意味着我们以前已经有 funcion.toString() 方法了。

但是 ES10 进一步尝试标准化所有对象和内置函数的字符串表示。 以下是各种新案例:

典型的例子:

1
function () { console.log('Hello there.'); }.toString();

控制台输出(函数体的字符串格式:)

1
⇨ function () { console.log('Hello there.'); }

下面是剩下的例子:

直接在方法名 .toString()

1
2
Number.parseInt.toString();
⇨ function parseInt() { [native code] }

绑定上下文:

1
2
function () { }.bind(0).toString();
⇨ function () { [native code] }

内置可调用函数对象:

1
2
Symbol.toString();
⇨ function Symbol() { [native code] }

动态生成的函数:

1
2
function* () { }.toString();
⇨ function* () { }

prototype.toString

1
2
Function.prototype.toString.call({});
⇨ Function.prototype.toString requires that 'this' be a Function"

可选的 Catch Binding

在过去,try/catch 语句中的 catch 语句需要一个变量。 try/catch 语句帮助捕获终端级别的错误:

1
2
3
4
5
6
7
8
try {
// Call a non-existing function undefined_Function
undefined_Function("I'm trying");
}
catch(error) {
// Display the error if statements inside try above fail
console.log( error ); // undefined_Function is undefined
}

在某些情况下,所需的错误变量是未使用的:

1
2
3
4
5
6
7
8
try {
JSON.parse(text); // <--- this will fail with "text not defined"
return true; <--- exit without error even if there is one
}
catch (redundant_sometmes) <--- this makes error variable redundant
{
return false;
}

编写此代码的人通过尝试强制 true 退出 try 子句。但是,这并不是实际发生的情况

1
2
3
4
5
6
7
8
9
(() => {
try {
JSON.parse(text)
return true
} catch(err) {
return false
}
})()
=> false

在 ES10 中,捕获错误的变量是可选的
现在可以跳过错误变量:

1
2
3
4
5
6
7
8
try {
JSON.parse(text);
return true;
}
catch
{
return false;
}

目前还无法测试上一个示例中的 try 语句的结果,但一旦它出来,我将更新这部分。

标准化 globalThis 对象

这在ES10之前, globalThis 还没有标准化。

在产品代码中,你可以自己编写这个怪物,在多个平台上“标准化”它:

1
2
3
4
5
6
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};

但即使这样也不总是奏效。因此,ES10 添加了 globalThis 对象,从现在开始,该对象用于在任何平台上访问全局作用域:

1
2
3
4
5
6
7
8
9
// 访问全局数组构造函数
globalThis.Array(0, 1, 2);
⇨ [0, 1, 2]

// 类似于 ES5 之前的 window.v = { flag: true }
globalThis.v = { flag: true };

console.log(globalThis.v);
⇨ { flag: true }

Symbol.description

description 是一个只读属性,它返回 Symbol 对象的可选描述。

1
2
3
4
let mySymbol = 'My Symbol';
let symObj = Symbol(mySymbol);
symObj; // Symbol(My Symbol)
symObj.description; // "My Symbol"

Hashbang 语法

也就是 unix 用户熟悉的 shebang。它指定一个解释器(什么将执行JavaScript文件?)。

ES10标准化,我不会对此进行详细介绍,因为从技术上讲,这并不是一个真正的语言特性,但它基本上统一了 JavaScript 在服务器端的执行方式。

1
$ ./index.js

代替

1
$ node index.js

ES10类:private、static 和 公共成员

新的语法字符 #octothorpe(hash tag)现在用于直接在类主体的范围内定义变量,函数,getter 和 setter ……以及构造函数和类方法。

下面是一个毫无意义的例子,它只关注新语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Raven extends Bird {
#state = { eggs: 10};
// getter
get #eggs() {
return state.eggs;
}
// setter
set #eggs(value) {
this.#state.eggs = value;
}
#lay() {
this.#eggs++;
}
constructor() {
super();
this.#lay.bind(this);
}
#render() {
/* paint UI */
}
}

尾声

ECMAScript的演化不会停止,但是我们完全没必要害怕。除了ES6这个史无前例的版本带来了海量的信息和知识点以外,之后每年一发的版本都仅仅带有少量的增量更新,一年更新的东西花半个小时就能搞懂了,完全没必要畏惧。