作者:@林不渡(linbudu)  校对:@吞吞(legendecas)

本次 TC39 会议中,有部分提案在此前会议的讨论基础上进行了更新,如 Intl.Segmenter 提案在 10 月会议中已进入到 Stage 4,在此次会议中更新为 Intl.Segmenter V2 提案,并进入到 Stage 1。另外,较受关注的 proposal-record-tuple、proposal-destructuring-private 与 proposal-decorators 等提案也在本次会议中进行了更新,但没有推进到下一阶段。
Stage 3 → Stage 4
从 Stage 3 进入到 Stage 4 有以下几个门槛:
  1. 必须编写与所有提案内容对应的 tc39/test262 测试,用于给各大 JavaScript 引擎和 transpiler 等实现检查与标准的兼容程度,并且 test262 已经合入了提案所需要的测试用例;
  2. 至少要有两个实现能够兼容上述 Test 262 测试,并发布到正式版本中;
  3. 发起了将提案内容合入正式标准文本 tc39/ecma262 的 Pull Request,并被 ECMAScript 编辑签署同意意见。

Extend timeZoneName

提案链接:https://github.com/tc39/proposal-intl-extend-timezonename


这一提案为 Intl.DateTimeFormat 方法扩展了
timeZoneName
选项的可用值,来使得对日期和时间的格式化行为更加精确。


timeZoneName
选项主要控制在返回结果中时区名称的展示方式:

// 不指定时,默认不会显示时区名称:"2020/12/20"
newIntl
.DateTimeFormat(
'zh-cn'
).format(date);


// 使用精简的时区名称:"2020/12/20 GMT+8"
newIntl
.DateTimeFormat(
'zh-cn'
, { 
timeZoneName
"short"
 }).format(date);


// 使用本地化的时区名称:"2020/12/20 中国标准时间"
newIntl
.DateTimeFormat(
'zh-cn'
, { 
timeZoneName
"long"
 }).format(date);


此提案在原可用值
short
long
的基础上引入了新的四个值:

  • shortOffset,如 GMT-8,即 short 下的PST(Pacific Standard Time),简短地本地化 GMT 格式。
  • longOffset,如 GMT-08:00,精确地本地化 GMT 格式。
  • shortGeneric,如 PT(Pacific Time 的缩写),简短的通用时区名称。
  • longGeneric,如 太平洋时间,即 long 选项下的太平洋标准时间,精确的通用时区名称。

Intl.DisplayNames V2

提案链接:https://github.com/tc39/intl-displaynames-v2
此提案的上一个版本参见:https://github.com/tc39/proposal-intl-displaynames
Intl.DisplayNames
API的主要目的是通过浏览器内置的数据,来减少在进行本地化工作时,开发者需要在代码中显式携带的 i18n 相关数据。此提案已经支持了语言、地区、货币等部分的本地化能力,V2 版本的主要变更是为此提案新增了包括历法(calendar)、时间单位(dateTimeField)等在内的更多能力。


首先看一下上一版本中支持的对于地区的本地化能力:
var
 regionNames = 
newIntl
.DisplayNames([
'zh-CN'
], {
type
'region'
});

console
.log(regionNames.of(
'419'
)); 
// "拉丁美洲"
console
.log(regionNames.of(
'BZ'
)); 
// "贝里斯"
console
.log(regionNames.of(
'US'
)); 
// "美国"
console
.log(regionNames.of(
'BA'
)); 
// "波斯尼亚与黑塞哥维那"
console
.log(regionNames.of(
'MM'
)); 
// "缅甸"

V2 版本中引入的对于时间单位的本地化:
var
 dateTimeFieldNames = 
newIntl
.DisplayNames(
"zh"
, { 
type
"dateTimeField"
 })


dateTimeFieldNames.of(
"era"
// "纪元"
dateTimeFieldNames.of(
"year"
// "年"
dateTimeFieldNames.of(
"month"
// "月"
dateTimeFieldNames.of(
"quarter"
// "季度"
dateTimeFieldNames.of(
"weekOfYear"
// "周"
dateTimeFieldNames.of(
"weekday"
// "工作日"
dateTimeFieldNames.of(
"dayPeriod"
// "上午/下午"
dateTimeFieldNames.of(
"day"
// "日"
dateTimeFieldNames.of(
"hour"
// "小时"
dateTimeFieldNames.of(
"minute"
// "分钟"
dateTimeFieldNames.of(
"second"
// "秒"
Stage 2 → Stage 3
从 Stage 2 进入到 Stage 3 有以下几个门槛:
  1. 撰写了包含提案所有内容的标准文本,并有指定的 TC39 成员审阅并签署了同意意见;
  2. ECMAScript 编辑签署了同意意见。

Array Grouping

提案链接:https://github.com/tc39/proposal-array-grouping

在对数组进行操作时,我们经常会遇到需要将数组中的元素根据某些特征来进行分类的场景。这个提案引入了一个新的数组方法
Array.prototype.groupBy
,来实内置的数组分组能力(类似于 Lodash 中的 groupBy 方法):

const
 array = [
1
2
3
4
5
];


// =>  { odd: [1, 3, 5], even: [2, 4] }
array.groupBy(
(num, index, array) =>
 {

return
 num % 
2
 === 
0
 ? 
'even'
'odd'
;

});

另外,这一提案还引入了 Array.prototype.groupByToMap 方法,此方法接收的处理函数返回可以一个对象,方法的返回值为 Map 结构:
const
 odd  = { 
odd
true
 };

const
 even = { 
even
true
 };


// =>  Map { {odd: true}: [1, 3, 5], {even: true}: [2, 4] }
array.groupByToMap(
(num, index, array) =>
 {

return
 num % 
2
 === 
0
 ? even: odd;

});
Stage 1 → Stage 2
从 Stage 1 进入到 Stage 2 需要完成撰写包含提案所有内容的标准文本的初稿。


Array.fromAsync

提案链接:https://github.com/tc39/proposal-array-from-async

Array.from 方法用于从类数组(Array-Like)或可迭代对象(Iterable)创建一个新的数组实例。ECMAScript 2018 引入了异步可迭代对象(Async Iterable)以及 for await of 语法以后,由于缺少直接从异步可迭代对象生成数组的内置方法,社区出现了 it-all 这一类工具库,它的源码也非常简单:
const
 all = 
async
 (source) => {

const
 arr = []


forawait
 (
const
 entry 
of
 source) {

    arr.push(entry)

  }

return
 arr

}


Array.fromAsync 提案则带来了语言层面的内置支持,如一个异步 Generator 函数使用 Array.fromAsync 的例子:
asyncfunction * asyncGen (n
{

for
 (
let
 i = 
0
; i < n; i++)

yield
 i * 
2
;

}

// [0, 2, 4, 6].
const
 arr = [];

forawait
 (
const
 v 
of
 asyncGen(
4
)) {

  arr.push(v);

}

// 两种方式的结果是一致的
const
 arr = 
awaitArray
.fromAsync(asyncGen(
4
));


注意,fromAsync 方法返回的是一个 Promise。

除了接受一个类数组或异步可迭代对象以外,类似于 from 方法,fromAsync 方法还能接受两个可选的参数。

  • mapFn,类似于 forEach 与 map,此回调函数会被应用在每一个数组成员 yield 后返回的值,这一点类似于 Array.from 接受的 mapFn 参数,但不同之处在于 fromAsync 中这一回调函数可以是异步的。
  • thisArg,调用 fromAsync 时的 this 对象。

对于错误处理,由于 fromAsync 方法返回一个 Promise,在创建过程中如果出现错误(包括读取(异步)可迭代对象、回调函数执行中出错等),则此 Promise 状态将被置为 rejected。


CoreJS 中已经提供了相应的 polyfill,见 array-from-async。


RegExp Modifiers

提案链接:https://github.com/tc39/proposal-regexp-modifiers


目前,RegExp 支持多种执行模式,包含
i
(大小写通配),
m
(多行匹配),
s
(单行匹配),还有目前同样作为 TC39 提案的
x
(增强模式,见RegExp X Mode),等等。但是这些模式只能对整个正则表达式启用,而无法控制只对于其中的某一个部分生效,这带来了一定的限制。

RegExp Modifiers 提案在两月前的 10 月会议上由 stage 0 推进到 stage 1,为正则表达式引入了两种新的子表达式:
  • 自约束(self-bounded)(?imsx-imsx:subexpression) :在子表达式作用域内启用或禁用 flag。如(?-i:A(?i:B)C) 匹配 ABCAbC,但是不能匹配 aBC  或 ABc
  • 无约束(unbounded)(?imsx-imsx):从当前位置开始到表达式结尾启用 flag,并在遇到下一个相同修饰符时 flag 被覆盖。如(?-i)A(?i)B(?-i)C将匹配 ABCAbC(?-i) 代表在作用域内取消大小写通配,(?i) 代表在作用域内启用大小写通配),相反的,(?-i) 则代表取消。

在本次的 12 月 TC39 会议上,unbounded 模式被移除,并且不会再作为此提案的一部分,以下是 self-bounded 的使用示例

// 为 [a-z] 表达式取消大小写通配模式
const
 re1 = 
/^[a-z](?-i:[a-z])$/i
;

re1.test(
"ab"
); 
// true
re1.test(
"Ab"
); 
// true
re1.test(
"aB"
); 
// false

RegExp Buffer Boundaries

提案链接:https://github.com/tc39/proposal-regexp-buffer-boundaries


目前在正则表达式中,
^
$
对于内容开头与结尾的匹配会受到
m
(多行匹配模式)flag 的影响:
const
 pattern = 
String
.raw
`^foo$`
;

const
 re1 = 
newRegExp
(pattern, 
"u"
);

re1.test(
"foo"
); 
// true
re1.test(
"foo\nbar"
); 
// false

const
 re2 = 
newRegExp
(pattern, 
"um"
);

re1.test(
"foo"
); 
// true
re1.test(
"foo\nbar"
); 
// true

RegExp Buffer Boundaries 提案在两月前的 10 月会议上由 stage 0 推进到 stage 1,引入了三种缓冲边界(Buffer Boundaries)标志,用于在正则表达式中匹配原始输入的开头或结尾。这一行为类似
^
或者
$
(但缓冲边界不会被
m
flag 影响 )。

  • \A:匹配输入的开头;
  • \z:匹配输入的结尾;
  • \Z:零宽断言,由缓冲边界的换行符(可选)组成,等价于 (?=\R?\z)

在本次的 12 月 TC39 会议上,零宽断言
\Z
被暂时移除,但仍然被保留为提案的一部分,只是没有进入 Stage 2。


其使用示例如下:
const
 pattern = 
String
.raw
`\Afoo\z`
;

const
 re1 = 
newRegExp
(pattern, 
"u"
);

re1.test(
"foo"
); 
// true
re1.test(
"foo\nbar"
); 
// false

// 启用多行匹配
const
 re2 = 
newRegExp
(pattern, 
"um"
);

re1.test(
"foo"
); 
// true
re1.test(
"foo\nbar"
); 
// false
Stage 0 → Stage 1
从 Stage 0 进入到 Stage 1 有以下门槛:
  1. 找到一个 TC39 成员作为 champion 负责这个提案的演进;
  2. 明确提案需要解决的问题与需求和大致的解决方案;
  3. 有问题、解决方案的例子;
  4. 对 API 形式、关键算法、语义、实现风险等有讨论、分析。
    Stage 1 的提案会有可预见的比较大的改动,以下列出的例子并不代表提案最终会是例子中的语法、语义。

Intl.Segmenter V2

提案链接:https://github.com/tc39-transfer/proposal-intl-segmenter-v2
上一版本的提案链接:https://github.com/tc39/proposal-intl-segmenter

很多自然语言脚本(中文,英文,法文等等)都有词分割与句分割。Unicode UAX 29 定义了文本元素的分割算法,可以在文本段落中找出不同文本元素的分界线(甚至包括如中文,韩文,日文,泰文等基于词典分割的东亚语言),这对于实现更加可靠的输入法、文本编辑器等一系列涉及到文本处理的工作都有相当大的裨益。


将 Unicode UAX 29 中定义的文本元素、词句分割算法在浏览器、JavaScript 中原生实现后,相比于开发者们引入自己的实现方案来说,可以节省非常多的带宽与内存(不用再额外下载 CJK 词典了)。


上一版本的 Segmenter 提案已经支持了基于字母(grapheme)、词(word)、段落(sentence)的分割能力,但没有引入基于行(line)的分割能力。主要考量是行分割能力通常使用在文字的布局编排上,这涉及到更多的 API 以及相关工作量,甚至涉及到字符的实际渲染宽度。


上一版本中已支持的基于词的分割:
// 创建一个专用于特定语言的分词器,如这里是中文
let
 segmenter = 
newIntl
.Segmenter(
"zh-CN"
, {
granularity
"word"
});


// 使用此分词器处理输入
let
 input = 
"我不是,我没有,你别瞎说。"
;

let
 segments = segmenter.segment(input);


for
 (
let
 {segment, index, isWordLike} 
of
 segments) {

console
.log(
"segment at code units [%d, %d): «%s»%s"
,

    index, index + segment.length,

    segment,

    isWordLike ? 
" (word-like)"
 : 
""
  );

}


其切分结果包含三个属性:

  • segment:此拆分单元的长度;
  • index:此拆分单元的起始位置;
  • isWordLike:当创建分词器时,如果设置粒度(granularity)精确为 "word" 级别,且此拆分单元近似于当前语言的单个单词("word-like"),则返回 true
  • input:原始输入值;

结果如下:
// segment at code units [0, 3): «我不是» (word-like)
// segment at code units [3, 4): «,»
// segment at code units [4, 5): «我» (word-like)
// segment at code units [5, 7): «没有» (word-like)
// segment at code units [7, 8): «,»
// segment at code units [8, 9): «你» (word-like)
// segment at code units [9, 10): «别» (word-like)
// segment at code units [10, 12): «瞎» (word-like)
// segment at code units [11, 12): «说» (word-like)
// segment at code units [12, 13): «。»

而 V2 版本中引入的基于行的分割:
var
 s = 
newIntl
.Segmenter(
"zh-cn"
, { 
granularity
"line"
 });

var
 ss = s.segment(

"飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动"
);


for
 (s 
of
 ss) {

console
.log(
JSON
.stringify(s));

}

// { "segment": "飞", "index": 0, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "虎", "index": 1, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "队", "index": 2, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "正", "index": 3, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "式", "index": 4, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "名", "index": 5, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "称", "index": 6, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "为", "index": 7, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "「中", "index": 8, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "华", "index": 10, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "民", "index": 11, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "国", "index": 12, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "空", "index": 13, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "军", "index": 14, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "美", "index": 15, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "籍", "index": 16, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "志", "index": 17, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "愿", "index": 18, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "大", "index": 19, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "队」,\n", "index": 20, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": true }
// { "segment": "1940 ", "index": 24, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "年", "index": 29, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "代", "index": 30, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "早", "index": 31, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "期", "index": 32, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "在", "index": 33, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "缅", "index": 34, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "甸", "index": 35, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "展", "index": 36, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "开", "index": 37, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "行", "index": 38, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false }
// { "segment": "动", "index": 39, "input": "飞虎队正式名称为「中华民国空军美籍志愿大队」,\n1940 年代早期在缅甸展开行动", "isHardBreak": false

返回结果中除
segmentindexinput
以外,还新增了
isHardBreak
属性来表示当前的分割是否是强制的(如使用了
\n
换行符)。

结语
由贺师俊牵头,阿里巴巴前端标准化小组等多方参与组建的 JavaScript 中文兴趣小组(JSCIG,JavaScript Chinese Interest Group)在 GitHub 上开放讨论各种 ECMAScript 的问题,非常欢迎有兴趣的同学参与讨论:https://github.com/JSCIG/es-discuss/discussions。

关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向
继续阅读
阅读原文