ECMAScript特性进化史ES7-ES12...持续更新

536 阅读19分钟

ECMAScript特性进化史

ES7(2016)

  • includes

  • 指数运算符

Array.prototype.includes

// method:inclues的ts定义
Array<string | number>
  .includes(searchElement: string | number, fromIndex?: number): boolean
  
// 类比indexOf去判断数组中有没有'a'
arr.indexOf('a') > -1
arr.includes('a') // 语义化更强

虽然定义里Array只能传入string和number,但是其实也能传object进去,只不过考虑object存入的不是值而是地址 ,这样会导致同样 的object对象,includes也会返回false。

指数运算符

// 指数运算符,和+ -一样的用法
2 ** 10 

// 类比Math.pow()
Math.pow(2, 10)

ES8(2017)

  • Object.values()

  • Object.entries()

  • String.prototype.padStart

  • String.prototype.padEnd

  • 函数参数尾后逗号

  • Object.getOwnPropertyDescriptors()

Object.values()

返回对象的属性值,不包括继承的值

ObjectConstructor.values<T>(o: {
    [s: string]: T;
} | ArrayLike<T>): T[]

// 返回对象的属性值,不包括继承值
const obj = {test1: 1, test2: 2}
Object.values(obj) // [1, 2]


// 刚好和Object.keys()配对,Object.keys()返回键值
Object.keys(obj) // ['test1', 'test2']
// ts定义
ObjectConstructor.keys(o: object): string[]

Object.entries()

返回一个包含对象键值的数组,每个数组里每一位对应着一个键值的数组

ObjectConstructor.entries<T>(o: {
    [s: string]: T;
} | ArrayLike<T>): [string, T][]

// 是Object.values()和Object.entries()的结合体
const obj = {test1: 1, test2: 2}
Object.entries(obj) // [['test1', 1], ['test2', 2]]

padStart和padEnd

会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串

// maxLength表示填充后字符串的最大的长度
String.padStart(maxLength: number, fillString?: string): string
String.padEnd(maxLength: number, fillString?: string): string


// 举例1:希望所有的日期字符串都是两位,那么需要在个位的时候补0
const today = '5'
today.padStart(2, '0') // '05'

// 举例2:隐藏身份证
const id = '2034399002125581'
id.slice(0, 4).padEnd(id.length, '*') // '2034************'

尾后逗号

JavaScript 一开始就支持数组字面量 中的尾后逗号,随后在ES5中将对象字面量 添加了尾后逗号。最近ES8又将其添加到函数参数 中。

作用:为了解决多人协同工作时,因为添加新属性时会在上一个属性加逗号导致git不必要的行变更。所以现在函数参数也可以加尾后逗号。

JSON不允许尾后逗号

// 最开始就有:数组字面量
let arr = [1, 2, 3,] // 等价于[1,2,3]
let sparseArr = [1, 2, 3,,,] // 不要加多了
sparseArr.length // 5

// ES5:对象字面量 
let object = {
  foo: "bar",
  baz: "qwerty",
  age: 42,
}

// ES8:函数参数
function func(p,) {} // 等价于function f(p) {}
(p,) => {} // 等价于(p) => {};
func(p,) // 等价于func(p)
// JS自带的函数调用也符合这个规则
Math.max(10, 20,); // 等价于Math.max(10, 20);


// 注意:三点运算符和无参数的时候函数不支持尾后逗号
(...p,) => {} // ❌
(,) => {} // ❌

函数参数的尾后逗号兼容性在IE9及以上。

Object.getOwnPropertyDescriptors()

用来获取一个对象的所有自身属性的描述符。比如configurable、enumerable、get和set等。

ObjectConstructor.getOwnPropertyDescriptors<T>(o: T): { [P in keyof T]: TypedPropertyDescriptor<T[P]>; } & {
    [x: string]: PropertyDescriptor;
}

const obj = {
  test: '1',
  func() { ... }
}
Object.getOwnPropertyDescriptors(obj) 
{
  test: {
   value: '1',
   configurable: true,
   enumerable: true,
   writable: true
  },
  func: {
   value: func() { ... },
   configurable: true,
   enumerable: true,
   writable: true
  }  
}

ES9(2018)

  • Promise.finally

  • 针对对象的...运算符

  • 正则表达式

    - 对匹配组命名

    - s修饰符的dotAll模式

    - 新增Unicode类写法

Promise.finally

finally方法用于指定不管 Promise 对象最后状态如何都会执行 的操作。可以用于异步操作的一些善后的操作,比如清除定时器、解绑一些东西,这时候不需要知道Promise确定后的值。所以finally里的反应函数不接收 任何参数。

finally的具体实现方式可以参见这篇文章Promise,里面详细讲解了finally的本质并给出了实现的源码。

promise
  .then(res => {})
  .catch(err => {})
  .finally(() => { dosomething... }) 

针对对象的...运算符

ES6中加入了...运算符可以直接展开数组,也可以用在函数的参数里。在ES9之后

这个不仅可以应用到数组也可以应用到对象。

// 对象中使用
const obj1 = {
    a: 1,
    b: 2,
    c: 3
  };
  const obj2 = {
    ...obj1,
    d: 4
  };
  console.log(obj2); // {a:1,b:2,c:3,d:4}

 // 函数中使用,规则和数组一样只能在参数结尾使用
test(obj1);
function test ({a, ...rest}) {
  // a = 1
  // rest = {b:2, c:3} 
} 

正则表达式

具名匹配组

在正则表达式中加上括号,使用exec可以得到匹配出括号的信息。

const re = /(\d{4})-(\d{2})-(\d{2})/;
// 三个括号匹配出三个信息
const matchObj = re.exec('2012-8-17');
const year = matchObj[1]; // 2012
const month = matchObj[2]; // 8
const day = matchObj[3]; // 17

这个匹配问题就在于只能通过数组的索引方式去取到每个匹配的值,每个组的含义从索引上看不出来。如果正则表达式变了,从索引上取值的方式还要跟着改变,比较麻烦。ES9引入具名匹配组解决这个问题,可以为每一个匹配加一个名字。

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
let result = re.exec('2015-01-02');
// result.groups.year === '2015';
// result.groups.month === '01';
// result.groups.day === '02';

// result[0] === '2015-01-02';
// result[1] === '2015';
// result[2] === '01';
// result[3] === '02';

s修饰符的dotAll模式

正则表达式里的符号.表示匹配除了行终止符 之外的所有单字符,增加修饰符s后可以使得.包含行终止符。

行终止符就是使得一行终止的符号,包括:

  • U+000A 换行符(\n

  • U+000D 回车符(\r

  • U+2028 行分隔符(line separator)

  • U+2029 段分隔符(paragraph separator)

const reg1 = /cool.bro/;
const reg1 = /cool.bro/s;
reg1.test('cool\nbro'); // false
reg2.test('cool\nbro'); // true

这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

新增Unicode类写法

在ES9中新增了使用\p{...}\P{...},允许正则表达式匹配某一类Unicode属性的所有字符。

具体可以在TC39提案里看到。

// \p{Script=Greek}指定匹配一个希腊文字母,所以匹配π成功。
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π'); // true

// 用法:unicode属性名=属性值
\p{UnicodePropertyName=UnicodePropertyValue}

\P{...}\p{...}的反向匹配,即匹配不满足条件的字符。

因为Unicode的种类繁多,这种方法会匹配到所有满足的种类。例如:

// \p{Number}可以匹配到所有类型的数字,比如罗马字符
// 匹配所有数字
const regex = /^\p{Number}+$/u;
regex.test('²³¹¼½¾'); // true
regex.test('㉛㉜㉝'); // true
regex.test('ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ'); // true

ES10(2019)

  • JSON

    - 行分割符和段分隔符在字符串中不会在JSON解析中返回SyntaxError错误

    - JSON.stringfy

  • Array

    - flat和flatMap方法

  • String

    - trimeStart和trimEnd方法

    - matchAll方法

  • Object的fromEntries方法

  • Symbol的description属性

  • Function的toString方法

  • 修改try catch的必要参数

JSON

直接在字符串中输入行分隔符和段分隔符

在JavaScript字符串中可以直接输入字符或者字符的转义形式来表示字符。比如“微”的Unicode码点是U+5fae,那么在JS代码中以下是等价的:

"微" === "\u5fae"  // true

在ES10之前,以下这5个字符只能用转义形式不能在字符串中通过直接输入来表示:

  • U+005C:反斜杠(reverse solidus)

  • U+000D:回车(carriage return)

  • U+2028:行分隔符(line separator)

  • U+2029:段分隔符(paragraph separator)

  • U+000A:换行符(line feed)

也就是说在JavaScript的字符串里想要反斜杠,要么通过\符号转义反斜杠"\\",要么用反斜杠的转义形式"\u005c"。那么问题就来了,前端不允许直接输入,但是在JSON中是允许字符串里直接输入行分隔符和段分隔符的,所以导致当前端使用JSON.parse解析后台传给前端的JSON数据时就可能会直接报错。所以在ES10允许JavaScript字符串直接输入行分隔符和段分隔符,这样就不会报错了。

// ES10之前
const json = "'\u2028'";
JSON.parse(json); // 可能报错

// ES10
console.log(eval("'\u2028'")); // 不会报错

JSON.stringfy优化

在ES6之后可以用Unicode表示字符,但是仅限于\u0000-\uffff码点之间的字符,超过范围的字符有两种表示形式:

  1. 使用两个码点拼接表示,比如“𠮷”这个字的Unicode码点是\u20bb7,直接使用JavaScript会理解为\u20bb+7,结果就变为"\u20bb7" === " 7",\u20bb对应的是一个奇怪的字符无法显示这里用空格表示,后面跟着7。所以使用双码点拼接可以表示"\uD842\uDFB7" === "𠮷"。并且这个两个码点单独或者交换顺序都是无法使用的。

  2. 也可以通过花括号{}的形式来表示字符“𠮷”"\u{20bb7}" === "𠮷"

在JSON的标准中是使用UTF-8编码的,但是JavaScript中超出\ufff的表示方式导致JSON.stringfy()有可能会返回单个码点,不满足UTF-8的编码标准。

例如:

// 对于0xD800-0xDFF的码点JSON返回单个码点
JSON.stringfy("\u{d843}"); // 返回"\ud843"
// 或者是错误的双码点表示形式,将“𠮷”的码点交换顺序
JSON.stringfy("\udfb7\ud842"); // 返回"\udfb7\ud842"

在ES10里,JSON.stringfy不会直接将0xD800-0xDFF的单个码点或者不配对的形式返回,而是将它们转义为码点字符串返回,交给使用者自己来做下一步处理。

// 对于0xD800-0xDFF的码点JSON返回单个码点
JSON.stringfy("\u{d843}"); // 返回"'\\u{d843}'"
// 或者是错误的双码点表示形式,将“𠮷”的码点交换顺序
JSON.stringfy("\udfb7\ud842"); // 返回"'\\udfb7\\ud842'"

Array

Array.prototype.flat()

顾名思义就是将嵌套的数组拍平,变成一维数组并且不影响原数组。

Array<any>.flat<A, D>(this: A, depth?: D): FlatArray<A, D>[]
depth默认为1

// flat方法会自动去掉数组的空项
const arr = [1,[2,[3]],,[4]];
arr.flat(); // [1,2,[3],4] 不输入参数默认是遍历一层
arr.flat(2); // [1,2,3,4]
arr.flat(Infinity); // 展开任意深度的数组

Array.prototype.flatMap()

flatMap相当于对数组中的每一个元素都执行一次函数(类似于Array.prototype.map()),然后对每个元素执行一次flat()),方法传递的参数也和map方法一样。

flatMap(callback: (this: undefined, value: any, index: number, array: any[]) => any, thisArg?: undefined): any[]

arr.flatMap((val, index, arr) => {
  // do something...
});
// 举例
const arr = [1,2,3];
arr.flatMap((val) => [val, val*2] ); // [1,2,2,4,3,6]

String

trimStart和trimEnd

对于trim()大家都不陌生,就是去除空白字符的,这两个方法用来去除字符串的首位空白字符的。

matchAll

matchAll方法是返回一个正则表达式在当前字符串的所有匹配,和match不同的是match方法返回的是匹配结果的数组,而matchAll返回的是一个迭代器(Iterator),是RegExp类的exec方法的集合。

String.matchAll(regexp: RegExp): IterableIterator<RegExpMatchArray>
RegExp.exec(string: string): RegExpExecArray

// exec方法
const str = "test1test2";
const reg /test(\d?)/;
// 无论reg加不加全局标志g,exec都只会返回第一个匹配组
reg.exec(str); // ["test1","1",index:0,input:"test1test2",groups:undefined]


// matchAll是所有exec的结果的组合,是一个正则表达式
const matches = str.matchAll(reg); // RegExpStringIterator {}
// 可以用for...of循环遍历取出结果,也可以使用...运算符将迭代器转化为数组
for (const match of matches) {
  console.log(match); 
  //得到: ["test1","1",index:0,input:"test1test2",groups:undefined]和["test2","2",index:5,input:"test1test2",groups:undefined]
}
...str.matchAll(reg);
Array.from(str.matchAll(reg));

Object.fromEntries()

Object.entries是将对象转化为键值对形式的数组,而Object.fromEntries是将键值对形式的结构转化为对象。

ObjectConstructor.fromEntries<T = any>(entries: Iterable<readonly [string | number | symbol, T]>): { [k: string]: T; } 

// 先看Object.entries
const obj = {
  a: 'somestring',
  b: 42
};
Object.entries(obj); // [['a', 'something'], ['b', 42]]

// Object.fromEntries是entries的反向
const arr = [['a', 'something'], ['b', 42]];
Object.fromEntries(arr); // {a: 'somestring', b: 42}

// 看上面的TS结构,fromEntries里接收Iterable结构
// 比如:可以将Map转为对象
const map = new Map();
map.set('a', 'something');
map.set('b', 42);
Object.fromEntries(map); // {a: 'somestring', b: 42}

Symbol的description属性

在ES10之前,对于Symbol的类型,在Symbol('description')里传入对当前Symbol变量的描述,但是创建之后访问描述需要将Symbol转为字符串,然后进行字符串处理才可以。ES10增加了description属性直接访问。

const s = new Symbol('s1');
s.description; // 's1'

Function的toString返回更多信息

Function的toString方法不只是返回函数本身,函数里的注释和空格都会返回,保持和原来的一模一样的格式。

try catch参数绑定修改

以前的try catch必须要绑定error参数,ES10之后变为可选。

// 之前的try catch大家一般这么用
try {
  nonExistentFunction();
}
catch(error) {
  console.error(error);
  // expected output: ReferenceError: nonExistentFunction is not defined
  // Note - error messages will vary depending on browser
}

// 现在可以选择省略error参数
try {
  doSomethingThatMightThrow();
} catch { // → No binding or parameter!
  handleException();
}

ES11(2020)

  • 可选链运算符——?.
  • null判断运算符——??
  • 新的基本数据类型BigInt
  • globalThis顶层对象

可选链运算符(optional chaining)

如果需要取一个嵌套很多层的对象属性,比如a.b.c.d,如果在d属性之前有元素是undefined或者null就会导致整个程序中断并抛出Uncaught TypeError: Cannot read properties of undefined。

为了不出现这个情况,我们一般会采用下面这两种方式保证程序不中断:

// 1. 每一层做判断
a && a.b && a.b.c && a.b.c.d || 'default value'
​
// 2.三元运算符
const val = a ? a.b : 'default value'

这两种方式都非常麻烦,所以可选链运算符?.就是来解决这个问题的。

用可选链上述可以改写为:

// 可选链方式改写
a?.b?.c?.d || 'default value'

为了能快速理解,可以拆开理解这个运算符(非官方理解,自己瞎想的):用来判断左边的对象是否不存在(undefined或者null),如果是就直接返回undefined ,不再继续运算下去。.和之前一样是用来取下一层值的。也就是说任意用.运算符的地方都可以利用这个可选链,所以,同理,这个不仅可以应用到属性,也可以应用到对象的方法中。

以下是可选链的几种用法:

a?.b // a == null ? undefined : a.b
a?.[x] // a == null ? undefined : a[x]
a?.b() // a == null ? undefined : a.b() 这个只能证明b存在,不能证明b是函数,所以调用也有可能报错
a?.() // a == null ? undefined : a() 同上,也有可能报错// 官方proposal写了这三种使用方式
obj?.prop       // optional static property access
obj?.[expr]     // optional dynamic property access
func?.(...args) // optional function or method call// 对于函数来说
if (myForm.checkValidity?.() === false) {
  // 表单校验失败
  return; // 如果老式浏览器没有checkValidity方法就返回undefined不会报错
}
​

链式运算符还有一些场景需要注意,比如:

delete a?.b // 等价于 a == null ? undefined : delete a.b
// 如果a不存在三元运算符只会运行左边,直接返回undefined不会走到右边deletea?.[++b] // a == null ? undefined : a[++b]
// 所以a如果不存在的话,b也不会自增运算,理由同上

目前optional chaining已经进入stage 4,chrome已经支持,放心使用。下面是兼容性展示。

image.png

null判断运算符

在上面的可选链操作中,我们写了一些这样的代码:

a && a.b && a.b.c && a.b.c.d || 'default value'
a?.b?.c?.d || 'default value'
​

在添加默认值时都使用||运算符,不过这个运算符在左边的值为false和0的时候也会返回default value。有时候,开发人员写这个是想实现如果左边的值不存在(undefined和null)的情况下返回默认值,但是||并不能完全实现我们的意图,所以??运算符就是用来解决这个问题,代替||运算符,只会在左边是undefined和null的情况下才返回默认值。

上述代码改为:

a?.b?.c?.d ?? 'default value';
​
// 这个运算符很适合判断函数参数是否赋值
// 如果使用||的话就不太对,false和0也算赋值
function Test (props) {
  const enable = props.enabled ?? true;
}

??非常适合和?.一起使用。

有一个需要注意的问题:??有一个运算优先级问题,它与&&||的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。

bigInt

ES6多了symbol,新增BigInt之后,现在ES10基本数据类型变为7种:undefined/null/string/number/boolean/symbol/bigint。

globalThis顶层对象

以下来自阮一峰的ECMAScript6教程

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
  • Node 里面,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

  • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
  • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
  • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。

下面是在globalThis提出之前拿到顶层对象的两种兼容方法:

// 方法一
(typeof window !== 'undefined'
   ? window
   : (typeof process === 'object' &&
      typeof require === 'function' &&
      typeof global === 'object')
     ? global
     : this);
// 方法二
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');
};

在ES11里引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this。就不需要写上面那么代码做判断。

ECMA2021持续进展

根据TC39近期的一些提案,接下来介绍一些从stage3 → stage4的proposal,其实到了stage4的阶段就很有可能加入到ECMA的最新标准中。

at方法

在别的很多语言中,我们访问数组最后一个元素一般都是通过arr[-1]的方式,-1表示反向的索引 ,但是在JavaScript里只能通过arr[arr.length - 1]的方式。这是因为在JavaScript里,数组arr里的取值代表是属性值 ,并不是一个索引值,所以在JavaScript中使用arr[-1]不会报错,只是有可能得到的结果不是我们期待的结果。

这次的提案提出的at方法,用来解决Array和String无法使用负索引的问题。可以使用它访问任意的索引(无论正负)。

at(index: number): number

const arr = [1, 2];
arr.at(-1); // 2

const str = 'helloworld';
str.at(-1); // 'd'

该特性已经在Chrome92上 默认启用。

await顶层使用

在学习async执行异步操作时,都会强调await是一定要在async函数里使用的。

await test(); // ❌,不在async函数内
async function fun() {
  await test(); // 正确使用
}

在某些场景下,需要在没有async函数的情况下调用异步方法执行一些异步逻辑的时候,因为这个限制只能写一个立即调用的异步函数来解决:

// 对导出的value进行异步处理
import { setTimeout } from 'timers/promises';
let value;
export { value };

// 立即执行异步函数
(async () => {
  await setTimeout(100);
  value = 'test';
  throw new Error('err');
})();

这样写的话,有很多问题:

  • 因为自执行函数的原因导入这个模块的地方无法捕获异步抛出的错误

  • 导入的地方无法知晓这个函数什么时候执行结束。

更健壮的做法是将自执行异步函数返回的Promise值作为模块导出,让使用的地方通过Promise值来保证异常处理和逻辑顺序执行。

// a.mjs
import { setTimeout } from 'timers/promises';
let value;
export { value };

export default (async () => {
  await setTimeout(100);
  value = 'test';
})();

// 在b.mjs引用时
import promise, { value } from './a.mjs';
export function outputValue() { 
  return value; 
}

promise.then(() => {
  // 使用promise来输出异步的值,确保执行结束
  console.log(outputValue());
}); 

这样写使得所有需要模块导出值的地方需要写Promise等待异步完成。

本次提案使得await可以顶层使用,下面看写法:

// 对导出的value进行异步处理
// a.mjs
import { setTimeout } from 'timers/promises';
let value;
export { value };
// 直接使用await
await setTimeout(100);
value = 'test';

// b.mjs
import { value } from './a.mjs';
export function outputValue() { 
  return value; 
}
console.log(outputValue());// 可以直接输出

ES12(2021)

2022.08.22添加

数字分隔符

类似于数字里的,分隔符1,000,000,ES12允许数字用下划线_进行分割。这样子当数字比较大的时候,更加方便去辨认。这个分隔符只是为了展示并不是数字拼接上了_符号,对数字进行toString后也是正常显示。

const num = 7_500_054_112;// 等价于7500054112
num.toString(); //'7500054112'

Promise.any and Promise.allSettled

原来Promise有两个方法叫Promise.allPromise.race,这是在ES2015添加的。Promise.allSettled是添加在ES2020里的,放在这里说是为了比较。这次ES2021添加的方法是Promise.any,后续添加的两个方法是对之前两种方法的场景补充。

👇🏻具体对比在下面的表格 image.png

Promise.any vs. Promise.race

首先说Promise.race(iterable),race是竞争的意思,也就是说只要一旦迭代器中的某个 promise 成功(fulfilled)或失败(rejected) 了,就会直接返回这个结果。就像一起竞争一样,比赛出谁先第一个进行状态的转变。

再对比新加的Promise.any,返回的是第一个成功的结果,如果全部都失败才会返回失败的结果,并且是第一个失败的结果。

Promise.all vs. Promise.allSettled

首先说Promise.all,all就是全部的意思,也就是会返回一个数组包含所有promise成功的结果,但是一旦有失败就会停止然后返回失败的结果,不管其他promise有没完成。

再说Promise.allSettled,返回一个在所有给定的 promise 都已经fulfilledrejected后的 promise,也是说就算失败也是所有都失败的结果,相当于得到每一个结果。

.