本文翻译自proposal-pattern-matching,同时会加上一些这篇文章中的例子来帮助理解。
ECMAScript模式匹配语法
Stage 0提案
提案者: Brian Terlson (Microsoft, @bterlson), Sebastian Markbåge (Facebook, @sebmarkbage)
|
|
模式匹配是基于数据的结构来选择不同行为的手段之一,其方式类似于解构。比如,你可以毫不费力地用指定的属性来匹配对象并且将这些属性的值绑定到匹配分支上。模式匹配使非常简洁和高度可读的函数式模式成为可能,并且已存在于许多语言之中。这个提案从Rust和F#汲取了许多灵感。
本提案当前处于stage 0阶段,因此不排除会有重大的变更。任何反馈和意见都不胜感激。请使用issue来提交问题或者想法,以及发送pull request来更新内容。修订、澄清,尤其是使用示例都将十分有帮助。
语法概览
|
|
对象和数组模式的语法有意地设计成与解构保持一致,因为出于以下理由这是有好处的。首先,它与开发者已经熟悉的现有语法相一致。其次,它允许在类似的场景中使用模式匹配和解构(例如,将来给multi-methods
[1]等的提案)。然而在实际中,模式匹配JavaScript数值需要比简单的解构更具表达能力。这份提案添加了额外的模式来填补空白。为了提升本提案的实用性和表达能力,与解构更进一步分离也许也是合理的(比如,类似于#17那样)。
对象模式
对象模式用指定的属性来匹配对象,匹配的对象上可以包含额外的属性。例子:
|
|
|
|
数组模式
数组模式匹配类数组对象(拥有length
属性的对象)。...
绑定(可以是匿名的)用于匹配任意长度的数组。例子:
|
|
也可以让数组模式支持可迭代对象,然而这个设计是否可取尚不明确。首先,模式匹配的用户是不是期望数组模式去匹配任何实现了Symbol.iterable
的对象还不清楚。其次,由于遍历一个可迭代对象是有副作用的,因此它会让人们困惑可迭代对象在各个匹配分支上是处于什么样的状态,而且也不清楚默认有副作用的模式匹配是不是个好主意。
尽管如此,解构确实是可以工作在可迭代对象上的,因此这在一致性方面颇具争议。
字面量模式
字面量模式是string型,number型,bool型,null以及undefined的字面量,并且精确匹配该值。例子:
|
|
标识符模式与Symbol.matches
标识符会查找它们运行时候的值。一个数值是匹配的只要它实现了Symbol.matches
方法,并且该方法在输入数值是匹配的时候返回了真值(另外解构Symbol.matches
返回值的一种方法请参考下面可选拓展部分)。
这个功能带来了一些优点。第一,它允许匹配正则表达式。当然也可以考虑加个RegExp模式,但是正则表达式(特别是很复杂的那些)通常不会用‘inline’的方式声明。
第二,它允许简单的类型/instanceof检查——一个类型可以实现它自己的Symbol.matches
方法用于决定某个值是不是该类型。一个简单的实现可以仅仅是return value instanceof this.constructor
。如此简单的实现可以在通过class
关键字新建类型的时候默认添加上。
第三,更加一般地,它围绕数值间的互相匹配创建了一个协议。这在未来的提案中也许会很有用,比如interface
提案,用于添加类似于nominal interface
[2]或者tagged union discrimination
[3]的东西。
|
|
|
|
更多的例子
嵌套模式
模式可以嵌套。例如:
|
|
上述模式中的true
也可以是任何其他模式(在这个例子中它是字面量模式)。
match嵌套
由于match是一个表达式,你可以在一个match分支的后项中进一步匹配。考虑:
|
|
设计目标与替代方案
没有fall-through
Fall-through
[4]可以通过continue
关键字实现。如果没有匹配的模式,这将会是一个运行时的错误。
声明vs表达式
将match
设计为声明会使它看起来与switch
子句非常相像。然而与switch
相像可能会是有问题的,因为分支的行为会表现地不同。使用switch
做为match
的思维模型是有帮助的,但并不能揭示全部。
同时也没有足够的理由要让这个语法只支持声明。解析上下文或者仅限声明的match
所存在的困难会限制其实用性。另一方面,表达式形式的match
可以很方便地在各个地方使用,尤其是做为箭头函数的函数体。
匹配分支语法
对于匹配体的语法存在很多选择,大体上说有:类case语法,类箭头函数语法,以及只包含表达式的语法。
类Case语法的分支
类Case语法的分支包含声明。这种分支的后项按一个声明接一个声明的顺序执行直到遇到流程控制关键字(或者达到case结构的末尾)。类Case的分支非常有用因为它们允许声明做为子元素,throw
声明就经常会用到。
类Case的分支在语句构成上比较难处理,因为你需要一个关键字来表示一个分支的开始。一个显而易见的选择是使用case
。找到其他符合语境的关键字会比较困难,但也许也不是完全不可能。
另外,由于match
表达式的值是第一个匹配分支执行后的值,模式匹配的用户将不得不理解语义上不是JS开发者通常所认为的那样的completion value
[5]。
最后,类case的分支在更小的场景中使用会让模式匹配显得繁琐。
类箭头函数语法的分支
箭头函数支持一个表达式或者一个可选的语句块。把这应用到我们的模式匹配的语法带来了两个很好的特性:简洁而不失可拓展性,和用,
分隔的分支。这个方案应用到了上述所有例子中。
仅表达式语法的分支
你也可以只允许声明出现在一个分支中,然后依赖于do
表达式来提供声明。不过这看起来有点不如类箭头函数分支友好。
Else分支语法
我这里没有过多介绍else
是因为它与JavaScript的其他部分是一致的,但你也许会更喜欢类似_
这样的更加简洁的语法(尤其是如果你用过F#)。_
还有可以绑定到一个值的好处,这可以保证你以无副作用的方式引用一个值。考虑:
|
|
可选拓展
对象和数组模式匹配数值
数组模式可以拓展成允许带一个数值以用任何二元操作符将属性或者元素和某个特定值进行比较。例如:
|
|
If判断
可以在匹配分支上下文中对待匹配值做各种测试往往是便利的。例如:
|
|
解构运行时的匹配结果
支持解构运行时的匹配结果可能是很有用的,尤其是正则表达式的匹配。考虑:
|
|
正则表达式对象的Symbol.matches
方法会被调用,并且如果匹配成功了,则返回match对象。这个match对象可以通过->
子句更进一步解构。
匹配多个模式
有些时候匹配多个模式是有用的。这可以通过使用||
来分割多个模式(类似于Rust和F#)。你也可以通过&&
来要求一个值匹配多个模式。
对象默认‘严格匹配’
在上文提案中,对象上的额外属性是允许的,而更长的数组则是不允许的除非你显示使用...
来匹配。这同样也可以应用到对象上:{x}
将仅匹配只拥有一个名为x的属性的对象,而{x,...}
匹配任何拥有属性x的对象。但是,对象匹配的主要使用场景很可能并不关心额外的属性,而且通过_
属性或者Symbol键值来扩充对象以添加额外的metadata是很普遍的,提案的语法看起来是没问题的(当然linter也可以将这强制为显示的)。
数组模式匹配可迭代对象
在上文提案中,数组模式仅对拥有length
属性的类数组对象有效。它可以拓展为支持任何可迭代对象,但必须注意避免产生副作用以及在各匹配分支间移动时多次遍历可迭代对象。
没有围绕匹配数值的圆括号
(我认为)cover grammar
[6]是可以避免的,通过进一步摆脱switch
的语法同时省略match
数值旁边的圆括号:
|
|
这移除了和对一个名为match
函数的调用之间的歧义。
内置Symbol.matches实现
Symbol.matches
可以在许多内置类型上实现,比如Number和String,用于匹配该类型的数值。另外,类可以创建Symbol.matches
方法来为你做instanceof
检查。
译注
nominal interface:标明型别系统,即若要两个类型相等,则它们必须要有相同的名字。相应的还有结构型別系統,即结构相同的类型是相等的。想要了解更多可以查看这篇文章;
tagged union discrimination:typescript 2.0支持这个,还是放个文章自行感受下😏;
Fall-through:这个是指在
switch
的case
字句里面如果没写break
,那么代码会从匹配的地方开始执行一直碰到break
为止。例如:12345678910let a = 1;switch (a) {case 1:console.log(1);case 2:console.log(2);break;}// -> 1// -> 2completion value:直观地说,
completion value
就是在控制台执行代码时输出的值,而代码级别的捕获目前只能通过eval
函数的返回值,另外还有一份提案提议用do
表达式来捕获;cover grammar:这个应该是用来解决语法冲突的,比如文中的match表达式和match函数调用,另外需要
cover grammar
的例子是对象字面量和解构,都用到了{}
;