lucknessbuaa

ECMAScript中的模式匹配

原文链接: ponyfoo.com

JavaScript中关于模式匹配有一个处于stage 0阶段的提案。在这篇文章中,我们会讲解这个提案的内容和作用。

提案文档中照常会有一些示例代码,下面是其中之一。

let length = vector => match (vector) {
  { x, y, z }: Math.sqrt(x ** 2 + y ** 2 + z ** 2),
  { x, y }: Math.sqrt(x ** 2 + y ** 2),
  [...]: vector.length,
  else: {
    throw new Error(`Unknown vector type`)
  }
}

理解上面的代码可能有点儿困难,里面充满了我们不熟悉的代码,并同时使用了箭头函数,let赋值语句和一系列幂操作符。让我们从简单的例子开始。

ECMAScript模式匹配的基本类型

下面的例子是一个match表达式,接收point作为参数。当point同时包含x属性和y属性的时候,表达式返回[point.x, point.y]

const point = { x: 5, y: 7 }
const result = match (point) {
  { x, y }: [point.x, point.y]
}
console.log(result) // <- [5, 7]

为了方便起见,可以把上面的代码转化成一个函数。

function matchPoint(point) {
  return match (point) {
    { x, y }: [point.x, point.y]
  }
}

或者箭头函数。看下面的代码是不是很简洁?

const matchPoint = point => match (point) {
  { x, y }: [point.x, point.y]
}

我们还可以继续简化!在上面的{x, y}模式中,xy会被绑定到point变量的同名属性中,也就是说我们可以把代码改成下面的形式。注意在每一个匹配项中,xy会被绑定当且仅当当前匹配项是满足的。

const matchPoint = point => match (point) {
  { x, y }: [x, y]
}

此外,match表达式也可用于匹配数组。在下面的例子中,我们匹配了有两个元素的数组,并且把这两个元素绑定到xy上。仅仅为了有趣,我们把这个函数取名为flipPoint

const flipPoint = point => match (point) {
  { x, y }: [x, y],
  [x, y]: { x, y }
}
flipPoint([3, 7]) // { x: 3, y: 7 }
flipPoint({ x: 3, y: 7 }) // [3, 7]

注意如果point变量和任何模式都不匹配的话,则会触发一个运行时错误。

matchPoint({ x: 3, z: 7 })
// <- Error

但是,我们可以有选择性的使用一个else来处理匹配都不满足的情况。

const matchPoint = point => match (point) {
  { x, y }: [x, y],
  else: [0, 0]
}

matchPoint({ x: 3, z: 7 })
// <- [0, 0]

对于每个匹配项,除了隐式地返回一个表达式之外,还可以使用代码块。类似于箭头函数的使用方式。

const matchPoint = point => match (point) {
  { x, y }: {
    return [x, y]
  },
  else: {
    throw new Error(`That's not even a point!`)
  }
}

更多的匹配类型

匹配还包括字面匹配。也就是说我们不仅可以匹配像0这样的数字,像'two'这样的字符串,还可以匹配nullundefinedtruefalse。咋一看可能没什么用处,但是当你恰好有这种使用场景的时候,使用它们就会很方便。

匹配对象的时候遵循包含性{x, y}模式同样会匹配{x, y, z}

const matchPoint = point => match (point) {
  { x, y }: [x, y]
}
matchPoint({ x: 2, y: 5, z: -1 })
// <- [2, 5]

当我们想像解构操作一样获取其他属性的时候(比如一些可选属性),我们可以使用类似的...操作符。

const matchPoint = point => match (point) {
  { x, y, ...options }: { point: [x, y], options }
}
matchPoint({ x: 2, y: 5, radius: 50, width: 3 })
// <- { point: [2, 5], options: { radius: 50, width: 3 } }

对象匹配允许嵌套。我们可以使用{x: 3, y: 4}来匹配x属性是3,y属性是4的对象,举例如下。

const matchNullPoint = point => match (point) {
  { x: 0, y: 0 }: [x, y]
}
matchNullPoint({ x: 0, y: 0 })
// <- [0, 0]

嵌套模式中也可以包含对象匹配项。

const isUSD = item => match (item) {
  { options: { currency: 'USD' } }: true,
  else: false
}
isUSD({ value: 19.99, options: { currency: 'USD' } })
// <- true
isUSD({ value: 19.99, options: { currency: 'ARS' } })
// <- false

数组模式匹配有点儿不同之处是它默认是排外的:模式[]只能匹配具有length属性的空类数组对象,不同于模式{}可以匹配任何对象。

数组模式可以通过添加...变成包含性的。不像rest变量,spread语法和对象模式匹配语法,我们没有必要给rest变量命名。我们可以简单的使用[...]来匹配任意长度的数组。或者我们可以使用[first, ...]来匹配长度至少是1的数组(原文是匹配任意长度的数组),并且把它的第一个元素绑定到first上。使用[...rest]会把所有的元素绑定到rest上。

和对象模式匹配类似,数组也支持嵌套。下面的例子匹配一个数组,这个数组只有一个元素(因为数组模式是排外的,除非使用...),并且这个元素是一个对象,并且这个对象同时有xy属性(或许还有其他属性因为对象是包含性的)。

const matchPoint = point => match (point) {
  [{ x, y }]: [x, y]
}
matchPoint([{ x: 1, y: 2 }]) // <- [1, 2]

标识符和Symbol.matches

我们也可以使用正则表达式进行匹配。注意只能使用标识符作为有效的匹配模式,也就是下面例子中的numbers标识符,不能直接使用字面的正则表达式或任意类型的表达式。这会使得match在减少语法复杂性的同时保持功能的强大。

const numbers = /^-?\d+,\s*-?\d+$/
const matchPoint = point => match (point) {
  { x, y }: [x, y],
  [x, y]: [x, y],
  numbers: point.split(/,\s*/).map(n => parseInt(n))
}

matchPoint({ x: 7, y: -3 }) // <- [7, -3]
matchPoint([7, -3]) // <- [7, -3]
matchPoint(`7, -3`) // <- [7, -3]

多亏了symbols,我们才能够使用正则表达式进行匹配。该提案使用模式的Symbol.matches方法来判断接收到的值是否匹配。

const threeDigitNumber = {
  [Symbol.matches](value) {
    return value >= 100 && value < 1000
  }
}

现在,我们可以在匹配模式中使用threeDigitNumber标识符。

const matchPoint = point => match (point) {
  threeDigitNumber: point.toString.split(``).map(n => parseInt(n))
}
matchPoint(735) // <- [7, 3, 5]

该提案正在积极发展,一些有用的Symbol.matches扩展和内置应用也正在被考虑。

如果类似基本类型模式匹配Symbol.matches的技术被原生支持的话,我们就有能力做一些类似于在原生的JavaScript中(至少在运行时)做类型检查的事情。使用类似的语法进行有趣的静态类型检查就有可能实现。?

const matchPoint = point => match (point) {
  { x: Number, y: Number }: [x, y]
}
matchPoint({ x: 1, y: 2 }) // <- [1, 2]
matchPoint({ x: 1, z: 2 }) // <- Error
matchPoint({ x: 1, y: 'two' }) // <- Error

一如既往,记住这个提案还在stage 0阶段,因此提案的内容很有可能会有变动,更甚者有可能不会成为官方JavaScript的语言特性。?