2021年,快速了解 ES2022 新特性(二)

前端瓶子君

共 10627字,需浏览 22分钟

 · 2021-11-17

点击上方 前端瓶子君,关注公众号

回复算法,加入前端编程面试算法每日一题群



接着上一篇快速了解ES特性之一(ES7-ES8)[3] ,今天我们主要来讲ES新特性之 ES9 。上篇中我们已经讲了ES的由来,版本对应规则等等内容,这里我就不再赘述。让我们直接开始吧

快速了解ES特性 是我的系列文章,现在已有

  1. 快速了解ES特性之一(ES7-ES8)[4]
  2. 快速了解ES特性之二(ES9)[5]

ES2018 / ES9

Lifting template literal restriction 提升模板文字限制

字符串模板语法是 ES6 的特性(很不巧的是,我根据 已完成的ES提案[6] 这里来讲的,所以没有写 ES6 相关的内容,后面会写一个来补充)。在 ES6 的版本中,我们不能在带标签的模板字符串中插入像 \unicode 之类的 错误 转义字符,如果使用这类错误的转义字符导致 bad escape sequence: \unicode 的错误。在 ES9 中则放开了这些限制,让这些错误的转义也可以正确的执行。下面我们举例说明一下

function hi(strings{
  console.log(strings);
}
let words = hi`hi, \ustar;`;
复制代码

这里的 \ustar 很明显不是一个正确的 Unicode ,我们这里使用 es-check[7] 指定 es6 来检验一下是否正确

es-check es6 test.js
// SyntaxError: Bad character escape sequence (4:21)
复制代码

很明显和我们上文中说的错误一样,无法正确识别转义字符。我们这次将 es-check[8] 版本指定为 es9 ,再次检测,no errors!

es-check es9 test.js
// ES-Check: there were no ES version matching errors!
复制代码

类似的像 \x 打头的非十六进制、 \123 这种错误的转义,现在在 ES9 中都是 ok 的。

s (dotAll) flag for regular expressions

在正则表达式模式中,我们可以使用点 . 匹配任意的单个字符。但是这里面却有两个例外,默认:

  • . 不匹配 星体字符
  • . 不匹配 行终止符

默认不匹配 星体字符,我们可以通过给正则设置 u (unicode) 这个标志来解决这个问题。但是对于 行终止符 来说,却没有一个类似的 flag 来解决这个问题。按照正常的正则语义,点 . 可以匹配任意字符,但实际上只能识别下面列出的 行终止符

  • U+000A 换行 (LF) (\n)
  • U+000D 回车 (CR) (\r)
  • U+2028 线分隔符
  • U+2029 段落分隔符

在实际的场景中,还有一些字符可以被认为是 行终止符 ,比如说:

  • U+000B 垂直制表符 (\v)
  • U+000C 换页 (\f)
  • U+0085 下一行

java 中,我们可以指定 flag Pattern.DOTALL 来让 . 匹配所有;在 C# 则用 RegexOptions.Singleline 来匹配所有。所以 es9 新增了一个 flag s,来补充上面的场景。下面我们举例来说明一下

const str = `
hello
world
`
;
const r1 = /hello.world/;
console.log(r1.test(str), r1.dotAll, r1.flags);
// false false
const r2 = /hello.world/s; // 添加 's' flag
console.log(r2.test(str), r2.dotAll, r2.flags);
// true true s
复制代码

有的同学可能会有这样的疑问,这里为啥叫 ss 代表的 singleline 不会和 m ( multiline ) 冲突吗 ?用官方的话来说:s 代表就是 singleline,也是 dotAll,含义一致。我们不好意思加一个新的标记来做这件事情。s ( dotAll )表示的是让 . 可以匹配任意字符,和 m ( multiline ) 标记不冲突。大家爱咋用就咋用 🤪

RegExp named capture groups 正则命名捕获组

这个特性就比较花哨了,在以往的代码中,假如我们要匹配一个日期中的年月日,我们可能是这样做的

const str = "2021-10-24";
const r1 = /(\d{4})-(\d{2})-(\d{2})/;
const groups = r1.exec(str);
console.log("year:", groups[1], "month:", groups[2], "day:", groups[3]);
// year: 2021 month: 10 day: 24
复制代码

没毛病啊,老铁 😁,我所知道的几门语言都是这样做的。在这个语法出来之前,我都没有思考过直接用上面的语法写有啥毛病。按照定式思维,大家都这样做,那这样做就是对的。但是这个用官方的话来说:我们如果按常规的写法来写正则,我们在使用这个 groups 的时候,假如我要获取 月份 这个值,那么就需要仔细看一下这个正则表达式,我们用 括号 包的月份这个值,数一下,在第二个,按照正则的规则,groups 的第 0 个是表达式匹配的整串,要获取后面分组出来的值需要 +1 ,所以我们要获取 月份 这个值的索引应该是 2 ,最终结果:groups[2] 。是不是很麻烦,还很容易错?我:黑人问号.jpg。要这么说也没毛病,现在我们来看看这个规范是怎么样的:在先前分组的内容前面加上一个 ?<给分组取的别名>,整的在一起就是 (?<给分组取的别名>...) 。我们把上面的示例修改一下

const str = "2021-10-24";
const r1 = /(?\d{4})-(?\d{2})-(?\d{2})/;
const exec = r1.exec(str);
console.log("year:", exec[1], "month:", exec[2], "day:", exec[3]);
console.log("year:", exec.groups.year, "month:", exec.groups.month, "day:", exec.groups.day);
// const { groups: { year, month, day } } = exec;
// console.log("year:", year, "month:", month, "day:", day);
// year: 2021 month: 10 day: 24
// year: 2021 month: 10 day: 24
复制代码

是不是花哨起来了?no,no,no。这并不是全部,我们还可以进行更花哨的匹配。如果我们需要匹配一个复杂重复字符串,比如我们需要匹配一个文本是否包含 aaabbbccc任何字符aaabbbccc任何字符aaabbbccc ,常规的写法如下

const str = 'asjdhkjlhsdkjaaabbbcccahsdkjashdjhsaaaabbbcccasjhdkljdhjkdaaaabbbcccashdlkj';
const r1 = /a{3,}b{3}c{3,}.+a{3,}b{3}c{3,}.+a{3,}b{3}c{3,}/;
const result = r1.test(str);
console.log(result);
// true
复制代码

是不是感觉上面的正则有些繁琐呢?这里我们可以使用这个命名捕获进行更加花哨的使用,这也是命名捕获的一个新特性,如果我们需要匹配一个和前某个表达式一样的结果,我们可以在表达式中用 \k 来替换,而不是重新写一次一样的表达式

const str = 'asjdhkjlhsdkjaaabbbcccahsdkjashdjhsaaaabbbcccasjhdkljdhjkdaaaabbbcccashdlkj';
const r1 = /(?a{3,}b{3}c{3,}).+\k.+\k/;
const result = r1.test(str);
console.log(result);
// true
复制代码

现在看起来是不是要好很多呢?上面的例子比较简单,如果遇上更加复杂的情况,这个特性可以帮我们少写很多重复的规则,并且更加的准确,减少出错的概率。

这个命名捕获同样适用于字符串替换,我们还是用上面的日期做示例吧。假如我们需要把一个日期从 yyyy-MM-dd 变成 dd/MM/yyyy ,传统的写法是

const str = "2021-10-24";
const r1 = /(\d{4})-(\d{2})-(\d{2})/;
const rd = str.replace(r1,"$3/$2/$1");
console.log(rd);
// 24/10/2021
复制代码

我们修改成命名捕获

const str = "2021-10-24";
const r1 = /(?\d{4})-(?\d{2})-(?\d{2})/;
const rd = str.replace(r1,"$/$/$");
console.log(rd);
// 24/10/2021
复制代码

确实更加快速确准,且不易出错

Rest/Spread Properties

ES6 中我们已经有了对数组的解构赋值的剩余元素和数组字符串的展开方法。在 ES9 中则增加了对象的解构赋值剩余属性和对象字面量的展开。这个没有啥多说的,直接举例吧

const obj = {
  x1y2z3a"x"b"y"c"z",
};
const { x, y, z, ...letter } = obj;
console.log(x, y, z, letter);
// 1 2 3 { a: 'x', b: 'y', c: 'z' }
复制代码

这个 Rest Properties ,我不知道用那个词来准确描述它,但是用起来却是很顺手。就如上面例子中的,我们从对象中解出 x, y, z ,并用 ... 这个展开符,将剩下的 a, b, c 重新组合到了新的对象 letter 中,原始的 obj 被吃光抹尽,所有都被提取出来了。所以 Rest Properties 就是让 x, y, z 出来接客,让剩下的 a, b, cletter 中休息???👾

我们接着讲展开,使用上面的例子

// ...
const objClone = {
  x, y, z, ...letter,
};
console.log(objClone);
// { x: 1, y: 2, z: 3, a: 'x', b: 'y', c: 'z' }
复制代码

这里展开符就和其义一毛一样了,我们把 letter 里面的 a, b, c ,展开成一个个的属性复制给 objClone 。这里就没有什么理解偏差,展开符嘛,不就是把这些元素啊,属性啊,拿出来给新的对象吗?

RegExp Lookbehind Assertions 正则表达式回溯断言

ES9 之前 EMACScript 正则只支持先行断言,到了 ES9 正式支持后行断言。这里我不多做展开讲,因为这个不涉及特殊的语法,后面我会单独写一篇文章来讲解 正则表达式 ,各语言基本通用,一把梭哈。

下面我就简简单单举个例子:假如我们需要匹配一个字符串 xyz 只有当它前面是 uvw 时才匹配

在没有后行断言的时候,如果要匹配,我们一般要这样写

const str = "rstuvwxyz123";
const r = /uvw(xyz)/;
const result = r.exec(str);
console.log(result);
// [ 'uvwxyz', 'xyz', index: 3, input: 'rstuvwxyz123', groups: undefined ]
复制代码

有了后行断言之后

const str = "rstuvwxyz123";
const r = /(?<=uvw)xyz/;
const result = r.exec(str);
console.log(result);
// [ 'xyz', index: 6, input: 'rstuvwxyz123', groups: undefined ]
复制代码

有的同学肯定就疑惑了?上面那种不是更简单吗 🤣 。我觉得有道理,所以我把结果打印了一手。如果我们仅仅判断一下是否匹配,确实用第一种方式,如果我们需要匹配结果,这两种写法就有了区别。如果使用后行断言,uvw 是不会出现在匹配结果中的。

上面我们讲的仅仅只是后行断言的 正面断言 ,还有与之相反的 负面断言(反向否定查找) 。我们接着使用上面的例子,但是这次我们需要的是 xyz 前面不是 uvw 时才匹配

const str = "rstuvwxyz123";
const r = /(?;
const result = r.exec(str);
console.log(result);
// null
复制代码

现在 xyz 前面紧挨着 uvw ,所以啥都匹配不到,我们修改一下,在 uvw 中间插入一个 0

const str = "rstuv0wxyz123";
const r = /(?;
const result = r.exec(str);
console.log(result);
// [ 'xyz', index: 7, input: 'rstuv0wxyz123', groups: undefined ]
复制代码

现在就我们可以匹配到了

RegExp Unicode Property Escapes 正则表达式中的 Unicode 属性转义

我们在使用正则表达式的时候,常常需要匹配出不同的语言文字特殊符号等。比如说我们常见的需求:某个表单中不能够输入 emoji 或者其他的特殊字符,一丢丢的其他语种啊之类的,按照先前的写法,我们常常得引入三方库来匹配这些 奇怪的字符 ,但是这些 奇怪的字符 的范围一直在不停的变化,所以我们引入的这些库就存在需要经常更新的问题。比如 emoji 就是更新狂魔,要匹配 emoji 就不得不不断更新 emoji 匹配库才能正确的过滤掉它们。现在我们只需要这样

const str = "hi, 🤣";
const r1 = /\p{Emoji}/gu;
console.log(r1.exec(str));
// ['🤣', index: 4, input: 'hi, 🤣', groups: undefined]
复制代码

就可以轻易的匹配到 emoji 表情,???有的同学可能直接就黑人问号.jpg了,对的,你没有看错,只需要这样你就可以匹配到 emoji 了,这里也涉及了一丢丢 Unicode Emoji 的东西,想了解详情的同学,可以在 这儿[9] 查看到详情和更多类似花括号包裹的 Emoji 这样的东西,比如更常用的 Emoji_Presentation

如果我想要匹配不是 Emoji 的呢?我们只需要把小 p 换成大 P 即可,是不是很简单呢?

const str = "hi, 🤣";
const r2 = /(\P{Emoji})*/gu;
console.log(r2.exec(str));
// ['hi, ', ' ', index: 0, input: 'hi, 🤣', groups: undefined]
复制代码

这个特性很强,但是建议先不急着用,em em em,毕竟也得看浏览器嘛。当然这个特性也不止这点东西,想要了解更多这个特性的同学,可以到这儿 MDN \- Unicode property escapes[10] 仔细研读,我也不多讲了,大家加油

Promise.prototype.finally

这个就真的没啥讲头了咯,这个方法是在 Promise then 或者 catch 执行完之后执行。一般就用来修改加载状态或者某种用完就需要关闭的资源,比如:

this.loading = true;
xxxApi
  .listUser()
  .then((resp) => {
    // do something...
  })
  .catch((e) => {
    // do something...
  })
  .finally(() => {
    this.loading = false;
  });
复制代码

不单单只是上面说的两种场景,如果我们需要在任务执行完做某件事的时候,都可以用这个方法实现。

Asynchronous Iteration 异步迭代器

ES9 中,新增了 for-await-of 的用法。对于同步的迭代器,假如我们有一个 Promise 数组,要等着 Promise 元素一个个执行完,如果按我们之前的方式

function newPromise(delay{
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`resolve:`, delay);
      resolve(delay);
    }, delay);
  });
}

async function test() {
  const arr = [newPromise(3000), newPromise(2000), newPromise(4000)];
  const before = Date.now();
  for (const item of arr) {
    console.log(Date.now(), await newPromise(1234));
    console.log(Date.now(), await item);
  }
  console.log(Date.now() - before);
}
test();
// resolve: 1234
// 1635088618117 1234
// resolve: 2000
// resolve: 3000
// 1635088619374 3000
// resolve: 4000
// resolve: 1234
// 1635088621117 1234
// 1635088622369 2000
// resolve: 1234
// 1635088622369 1234
// 1635088623616 4000
// 5500
复制代码

如果我们换成 for-await-of

async function test2() {
  const arr = [newPromise(3000), newPromise(2000), newPromise(4000)];
  const before = Date.now();
  for await (const item of arr) {
    console.log(Date.now(), await newPromise(1234));
    console.log(Date.now(), item);
  }
  console.log(Date.now() - before);
}
test2();
// resolve: 2000
// resolve: 3000
// resolve: 4000
// resolve: 1234
// 1635088545345 1234
// 1635088546584 3000
// resolve: 1234
// 1635088546584 1234
// 1635088547826 2000
// resolve: 1234
// 1635088547826 1234
// 1635088549066 4000
// 6733
复制代码

如果就单单看结果而言,基本没差,最终结果都一样。但实际的执行方式还是有差别:

  • 直接在循环中 await 的方式,是每一个独立的 Promise 的等待,在第一个的打印中我们可以看出来,所有的任务根据 delay 的时长,依次 resolve
  • 使用 for-await-of 会将数组的统一处理,然后再执行循环体内的代码。

说到底还是往事件循环里推事件的时机不同,实际业务场景中的使用,还得看各位同学的需求,不同的写法还是有差别的,别弄错了就成。

说完同步,现在我们来说说 异步迭代器 ,在 ES9 中新增了一个 Symbol.asyncIterator[11]符号用来给对象自定义默认的异步迭代器。

下面我们举个例子来创建一个异步可迭代对象

const ai = {
  // 整一个方法用来结束
  dispose() {
    this.disposed = true;
  },
  [Symbol.asyncIterator]() {
    return {
      // 下面的方法都用上箭头函数,免得手写that
      next() => {
        return new Promise((resolve, _) => {
          setTimeout(() => {
            resolve({
              done: !!this.disposed,
              valueDate.now(),
            });
          }, 200);
        });
      },
    };
  },
};

async function test() {
  for await (const it of ai) {
    console.log(it);
  }
}

test();
setTimeout(() => {
  ai.dispose();
}, 1000); // 一秒后结束

// 1635170851461
// 1635170851679
// 1635170851881
// 1635170852084
复制代码

上面的例子中,我们在一个对象中,声明了一个 Symbol.asyncIterator 的属性方法,这个属性方法返回一个对象,对象中包含一个 next 方法。看起来是不是很熟悉?我们声明一个同步迭代器,也是这样做的。再看看这个方法的返回值,如果使用同步迭代器,我们直接返回 { value , done } 即可,value 表示实际的值,done 是个布尔值,用来标记迭代器是否迭代完成。在异步迭代器中,我们返回的是一个 Promise 对象,返回的内容则是 Promise resolve({ value , done })valuedone 的含义和上述同步迭代器一致。

有的同学可能想,这么写是不是太麻烦了,我们在同步迭代器中可以这样写

const iterator = {
  [Symbol.iterator]: function* () {
    yield `a`;
    yield `b`;
    yield `c`;
  },
};
for (const it of iterator) {
  console.log(it);
}
// a
// b
// c
复制代码

那在异步迭代器中是不是也可以这样写?答案当然是肯定的咯,我们只需要在 function 前面加个 async 用来表示是个异步迭代器即可

const asyncIterator = {
  [Symbol.asyncIterator]: async function* () {
    yield `a`;
    yield `b`;
    yield `c`;
  },
};

(async () => {
  for await (const x of asyncIterator) {
    console.log(x);
  }
})();
// a
// b
// c
复制代码

看起来是不是 ojbk ?同样

functionit() {
  yield `a`;
  yield `b`;
  yield `c`;
}

for (const i of it()) {
  console.log(i);
}
// a
// b
// c
async functionait() {
  yield `a`;
  yield `b`;
  yield `c`;
}

(async () => {
  for await (const i of ait()) {
    console.log(i);
  }
})();
// a
// b
// c
复制代码

这个看起来是不是感觉N多语言都有这个?看起来就是标准的 Stream 实现,大家都大同小异,这样也挺好,方便理解嘛,一个懂了,其他的基本也就ok了

总结

文章到这儿又要和大家说再见了,寥寥几个特性有的时候真的恨不得写个几万字,因为涉及的东西实在是太多了,不是简单几句就能说明白的。但是呢,这个时间它不允许啊,我也就只能含泪和大家说晚安了。

这篇文章也写了我几天,写写这,写写那儿的,总算也是写完了。感觉这个 ES9 全是正则相关的 🤣,文章里面我给自己留了一个作业,有空水一篇正则使用的文章。这个正则我相信很多同学都是迷迷糊糊的,我见过不少N年经验的对正则也不甚了解。所以这个正则,我肯定是要水的,尽请期待一下吧,不说包会,至少能够和别人吹吹牛皮。


如果文章对您有帮助的话,欢迎 点赞评论关注收藏分享 ,您的支持是我码字的动力,万分感谢!!!🌈

如果文章内容出现错误的地方,欢迎指正,交流,谢谢😘

最后,大家可以 点击这儿[12] 加入QQ群 FlutterCandies🍭[13] 和各路大佬们进行交流哦,这里大家说话都超好听的


关于本文

来源:尽管如此世界依然美丽

https://juejin.cn/post/7023037816204427272


最后

欢迎关注【前端瓶子君】✿✿ヽ(°▽°)ノ✿
回复「算法」,加入前端编程源码算法群,每日一道面试题(工作日),第二天瓶子君都会很认真的解答哟!
回复「交流」,吹吹水、聊聊技术、吐吐槽!
回复「阅读」,每日刷刷高质量好文!
如果这篇文章对你有帮助,在看」是最大的支持
 》》面试官也在看的算法资料《《
“在看和转发”就是最大的支持


浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报