一、ES2017
1. 对象尾逗号
- es2017允许函数对象的定义调用时参数可以加入尾逗号,以及json对象array对象都允许
function test(num1,num2,){}
test(1,2,);
let arr = [1,2,3,]
let obj = {a:1,b:2,}
2. String.padStart和String.padEnd
- padStart:[length,string] 在字符串首位开始添加string直到满足length为止并返回新的字符串;
- 若string+原字符串的长度大于length, 则多余string不添加;
- 若string+原字符串的长度小于length, 则重复string,直到满足length;
- padEnd: [length,string] 与padStart一致 唯一区别是在末尾添加;
console.log ( "test".padStart (8,"123456789123") )//1234test
console.log ( "test".padStart (8,"12") )//1212test
console.log ( "test".padEnd (8,"123456789123") )//test1234
console.log ( "test".padEnd (8,"12") )//test1212
3. Object.values/Object.entries
- values: [obj],返回obj自身可枚举属性的属性值的集合;
- Symbol属性不可获取
- 原型、继承属性不可获取
let obj = {x:65,a:89,b:1,[Symbol()]:78}
console.log(Object.values(obj))//[65,89,1]
function fn(){}
fn.prototype.c = {x:7}
fn.b = function(){};
console.log(Object.values(fn))//[Function]
var fns = new fn();
console.log(Object.values(fns))//[]
- entries:[obj], 与values类似,返回的一个2元素的数组
- 若obj为数组,2元素数组第0项为第1项在原数组所在下标
- 若obj具有key-value的结构则2元素数组第0项为key第1项为value;
let obj = [1,2,3,]
console.log(Object.entries(obj))//[ ['0',1], ['1',2], ['2',3] ]
let obj = {a:1,b:2}
console.log(Object.entries(obj))//[ ['a',1], ['b',2] ]
ES2017总结
- Async 函数呈现更清晰的 Promise 语法
- Object.values 方法返回一个给定对象自己的所有可枚举属性值的数组,值的顺序与使用for…in循环的顺序相同(区别在于for…in循环枚举原型链中的属性)
- Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用for…in循环遍历改对象时返回的顺序一致(区别在于for…in循环也枚举原型链中的属性)
- Object.getOwnPropertyDescriptors()返回一个对象的所有自身属性的描述符(.value,.writable,.get,.set,.configurable,enumerable)
- padStart()和padEnd(),填充字符串达到当前长度
- 结尾逗号,数组定义和函数参数列表
- ShareArrayBuffer和Atomics用于从共享内存位置读取和写入
二、ES2018
1. 异步迭代
在async/await
的某些时刻,你可能尝试在同步循环中调用异步函数。例如:
async function process(array) {
for (let i of array) {
await doSomething(i);
}
}
这段代码不会正常运行,下面这段同样也不会:
async function process(array) {
array.forEach(async i => {
await doSomething(i);
});
}
这段代码中,循环本身依旧保持同步,并在在内部异步函数之前全部调用完成。
ES2018引入异步迭代器(asynchronous iterators)
,这就像常规迭代器,除了next()
方法返回一个Promise
。因此await
可以和for...of
循环一起使用,以串行的方式运行异步操作。例如:
async function process(array) {
for await (let i of array) {
doSomething(i);
}
}
2. Promise.finally()
一个Promise
调用链要么成功到达最后一个.then()
,要么失败触发.catch()
。在某些情况下,你想要在无论Promise
运行成功还是失败,运行相同的代码,例如清除,删除对话,关闭数据库连接等。
.finally()
允许你指定最终的逻辑:
function doSomething() {
doSomething1()
.then(doSomething2)
.then(doSomething3)
.catch(err => {
console.log(err);
})
.finally(() => {
// finish here!
});
}
3. Rest/Spread 属性
ES2015引入了Rest参数和扩展运算符。三个点(…)仅用于数组。Rest参数语法允许我们将一个布丁数量的参数表示为一个数组。
restParam(1, 2, 3, 4, 5);
function restParam(p1, p2, ...p3) {
// p1 = 1
// p2 = 2
// p3 = [3, 4, 5]
}
展开操作符以相反的方式工作,将数组转换成可传递给函数的单独参数。例如Math.max()
返回给定数字中的最大值:
const values = [99, 100, -1, 48, 16];
console.log( Math.max(...values) ); // 100
或者你可以使用它给函数传递参数:
restParam({
a: 1,
b: 2,
c: 3
});
function restParam({ a, ...x }) {
// a = 1
// x = { b: 2, c: 3 }
跟数组一样,Rest参数只能在声明的结尾处使用。此外,它只适用于每个对象的顶层,如果对象中嵌套对象则无法适用。
扩展运算符可以在其他对象内使用,例如:
const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { ...obj1, z: 26 };
// obj2 is { a: 1, b: 2, c: 3, z: 26
可以使用扩展运算符拷贝一个对象,像是这样obj2 = {...obj1},但是 这只是一个对象的浅拷贝。另外,如果一个对象A的属性是对象B,那么在克隆后的对象cloneB中,该属性指向对象B。
正则表达式命名捕获组(Regular Expression Named Capture Groups)
JavaScript正则表达式可以返回一个匹配的对象——一个包含匹配字符串的类数组,例如:以YYYY-MM-DD的格式解析日期:
const
reDate = /([0-9]{4})-([0-9]{2})-([0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match[1], // 2018
month = match[2], // 04
day = match[3]; // 30
这样的代码很难读懂,并且改变正则表达式的结构有可能改变匹配对象的索引。
ES2018允许命名捕获组使用符号?
,在打开捕获括号(后立即命名,示例如下:
const
reDate = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/,
match = reDate.exec('2018-04-30'),
year = match.groups.year, // 2018
month = match.groups.month, // 04
day = match.groups.day; // 30
任何匹配失败的命名组都将返回undefined。
命名捕获也可以使用在replace()
方法中。例如将日期转换为美国的 MM-DD-YYYY 格式:
const
reDate = /(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})/,
d = '2018-04-30',
usDate = d.replace(reDate, '$-$-$');
5. 正则表达式反向断言(lookbehind)
目前JavaScript在正则表达式中支持先行断言(lookahead)。这意味着匹配会发生,但不会有任何捕获,并且断言没有包含在整个匹配字段中。例如从价格中捕获货币符号:
const
reLookahead = /\D(?=\d+)/,
match = reLookahead.exec('$123.89');
console.log( match[0] ); // $
ES2018引入以相同方式工作但是匹配前面的反向断言(lookbehind),这样我就可以忽略货币符号,单纯的捕获价格的数字:
const
reLookbehind = /(?<=\D)\d+/,
match = reLookbehind.exec('$123.89');
console.log( match[0] ); // 123.89
以上是 肯定反向断言,非数字\D
必须存在。同样的,还存在 否定反向断言,表示一个值必须不存在,例如:
const
reLookbehindNeg = /(?
6. 正则表达式dotAll模式
正则表达式中点.匹配除回车外的任何单字符,标记s
改变这种行为,允许行终止符的出现,例如:
/hello.world/.test('hello\nworld'); // false
/hello.world/s.test('hello\nworld'); // true
7. 正则表达式 Unicode 转义
到目前为止,在正则表达式中本地访问 Unicode 字符属性是不被允许的。ES2018添加了 Unicode 属性转义——形式为\p{...}
和\P{...}
,在正则表达式中使用标记 u
(unicode) 设置,在\p
块儿内,可以以键值对的方式设置需要匹配的属性而非具体内容。例如:
const reGreekSymbol = /\p{Script=Greek}/u;
reGreekSymbol.test('π'); // true
此特性可以避免使用特定 Unicode 区间来进行内容类型判断,提升可读性和可维护性。
8. 非转义序列的模板字符串
最后,ES2018 移除对 ECMAScript 在带标签的模版字符串中转义序列的语法限制。
之前,\u
开始一个 unicode 转义,\x
开始一个十六进制转义,\
后跟一个数字开始一个八进制转义。这使得创建特定的字符串变得不可能,例如Windows文件路径 C:\uuu\xxx\111
。更多细节参考模板字符串。
三、ES2019
1. String.prototype.trimStart() / String.prototype.trimEnd()
在接收用户输入的文本,我们经常会把头尾的空格文本去掉,来规避展示的不受控情况。自ES5来,String.prototype.trim()
被用于去除头尾上的空格、换行符等,现在通过trimStart()
,trimEnd()
来头和尾进行单独控制。trimLeft()
、trimRight()
是他们的别名。
const string = ' Hello ES2019! ';
string.trimStart();
// 'Hello ES2019! '
string.trimEnd();
// ' Hello ES2019!'
2. Object.fromEntries()
ES2017为我们引入了Object.entries
把一个对象转为[key, value]
键值对的形式,可以运用于像 Map 这种结构中。凡事有来有回,Object.fromEntries()
用于把键值对还原成对象结构。
const entries = [ ['foo', 'bar'] ];
const object = Object.fromEntries(entries);
// { foo: 'bar' }
ES2017引入了Object.entries()
, 这个方法可以将对象转换为数组,这样对象就可以使用数组原型中的众多内置方法,比如map, filter、reduce,举个例子,我们想提取下列对象obj中所有value大于21的键值对,如何操作呢?
// ES10之前
const obj = {
a: 21,
b: 22,
c: 23
}
console.log(Object.entries(obj)) // [['a',21],["b", 22],["c", 23]]
let arr = Object.entries(obj).filter(([a, b]) => b > 21) // [["b", 22],["c", 23]]
let obj1 = {}
for (let [name, age] of arr) {
obj1[name] = age
}
console.log(obj1) // {b: 22, c: 23}
上例中得到了数组arr,想再次转化为对象,就需要手动写一些代码来处理,但是有了Object.fromEntries()
就很容易实现
// 用Object.fromEntries()来实现
const obj = {
a: 21,
b: 22,
c: 23
}
let res = Object.fromEntries(Object.entries(obj).filter(([a, b]) => b > 21))
console.log(111, res) // {b: 22, c: 23}
3. Array.prototype.flat() / Array.prototype.flatMap()
把数组展平是Array原型给我们带来的新特性,通过传入层级深度参数(默认为1),来为下层数组提升层级。如果想提升所有层级可以写一个比较大的数字甚至是Infinity
,当然不推荐这么做。
[1, 2, [3, 4]].flat();
// [ 1, 2, 3, 4 ]
[1, 2, [3, 4, [5, 6]]].flat(2);
// [ 1, 2, 3, 4, 5, 6 ]
[1, 2, [3, 4, [5, 6]]].flat()
// [1, 2, 3, 4, [5, 6]]
有了flat方法,那自然而然就有Array.prototype.flatMap()
方法,flatMap()
方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。从方法的名字上也可以看出来它包含两部分功能一个是 map,一个是 flat(深度为1)。
Array.prototype.flatMap()
它是Array.prototype.map()
和Array.prototype.flat()
的组合,通过对map调整后的数据尝试展平操作。
let arr = [1, 2, 3]
console.log(arr.map(item => [item * 2]).flat()) // [2, 4, 6]
console.log(arr.flatMap(item => [item * 2])) // [2, 4, 6]
[1, 2, [3, 4]].flatMap(v => {
if (typeof v === 'number') {
return v * 2
} else {
return v.map(v => v * 2)
}
})
// [2, 4, 6, 8]
4. catch 的参数改为可选
在进行try...catch
错误处理过程中,如果没有给catch传参数的话,代码就会报错。有时候我们并不关心错误情况,如:
const isValidJSON = json => {
try {
JSON.parse(json);
return true;
} catch (unusedError) {
// Unused error parameter
return false;
}
};
在新规范中,我们可以省略catch绑定的参数和括号。
const isValidJSON = json => {
try {
JSON.parse(json);
return true;
} catch {
return false;
}
};
5. Symbol.description
我们知道,Symbol
的描述只被存储在内部的 [[Description]],没有直接对外暴露,我们只有调用 Symbol
的 toString()
时才可以读取这个属性:
Symbol
是ES6中引入的基本数据类型,可以用作对象属性的标识符。描述属性是只读的,可用于获取符号对象的描述,更好了解它的作用。
Symbol('desc').description; // "desc"
Symbol('').description; // ""
Symbol().description; // undefined
const symbol = Symbol('This is a Symbol');
symbol;
// Symbol(This is a Symbol)
Symbol.description;
// 'This is a Symbol'
6. JSON Superset 超集
之前如果JSON字符串中包含有行分隔符(\u2028) 和段落分隔符(\u2029),那么在解析过程中会报错。
JSON.parse('"\u2028"');
// SyntaxError
现在ES2019对它们提供了支持。
JSON.parse('"\u2028"');
// ''
7. JSON.stringify() 加强格式转化
我们看一下熟知的emoji表现:
''.length;
// 2
JavaScript将emoji解释为两个字符的原因是UTF-16将emojis(以及其他不寻常的字符)表示为两个代理项的组合。我们的emoji用字符’\uD83D’和’\uDE0E’编码。但是如果试图单独编写这样一个字符,例如’\uD83D’,则会认为这是一个无效的文本字符串。在早期版本中,这些字符将替换为特殊字符:
JSON.stringify('\uD83D');
// '"�"'
现在在字符代码之前插入转义字符,结果仍是可读且有效的UTF-8/UTF-16代码:
JSON.stringify('\uD83D');
// '"\\ud83d"'
8. Array.prototype.sort() 更加稳定
之前,规范允许不稳定的排序算法,如快速排序。
let array = [
{a: 1, b: 2},
{a: 2, b: 2},
{a: 1, b: 3},
{a: 2, b: 4},
{a: 5, b: 3}
];
array.sort((a, b) => a.a - b.a);
// [{a: 1, b: 2}, {a: 1, b: 3}...] / [{a: 1, b: 3}, {a: 1, b: 2}...]
在之前的排序中,可能出现[{a: 1, b: 2}, {a: 1, b: 3}...]
、[{a: 1, b: 3}, {a: 1, b: 2}...]
等多种情况。
现在所有主流浏览器都使用稳定的排序算法。实际上,这意味着如果我们有一个对象数组,并在给定的键上对它们进行排序,那么列表中的元素将保持相对于具有相同键的其他对象的位置。
9. Function.prototype.toString() 重新修订
从ES2019开始,Function.prototype.toString()
将从头到尾返回源代码中的实际文本片段。这意味着还将返回注释、空格和语法详细信息。
function /* a comment */ foo() {}
之前,Function.prototype.toString()
只会返回了函数的主体,但没有注释和空格。
foo.toString();
// 'function foo() {}'
但现在,函数返回的结果与编写的一致。
foo.toString();
// 'function /* a comment */ foo () {}'
10. String.prototype.matchAll
如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出。
function collectGroup1 (regExp, str) {
const matches = []
while (true) {
const match = regExp.exec(str)
if (match === null) break
matches.push(match[1])
}
return matches
}
console.log(collectGroup1(/"([^"]*)"/g, `"foo" and "bar" and "baz"`))
// [ 'foo', 'bar', 'baz' ]
值得注意的是,如果没有修饰符 /g, .exec() 只返回第一个匹配。现在通过ES9的String.prototype.matchAll()
方法,可以一次性取出所有匹配。
function collectGroup1 (regExp, str) {
let results = []
for (const match of str.matchAll(regExp)) {
results.push(match[1])
}
return results
}
console.log(collectGroup1(/"([^"]*)"/g, `"foo" and "bar" and "baz"`))
// ["foo", "bar", "baz"]
上面代码中,由于string.matchAll(regex)返回的是遍历器,所以可以用for…of循环取出。
四、ES2010
1. Promise.allSettled
Promise.all()
缺陷
都知道 Promise.all()
具有并发执行异步任务的能力。但它的最大问题就是如果其中某个任务出现异常(reject),所有任务都会挂掉,Promise直接进入 reject
状态。
想象这个场景:你的页面有三个区域,分别对应三个独立的接口数据,使用 Promise.all()
来并发三个接口,如果其中任意一个接口服务异常,状态是reject
,这会导致页面中该三个区域数据全都无法渲染出来,因为任何 reject
都会进入catch回调, 很明显,这是无法接受的,如下:
Promise.all([
Promise.reject({code: 500, msg: '服务异常'}),
Promise.resolve({ code: 200, list: []}),
Promise.resolve({code: 200, list: []})
])
.then((ret) => {
// 如果其中一个任务是 reject,则不会执行到这个回调。
RenderContent(ret);
})
.catch((error) => {
// 本例中会执行到这个回调
// error: {code: 500, msg: "服务异常"}
})
我们需要一种机制,如果并发任务中,无论一个任务正常或者异常,都会返回对应的的状态(fulfilled 或者 rejected)与结果(业务value 或者 拒因 reason),在 then 里面通过 filter 来过滤出想要的业务逻辑结果,这就能最大限度的保障业务当前状态的可访问性,而 Promise.allSettled()
就是解决这问题的。
Promise.allSettled([
Promise.reject({code: 500, msg: '服务异常'}),
Promise.resolve({ code: 200, list: []}),
Promise.resolve({code: 200, list: []})
])
.then((ret) => {
/*
0: {status: "rejected", reason: {…}}
1: {status: "fulfilled", value: {…}}
2: {status: "fulfilled", value: {…}}
*/
// 过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染
RenderContent(ret.filter((el) => {
return el.status !== 'rejected';
}));
});
2. 可选链(Optional chaining)
可选链
可让我们在查询具有多层级的对象时,不再需要进行冗余的各种前置校验。
日常开发中,我们经常会遇到这种查询
var name = user && user.info && user.info.name;
又或是这种
var age = user && user.info && user.info.getAge && user.info.getAge();
这是一种丑陋但又不得不做的前置校验,否则很容易命中 Uncaught TypeError: Cannot read property...
这种错误,这极有可能让你整个应用挂掉。
用了 Optional Chaining ,上面代码会变成
var name = user?.info?.name;
var age = user?.info?.getAge?.();
可选链中的 ?
表示如果问号左边表达式有值, 就会继续查询问号后面的字段。根据上面可以看出,用可选链可以大量简化类似繁琐的前置校验操作,而且更安全。
3. 空值合并运算符(Nullish coalescing Operator)
当我们查询某个属性时,经常会遇到,如果没有该属性就会设置一个默认的值。比如下面代码中查询玩家等级。
var level = user.data.level || '暂无等级';
在JS中,空字符串、0 ,当进行逻辑操作符判时,会自动转化为 false。在上面的代码里,如果玩家等级本身就是 0 级, 变量 level 就会被赋值 暂无等级
字符串,这是逻辑错误,有时候业务上,我们只需容错取值查询到undefined
或者 null
,比如:
// {
// "level": null
// }
var level = user.level !== undefined && user.level !== null
? user.level
: '暂无等级';
来看看用空值合并运算符如何处理
// {
// "level": 0
// }
var level = user.level ?? '暂无等级'; // level -> 0
// {
// "an_other_field": 0
// }
var level = user.level ?? '暂无等级'; // level -> '暂无等级'
用空值合并运算在逻辑正确的前提下,代码更加简洁。
空值合并运算符 与 可选链 相结合,可以很轻松处理多级查询并赋予默认值问题。
4. dynamic-import
按需 import 提案几年前就已提出,如今终于能进入ES正式规范。这里个人理解成“按需”更为贴切。现代前端打包资源越来越大,打包成几M的JS资源已成常态,而往往前端应用初始化时根本不需要全量加载逻辑资源,为了首屏渲染速度更快,很多时候都是按需加载,比如懒加载图片等。而这些按需执行逻辑资源都体现在某一个事件回调中去加载。
el.onclick = () => {
import(`/path/current-logic.js`)
.then((module) => {
module.doSomthing();
})
.catch((err) => {
// load error;
})
}
当然,webpack目前已很好的支持了该特性。
5. globalThis
Javascript 在不同的环境获取全局对象有不通的方式,node 中通过 global, web中通过 window, self 等,有些甚至通过 this 获取,但通过 this 是及其危险的,this 在 js 中异常复杂,它严重依赖当前的执行上下文,这些无疑增加了获取全局对象的复杂性。
过去获取全局对象,可通过一个全局函数
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');
};
var globals = getGlobal();
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/globalThis
而 globalThis 目的就是提供一种标准化方式访问全局对象,有了 globalThis 后,你可以在任意上下文,任意时刻都能获取到全局对象。
6. BigInt
Js 中 Number类型只能安全的表示-(2^53-1)
至 2^53-1
范的值,即Number.MIN_SAFE_INTEGER 至Number.MAX_SAFE_INTEGER,超出这个范围的整数计算或者表示会丢失精度。
var num = Number.MAX_SAFE_INTEGER; // -> 9007199254740991
num = num + 1; // -> 9007199254740992
// 再次加 +1 后无法正常运算
num = num + 1; // -> 9007199254740992
// 两个不同的值,却返回了true
9007199254740992 === 9007199254740993 // -> true
为解决此问题,ES2020提供一种新的数据类型:BigInt。 使用 BigInt 有两种方式:
- 在整数字面量后面加n。
var bigIntNum = 9007199254740993n;
- 使用
BigInt
函数。
var bigIntNum = BigInt(9007199254740);
var anOtherBigIntNum = BigInt('9007199254740993');
通过 BigInt
, 我们可以安全的进行大数整型计算。
var bigNumRet = 9007199254740993n + 9007199254740993n; // -> -> 18014398509481986n
bigNumRet.toString(); // -> '18014398509481986'
注意:
- BigInt 是一种新的数据原始(primitive)类型
typeof 9007199254740993n; // -> 'bigint'
- 尽可能避免通过调用函数 BigInt 方式来实例化超大整型。因为参数的字面量实际也是 Number 类型的一次实例化,超出安全范围的数字,可能会引起精度丢失。