前言
Ecma International
Ecma International 是一家国际性会员制度的信息和电信标准组织,它是和企业密切相连的组织,所以Ecma国际制定的规范标准都是由各类企业来做主要的制定和推广。 ECMA-262是Ecma组织下的TC39小组制定的关于脚本语言的规范标准。
TC39
TC39 是所属于 Ecma International,由 JavaScript 开发者、实现者、学者等组成的团体,与 JavaScript 社区合作维护和发展 JavaScript 的标准。 TC39委员会部分成员名单
TC39 提案过程
每个 ECMAScript 的提案都将经过以下阶段,从 Stage 0 开始,从一个阶段到下一个阶段的进展必须得到 TC39 的批准:
- stage-0:还是一个设想,只能由TC39成员或TC39贡献者提出;
- stage-1:提案阶段,比较正式的提议,只能由TC39成员发起,这个提案要解决的问题必须有正式的书面描述;
- stage-2:草案,有了初始规范,必须对功能语法和语义进行正式描述,包括一些实验性的实现;
- stage-3:候选,该提议基本已经实现,需要等待实验验证,用户反馈及验收测试通过;
- stage-4:已完成,必须通过 Test262 验收测试,下一步就纳入ECMA标准。
ECMA-262
ECMA-262标准定义了ECMAscript
语言规范。这个这个标准也叫成为ECMAscript
语言规范(ECMAScript Language Specification),简称ES规范。
1997年ECMA组织发布了MCMA-262的标准,该标准制定了MCMAscript
语言规范。以此语言规范为蓝本,TC39委员会制定了ECMAscript
规范并在当年正式发布,这标志着ES规范的诞生。
ES规范从1997发布到现在为止已经拥有十一个版本。ECMAscript
是基于几种原始技术,最著名的是javascript
(netscape navigator 2.0)、jscript
(microsoft ie3)、ECMA-262第六版 也就是我们常说的ES6
(或者叫ES2015
)。
ECMAScript和JavaScript
根据上面分析可以得到了如下结论:
- ECMA-262是ECMA国际组织发布的一个技术标准。
- ECMA-262中,为ECMAScript这门编程语言定义了规范。
- JavaScript是一门遵循了 ECMAScript编程语言规范的编程语言。
那ES5,ES6,ES7...又是什么呢?
答:它们是ECMA-262标准的版本号。 2015年ECMA国际决定每年发布一个ECMAScript版本,所以ES6被重命名为ES2015,用来体现发布的年份,因此ES6和ES2015是一个意思!以后每年的版本我们称为ES2016
(ES7
),ES2017
(ES8
),ES2018
(ES9
)。
ES2020
globalThis
globalThis——通用顶层对象,是在任何环境下指向全局环境下的this。
before
- Browser:顶层对象是window;
- Node:顶层对象是global;
- WebWorker:顶层对象是self。
now
直接使用通用顶层对象:globalThis
。在上述三种环境中均可直接使用globalThis
获得全局this
。
因此,globalThis
目的是提供一种标准化方式访问全局对象,有了 globalThis
后,你可以在任意上下文,任意时刻都能获取到全局对象。
如果您在浏览器上,globalThis
将为window
,如果您在Node
上,globalThis则将为global
。因此,不再需要考虑不同的环境问题。
globalThis
是一个全新的标准方法用来获取全局this
。
// 在全局对象中挂载一个`v`属性,赋值为`true`:
// es11之前的解决方案
const 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");
};
getGlobal().v = true;
console.log(getGlobal().v); // true
// es11用如下方式定义
globalThis.v1 = true;
console.log(globalThis.v1); // true
BigInt
BigInt大整数——任意位数的整数(新增的数据类型,使用n结尾)。
在javascript整数运算中,当计算值和结果值在Number.MIN_SAFE_INTEGER
至Number.MAX_SAFE_INTEGER
范围时才能保证运算结果正确,超出这个范围的整数计算或者表示会丢失精度。
因此es11加入了BigInt
作为第七个原始数据类型,用于精确计算大整数运算。
一些BigInt方法
- BigInt():转换普通数值为BigInt类型;
- BigInt.parseInt():近似于Number.parseInt(),将一个字符串转换成指定进制的BigInt类型。
注意点;
- BigInt同样可使用各种进制表示,都要加上后缀;
- BigInt与Number是两种值,它们之间并不是全等的,可以进行比较,但不能进行计算;
- typeof运算符对于BigInt类型的数据返回bigint。
let bigintA = BigInt(Number.MAX_SAFE_INTEGER);
let intB = Number.MAX_SAFE_INTEGER;
console.log(bigintA); // 9007199254740991n
console.log(bigintA == intB);// true
console.log(bigintA === intB);// false
console.log(bigintA * 2n); // 18014398509481982n
可选链操作符(?.)
可选链操作符(?.
),当我们想要查找多层级的对象时,需要进行冗余的前置校验进行属性保护,否则容易报错导致程序停止运行。
而使用es11中的链判断操作符可以大大简化这种前置校验:
const user = {
info: {
name: "hahahh",
},
};
// 在es11之前的写法
let name1 = user && user.info && user.info.name;
//es11之后的写法
let name2 = user?.info?.name;
console.log(name1); // hahahh
console.log(name2); // hahahh
user.info = null;
name1 = user && user.info && user.info.name;
name2 = user?.info?.name;
console.log(name1); // null
console.log(name2); // undefined
空值合并运算符(??)
空值合并操作符(??
)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。
当我们查询某个属性时,经常会给没有该属性就设置一个默认的值,比如下面两种方式:
let a = 0,
b = true;
let c = a ? a : b; // 方式1-三目运算符
let d = a || b; // 方式2-逻辑运算符||
console.log(c);// true
console.log(d);// true
但是上面两种方法有一个明显的弊端,它会覆盖所有的假值(0 , ' ' , false
),因为左侧的操作数会被强制转换成布尔值用于求值,而这些值在某些情况下可能是有效的,使用空位合并操作符即可避免这种问题。其只有在第一个操作数为 undefined
或 null
,才会返回其右侧表达式或变量的值。
let a = 0,
b = true;
let c = a ? a : b; // 方式1-三目运算符
let d = a || b; // 方式2-逻辑运算符||
console.log(c); // true
console.log(d); // true
let e = a ?? b
console.log(e); // 0
import()动态导入
import是es6中引入的全新指令,是esm规范的基础指令。
现代前端的打包资源越来越大,这些引入的资源越多,首屏加载的速度就会越慢,而前端应用初始化时并不需要全量的加载逻辑资源,,为了首屏渲染速度更快,就可以把这些模块进行按需导入。在之前,我们一般会通过webpack等打包工具对js module进行split,然后使用异步import进行加载(原理是重新发起一个get请求获取js脚本)。import()
指令的出现进一步规范这种异步加载能力!
用法
关键字import可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个promise
。默认地,导入的模块会在挂载在promise
对象的default
属性下。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>dynamic-import</title>
</head>
<body>
<script>
function handleClick() {
import("./module.js")
.then((module) => {
console.log(module.default());
})
.catch((err) => {
console.error("error~");
});
}
</script>
<button onclick="handleClick()">按需加载</button>
</body>
</html>
当然,可以使用async-await
语法糖来解析import()
返回的promise
对象:
<script>
async function handleClick() {
const fn = await import("./module.js");
fn.default();
}
</script>
特点
与import声明相比,import()有如下特点如下:
- 能够在函数、分支等非顶层作用域使用,按需加载、懒加载都不是问题;
- 模块标识支持变量传入,可动态计算确定模块标识;
- 不仅限于module,在普通的script中也能使用。
注意:虽然长的像函数,但import()实际上是个操作符,因为操作符能够携带当前模块相关信息(用来解析模块表示),而函数不能。
适用场景
- 按需加载(比如点击时加载某个文件或者模块);
- 条件加载(比如if判断模块中);
- 动态的模块路径(比如模块路径是实时生成)。
如果希望按照一定的条件或者按需加载模块的时候,动态
import()
是非常有用的。而静态型的import
是初始化加载依赖项的最优选择,使用静态import
更容易从代码静态分析工具和tree shaking
中受益。
for-in遍历顺序
for-in操作符一般用来遍历对象的key。遍历操作必然涉及到遍历顺序,在此之前for-in的遍历顺序各浏览器厂商是有一些差异的。es11规范了这一点,保证了不同浏览器引擎的遍历结果一致!
规则
以遍历对象为例, 会先遍历出整数属性,并且以升序的顺序遍历得到对象的key,然后其他属性按照创建时候的顺序进行遍历:
const obj = {
x2: "x2",
x1: "x1",
3: "3",
1: "1",
2: "2",
"3xx": "3xx",
};
for (let key in obj) {
console.log(key); // 1 2 3 x2 x1 3xx
}
补充
for-in
遍历对象要点:
- 遍历不到 Symbol 类型的属性;
- 遍历过程中,目标对象的属性能被删除,忽略掉尚未遍历到却已经被删掉的属性;
- 遍历过程中,如果有新增属性,不保证新的属性能被当次遍历处理到;
- 属性名不会重复出现(一个属性名最多出现一次);
- 目标对象整条原型链上的属性都能遍历到。
String.prototype.matchAll
字符串处理的一个常见场景是想要匹配出字符串中的所有目标子串,然后进行输出或替换。 es11里给String对象新增了一个原型方法:matchAll,可以一次性取出所有匹配。语法:
str.matchAll(regexp)
- 参数:regexp
正则表达式对象。如果所传参数不是一个正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp 。- 返回值
一个迭代器(不可重用,结果耗尽需要再次调用方法,获取一个新的迭代器)。- 注意
RegExp必须是设置了全局模式g的形式,否则会抛出异常TypeError。
matchAll()与match()的区别
matchAll()
不用正则也会匹配所有符合的字符串(如果使用正则,不能使用不带g修饰符的正则,否则会报错);match()
使用带g的正则表达式则才能可以匹配所有,否则只匹配第一个;- 在有多个匹配时
matchAll()
不像match()
一样返回数组,而是返回一个迭代器,可以使用for-of
遍历、数组新增的扩展符(…
)结果或Array.from()
转换为数组。
const str = "<text>JS</text><text>正则</text>";
const matchStr = str.match(/<text>(.*?)<\/text>/);
const allMatchs = str.matchAll(/<text>(.*?)<\/text>/g);
console.log(matchStr[0]);
console.log(allMatchs);
for (const match of allMatchs) {
console.log(match[0]);
}
结果:
Promise.allSettled()
Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。
当有多个彼此不依赖的异步任务成功完成时,或者您总是想知道每个promise的结果时,通常使用它。
相比之下,Promise.all() 更适合彼此相互依赖或者在其中任何一个reject时立即结束。
示例:(实现一个filter,过滤掉rejected状态的Promise)
async function fulfilled() {
return Promise.allSettled([
Promise.reject({ code: 500, msg: "服务异常" }),
Promise.resolve({ code: 200, list: [] }),
Promise.resolve({ code: 200, list: [] }),
]).then((res) => {
console.log("all = ", res);
// 过滤掉 rejected 状态
return new Promise((resolve) => {
resolve(
res.filter((el) => {
return el.status !== "rejected";
})
);
});
});
}
fulfilled().then((res) => {
console.log("fulfilled = ", res);
});
结果:
ES2021
WeakRef
WeakRef对象允许保留对另一个对象的弱引用,而不会阻止被弱引用对象被GC回收。
描述
WeakRef对象包含对对象的弱引用,这个弱引用被称为该WeakRef
对象的target
或者是referent
。对对象的弱引用是指当该对象应该被GC回收时不会阻止GC
的回收行为。而与此相反的,一个普通的引用(默认是强引用)会将与之对应的对象保存在内存中。只有当该对象没有任何的强引用时,JavaScript引擎GC
才会销毁该对象并且回收该对象所占的内存空间。如果上述情况发生了,那么你就无法通过任何的弱引用来获取该对象。
比如储存DOM
节点,而不用担心这些节点被移除时,会引发内存泄漏。必须使用new
关键字创建新的WeakRef
,并把对象作为参数放入括号内,当你想读取被引用的对象时,可以在弱引用对象上调用deref()
方法来实现,下面是一个很简单的例子:
let obj = {
name: "Cache",
size: "unlimited",
};
const myWeakRef = new WeakRef(obj);
console.log(myWeakRef.deref());
console.log(myWeakRef.deref().name);
console.log(myWeakRef.deref().size);
结果:
FinalizationRegistry
FinalizationRegistry
(终结器)对象可以让你在对象被垃圾回收时请求一个回调。
FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,执行该对象拥有的终结器回调方法,即执行清理回调(清理回调有时被称为 finalizer )。
使用方法
- 创建
registry
:
const registry = new FinalizationRegistry(heldValue => {
// ....
});
- 调用
register
方法,注册任何你想要清理回调的对象,传入该对象和所含的值:
registry.register(theObject, "some value");
- 当需要解除对theObject对象的清理回调时:
registry.unregister(theObject);
简单示例
const a = {};
const obj = new WeakRef(a);
const fina = new FinalizationRegistry((v) => {
console.log(v);
});
fina.register(a, "a被回收了");
console.log(a); // {}
/**** 假设此刻a被回收 ****/
// 首先打印:a被回收了
console.log(obj.deref()); // undefined
String.prototype.replaceAll()
replaceAll() 方法返回一个新字符串,新字符串所有满足 pattern 的部分都已被replacement 替换。pattern可以是一个字符串或一个 RegExp, replacement可以是一个字符串或一个在每次匹配被调用的函数。
语法
const newStr = str.replaceAll(regexp|substr, newSubstr|function)
注意:当使用一个
regex
时,您必须设置全局(“ g”)标志, 否则,它将引发 TypeError:“必须使用全局 RegExp 调用 replaceAll”。
示例
let str = "a+b+c";
let s1 = str.replace(/\+/g, " ");
let s2 = str.split("+").join(" ");
let s3 = str.replaceAll("+", " ");
console.log(s1);
console.log(s2);
console.log(s3);
结果:
逻辑赋值操作符
逻辑赋值运算符结合了逻辑运算(&&,||或??)和赋值运算符的功能。
我们知道,下面的代码:
let a = 0;
a = a + 2;
可以简写为:
let a = 0;
a += 2;
ES2021中将逻辑运算符||
,&&
,??
和=
运算符结合,得到了||=
,&&=
,??=
三个逻辑赋值操作符:
let a = true,
b = false,
c;
a ||= b; // 等同于 a = a || b,true
a &&= b; // 等同于 a = a && b,false
c ??= b; // 等同于 c = c ?? b,false
Promise.any()
Promise.any() 接收一个Promise
可迭代对象(数组、Set、Map),只要其中的一个promise
成功,就返回那个已经成功的promise
。如果可迭代对象中没有一个promise
成功(即所有的promises
都失败/拒绝),就返回一个失败的promise
和AggregateError
类型的实例,它是Error
的一个子类,用于把单一的错误集合在一起。这个方法可以看做是Promise.all()
的反操作。
简单示例:
const promise1 = new Promise((resolve, reject) => {
console.log("promise1 done!");
reject("我promise1失败了");
});
const promise2 = new Promise((resolve, reject) => {
console.log("promise2 done!");
setTimeout(resolve, 500, "我promise2成功了");
});
const promise3 = new Promise((resolve, reject) => {
console.log("promise3 done!");
setTimeout(resolve, 100, "我promise3成功了");
});
Promise.any([promise1, promise2, promise3]).then((value) => {
console.log("promise.any = ", value);
});
结果:
如果没有成功的 promise,Promise.any()返回AggregateError错误:
const promise1 = new Promise((resolve, reject) => {
console.log("promise1 done!");
reject("我promise1失败了");
});
const promise2 = new Promise((resolve, reject) => {
console.log("promise2 done!");
reject("我promise2失败了");
});
const promise3 = new Promise((resolve, reject) => {
console.log("promise3 done!");
reject("我promise3失败了");
});
Promise.any([promise1, promise2, promise3]).then((value) => {
console.log(value);
});
结果:
数字分隔符
通过这个功能,我们利用 “(_
【U+005F
】)“ 分隔符来将数字分组,提高数字的可读性:
let x = 233_333_333;
// x 的值等同于 233333333,只是这样可读性更强
console.log(x);
Intl.ListFormat
Intl.ListFormat 是一个语言相关的列表格式化构造器。语法:
new Intl.ListFormat([locales[, options]])
ListFormat 对象的构造方法有两个参数,皆为可选。
locales:是一个语言标识,符合 BCP 47 语言标注的字符串或字符串数组,可选值有en、zh-Hans-CN、it、de等等;
options:第二个参数是一个选项对象 -- 包含了localeMatcher、style 和 type 三个属性。
示例:
const arr = ["Pen", "Pencil", "Paper"];
let obj = new Intl.ListFormat("en", { style: "short", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil, & Paper
obj = new Intl.ListFormat("zh-Hans-CN", {
style: "short",
type: "conjunction",
});
console.log(obj.format(arr)); // Pen, Pencil, Paper
obj = new Intl.ListFormat("en", { style: "long", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil, and Paper
obj = new Intl.ListFormat("en", { style: "narrow", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil, Paper
// 传入意大利语标识
obj = new Intl.ListFormat("it", { style: "short", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil e Paper
// 传入德语标识
obj = new Intl.ListFormat("de", { style: "long", type: "conjunction" });
console.log(obj.format(arr)); // Pen, Pencil e Paper
结果: