即将到来的 ECMAScript 2022 新特性

大家好,我是若川。持续组织了5个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。

ECMAScript 规范每年都会更新一次,正式标准化 JavaScript 语言的 ECMAScript 的下一次年度更新将在 2022 年 6 月左右获得批准。自 2015 年以来,TC39 团队成员每年都会一起讨论可用的提案,并发布已接受的提案。今年是 ECMAScript 的第 13 版,其中包括许多实用的功能。所有在 2022 年 3 月之前达到阶段 4 的提案都将包含在全新的 ECMAScript 2022 标准中。

对于一个提案,从提出到最后被纳入ES新特性,TC39的规范中分为五步:

  • stage0(strawman),任何TC39的成员都可以提交。

  • stage1(proposal),进入此阶段就意味着这一提案被认为是正式的了,需要对此提案的场景与API进行详尽的描述。

  • stage2(draft),演进到这一阶段的提案如果能最终进入到标准,那么在之后的阶段都不会有太大的变化,因为理论上只接受增量修改。

  • state3(candidate),这一阶段的提案只有在遇到了重大问题才会修改,规范文档需要被全面的完成。

  • state4(finished),这一阶段的提案将会被纳入到ES每年发布的规范之中

目前,一些提案还处于第三阶段,一些提案已经进入第四阶段。提案的功能将在达到第 4 阶段后被添加到新的ECMAScript标准中,这意味着它们已获得 TC-39 的批准,通过了测试,并且至少有两个实现。下面就来看看 ECMAScript 2022 预计会推出的新功能吧~

一、Top-level Await

在ES2017中引入了 async 函数和 await 关键字,以简化 Promise 的使用,但是 await 关键字只能在 async 函数内部使用。尝试在异步函数之外使用 await 就会报错:SyntaxError - SyntaxError: await is only valid in async function。

顶层 await 允许我们在 async 函数外面使用 await 关键字,目前提案正处于第 4 阶段,并且已经在三个主要的浏览器的 JavaScript 引擎中实现,模块系统会协调所有的异步 promise。

顶层 await 允许模块充当大型异步函数,通过顶层 await,这些ECMAScript模块可以等待资源加载。这样其他导入这些模块的模块在执行代码之前要等待资源加载完再去执行。下面来看一个简单的例子。

由于 await 仅在 async 函数中可用,因此模块可以通过将代码包装在 async 函数中来在代码中包含 await:

// a.jsimport fetch  from "node-fetch";let users;export const fetchUsers = async () => {const resp = await fetch('https://jsonplaceholder.typicode.com/users');users =  resp.json();}fetchUsers();export { users };
// usingAwait.jsimport {users} from './a.js';console.log('users: ', users);console.log('usingAwait module');

我们还可以立即调用顶层async函数(IIAFE):

import fetch  from "node-fetch";(async () => {const resp = await fetch('https://jsonplaceholder.typicode.com/users');users = resp.json();})();export { users };

这样会有一个缺点,直接导入的 users 是 undefined,需要在异步执行完成之后才能访问它:

// usingAwait.js
import {users} from './a.js';console.log('users:', users); // undefinedsetTimeout(() => {console.log('users:', users);
}, 100);console.log('usingAwait module');

当然,这种方法并不安全,因为如果异步函数执行花费的时间超过100毫秒, 它就不会起作用了,users 仍然是 undefined。

另一个方法是导出一个 promise,让导入模块知道数据已经准备好了:

//a.js
import fetch  from "node-fetch";
export default (async () => {const resp = await fetch('https://jsonplaceholder.typicode.com/users');users = resp.json();
})();
export { users };//usingAwait.js
import promise, {users} from './a.js';
promise.then(() => { console.log('usingAwait module');setTimeout(() => console.log('users:', users), 100); 
});

虽然这种方法似乎是给出了预期的结果,但是有一定的局限性:导入模块必须了解这种模式才能正确使用它。

而顶层await就可以消除这些缺点:

// a.jsconst resp = await fetch('https://jsonplaceholder.typicode.com/users');const users = resp.json();export { users};// usingAwait.jsimport {users} from './a.mjs';console.log(users);console.log('usingAwait module');

顶级 await 在以下场景中将非常有用:

  • 动态加载模块

const strings = await import(`/i18n/${navigator.language}`);
  • 资源初始化

const connection = await dbConnector();
  • 依赖回退

let translations;
try {translations = await import('https://app.fr.json');
} catch {translations = await import('https://fallback.en.json');
}

目前,在这些地方已经支持 Top-level await:

  • V8 v8.9

  • Webpack 5.0.0

  • Babel

  • Chrome DevTools REPL

  • Node REPL

二、类的实例成员

1. 公共实例字段

公共类字段允许我们使用赋值运算符 (=) 将实例属性添加到类定义中。下面来一个计数器的例子:

import React, { Component } from "react";export class Incrementor extends Component {constructor() {super();this.state = {count: 0,};this.increment = this.increment.bind(this);}increment() {this.setState({ count: this.state.count + 1 });}render() {return (<button onClick={this.increment}>Increment: {this.state.count}</button>);}
}

在这个例子中,在构造函数中定义了实例字段和绑定方法,通过新的类语法,我们可以使代码更加直观。新的公共类字段语法允许我们直接将实例属性作为属性添加到类上,而无需使用构造函数方法。这样就简化了类的定义,使代码更加简洁、可读:

import React from "react";export class Incrementor extends React.Component {state = { count: 0 };increment = () => this.setState({ count: this.state.count + 1 });render = () => (<button onClick={this.increment}>Increment: {this.state.count}</button>);
}

有些小伙伴可能就疑问了,这个功能很早就可以使用了呀。但是它现在还不是标准的 ECMAScript,默认是不开启的,如果使用 create-react-app 创建 React 项目,那么它默认是启用的,否则我们必须使用正确的babel插件才能正常使用(@babel/preset-env)。

下面来看看关于公共实例字段的注意事项:

  • 公共实例字段存在于每个创建的类实例上。它们要么是在Object.defineProperty()中添加,要么是在基类中的构造时添加(构造函数主体执行之前执行),要么在子类的super()返回之后添加:

class Incrementor {count = 0
}const instance = new Incrementor();
console.log(instance.count); // 0
  • 未初始化的字段会自动设置为 undefined:

class Incrementor {count
}const instance = new Incrementor();
console.assert(instance.hasOwnProperty('count'));
console.log(instance.count);  // undefined
  • 可以进行字段的计算:

const PREFIX = 'main';class Incrementor {[`${PREFIX}Count`] = 0
}const instance = new Incrementor();
console.log(instance.mainCount);   // 0

2. 私有实例字段、方法和访问器

默认情况下,ES6 中所有属性都是公共的,可以在类外检查或修改。下面来看一个例子:

class TimeTracker {name = 'zhangsan';project = 'blog';hours = 0;set addHours(hour) {this.hours += hour;}get timeSheet() {return `${this.name} works ${this.hours || 'nothing'} hours on ${this.project}`;}
}let person = new TimeTracker();
person.addHours = 2; // 标准 setter
person.hours = 4;    // 绕过 setter 进行设置
person.timeSheet;

可以看到,在类中没有任何措施可以防止在不调用 setter 的情况下更改属性。

而私有类字段将使用哈希#前缀定义,从上面的示例中,我们可以修改它以包含私有类字段,以防止在类方法之外更改属性:

class TimeTracker {name = 'zhangsan';project = 'blog';#hours = 0;  // 私有类字段set addHours(hour) {this.#hours += hour;}get timeSheet() {return `${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}`;}
}let person = new TimeTracker();
person.addHours = 4; // 标准 setter
person.timeSheet     // zhangsan works 4 hours on blog

当我们尝试在 setter 方法之外修改私有类字段时,就会报错:

person.hours = 4 // Error Private field '#hours' must be declared in an enclosing class

我们还可以将方法或 getter/setter 设为私有,只需要给这些方法名称前面加#即可:

class TimeTracker {name = 'zhangsan';project = 'blog';#hours = 0;   // 私有类字段set #addHours(hour) {this.#hours += hour;}get #timeSheet() {return `${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}`;}constructor(hours) {this.#addHours = hours;console.log(this.#timeSheet);}
}let person = new TimeTracker(4); // zhangsan works 4 hours on blog

由于尝试访问对象上不存在的私有字段会发生异常,因此需要能够检查对象是否具有给定的私有字段。可以使用 in 运算符来检查对象上是否有私有字段:

class Example {#fieldstatic isExampleInstance(object) {return #field in object;}
}

查看更多公有和私有字段提案信息:https://github.com/tc39/proposal-class-fields

3. 静态公共字段

在ES6中,不能在类的每个实例中访问静态字段或方法,只能在原型中访问。ES 2022 将提供一种在 JavaScript 中使用 static 关键字声明静态类字段的方法。下面来看一个例子:

class Shape {static color = 'blue';static getColor() {return this.color;}getMessage() {return `color:${this.color}` ;}
}

我们可以从类本身访问静态字段和方法:

console.log(Shape.color); // blueconsole.log(Shape.getColor()); // blueconsole.log('color' in Shape); // trueconsole.log('getColor' in Shape); // trueconsole.log('getMessage' in Shape); // false

实例不能访问静态字段和方法:

const shapeInstance = new Shape();console.log(shapeInstance.color); // undefinedconsole.log(shapeInstance.getColor); // undefinedconsole.log(shapeInstance.getMessage());// color:undefined

静态字段只能通过静态方法访问:

console.log(Shape.getColor()); // blue
console.log(Shape.getMessage()); //TypeError: Shape.getMessage is not a function

这里的 Shape.getMessage() 就报错了,这是因为 getMessage 不是一个静态函数,所以它不能通过类名 Shape 访问。可以通过以下方式来解决这个问题:

getMessage() {return `color:${Shape.color}` ;
}

静态字段和方法是从父类继承的:

class Rectangle extends Shape { }console.log(Rectangle.color); // blue
console.log(Rectangle.getColor()); // blue
console.log('color' in Rectangle); // true
console.log('getColor' in Rectangle); // true
console.log('getMessage' in Rectangle); // false

4. 静态私有字段和方法

与私有实例字段和方法一样,静态私有字段和方法也使用哈希 (#) 前缀来定义:

class Shape {static #color = 'blue';static #getColor() {return this.#color;}getMessage() {return `color:${Shape.#getColor()}` ;}
}
const shapeInstance = new Shape();
shapeInstance.getMessage(); // color:blue

私有静态字段有一个限制:只有定义私有静态字段的类才能访问该字段。这可能在我们使用 this 时导致出乎意料的情况:

class Shape {static #color = 'blue';
static #getColor() {return this.#color;
}
static getMessage() {return `color:${this.#color}` ;
}
getMessageNonStatic() {return `color:${this.#getColor()}` ;
}
}class Rectangle extends Shape {}console.log(Rectangle.getMessage()); // Uncaught TypeError: Cannot read private member #color from an object whose class did not declare it
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // TypeError: Cannot read private member #getColor from an object whose class did not declare it

在这个例子中,this 指向的是 Rectangle 类,它无权访问私有字段 #color。当我们尝试调用 Rectangle.getMessage() 时,它无法读取 #color 并抛出了 TypeError。可以这样来进行修改:

class Shape {static #color = 'blue';static #getColor() {return this.#color;}static getMessage() {return `${Shape.#color}`;}getMessageNonStatic() {return `color:${Shape.#getColor()} color`;}
}class Rectangle extends Shape {}
console.log(Rectangle.getMessage()); // color:blue
const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // color:blue

静态字段目前是比较稳定的,并且提供了各种实现:

cc7b02484be834d782fbc656ef6ffca8.png

5. 类静态初始化块

静态私有和公共字段只能让我们在类定义期间执行静态成员的每个字段初始化。如果我们需要在初始化期间像 try … catch 一样进行异常处理,就不得不在类之外编写此逻辑。该提案就提供了一种在类声明/定义期间评估静态初始化代码块的优雅方法,可以访问类的私有字段。

先来看一个例子:

class Person {static GENDER = "Male"static TOTAL_EMPLOYED;static TOTAL_UNEMPLOYED;try {// ...} catch {// ...}
}

上面的代码就会引发错误,可以使用类静态块来重构它,只需将try...catch包裹在 static 中即可:

class Person {static GENDER = "Male"static TOTAL_EMPLOYED;static TOTAL_UNEMPLOYED;static {try {// ...} catch {// ...}}
}

此外,类静态块提供对词法范围的私有字段和方法的特权访问。这里需要在具有实例私有字段的类和同一范围内的函数之间共享信息的情况下很有用。

let getData;class Person {#xconstructor(x) {this.#x = { data: x };}static {getData = (obj) => obj.#x;}
}function readPrivateData(obj) {return getData(obj).data;
}const john = new Person([2,4,6,8]);readPrivateData(john); // [2,4,6,8]

这里,Person 类与 readPrivateData 函数共享了私有实例属性。

三、Temporal

JavaScript 中的日期处理 Date() 对象一直是饱受诟病,该对象是1995 年受到 Java 的启发而实现的,自此就一直没有改变过。虽然Java已经放弃了这个对象,但是 Date() 仍保留在 JavaScript 中来实现浏览器的兼容。

Date() API 存在的问题:

  • 只支持UTC和用户的PC时间;

  • 不支持公历以外的日历;

  • 字符串到日期解析容易出错;

  • Date 对象是可变的,比如:

const today = new Date();
const tomorrow = new Date(today.setDate(today.getDate() + 1));console.log(tomorrow);  
console.log(today);

此时,两个时间输出是一样的,不符合我们的预期。正因为 Date() 对象存在的种种问题。平时我们经常需要借助moment.jsDay.js等日期库,但是它们的体积较大,有时一个简单的日期处理就需要引入一个库,得不偿失。

目前,由于Date API 在很多库和浏览器引擎中的广泛使用,没有办法修复API的不好的部分。而改变Date API 的工作方式也很可能会破坏许多网站和库。

正因如此,TC39提出了一个全新的用于处理日期和时间的标准对象和函数——Temporal。新的Temporal API 提案旨在解决Date API的问题。它为 JavaScript 日期/时间操作带来了以下修复:

  • 仅可以创建和处理不可变Temporal对象;

  • 提供用于日期和时间计算的简单 API;

  • 支持所有时区;

  • 从 ISO-8601 格式进行严格的日期解析;

  • 支持非公历。

Temporal 将取代 Moment.js 之类的库,这些库很好地填补了 JavaScript 中的空白,这种空白非常普遍,因此将功能作为语言的一部分更有意义。

由于该提案还未正式发布,所以,可以借助官方提供的prlyfill来测试。首选进行安装:

npm install @js-temporal/polyfill

导入并使用:

import { Temporal } from '@js-temporal/polyfill';console.log(Temporal);

Temporal 对象如下:

debcd3bc889f2884b4c795acfc24d086.png

下面就来看看 Temporal 对象有哪些实用的功能。

1. 当前时间和日期

Temporal.Now 会返回一个表示当前日期和时间的对象:

// 自1970年1月1日以来的时间(秒和毫秒)
Temporal.Now.instant().epochSeconds;
Temporal.Now.instant().epochMilliseconds;// 当前位置的时间
Temporal.Now.zonedDateTimeISO();// 当前时区
Temporal.Now.timeZone();// 指定时区的当前时间
Temporal.Now.zonedDateTimeISO('Europe/London');

2. 实例时间和日期

Temporal.Instant 根据 ISO 8601 格式的字符串返回一个表示日期和时间的对象,结果会精确到纳秒:

6b512eebd44b7b0dc334a1176c9725a4.png
Temporal.Instant.from('2022-02-01T05:56:78.999999999+02:00[Europe/Berlin]');
// 输出结果:2022-02-01T03:57:18.999999999Z 
Temporal.Instant.from('2022-02-011T05:06+07:00');
// 输出结果:2022-01-31T22:06:00Z

除此之外,我们还可以获取纪元时间的对应的日期(UTC 1970年1月1日0点是纪元时间):

Temporal.Instant.fromEpochSeconds(1.0e8);
// 输出结果:1973-03-03T09:46:40Z

3. 时区日期和时间

Temporal.ZonedDateTime 返回一个对象,该对象表示在特定时区的日期/时间:

new Temporal.ZonedDateTime(1234567890000, // 纪元时间Temporal.TimeZone.from('Europe/London'), // 时区Temporal.Calendar.from('iso8601') // 默认日历
);Temporal.ZonedDateTime.from('2025-09-05T02:55:00+02:00[Africa/Cairo]');Temporal.Instant('2022-08-05T20:06:13+05:45').toZonedDateTime('+05:45');
// 输出结果:Temporal.ZonedDateTime.from({timeZone: 'America/New_York',year: 2025,month: 2,day: 28,hour: 10,minute: 15,second: 0,millisecond: 0,microsecond: 0,nanosecond: 0
});
// 输出结果:2025-02-28T10:15:00-05:00[America/New_York]

4. 简单的日期和时间

我们并不会总是需要使用精确的时间,因此 Temporal API 提供了独立于时区的对象。这些可以用于更简单的活动。

  • Temporal.PlainDateTime:指日历日期和时间;

  • Temporal.PlainDate:指特定的日历日期;

  • Temporal.PlainTime:指一天中的特定时间;

  • Temporal.PlainYearMonth:指没有日期成分的日期,例如“2022 年 2 月”;

  • Temporal.PlainMonthDay:指没有年份的日期,例如“10 月 1 日”。

它们都有类似的构造函数,以下有两种形式来创建简单的时间和日期:

new Temporal.PlainDateTime(2021, 5, 4, 13, 14, 15);
Temporal.PlainDateTime.from('2021-05-04T13:14:15');new Temporal.PlainDate(2021, 5, 4);
Temporal.PlainDate.from('2021-05-04');new Temporal.PlainTime(13, 14, 15);
Temporal.PlainTime.from('13:14:15');new Temporal.PlainYearMonth(2021, 4);
Temporal.PlainYearMonth.from('2019-04');new Temporal.PlainMonthDay(3, 14);
Temporal.PlainMonthDay.from('03-14');

5. 日期和时间值

所有 Temporal 对象都可以返回特定的日期/时间值。例如,使用ZonedDateTime:

const t1 = Temporal.ZonedDateTime.from('2025-12-07T03:24:30+02:00[Africa/Cairo]');t1.year;        // 2025
t1.month;       // 12
t1.day;         // 7
t1.hour;        // 3
t1.minute;      // 24
t1.second;      // 30
t1.millisecond; // 0
t1.microsecond; // 0
t1.nanosecond;  // 0

其他有用的属性包括:

  • dayOfWeek(周一为 1 至周日为 7)

  • dayOfYear(1 至 365 或 366)

  • weekOfYear(1 到 52,有时是 53)

  • daysInMonth(28、29、30、31)

  • daysInYear(365 或 366)

  • inLeapYear(true或false)

6. 比较和排序日期

所有 Temporal 对象都可以使用 compare() 返回整数的函数进行比较。例如,比较两个ZonedDateTime对象:

Temporal.ZonedDateTime.compare(t1, t2);

这个比较结果会有三种情况:

  • 当两个时间值相等时,返回 0;

  • 当 t1 在 t2 之后时,返回 1;

  • 当 t1 在 t2 之前时,但会 -1;

const date1 = Temporal.Now,
const date2 = Temporal.PlainDateTime.from('2022-05-01');Temporal.ZonedDateTime.compare(date1, date2); // -1

compare() 的结果可以用于数组的 sort() 方法来对时间按照升序进行排列(从早到晚):

const t = ['2022-01-01T00:00:00+00:00[Europe/London]','2022-01-01T00:00:00+00:00[Africa/Cairo]','2022-01-01T00:00:00+00:00[America/New_York]'
].map(d => Temporal.ZonedDateTime.from(d)).sort(Temporal.ZonedDateTime.compare);

7. 日期计算

提案还提供了几种方法来对任何 Temporal 对象执行日期计算。当传递一个Temporal.Duration对象时,它们都会返回一个相同类型的新的 Temporal,该对象使用years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds 和 nanoseconds 字段来设置时间。

const t1 = Temporal.ZonedDateTime.from('2022-01-01T00:00:00+00:00[Europe/London]');t1.add({ hours: 8, minutes: 30 }); // 往后8小时30分t1.subtract({ days: 5 });  // 往前5天t1.round({ smallestUnit: 'month' });  // 四舍五入到最近的月份

until() 和 since() 方法会返回一个对象,该 Temporal.Duration 对象描述基于当前日期/时间的特定日期和时间之前或之后的时间,例如:

t1.until().months; // 到t1还有几个月t2.until().days;  // 到t2还有几天t3.since().weeks; // t3已经过去了几周

equals() 方法用来确定两个日期/时间值是否相同:

const d1 = Temporal.PlainDate.from('2022-01-31');
const d2 = Temporal.PlainDate.from('2023-01-31');
d1.equals(d2);  // false

8. 使用国际化 API 格式化日期

虽然这不是 Temporal API 的一部分,但 JavaScript Intl(国际化)API提供了一个 DateTimeFormat() 构造函数,可以用于格式化 Temporal 或 Date 对象:

const d = new Temporal.PlainDate(2022, 3, 14);// 美国日期格式:3/14/2022
new Intl.DateTimeFormat('en-US').format(d);// 英国日期格式:14/3/2022
new Intl.DateTimeFormat('en-GB').format(d);// 西班牙长日期格式:miércoles, 14 de abril de 2022
new Intl.DateTimeFormat('es-ES', { dateStyle: 'full' }).format(d);

附:

  • TC39 关于 Temporal 的提案进度:https://tc39.es/proposal-temporal/

  • Chrome 关于 Temporal 的实现进度:https://chromestatus.com/feature/5668291307634688#details

四、内置对象

1. Object.hasOwn()

在ES2022之前,可以使用 Object.prototype.hasOwnProperty() 来检查一个属性是否属于对象。

提案中的 Object.hasOwn 特性是一种更简洁、更可靠的检查属性是否直接设置在对象上的方法。

const example = {property: '123'
};console.log(Object.prototype.hasOwnProperty.call(example, 'property'));
console.log(Object.hasOwn(example, 'property'));

2. at()

at() 是一个数组方法,用于通过给定索引来获取数组元素。当给定索引为正时,这种新方法与使用括号表示法访问具有相同的行为。当给出负整数索引时,就会从数组的最后一项开始检索:

const array = [0,1,2,3,4,5];console.log(array[array.length-1]);  // 5
console.log(array.at(-1));  // 5console.log(array[array.lenght-2]);  // 4
console.log(array.at(-2));  // 4

除了数组,字符串也可以使用at()方法进行索引:

const str = "hello world";console.log(str[str.length - 1]);  // d
console.log(str.at(-1));  // d

3. cause

在 ECMAScript 2022 提案中,new Error() 中可以指定导致它的原因:

function readFiles(filePaths) {return filePaths.map((filePath) => {try {// ···} catch (error) {throw new Error(`While processing ${filePath}`,{cause: error});}});
}

4. 正则表达式匹配索引

这个新提案已经进入第 4 阶段,它将允许我们利用 d 字符来表示我们想要匹配字符串的开始和结束索引。以前,我们只能在字符串匹配操作期间获得一个包含提取的字符串和索引信息的数组。在某些情况下,这是不够的。因此,在这个新提案中,如果设置标志 /d,将额外获得一个带有开始和结束索引的数组。

const matchObj = /(a+)(b+)/d.exec('aaaabb');console.log(matchObj[1]) // 'aaaa'
console.log(matchObj[2]) // 'bb'

由于 /d 标识的存在,matchObj还有一个属性.indices,它用来记录捕获的每个编号组:

console.log(matchObj.indices[1])  // [0, 4]
console.log(matchObj.indices[2])  // [4, 6]

我们还可以使用命名组:

const matchObj = /(?<as>a+)(?<bs>b+)/d.exec('aaaabb');console.log(matchObj.groups.as);  // 'aaaa'
console.log(matchObj.groups.bs);  // 'bb'

这里给两个字符匹配分别命名为as和bs,然后就可以通过groups来获取到这两个命名分别匹配到的字符串。

它们的索引存储在 matchObj.indices.groups 中:

console.log(matchObj.indices.groups.as);  // [0, 4]
console.log(matchObj.indices.groups.bs);  // [4, 6]

匹配索引的一个重要用途就是指向语法错误所在位置的解析器。下面的代码解决了一个相关问题:它指向引用内容的开始和结束位置

const reQuoted = /“([^”]+)”/dgu;
function pointToQuotedText(str) {const startIndices = new Set();const endIndices = new Set();for (const match of str.matchAll(reQuoted)) {const [start, end] = match.indices[1];startIndices.add(start);endIndices.add(end);}let result = '';for (let index=0; index < str.length; index++) {if (startIndices.has(index)) {result += '[';} else if (endIndices.has(index+1)) {result += ']';} else {result += ' ';}}return result;
}console.log(pointToQuotedText('They said “hello” and “goodbye”.'));
// '           [   ]       [     ]  '

bd4415de51ec9443c63d316ca39049c3.gif

················· 若川简介 ·················

你好,我是若川,毕业于江西高校。现在是一名前端开发“工程师”。写有《学习源码整体架构系列》20余篇,在知乎、掘金收获超百万阅读。
从2014年起,每年都会写一篇年度总结,已经写了7篇,点击查看年度总结。
同时,最近组织了源码共读活动,帮助3000+前端人学会看源码。公众号愿景:帮助5年内前端人走向前列。

fbfec20fef1477c1d681ea0b3ed06dd0.png

识别方二维码加我微信、拉你进源码共读

今日话题

略。分享、收藏、点赞、在看我的文章就是对我最大的支持~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274958.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计类的五个原则_内容设计的5个原则

设计类的五个原则重点 (Top highlight)There are many heuristics and principles for creating good content. Some are created from a UX perspective, others from a content marketing point of view. They range from very long to very concise ones. I reviewed a larg…

Umi 4 RC 发布

大家好&#xff0c;我是若川。感谢大家一年以来的支持和陪伴。这一年疫情反复&#xff0c;年底应该有由于疫情不能回家的小伙伴。在这里先祝福大家&#xff0c;新年快乐。本打算今天不发文&#xff0c;但看到这篇觉得不错&#xff0c;就发一下。大家好&#xff0c;Umi 4 经过几…

figma下载_在Figma中将约束与布局网格一起使用

figma下载While doing research for the book “Designing in Figma”, I discovered a powerful way to lay out objects using a combination of Layout Grid and Constraints. The interface of Figma does not indicate a connection between the two, so it can be discov…

Java单元测试之JUnit4详解

2019独角兽企业重金招聘Python工程师标准>>> Java单元测试之JUnit4详解 与JUnit3不同&#xff0c;JUnit4通过注解的方式来识别测试方法。目前支持的主要注解有&#xff1a; BeforeClass 全局只会执行一次&#xff0c;而且是第一个运行Before 在测试方法运行之前运行…

我在黑暗中看到你眼中的月光_你好黑暗,我的老朋友

我在黑暗中看到你眼中的月光(Originally published on https://web.dev/prefers-color-scheme/.)(最初发布于https://web.dev/prefers-color-scheme/ 。) 介绍 (Introduction) &#x1f4da; I have done a lot of background research on the history and theory of dark mod…

Element Plus 正式版发布啦!

大家好&#xff0c;我是若川。祝大家新年快乐&#xff0c;开工大吉。公众号回复「红包」可以领取源码共读红包封面。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时…

大型网站技术架构(一)大型网站架构演化

2019独角兽企业重金招聘Python工程师标准>>> 看完了有一本书&#xff0c;就应该有所收获&#xff0c;有所总结&#xff0c;最近把《大型网站技术架构》一书给看完了&#xff0c;给人的印象实在深刻&#xff0c;再加上之前也搞过书本上讲的反向代理和负载均衡以及ses…

永不示弱_永不过时的网页设计:今天和2000年的在线投资组合

永不示弱重点 (Top highlight)Philippe Starck, a renowned industrial designer, once said:著名的工业设计师Philippe Starck曾经说过&#xff1a; “A designer has a duty to create timeless design. To be timeless you have to think really far into the future, not …

如何使用 React 创建一个作品集网站

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。今天&#xff0c;你…

新的一年,如何高效学习前端前沿知识~

今天强烈推荐一些互联网行业内优质技术公众号&#xff0c;互联网人大部分都关注了&#xff0c;包括目前主流的公司技术团队号&#xff0c;技术社区号&#xff0c;个人技术号&#xff0c;这些号行业深耕已久&#xff0c;会给你带来事半功倍的效果。公众号那么多&#xff0c;文章…

RabbitMQ学习总结(7)——Spring整合RabbitMQ实例

2019独角兽企业重金招聘Python工程师标准>>> 1.RabbitMQ简介 RabbitMQ是流行的开源消息队列系统&#xff0c;用erlang语言开发。RabbitMQ是AMQP&#xff08;高级消息队列协议&#xff09;的标准实现。 官网&#xff1a;http://www.rabbitmq.com/ 2.Spring集成Rabbi…

谈谈对java中分层的理解_让我们谈谈网页设计中的卡片设计

谈谈对java中分层的理解“I want a card”, this is the first demand point that the customer said in the last issue when talking to me about demand. There is no doubt that the card type is excellent for both PC and mobile phones. From online shopping malls to…

1-jdk的安装与配置

1- Jvm、jdk、jre之间的关系 JVM&#xff1a;Java虚拟机&#xff0c;保证java程序跨平台。&#xff08;Java Virtual Machine&#xff09;JRE&#xff1a; Java运行环境&#xff0c;包含JVM和核心类库。如果只是想运行java程序&#xff0c;只要安装JRE即可。&#xff08;Java R…

来自未来,2022 年的前端人都在做什么?

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。来自上帝视角的总览…

qt ui指针和本类对象_您需要了解的有关UI设计的形状和对象的所有信息

qt ui指针和本类对象重点 (Top highlight)第1部分 (Part 1) So you’re thinking about becoming a UX/UI designer, but are afraid to start? Don’t worry. It’s easier than you think. You only need a solid foundation and a lot of dedication. I can’t help you wi…

2021 大前端技术回顾及未来展望

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列2021 …

skysat重访周期_重访小恶梦

skysat重访周期You awaken with a start, the nightmare still fogging your mind with terror. Rain falls through cracks in the ceiling above you. The room is sparse, metallic, desolate. Searching the pockets of your yellow raincoat, you find only a cigarette l…

记一次 Vue2 迁移 Vue3 的实践总结

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列一、V…

改错3-38

#include<iostream.h>class time{private:int hour,minute,second;public:void settime(int h,int m,int s) { hour(h>0&&h<24)?h:0; minute(m>0&&m<60)?m:0; second(s>0&&s<60)?s:0; }void sh…

魔兽怀旧网站模块下载_一个人的网站重新设计和怀旧

魔兽怀旧网站模块下载Despite how I look, I’m the kind kind of person that loves to play old video games. (Full disclosure: I look exactly like the kind of person that loves to play old video games).尽管我长得很帅&#xff0c;但我还是一个喜欢玩旧视频游戏的人…