For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-pattern-matching.md.
  • 简体中文
  • Pattern Matching S1

    中文标题:ECMAScript 模式匹配

    提案概览
    提案速览

    该提案向ECMAScript引入模式匹配表达式,解决除字符串正则之外缺乏结构化模式匹配的问题。它增加match(){}表达式、is布尔运算符和新的匹配器模式DSL,支持值模式、结构模式和组合子模式,并通过自定义匹配器实现用户可扩展性。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    ECMAScript 模式匹配

    状态

    阶段: 1

    规范文本: https://tc39.github.io/proposal-pattern-matching

    作者: 最初由 Kat Marchán(微软,@zkat__)撰写;现在由以下发起人负责。

    发起人: (按字母顺序排列)

    引言

    问题

    语言中有很多匹配的方法,但除了字符串的正则表达式之外,没有匹配模式的方法。然而,根据给定值中的模式采取不同动作是一个非常常见的需求:如果值具有foo属性则做X,如果它包含三个或更多项则做Y,等等。

    现有方法

    switch具有所需的结构 —— 给定一个值,提供多个可能的匹配条件,每个匹配条件都有不同的关联动作。但在实践中它严重不足:它不能出现在表达式位置;每个case需要显式的break以避免意外穿透;作用域不明确(一个case内的块作用域变量在其他case的作用域内可见,除非使用花括号);它只能进行===比较;等等。

    if/else具有必要的能力(你可以执行任何想要的比较),但即使对常见情况也过于冗长,要求作者多次显式列出进入值结构的路径,每次测试都要重复。它也仅支持语句(除非作者选择更难理解的三元语法),并且要求被测值存储在一个(可能是临时的)变量中。

    解决方案的优先级

    本节详细说明本提案的优先级。请注意,并非每位发起人都可能同意每个优先级。

    模式 匹配

    模式匹配构造是一个完整的条件逻辑构造,能做的不仅仅是模式匹配。因此,已经存在(而且将来还会有更多)需要做出的权衡。在这些情况下,我们应该优先考虑结构化模式匹配的易用性,而不是此构造的其他能力。

    switch的取代

    此特性必须易于搜索,以便教程和文档容易找到,并且特性容易学习和识别。因此,与switch语句不得有语法上的重叠。

    本提案旨在保留switch的优点,并消除使用它的任何理由。

    switch更好

    switch包含许多陷阱,如意外的case穿透和模糊的作用域。本提案应消除这些陷阱,同时引入switch目前无法提供的新能力。

    表达式语义

    模式匹配构造应可用作表达式:

    • return match { ... }
    • let foo = match { ... }
    • () => match { ... }
    • 等等。

    整个表达式的值是匹配到的任何子句的值。

    穷尽性和顺序

    如果开发者想要忽略某些可能的情况,他们应该显式指定。开发时错误比下游某处产生的生产时错误成本更低。

    如果开发者希望两个情况共享逻辑(我们称之为switch的“穿透”),他们应该显式指定。隐式穿透不可避免地静默接受有缺陷的代码。

    子句应始终按书写顺序检查,即从上到下。

    用户可扩展性

    用户域对象应该能够封装自己的匹配语义,而不必要地特权内建对象。这包括正则表达式(相对于字面量模式语法)、数值范围等。

    先前工作

    此提案向语言添加一个模式匹配表达式,部分基于现有的解构绑定模式

    此提案在2018年5月的TC39会议上被批准为阶段1,该演示文稿的幻灯片可在此处获得。其当前形式在2021年4月的会议上提交给TC39(幻灯片)。

    此提案借鉴了并部分重叠于CoffeeScriptRustPythonF#ScalaElixir/ErlangC++中的相应特性。最近,Flow获得了一个紧密匹配且明显受此处提出的语法启发的特性。

    用户域匹配

    提供类似匹配功能的社区库列表:

    • Optionals — 类似Rust的错误处理、选项和穷尽模式匹配,适用于TypeScript和Deno
    • ts-pattern — 适用于TypeScript的穷尽模式匹配库,具有智能类型推断。
    • babel-plugin-proposal-pattern-matching — 最小语法、高性能JavaScript模式匹配实现。
    • match-iz — 受TC39提案启发的小型函数式模式匹配库。
    • patcom — 与TC39提案功能对等,无需任何新语法

    规范

    本提案向JavaScript引入三个新概念:

    • “匹配器模式”(matcher pattern),一种与解构模式密切相关的新DSL,它允许递归测试值的结构和内容,同时提取一些结构到局部绑定中;
    • match(){}表达式,作为switch语句的通用替代品,使用匹配器模式解析为几个值之一;
    • is布尔运算符,允许对值进行一次性的模式匹配测试,可能还将该测试的绑定引入局部环境。

    匹配器模式

    匹配器模式是一种新的DSL,紧密受到解构模式的启发,用于递归测试值的结构和内容,同时提取该值的一些部分作为局部绑定供其他代码使用。

    匹配器模式可以分为三个一般变体:

    • 值模式,测试主题是否匹配某些标准,如“是字符串"foo"”或“匹配变量bar”。
    • 结构模式,测试主题是否匹配某些结构标准,如“具有属性foo”或“长度至少为3”,并允许你递归地将附加匹配器应用于该结构的各部分。
    • 组合子模式,允许你在同一主题上并行匹配多个模式,使用简单的布尔and/or逻辑。

    值匹配器

    有几种类型的值模式,执行不同类型的测试。

    原始模式

    所有原始值都可以直接用作匹配器模式,表示测试主题是否匹配指定值,使用SameValue语义(除非另有说明)。

    例如,1测试主题是否与1SameValue"foo"测试它是否与"foo"SameValue,等等。

    具体来说,布尔字面量、数字字面量、字符串字面量、未标记的模板字面量和null字面量都可以使用。

    • 数字字面量可以额外加上+-一元运算符:+是无操作(但请注意下面的0注释),但-对值取反,按预期工作。
    • 在模板字面量的插值表达式中,有关哪些绑定可见,请参阅绑定

    SameValue匹配语义的一个例外是模式0使用SameValueZero语义进行匹配。+0-0按正常方式使用SameValue匹配。(这具有效果:一个“无符号”的零模式将同时匹配正零和负零值,而“有符号”的零模式将只匹配适当符号的零值。)

    (关于SameValue语义的附加说明:它“按预期”工作于NaN值,正确匹配NaN值与NaN模式;它不进行任何隐式类型强制转换,因此1值不会匹配"1"模式。)

    注意:这些匹配中不进行强制转换:例如,如果你匹配一个字符串字面量,你的主题必须是字符串,否则即使它可以字符串化为匹配的字符串,也会自动失败。

    示例

    变量模式

    变量模式是一个“标识表达式”:foofoo.barfoo[bar]等,排除那些已经是原始值的如null,并且可选地加上+-一元运算符前缀。

    变量模式根据可见绑定解析标识符(有关详细信息,请参阅绑定),如果它有+-前缀,则将其转换为数字(通过toValue)并可能取反。如果结果是具有Symbol.customMatcher属性的对象,或者是函数,则它表示一个自定义匹配器测试。有关详细信息,请参阅自定义匹配器。否则,它表示测试主题与结果SameValue,类似于原始模式

    注意:这意味着,例如,保存数组的变量将只匹配那个精确的数组,通过对象等价;它不等同于数组模式进行的结构匹配。

    请注意,几个通常被认为是字面量的值,如Infinityundefined,实际上是绑定。由于原始模式和变量模式的处理大致相同,这个区别在这里可以保持学术性。

    注意:与原始模式一样,对主题(或模式值,除了+/-的效果,那是明确要求数字强制转换的)不进行强制转换,因此类型必须已经匹配。

    示例

    自定义匹配器

    如果变量模式解析到的对象在其原型链上具有Symbol.customMatcher属性,那么它就是一个“自定义匹配器”。

    为了确定模式是否匹配,将使用主题作为第一个参数,并将一个带有键"matchType"设置为"boolean"的对象作为第二个参数来调用自定义匹配器函数。

    如果它返回一个真值(当对其使用!!时变为true的值),则匹配成功;否则匹配失败。如果它抛出异常,则将错误向上传递。

    注意:提取器模式使用相同的机制,但允许对返回值进行进一步匹配,而不是仅限于返回true/false。

    示例
    内置自定义匹配器

    有几个JS对象默认安装了自定义匹配器。

    所有原始类型的类(Boolean、String、Number、BigInt、Symbol)都暴露了一个内置的Symbol.customMatcher静态方法,当且仅当主题是该类型的原始值(或装箱对象)时匹配。成功匹配的返回值(为了提取器模式的目的)是一个包含(可能自动解箱的)原始值的迭代器。

    class Boolean {
        static [Symbol.customMatcher](subject) {
            return typeof subject == "boolean";
        }
    }
    /* 其他原始类型类似 */

    Function.prototype有一个自定义匹配器,检查函数是否具有[[IsClassConstructor]]插槽(意味着它是来自class块的constructor()函数);如果是,它测试主题是否是那个类的对象(使用品牌检查验证,类似于Array.isArray());如果不是,它将函数作为谓词调用,传递主题,并返回返回值:

    /* 大致 */
    Function.prototype[Symbol.customMatcher] = function(subject) {
        if(isClassConstructor(this)) {
            return hasCorrectBrand(this, subject);
        } else {
            return this(subject);
        }
    }

    这样,谓词函数可以直接用作匹配器,如x is upperAlpha,类也可以直接用作匹配器来测试对象是否是该类的实例,如x is Option.Some

    RegExp.prototype有一个自定义匹配器,它在主题上执行正则表达式,如果匹配成功则匹配:

    RegExp.prototype[Symbol.customMatcher] = function(subject, {matchType}) {
        const result = this.exec(subject);
        if(matchType == "boolean") return result;
        if(matchType == "extractor") return [result, ...result.slice(1)];
    }
    示例

    绑定模式

    一个letconstvar关键字后跟一个有效的变量名(与任何其他地方的绑定语句相同)。绑定模式始终匹配,并且还引入一个绑定,将主题绑定到给定的名称,使用给定的绑定语义。

    绑定行为细节

    与正常的绑定语句一样,绑定模式引入的绑定在最近的块作用域(对于let/const)或最近的函数作用域(对于var)中建立。

    问题:或者在for/while头或函数参数列表中使用时,在明显的作用域中建立。我不确定正确的术语。

    绑定根据它们在模式中的存在而建立;无论绑定模式本身是否执行都无关紧要。(例如,[1, 2] is ["foo", let foo]将在块作用域中建立foo绑定,尽管第一个模式匹配失败并跳过了绑定模式。)

    在执行绑定模式之前,标准的TDZ规则适用。(例如,when [x, let x]是早期ReferenceError,因为当第一个模式运行并试图解引用x时,x绑定尚未初始化。)

    与标准绑定规则不同,在整个顶层模式的范围内,一个给定的名称可以出现在多个绑定模式中,只要所有实例使用相同的绑定类型关键字。如果这些绑定模式中有多个实际执行,则是运行时ReferenceError(有一个例外 - 请参阅or模式)。(此行为有先例:以前命名的捕获组在正则表达式内必须完全唯一。现在它们允许重复,只要它们位于替代项的不同分支中,如/foo(?<part>.*)|(?<part>.*)foo/。)

    示例
    (x or [let y]) and (z or {key: let y})

    在解析时有效:两个绑定模式都以let语义命名y。这在最近的块作用域中建立y绑定。

    如果xz匹配,但不是两个都匹配,则y被适当地绑定。如果都不匹配,y保持未初始化(所以使用它是运行时ReferenceError)。如果两者都匹配,则在执行第二个let y模式时抛出运行时ReferenceError,因为其绑定已经被初始化。

    (x or [let y]) and (z or {key: const y})

    早期ReferenceError,因为y被绑定两次且语义不同。

    x and let y and z and if(y == "foo")

    在解析时有效,在块作用域中建立y绑定。

    如果x不匹配,y保持未初始化,但守卫模式也被跳过,所以没有运行时错误(暂时)。如果z不匹配,y被初始化为匹配主题,但if()测试从不运行。

    [let x and String] or {length: let x}

    在解析时有效,建立x绑定。

    or模式允许覆盖已初始化的绑定,如果该绑定来自较早失败的子模式,以避免强制作者在可能失败的测试之后尴尬地安排绑定模式。

    因此,在此示例中,如果传入像[5]这样的对象,它将传递初始长度检查,执行let x模式并将x绑定到5,然后失败String模式,因为主题是Number。然后它将继续到下一个or子模式,并成功将x绑定到1,因为现有绑定是在失败的子模式中初始化的。

    结构模式

    结构模式让你测试主题的结构(其属性、长度等),然后使用附加的匹配器模式递归进入该结构。

    数组模式

    一个逗号分隔的零个或多个模式列表,用方括号括起来。它表示测试:

    1. 主题是可迭代的。
    2. 主题包含的迭代项数量恰好等于数组模式的长度。
    3. 每个项匹配关联的子模式。

    例如,["foo", {bar}]将在主题是一个恰好有两个项的可迭代对象,第一个项是字符串"foo",第二个项具有bar属性时匹配。

    数组模式的最后一个项可以可选地是“rest模式”:要么是字面量...,要么是...后跟另一个模式。无论哪种情况,rest模式的存在放宽了长度测试(上面列表中的2)为仅检查主题至少具有与数组模式一样多的项,忽略rest模式。也就是说,[a, b, ...]将只匹配包含2个或更多项的主题。

    如果...后跟一个模式,如[a, b, ...let c],则迭代器被完全耗尽,所有剩余的项被收集到一个Array中,然后将该数组作为rest模式测试的主题。

    注意:上述意味着[a, b]将从主题中拉取三个项:两个匹配子模式,第三个验证主题没有第三个项。[a, b, ...]将只从主题中拉取两个项来匹配子模式。[a, b, ...c]将耗尽主题的迭代器,验证它至少有2个项(用于匹配子模式),然后拉取剩余项匹配rest模式。

    数组模式的执行顺序如下:

    1. 从主题获取迭代器。如果失败,则匹配失败。
    2. 对于每个期望的项,直到子模式的数量(如果存在rest模式则忽略):
      1. 从迭代器拉取一个项。如果失败,则匹配失败。
      2. 执行相应的模式。如果不匹配则匹配失败。
    3. 如果没有rest模式,再从迭代器拉取一个项,验证它是{done: true}结果。如果是,返回成功;如果不是,返回失败。
    4. 如果有... rest模式,返回成功。
    5. 如果有...<pattern> rest模式,将迭代器的剩余项拉取到一个新的Array中,然后对该数组匹配模式。如果匹配,返回成功;否则返回失败。

    问题:还是应该先从迭代器拉取所有必要的值,然后执行所有匹配器?

    示例
    match (res) {
      when isEmpty: ...;
      when {data: [let page] }: ...;
      when {data: [let frontPage, ...let pages] }: ...;
      default: ...;
    }

    数组模式隐式检查主题的长度。

    第一个臂是变量模式,调用默认的Function.prototype自定义匹配器,它调用isEmpty(res),如果返回true则匹配。

    第二个臂是对象模式,其中包含一个数组模式,如果data恰好有一个元素,则匹配,并将该元素绑定到page供RHS使用。

    第三个臂在data具有至少一个元素时匹配,将第一个元素绑定到frontPage,并使用rest模式将任何剩余元素数组绑定到pages

    数组模式缓存

    为了允许生成器和其他“单次”迭代器的惯用用法能够合理地匹配多个数组模式,在匹配构造的范围内缓存迭代器及其结果。

    具体来说,每当主题匹配数组模式时,主题用作缓存中的键,其值是从主题获得的迭代器以及数组模式从主题拉取的所有项。

    每当要匹配数组模式时,首先检查缓存,并使用缓存中已拉取的项进行模式,仅在必要时从迭代器拉取新项。

    例如:

    function* integers(to) {
      for(var i = 1; i <= to; i++) yield i;
    }
    
    const fiveIntegers = integers(5);
    match (fiveIntegers) {
      when [let a]:
        console.log(`found one int: ${a}`);
        // 将生成器匹配数组模式。
        // 获取迭代器(只是生成器本身),
        // 然后拉取两个项:
        // 一个匹配`a`模式(成功),
        // 第二个验证迭代器只有一个项
        // (失败)。
      when [let a, let b]:
        console.log(`found two ints: ${a} and ${b}`);
        // 再次匹配数组模式。
        // 生成器对象已经被缓存,
        // 所以我们获取缓存的结果。
        // 我们总共需要三个项;
        // 两个检查模式,
        // 第三个验证迭代器只有两个项。
        // 缓存中已有两个,
        // 所以我们只需要再拉取一个(并失败模式)。
      default: console.log("more than two ints");
    }
    console.log([...fiveIntegers]);
    // 记录 [4, 5]
    // 匹配构造从生成器拉取了三个元素,
    // 所以之后剩下两个。

    当匹配构造执行完成时,所有缓存的迭代器都被关闭。

    对象模式

    一个逗号分隔的零个或多个“对象模式子句”列表,用花括号括起来。每个“对象模式子句”是<key>let/var/const <ident><key>: <pattern>,其中<key>是标识符或计算键表达式如[Symbol.foo]。它表示测试主题:

    1. 在其原型链上具有每个指定的属性。
    2. 如果键有相关的子模式,则该属性的值匹配子模式。

    <key>对象模式子句完全等同于<key>: voidlet/var/const <ident>对象模式子句完全等同于<ident>: let/var/const <ident>

    也就是说,when {foo, let bar, baz: "qux"}等同于when {foo: void, bar: let bar, baz: "qux"}:它测试主题具有foobarbaz属性,为bar属性的值引入bar绑定,并验证baz属性的值是字符串"qux"

    此外,对象模式可以包含一个“rest模式”:...后跟一个模式。与数组模式不同,单独...在对象模式中无效(因为没有严格检查需要放宽)。如果存在rest模式,则将所有未被对象模式子句匹配的可枚举自有属性收集到一个新对象中,然后对该对象匹配rest模式。(这与对象解构的行为一致。)

    问题:我们是否也需要key?: pattern模式子句?这将使其成为可选测试——如果主题具有此属性,验证它匹配模式。如果属性不存在导致模式被跳过,则处理来自模式的任何绑定,与来自跳过的or模式的绑定相同。

    问题:禁止__proto__?还是做点别的?

    对象模式的执行顺序如下:

    1. 对于每个非rest对象模式子句key: sub-pattern,按源代码顺序:
      1. 检查主题是否具有属性key(使用inHasProperty()语义)。如果没有,返回失败。
      2. 获取key属性的值,并对其匹配sub-pattern。如果匹配失败,返回失败。
    2. 如果有rest模式子句,收集主题的所有可枚举自有属性(在之前的步骤中未测试的),并将它们放入一个新Object中。对该rest模式匹配该对象。如果失败,返回失败。
    3. 返回成功。
    示例
    对象模式缓存

    类似于数组模式缓存,对象模式在匹配构造的范围内缓存其结果,以便多个子句不会可观察地多次检索相同的属性。

    (与数组模式缓存不同,数组模式缓存对于本提案与迭代器工作至关重要,对象模式缓存是很好的补充。它确实防范了一些奇怪情况,如非幂等的getter(特别是返回迭代器的getter),并帮助使幂等但昂贵的getter在模式匹配中可用而无需变形,但主要只是为了概念一致性。)

    每当主题匹配对象模式时,对于对象模式中的每个属性名,一个(<subject>, <property name>)元组用作缓存中的键,其值是属性的值。

    每当要匹配对象模式时,首先检查缓存,如果主题和该属性名已经在缓存中,则从缓存检索值,而不是从主题进行新的Get。

    例如:

    const randomItem = {
      get numOrString() { return Math.random() < .5 ? 1 : "1"; }
    };
    
    match (randomItem) {
      when {numOrString: Number}:
        console.log("Only matches half the time.");
        // 无论模式是否匹配,
        // 我们缓存(randomItem, "numOrString")对
        // 及其结果。
      when {numOrString: String}:
        console.log("Guaranteed to match the other half of the time.");
        // 由于(randomItem, "numOrString")已经被缓存,
        // 我们在这里重用结果;
        // 如果第一个子句是字符串,
        // 这里也是相同的字符串。
    }

    问题:这可能会引入大量缓存,主要用例只是确保迭代器缓存在顶层和嵌套在对象中都能工作。昂贵或非幂等的getter受益,但这是一个次要的好处。这种缓存可能是可丢弃的,但这意味着我们只在顶层缓存可迭代对象。

    提取器模式

    一个点分隔的标识符后跟一个带括号的“参数列表”,包含与数组匹配器相同的语法。表示一个自定义匹配器模式和一个数组模式的组合:

    1. 解析点分隔的标识符的可见绑定。如果得到一个具有Symbol.customMatcher属性的对象,并且该属性的值是一个函数,则继续;否则抛出XXX错误。

    2. 使用主题作为第一个参数,并将一个带有键"matchType"设置为"extractor"的对象作为第二个参数,调用自定义匹配器函数。让result为返回值。

    3. result参数列表模式进行匹配。

    注意:虽然自定义匹配器只要求返回值是真值假值,但提取器模式对类型更严格:该值必须是恰好truefalse,或者是Array,或者是可迭代对象。

    参数列表模式

    参数列表模式是提取器模式的一个子模式,与数组模式基本相同。它具有相同的语法,除了它由圆括号(())界定而不是方括号([])。它在几个主题上行为略有不同:

    • false主题总是匹配失败
    • true主题匹配,如同一个空数组
    • Array主题按索引匹配,而不是调用迭代器协议。

    如果主题是Array,则按如下方式匹配:

    1. 如果参数列表模式不以rest模式结尾,则主题的length属性必须恰好等于模式的长度,否则匹配失败。

    2. 如果参数列表模式确实以rest模式结尾,则主题的length属性必须大于或等于模式的长度 - 1,否则匹配失败。

    3. 对于参数列表模式的每个非rest子模式,获取主题的对应整数值属性,并对其匹配相应的子模式。

    4. 如果最后一个子模式是...<pattern>,收集主题的剩余整数值属性,直到但排除其length值,放入一个新的Array中,并匹配该模式。

    5. 如果任何匹配失败,整个参数列表模式匹配失败。否则成功。

    除了上述例外,参数列表模式的匹配方式与数组模式完全相同。

    问题:我们是否像缓存数组模式那样缓存参数列表?

    注意:这里的Array行为是为了性能,基于实现者的反馈。调用迭代器协议是昂贵的,我们不希望劝阻使用自定义匹配器,当远远预期的使用模式只是返回一个Array,而不是更复杂的可迭代对象。我们(目前)仍然坚持对数组匹配器使用迭代器协议,以匹配解构,但可能会改变。

    示例
    class Option {
      constructor() { throw new TypeError(); }
      static Some = class extends Option {
        constructor(value) { this.value = value; }
        map(cb) { return new Option.Some(cb(this.value)); }
        // 等等
        static [Symbol.customMatcher](subject) {
          if (subject instanceof Option.Some) { return [subject.value]; }
          return false;
        }
      };
    
      static None = class extends Option {
        constructor() { }
        map(cb) { return this; }
        // 使用默认的自定义匹配器,
        // 它只检查主题是否匹配类。
      };
    }
    
    let val = Option.Some(5);
    match(val) {
      when Option.Some(String and let a): console.log(`Got a string "${a}".`);
      when Option.Some(Number and let a): console.log(`Got a number ${a}.`);
      when Option.Some(...): console.log(`Got something unexpected.`);
      // 或者`Option.Some`,两者都行。
      // `Option.Some()`永远不会匹配,因为返回值
      // 是一个1项数组,不匹配`[]`
      when Option.None(): console.log(`Operation failed.`);
      // 或者`Option.None`,两者都行
      default: console.log(`Didn't get an Option at all.`)
    }

    问题:我们没有简单的方法访问“内置”自定义匹配器,所以上述回退到进行instanceof测试(而不是技术上更正确的品牌测试,内置的那个做的)。要“正确”工作,我必须定义一个没有自定义匹配器的类,然后取出自定义匹配器,保存到局部变量,并定义一个新的自定义匹配器,调用原始匹配器并在成功时返回[subject.value]。为了正确性,这是愚蠢的工作量。

    组合子模式

    有时你需要对单个值匹配多个模式,或者传递一个匹配多个模式之一的值,或者只是否定一个模式。所有这些都可以通过组合子模式实现。

    和模式

    两个或更多模式,每个由关键字and分隔。这表示测试主题通过所有子模式。任何模式都可以(在某些情况下必须,请参阅组合组合子)用圆括号括起来。

    短路适用;如果任何子模式匹配主题失败,匹配立即停止。

    and模式执行顺序如下:

    1. 对于每个子模式,按源代码顺序,将主题与子模式匹配。如果匹配失败,返回失败。
    2. 返回成功。

    或模式

    两个或更多模式,每个由关键字or分隔。这表示测试主题通过至少一个子模式。任何模式都可以(在某些情况下必须,请参阅组合组合子)用圆括号括起来。

    短路适用;如果任何子模式成功匹配主题,匹配立即停止。

    or模式执行顺序如下:

    1. 对于每个子模式,按源代码顺序,将主题与子模式匹配。如果成功匹配,返回成功。
    2. 返回失败。

    注意:如绑定行为细节中所定义,在失败的子模式中的绑定模式可以被后续子模式中的绑定模式覆盖而不会出错。也就是说,[let foo] or {length: let foo}在解析时和运行时都有效,即使foo绑定可能被初始化两次(给定像[1, 2]这样的主题)。

    非模式

    一个模式前面加上关键字not。这表示测试主题匹配子模式。该模式可以(在某些情况下必须,请参阅组合组合子)用圆括号括起来。

    组合组合子模式

    组合子模式不能在同一“级别”组合;它们之间没有优先级关系。相反,必须使用圆括号明确提供顺序。

    也就是说,foo and bar or baz是语法错误;必须写成(foo and bar) or bazfoo and (bar or baz)

    类似,not foo and bar是语法错误;必须写成(not foo) and barnot (foo and bar)

    守卫模式

    守卫模式语法为if(<expression>),表示测试表达式是否为真。这是一个任意的JS表达式,不是模式。

    match表达式

    match表达式是一种新类型的表达式,利用模式选择几个表达式之一来解析。

    匹配表达式看起来像:

    match(<subject-expression>) {
        when <pattern>: <value-expression>;
        when <pattern>: <value-expression>;
        ...
        default: <value-expression>;
    }

    也就是说,match头包含一个<subject-expression>,这是一个任意的JS表达式,求值为一个“主题”。

    match块包含零个或多个“匹配臂”,包括:

    • 关键字when
    • 一个模式
    • 一个字面量冒号
    • 一个任意的JS表达式
    • 一个分号(是的,必需)

    匹配臂之后,可选地包含一个“默认臂”,包括:

    • 关键字default
    • 一个字面量冒号
    • 一个任意的JS表达式
    • 一个分号

    在获取主题后,依次测试每个匹配臂,将主题与臂的模式进行匹配。如果匹配成功,则求值臂的表达式,并且match表达式解析为该结果。

    如果所有匹配臂都匹配失败,并且有默认臂,则求值默认臂的表达式,并且match表达式解析为该结果。如果没有默认臂,match表达式抛出TypeError

    绑定

    <subject-expression>是最近的块作用域的一部分。

    每个匹配臂和默认臂是独立的嵌套块作用域,涵盖臂的模式和表达式。(也就是说,不同的臂不能看到彼此的绑定,绑定不会逃逸match表达式。在每个臂内,它们遮蔽外部作用域的绑定。)

    示例

    match (res) {
      when { status: 200, let body, ...let rest }: handleData(body, rest);
      when { const status, destination: let url } and if (300 <= status && status < 400):
        handleRedirect(url);
      when { status: 500 } and if (!this.hasRetried): do {
        retry(req);
        this.hasRetried = true;
      };
      default: throwSomething();
    }

    此示例针对多个模式测试一个“响应”对象,基于.status属性分支,并在每个分支从响应中提取不同部分以在各个处理函数中处理。


    match (command) {
      when ['go', let dir and ('north' or 'east' or 'south' or 'west')]: go(dir);
      when ['take', /[a-z]+ ball/ and {let weight}: takeBall(weight);
      default: lookAround()
    }

    此示例是一个为基于文本的冒险游戏设计的牵强解析器。

    第一个匹配臂在命令是一个恰好有两个项的数组时匹配。第一个必须精确是字符串'go',第二个必须是给定的基本方向之一。注意使用和模式在验证(使用或模式)它是给定方向之一之前,使用绑定模式将数组中的第二个项绑定到dir

    第二个匹配臂稍微复杂。首先,使用正则表达式模式来验证对象字符串化为"something ball",然后对象模式验证它具有.weight属性并将其绑定到weight,以便权重可用于臂的表达式。

    语句与表达式

    为了最大的表达能力,match表达式是一个表达式,而不是语句。这允许在表达式上下文中轻松使用,如return match(val){...}

    当然,它也可以在语句上下文中使用,如上面的第一个示例。然而,匹配臂仍然只包含表达式。

    预计 do表达式将允许匹配臂执行语句(同样,如上面的第一个示例)。如果该提案未能推进,本提案的未来迭代将包括一些方式让匹配臂包含语句。(可能只是通过内联do-表达式功能。)

    is运算符

    is运算符是一个新的布尔运算符,形式为<subject-expression> is <pattern>。它返回一个布尔结果,指示主题是否匹配模式。

    绑定

    is的模式中建立的绑定在最近的块作用域中可见,如绑定模式中所定义。

    这包括在if()语句头中使用时:

    function foo(x) {
        if(x is [let head, ...let rest]) {
            console.log(head, rest);
        } else {
            // `head`和`rest`在这里有定义,
            // 但解引用时会抛出ReferenceError,
            // 因为如果模式失败,
            // 绑定模式一定没有执行。
        }
    }
    
    function bar(x) {
        if(x is not {let necessaryProperty}) {
            // 模式成功,因为`x.necessaryProperty`
            // 不存在。
            return;
        }
        // 这里模式失败,因为`x.necessaryProperty`
        // *存在*,所以绑定模式被执行了,
        // `necessaryProperty`绑定在这里可见。
        console.log(necessaryProperty);
    }

    当在for()头中使用时,通常的绑定作用域适用:绑定作用域限于for()头和块,并且在for-of的情况下,复制到内部每迭代绑定作用域。

    whiledo-while目前没有对头中内容的特殊作用域规则。我们建议它们采用与for-of块相同的规则:头位于围绕规则的新的作用域中,其绑定复制到围绕{}块的每迭代作用域。对于do-while,在第一次迭代中绑定处于TDZ,直到头执行。

    激励示例

    下面是我们期望模式匹配将被广泛使用的选定的情况。因此,我们希望尽可能优化这些情况的易用性。


    验证JSON结构。

    这是代码的简单解构版本,不事先对数据进行任何检查,只是拆开它并希望一切正确:

    var json = {
      'user': ['Lily', 13]
    };
    var {user: [name, age]} = json;
    print(`User ${name} is ${age} years old.`);

    带检查的解构,确保一切正确且形状符合预期:

    if ( json.user !== undefined ) {
      var user = json.user;
      if (Array.isArray(user) &&
          user.length == 2 &&
          typeof user[0] == "string" &&
          typeof user[1] == "number") {
        var [name, age] = user;
        print(`User ${name} is ${age} years old.`);
      }
    }

    完全相同的检查,但使用模式匹配:

    if( json is {user: [String and let name, Number and let age]} ) {
      print(`User ${name} is ${age} years old.`);
    }

    匹配fetch()响应:

    const res = await fetch(jsonService)
    match (res) {
      when { status: 200, headers: { 'Content-Length': let s } }:
        console.log(`size is ${s}`);
      when { status: 404 }:
        console.log('JSON not found');
      when { let status } and if (status >= 400): do {
        throw new RequestError(res);
      }
    };

    更简洁、更函数式的Redux reducer处理(与此Redux文档中的相同示例比较):

    function todosReducer(state = initialState, action) {
      return match (action) {
        when { type: 'set-visibility-filter', payload: let visFilter }:
          { ...state, visFilter };
        when { type: 'add-todo', payload: let text }:
          { ...state, todos: [...state.todos, { text, completed: false }] };
        when { type: 'toggle-todo', payload: let index }: do {
          const newTodos = state.todos.map((todo, i) => {
            return i !== index ? todo : {
              ...todo,
              completed: !todo.completed
            };
          });
    
          ({
            ...state,
            todos: newTodos,
          });
        }
        default: state // 忽略未知操作
      }
    }

    JSX中简洁的条件逻辑(通过Divjot Singh):

    <Fetch url={API_URL}>
      {props => match (props) {
        when {loading}: <Loading />;
        when {let error}: do {
          console.err("something bad happened");
          <Error error={error} />
        };
        when {let data}: <Page data={data} />;
      }}
    </Fetch>

    可能的未来增强

    Void模式

    关键字void是一个模式,始终匹配,并且不做其他事情。它在结构模式中有用,当你想要测试属性的存在而不关心它的值时。

    这是最可能移回主提案的提案;它之所以被拉出,仅仅是因为我们想确保它与Void绑定保持一致。

    async match

    如果match构造出现在允许await的上下文中,await已经可以在其中使用,就像在do表达式中一样。然而,就像async do表达式一样,能够在即使不在async function内部的情况下使用await并产生一个Promise,也有其用途。

    async match (await subject) {
      when { let a }: await a;
      when { let b }: b.then(() => 42);
      default: await somethingThatRejects();
    } // 产生一个Promise

    关系模式

    目前有一些模式表达各种类型的相等性,以及某种instanceof(针对类的自定义匹配器)。我们可以表达更多类型的基于运算符的检查,例如:

    match(val) {
        when < 10: console.log("small");
        when >= 10 and < 20: console.log("mid");
        default: "large";
    }

    通常,所有二元布尔运算符都可以使用,主题作为运算符的隐式左操作数。

    (这可能会稍微束缚我们未来对模式语法扩展的手脚,但我们不太可能想要以不同于它们在表达式上下文中的工作方式的方式重用现有的运算符。)

    默认值

    解构可以通过= <expr>提供默认值,当键不存在时使用。这对模式匹配有用吗?

    可选键似乎合理;目前它们需要重复模式,如({a, b} or {a})(如果不存在,b将在RHS中绑定为undefined)。

    我们需要/想要完整的默认值吗?在那里放置任意的JS表达式,没有任何类似包装字符来区分它与周围的模式,会不会使语法过于复杂?

    这将使我们更接近解构,这是很好的。

    解构增强

    解构和模式匹配应该保持同步,因此对一个的增强需要适用于另一个。

    catch集成

    允许catch语句有条件地捕获异常,节省一层缩进:

    try {
      throw new TypeError('a');
    } catch match (e) {
      when RangeError: ...;
      when /^abc$/: ...;
      // 不匹配,默认重新抛出e
    }

    或者可能只允许在catch头中进行is检查:

    try {
      throw new TypeError('a');
    } catch (e is RangeError) {
        ...
    } catch (e is /^abc$/) {
        ...
    }

    (在这两种情况中,用于主题的名称自动创建绑定,与今天的catch (e)相同。)

    守卫链

    一些合理的用例需要重复模式,例如:

    match (res) {
      when { status: 200 or 201, let pages, let data } and if (pages > 1):
        handlePagedData(pages, data);
      when { status: 200 or 201, let pages, let data } and if (pages === 1):
        handleSinglePage(data);
      default: handleError(res);
    }

    我们可能希望允许匹配构造被链接,其中子匹配构造能看到其父子句中引入的绑定,如果子类中没有匹配,则整个父子句失败。

    上述可以写成:

    match (res) {
      when { status: 200 or 201, let data } match {
        when { pages: 1 }: handleSinglePage(data);
        when { pages: >= 2 and let pages }: handlePagedData(pages, data);
      };
      default: handleError(res);
      // 如果状态指示错误,
      // 或如果数据没有匹配上述情况之一,
      // 特别是如果pages == 0
    }

    注意子构造中缺少<subject-expression>(只是match {...}),表示它从when链接,而不是作为RHS中独立的匹配构造(那样会,如果没有任何子句匹配,会抛出异常):

    match (res) {
      when { status: 200 or 201, let data }: match(res) {
        when { pages: 1}: handleSinglePage(data);
        when { pages: >= 2 and let pages}: handlePagedData(pages, data);
        // 只是一个RHS,所以如果pages == 0,
        // 内部构造匹配失败
        // 并抛出TypeError
      };
      default: handleError(res);
    }

    分隔冒号的存在或缺失也区分了这些情况,当然。