ES6 随手记(持续)

(包含 ES7 及以上,2023 年最新为 ES14)

  • update date: 2023-09-24 10:12:30
  • start date: 2018-08-27 13:00:45

ES7 ~ ES13 说明

p-es7.png

ECMA-262

Ecma 国际 (一个标准化组织)创建了 ECMA-262 规范,这个规范就是 ECMAScript 语言的官方标准。

Ecma 第 39 号技术委员会 (TC39)

是一组开发 ECMA-262 标准规范的人(Brendan Eich 和其他一些人)。

ECMA 规范最终由 TC39 敲定。TC39 由包括浏览器厂商在内的各方组成,他们开会推动 JavaScript 提案沿着一条严格的发展道路前进。

从提案到入选 ECMA 规范主要有以下几个阶段:

  • Stage 0: strawman——最初想法的提交。
  • Stage 1: proposal(提案)——由 TC39 至少一名成员倡导的正式提案文件,该文件包括 API 事例。
  • Stage 2: draft(草案)——功能规范的初始版本,该版本包含功能规范的两个实验实现。
  • Stage 3: candidate(候选)——提案规范通过审查并从厂商那里收集反馈
  • Stage 4: finished(完成)——提案准备加入 ECMAScript,但是到浏览器或者 Nodejs 中可能需要更长的时间。

1 对象设置变量键值

难以形容,直接上代码:

1
2
3
4
5
6
7
const key = 'testkey';
const obj = {
key1: 'value1',
[key]: 'testvalue',
};

console.log(obj.testkey); // 'testvalue'

babel 编译(ES5):

1
2
3
4
5
6
7
8
9
10
11
var _obj;

var key = 'testkey';
var obj =
((_obj = {
key1: 'value1',
}),
(_obj[key] = 'testvalue'),
_obj);

console.log(obj.testkey); // 'testvalue'

应用场景

对象的 key 设置有限制且希望更加语义化、或 key 是动态的,比如一些接口参数设置的场景。

2 String、Array 匹配查找

String.prototype.includes()Array.prototype.includes()方法。mdn
注:babel 不能编译,移动端兼容:ios9 及以上、安卓 5 及以上;PC:除 IE 外。

1
2
3
4
5
6
7
8
9
10
11
const str = 'abcdef';

console.log(str.includes('cd')); // true
console.log(str.includes('fg')); // false

const arr = [1, 2, 3, { c: 1 }, 'ddd', NaN];
console.log(arr.includes(3)); // true
console.log(arr.includes(5)); // false
console.log(arr.includes({ c: 1 })); // false,注意
console.log(arr.includes('ddd')); // true
console.log(arr.includes(NaN)); // true

语法

1
arr.includes(searchElement [, fromIndex])

其中:

  • searchElement:需要查找的元素值。
  • fromIndex(可选):从该索引处开始查找 searchElement。如果为负值,则按升序从 array.length + fromIndex 的索引开始搜索。默认为 0。

应用场景

绝大部分的字符串/数组匹配判断场景,比 indexOf()加判断方便。

3 使用对象解构来解析字符串数组

例:

1
2
const message = '1994,Micheal Wayne,Chine,michealwayne@163.com';
const { 2: country, 4: email } = message.split(',');

babel 编译:

1
2
3
4
5
var message = '1994,Micheal Wayne,Chine,michealwayne@163.com';

var _message$split = message.split(','),
country = _message$split[2],
email = _message$split[4];

4 还是那道题,变量 a、b 交换

1
2
3
4
5
6
7
let a = 1,
b = 2;

[a, b] = [b, a];

console.log(a); // 2
console.log(b); // 1

ps,上面代码 babel 结果:

1
2
3
4
5
6
7
8
9
var a = 1,
b = 2;

var _ref = [b, a];
a = _ref[0];
b = _ref[1];

console.log(a); // 2
console.log(b); // 1

5 数组 concat() 的另一种解决方式

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

let arr3 = [...arr1, ...arr2];
console.log(arr3); // [1, 2, 3, 4, 5, 6, 7]

ps,上面代码 babel 结果:

1
2
3
4
5
var arr1 = [1, 2, 3],
arr2 = [4, 5, 6, 7];

var arr3 = [].concat(arr1, arr2);
console.log(arr3); // [1, 2, 3, 4, 5, 6, 7]

6 ES6 模块加载

(CommonJS 和 ES6 module 的区别有很多,在此只记录基本的使用对应关系。对于它们的区别可参考《CommonJS 和 ES6 Module 究竟有什么区别?》

1
2
3
4
5
6
7
8
9
10
11
// a.js (CommonJS)
module.exports = {
name: 'Micheal',
age: 18,
};

// 等于
export default {
name: 'Micheal',
age: 18,
};

引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 方法1
import info from './a'; // info = { name: 'Micheal', age: 18 }

// 方法2
import { default as info } from './a'; // info = { name: 'Micheal', age: 18 }

// 方法3
import * as info from './a';
info.default;
/* info = {
* get default () { return module.exports; }
* get name () { return this.default.name }.bind(info)
* get age () { return this.default.age }.bind(info)
* }
*/

7 利用...运算符合并两个 Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj1 = {
a: 1,
};
const obj2 = {
b: 2,
c: [1, 2, 3],
};
const obj3 = {
a: 3,
d: 4,
};

const res = {
...obj1,
...obj2,
...obj3,
};

console.log(res);

babel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 实现拼接
var n =
Object.assign ||
function (e) {
for (var t = 1; t < arguments.length; t++) {
var r = arguments[t];
for (var n in r) Object.prototype.hasOwnProperty.call(r, n) && (e[n] = r[n]);
}
return e;
};

var obj1 = {
a: 1,
};
var obj2 = {
b: 2,
c: [1, 2, 3],
};
var obj3 = {
a: 3,
d: 4,
};

var res = n({}, obj1, obj2, obj3);
console.log(res);

应用场景

对象合并场景,比如默认值和设置值的合并

8 利用...运算符展开字符串

1
2
const str = 'abcdefg';
console.log(...str); // 'a' 'b' 'c' 'd' 'e' 'f' 'g'

*这种方式还能避免 js 将 32 位 Unicode 识别为两个字符。如

1
2
3
let str = '\uD83D\uDE80y';
console.log(str.length); // 3
console.log([...str].length); // 2

9 利用...运算符求极值

1
2
3
let nums = [1, 3, 5, 7, 9, 11, 13]

Math.max(…nums) // 13

10 JavaScript 中默认为(或提供)iterable 的标准内建值包括:

  • Arrays
  • Strings
  • Generators
  • Collections / TypedArrays

11 被忽视的 Symbol

Symbol 作为 ES6 新出的一种新的基本类型,我显然忽视了它。

每个从 Symbol() 返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的

语法:

1
Symbol([description])

其中 description 可选,是 symbol 的描述,可用于调试但不能访问 symbol 本身。

不支持语法:”new Symbol()“。围绕原始数据类型创建一个显式包装器对象从 ECMAScript 6 开始不再被支持。 然而,现有的原始包装器对象,如 new Boolean、new String 以及 new Number 因为遗留原因仍可被创建。

属性

  • lengthSymbol.length 为 0;
  • prototype:描述 symbol 构造函数的原型。

除了自己创建的 symbol,JavaScript 还内建了一些在 ECMAScript 5 之前没有暴露给开发者的 symbol,它们代表了内部语言行为。它们可以使用以下属性访问:

  • Symbol.iterator:一个返回一个对象默认迭代器的方法。被 for...of 使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const str = 'abc';
const ite = str[Symbol.iterator]();

ite.next().value; // 'a'
ite.next().value; // 'b'
ite.next().value; // 'c'

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable]; // [1, 2, 3]
  • Symbol.asyncIterator:一个返回对象默认的异步迭代器的方法。被 for await of 使用。
  • 正则表达式 symbols:Symbol.matchSymbol.replaceSymbol.searchSymbol.split
  • Symbol.hasInstance:一个确定一个构造器对象识别的对象是否为它的实例的方法。被 instanceof 使用。
1
2
3
4
5
6
class MyArray {
static [Symbol.hasInstance](instance) {
return Array.isArray(instance);
}
}
console.log([] instanceof MyArray); // true
  • Symbol.isConcatSpreadable:一个布尔值,表明一个对象是否应该 flattened 为它的数组元素。被 Array.prototype.concat() 使用。
1
2
3
4
5
6
7
var alpha = ['a', 'b', 'c'],
numeric = [1, 2, 3];

numeric[Symbol.isConcatSpreadable] = false;
var alphaNumeric = alpha.concat(numeric);

console.log(alphaNumeric); // 结果: ['a', 'b', 'c', [1, 2, 3] ]
  • Symbol.unscopables:拥有和继承属性名的一个对象的值被排除在与环境绑定的相关对象外。
  • Symbol.species:一个用于创建派生对象的构造器函数。
1
2
3
4
5
6
7
8
9
10
11
class MyArray extends Array {
// 覆盖 species 到父级的 Array 构造函数上
static get [Symbol.species]() {
return Array;
}
}
const a = new MyArray(1, 2, 3);
const mapped = a.map(x => x * x);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
  • Symbol.toPrimitive:一个将对象转化为基本数据类型的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果
const obj1 = {};
console.log(+obj1); // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ''); // "[object Object]"

// 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果
const obj2 = {
[Symbol.toPrimitive](hint) {
if (hint == 'number') {
return 10;
}
if (hint == 'string') {
return 'hello';
}
return true;
},
};
console.log(+obj2); // 10 -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ''); // "true" -- hint 参数值是 "default"
  • Symbol.toStringTag:用于对象的默认描述的字符串值。被 Object.prototype.toString() 使用。
1
2
3
4
5
6
7
class ValidatorClass {
get [Symbol.toStringTag]() {
return 'Validator';
}
}

Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]"

方法

  • Symbol.for(key):使用给定的 key 搜索现有的 symbol,如果找到则返回该 symbol。否则将使用给定的 key 在全局 symbol 注册表中创建一个新的 symbol。
1
2
3
4
5
6
7
8
9
Symbol.for('foo'); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
Symbol.for('foo'); // 从 symbol 注册表中读取键为"foo"的 symbol

Symbol.for('bar') === Symbol.for('bar'); // true,证明了上面说的
Symbol('bar') === Symbol('bar'); // false,Symbol() 函数每次都会返回新的一个 symbol

var sym = Symbol.for('mario');
sym.toString();
// "Symbol(mario)",mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串
  • Symbol.keyFor(sym):从全局 symbol 注册表中,为给定的 symbol 检索一个共享的?symbol key。
1
2
3
4
5
6
7
8
9
10
// 创建一个 symbol 并放入 Symbol 注册表,key 为 "foo"
var globalSym = Symbol.for('foo');
Symbol.keyFor(globalSym); // "foo"

// 创建一个 symbol,但不放入 symbol 注册表中
var localSym = Symbol();
Symbol.keyFor(localSym); // undefined,所以是找不到 key 的

// well-known symbol 们并不在 symbol 注册表中
Symbol.keyFor(Symbol.iterator); // undefined

唯一性

1
2
3
4
5
6
7
let a1 = Symbol('a'),
a2 = Symbol('a');

a1 == a2; // false

let b1 = Symbol('b');
a1 == b1; // false

什么情况下可以让两个 symbol 变量“相等”:Symbol.for

1
2
3
4
const a = Symbol.for('aa');
const b = Symbol.for('aa');
console.info('a===b', a === b); // a===b true
console.info('type', typeof a); // type symbol

因为 Symbol.for() 其实是带有类似重用机制的,具体的说,就是通过 Symbol.for() 创建变量时,传入的参数 ( 假设为 x ) 会作为 Symbol 变量的 key ,然后到全局中搜索,是否已经有相同 key 的 Symbol 变量,如果存在,则直接返回这个 Symbol 变量。如果没有,才会创建一个 key 为传入参数 x 的 Symbol 变量 ,并将这个变量写到全局,供下次创建时被搜索。

通过 Symbol.for() 创建的 Symbol 变量,传入的参数是否相等决定得到的 Symbol 变量是否相等。

既然通过 Symbol.for() 创建的 Symbol 变量的 key 这么重要,那我们怎么获取到这个 key 呢,那就要 Symbol.keyFor() 方法了,该函数会返回一个已经写到全局的 Symbol 变量的 key 值。这样获取到 Symbol 变量的 key, 就可以创建一个和原 Symbol 变量相等的变量了。

属性

Symbol 属性值如下图:
p-symbol.jpg

更多可见:mdn Symbol

12 WeakSet/WeakMap

WeakSetSet 类似,也是不重复的值的集合。它与 Set 有两个区别:

  • 第一,WeakSet 的成员只能是对象,而不能是其他类型的值。
  • 第二,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。即如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还存在于 WeakSet 之中。

原因:垃圾回收机制依赖引用计数,如果一个值的引用次数不为 0,垃圾回收机制就不会释放这块内存。结束使用改值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄露。而 WeakSet 里面的引用都不计入垃圾回收机制。

WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。由此可知,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,WeakSet 内部有多少成员取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,也因为垃圾运行机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。

同样的特点也适用于 WeakMap,在此不过多介绍。

应用场景:DOM、部署私有属性。更多可见:https://exploringjs.com/es6/ch_maps-sets.html#_use-cases-for-weakmaps

13 对象类型

JavaScript 中,对象可根据特征分为:

  • 宿主对象(host Objects):由 JavaScript 宿主环境提供的对象,它们的行为完全由宿主环境决定。比如 document 对象,Image()构造函数。
  • 内置对象(Built-in Objects):由 JavaScript 语言提供的对象。
    • 固有对象(Intrinsic Objects ):由标准规定,随着 JavaScript 运行时创建而自动创建的对象实例。
    • 原生对象(Native Objects):可以由用户通过 Array、RegExp 等内置构造器或者特殊语法创建的对象。
    • 普通对象(Ordinary Objects):由{}语法、Object 构造器或者 class 关键字定义类创建的对象,它能够被原型继承。

其中原生对象可见下表:

基本类型 基础功能和数据结构 错误类型 二进制操作 带类型的数组
Boolean Array Error ArrayBuffer Float32Array
String Date EvalError SharedArrayBuffer Float64Array
Number RegExp RangeError DataView Int8Array
Symbol Promise ReferenceError Int16Array
Object Proxy SyntaxError Int32Array
Map TypeError UInt8Array
WeakMap URIError UInt16Array
Set UInt32Array
WeakSet UInt8ClampedArray
Function

14 函数与 new

函数类型 new
普通函数 新对象
箭头函数 报错
方法 报错
生成器 报错
新对象
异步普通函数 报错
异步箭头函数 报错
异步生成器函数 报错

15 Array.from()

mdn Array.from>>

语法:

1
Array.from(arrayLike[, mapFn[, thisArg]])

其中参数:

  • arrayLike:想要转换成数组的伪数组对象或可迭代对象。
  • mapFn:(可选)如果指定了该参数,新数组中的每个元素会执行该回调函数。
  • thisArg:(可选)可选参数,执行回调函数 mapFn 时 this 对象。

返回:

  • 一个新的数组实例。
1
2
3
4
5
6
7
8
9
10
11
12
Array.from({ 0: 0, 1: 1, length: 2 }); // [0, 1]
Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']
Array.from(123); // []
Array.from(true); // []
Array.from(''); // []
Array.from(undefined); // Uncaught TypeError: undefined is not iterable (cannot read property Symbol(Symbol.iterator))
Array.from(Symbol(213)); // []
Array.from(new Set([1, 2, 3])); //[1, 2, 3]
let map = new Map();
map.set('one', 1);
map.set('two', 2);
Array.from(map); // [['one', 1], ['two', 2]]

将类数组转换成数组

如 arguments,DOM 集合。

1
2
3
function arguments2Arr() {
return Array.from(arguments);
}

克隆数组

1
2
3
let arr1 = [1, 2, 3];
let arr2 = Array.from(arr1);
console.log(arr1 === arr2); // false

利用递归深拷贝数组:

1
2
3
function deepCloneArr(val) {
return Array.isArray(val) ? Array.from(val, deepCloneArr) : val;
}

初始化填充数组

1
2
3
let arr1 = Array.from({ length: 3 }, () => 1); // [1, 1, 1]

let arr2 = new Array(3).fill(1); // [1, 1, 1]

如果填充内容为引用类型的时候,则表现不一样,如

1
2
3
4
5
let arr1 = Array.from({ length: 3 }, () => ({}));
let arr2 = new Array(3).fill({});

arr1[0] === arr1[2]; // false
arr2[0] === arr2[2]; // true

当然我们还可以利用 mapFn 第二个参数索引来做一些有意义的事,如

1
Array.from({ length: 5 }, (_, index) => index); // [0, 1, 2, 3, 4]

16 String.prototype.matchAll(regexp)

给定一个字符串和正则表达式,该方法返回所有与该字符串匹配正则表达式的结果的迭代器,包括捕获 groups。

如:

1
2
3
4
5
6
7
8
9
10
11
let reg = /t(e)(st(\d?))/g;

let str = 'test1test2';

let result = [...str.matchAll(regexp)];
console.log(array[0]); // ['test1', 'e', 'st1', '1'];
console.log(array[1]); // ['test2', 'e', 'st2', '2'];

// 对比match
let array2 = str.match(regexp);
console.log(array2); // ['test1', 'test2'];

17 Nullish value

一个 nullish 值要么是 null 要么是 undefined。nullish 值总是 falsy。

Nullish 的出现能减少存取值时的判断操作。

可选链式调用

可选链调用之前已经做过整理(http://blog.michealwayne.cn/2019/06/25/notes/%E3%80%90%E7%AC%94%E8%AE%B0%E3%80%91%E5%8F%AF%E9%80%89%E9%93%BE%E5%BC%8F%E8%B0%83%E7%94%A8optional-chaining/

可选链操作符( ?. )允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined

1
2
3
4
5
// old
const nestedProp = obj.first && obj.first.second;

// new
const nestedProp = obj.first?.second;

空值合并运算符??

空值合并操作符??)是一个逻辑操作符,当左侧的操作数为 Nullish value(null 或者 undefined) 时,返回其右侧操作数,否则返回左侧操作数。

1
2
3
4
5
6
7
8
9
// old
if (a !== null && a !== undefined ? a : b) {
// ...
}

// new
if (a ?? b) {
// ...
}

空值合并操作符经常用于数字金额的判断。

逻辑空赋值??=

逻辑空赋值运算符x ??= y) 仅在 x 是 Nullish value (nullundefined) 时对其赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// old
const a = {};

if (a.duration === unll || a.duration === undefined) {
a.duration = 10;
}
if (a.duration === unll || a.duration === undefined) {
a.duration = 20;
}

// new
const a = {};

a.duration ??= 10; // a.duration -> 10
a.duration ??= 20; // a.duration -> 10

18 箭头函数可能不知道的地方

name 属性

匿名函数和空箭头函数的 name 属性为"",如:

1
2
(function () {}).name; // ''
(() => 123).name; // ''

ES2015 起增加了函数名推断,可以在某些条件下检测函数名称。如

1
2
const func1 = () => 123;
func1.name; // 'func1'

在内联箭头函数中使用对象字面量可能会触发语法错误

js 认为花括号是代码块而不是对象。
如:

1
[1,2,3].map(num => { 'number': num })

这种情况下需要增加括号:

1
[1, 2, 3].map(num => ({ number: num }));

19 Object.is()

Object.is() 方法判断两个值是否为“同一个值”,返回一个 Boolean。同一个值:

  • 都是 undefined
  • 都是 null
  • 都是 truefalse
  • 都是相同长度的字符串且相同字符按相同顺序排列
  • 都是相同对象(意味着每个对象有同一个引用)
  • 都是数字且
    • 都是 +0
    • 都是 -0
    • 都是 NaN
    • 或都是非零而且非 NaN 且为同一个值

它与==以及===均不相同。==没什么好说,与===的主要区别在于:

  • === 运算符 (也包括 == 运算符) 将数字 -0+0 视为相等 ,而将Number.NaNNaN视为不相等。

polyfill

1
2
3
4
5
6
7
8
9
10
11
12
13
if (!Object.is) {
Object.is = function (x, y) {
// SameValue algorithm
if (x === y) {
// Steps 1-5, 7-10
// Steps 6.b-6.e: +0 != -0
return x !== 0 || 1 / x === 1 / y;
} else {
// Step 6.a: NaN == NaN
return x !== x && y !== y;
}
};
}

20 (ES13)类静态属性

默认情况下类的所有属性都是公共的,也就是说我们可以直接通过实例对属性进行更改。ES13 中我们可以使用 # 前缀去定义私有属性,私有属性只能在类方法中被更改。如:

1
2
3
4
5
6
7
8
9
10
class Test {
#num = 1;

set setNum (num) {
this.#num = num;
}

get getNum () {
return this.#num;
}

Ts 编译后产物:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
'use strict';
var __classPrivateFieldSet =
(this && this.__classPrivateFieldSet) ||
function (receiver, state, value, kind, f) {
if (kind === 'm') throw new TypeError('Private method is not writable');
if (kind === 'a' && !f) throw new TypeError('Private accessor was defined without a setter');
if (typeof state === 'function' ? receiver !== state || !f : !state.has(receiver))
throw new TypeError(
'Cannot write private member to an object whose class did not declare it'
);
return (
kind === 'a' ? f.call(receiver, value) : f ? (f.value = value) : state.set(receiver, value),
value
);
};
var __classPrivateFieldGet =
(this && this.__classPrivateFieldGet) ||
function (receiver, state, kind, f) {
if (kind === 'a' && !f) throw new TypeError('Private accessor was defined without a getter');
if (typeof state === 'function' ? receiver !== state || !f : !state.has(receiver))
throw new TypeError(
'Cannot read private member from an object whose class did not declare it'
);
return kind === 'm' ? f : kind === 'a' ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _Test_num;
class Test {
constructor() {
_Test_num.set(this, 1);
}
set setNum(num) {
__classPrivateFieldSet(this, _Test_num, num, 'f');
}
get getNum() {
return __classPrivateFieldGet(this, _Test_num, 'f');
}
}
_Test_num = new WeakMap();

可以看出主要通过一个局部变量_Test_num的 WeakMap 来实现私有属性的效果。

21 (ES12)数字分隔符

ES2021(ES12)中允许 JavaScript 的数值使用下划线(_)作为分隔符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let budget = 1_000_000_000_000;
budget === 10 ** 12; // true

// 不分位数
123_00 === 12_300; // true

12345_00 === 123_4500; // true
12345_00 === 1_234_500; // true

// 小数
0.000_001;

// 科学计数法
1e10_000;

但数值分隔符有几个使用注意点。

  • 不能放在数值的最前面(leading)或最后面(trailing)。
  • 不能两个或两个以上的分隔符连在一起。
  • 小数点的前后不能有分隔符。
  • 科学计数法里面,表示指数的 e 或 E 前后不能有分隔符。

因此以下声明会报错:

1
2
3
4
5
6
7
8
// 全部报错
3_.141
3._141
1_e12
1e_12
123__456
_1464301
1464301_