JavaScript中的模式匹配
模式是用於轉換輸入數據的規則。 以將數據與一個或多個邏輯結構進行比較,將數據分解為各個構成部分,或以各種方式從數據中提取信息。
安裝
JavaScript已經實現模式匹配解構功能,沒有實現模式匹配過濾功能。用模式來控制程序流,可以編寫更加聲明性,更加模塊化的代碼,請安裝structural-comparison
以支持此功能。structural-comparison
是一個函數庫。包括函數式編程的常用函數。開源於GitHub,見xp44mm/structural-comparison
倉庫。
npm i structural-comparison
用法:
import { match } from 'structural-comparison'
match: (pattern:string) -> (input:any) -> boolean
match
是一個柯里函數,模式參數是一個字符串,輸入參數可以是任何值,當匹配成功返回真,否則返回假。
示例
模式匹配分為字面量模式,類型測試模式,標識符模式,數組模式,對象模式,OR模式,以及它們的組合嵌套模式。
字面量模式
測試原子值。模式是基元值字面量,支持JSON中的所有字面量。包括null
,布爾值,數字值,字符串值。字符串是JSON的雙引號格式。不支持undefined
,NaN
,Infinity
等非JSON值。
test("value NULL", () => {
let y = match('null')
expect(y(null)).toEqual(true)
expect(y(3)).toEqual(false)
})
這里y
函數等價於:
let y = input => input === null
輸入對象相等(===
)於模式。
其他字面量模式的示例:
test("value boolean", () => {
let y = match('false')
let v = y(false)
let w = y(3)
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("value number", () => {
let y = match('123')
let v = y(123)
let w = y(3)
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("value quote", () => {
let y = match('""')
let v = y('')
let w = y(3)
expect(v).toEqual(true)
expect(w).toEqual(false)
})
類型模式
測試數據的數據類型。模式是數據類型的名稱,不帶雙引號。包括boolean
,string
,number
,function
。
test("value TYPE", () => {
let y = match('number')
let v = y(5)
let w = y(true)
expect(v).toEqual(true)
expect(w).toEqual(false)
})
這里y
函數等價於:
let y = input => typeof input === 'number'
輸入對象typeof
值等於模式中的數據類型名稱。
標識符模式
是一個合法的JavaScript標識符,除了標識符不包括$
字符,但是不能是類型名稱。模式中的標識符和類型名都是區分大小寫的,這和JavaScript語法一致。標識符模式始終成功匹配任何一個值。
test("value ID", () => {
let y = match('x')
let v = y(5)
let w = y({})
expect(v).toEqual(true)
expect(w).toEqual(true)
})
通配模式雖然是標識符,但實際上,是一個棄元(discard),棄元表示一個我們完全用不上的數值。僅用於占位。相同的名稱不會引起名稱沖突。
數組模式
匹配一個數組。數組匹配根據數組元素長度分為長度嚴格匹配,和最短長度匹配。嚴格匹配示例:
test("value array", () => {
let y = match('[]')
let v = y([])
let w = y({})
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("array elements", () => {
let input = '[1]'
let y = match(input)
let v = y([1])
let w = y([{ x: 0 }])
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("elements elements value", () => {
let input = '[1, 2]'
let y = match(input)
let v = y([1, 2])
let w = y([null, 1])
expect(v).toEqual(true)
expect(w).toEqual(false)
})
如果有省略號表示可以匹配任何更多的數組元素。最短長度匹配:
test("array ELLIPSIS", () => {
let input = '[...]'
let y = match(input)
let v = y([1])
let w = y({})
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("array elements ELLIPSIS", () => {
let input = '[null, ...]'
let y = match(input)
let v = y([null])
let w = y([null, 0])
let p = y([0])
expect(v).toEqual(true)
expect(w).toEqual(true)
expect(p).toEqual(false)
})
test("array ELLIPSIS elements", () => {
let input = '[ ..., null]'
let y = match(input)
let v = y([null])
let w = y([0, null])
let p = y([null, 0])
expect(v).toEqual(true)
expect(w).toEqual(true)
expect(p).toEqual(false)
})
test("array elements ELLIPSIS elements", () => {
let y = match('[1,2,...,4,5]')
let v = y([1,2,3,4,5])
let w = y([1,2,3])
expect(v).toEqual(true)
expect(w).toEqual(false)
})
數組語法不支持洞(連續逗號),不支持尾逗號。不支持迭代器。
數組模式大致編譯成如下:
let y = input => Array.isArray(input) && every elements matched
對象模式
匹配一個對象。如果有省略號表示對象可以有任何更多的屬性。只檢測自有屬性(Object.keys
),忽略原型中的屬性。對象語法支持特殊標識屬性,快捷屬性,屬性不支持尾逗號。
test("value object", () => {
let input = '{}'
let y = match(input)
let v = y({})
let w = y({ x: 0 })
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("object ELLIPSIS", () => {
let input = '{...}'
let y = match(input)
let v = y({})
let w = y({ x: 0 })
let p = y([])
expect(v).toEqual(true)
expect(w).toEqual(true)
expect(p).toEqual(false)
})
test("object properties", () => {
let input = '{x}'
let y = match(input)
let v = y({ x: 0 })
let w = y([null, 1])
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("object properties ELLIPSIS", () => {
let input = '{x,...}'
let y = match(input)
let v = y({ x: 0, y: 1 })
let w = y({})
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("properties properties prop", () => {
let input = '{x,y}'
let y = match(input)
let v = y({ x: 0, y: 1 })
let w = y({})
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("prop key value", () => {
let input = '{x:null}'
let y = match(input)
let v = y({ x: null })
let w = y([null, 1])
expect(v).toEqual(true)
expect(w).toEqual(false)
})
test("key QUOTE", () => {
let input = '{"1":null}'
let y = match(input)
let v = y({ '1': null })
let w = y([null, 1])
expect(v).toEqual(true)
expect(w).toEqual(false)
})
對象模式編譯成:
let y = obj => typeof obj === 'object' && obj && !Array.isArray(obj) && every props matched
OR模式
或模式,用一個豎杠符號連接兩個模式,兩個模式中任何一個模式成功即整體匹配成功。
test("OR", () => {
let y = match('string|{...}')
let a = y('')
let b = y({})
let c = y([])
let d = y(x => x)
expect(a).toEqual(true)
expect(b).toEqual(true)
expect(c).toEqual(false)
expect(d).toEqual(false)
})
OR模式編譯成:
let y = obj => pat1 matched || pat2 matched
嵌套模式。匹配任意深度數據結構。
memoize解析的結果
上面的代碼示例都用match(pattern)
來緩存,但是實際上,我們常用if else
條件選擇語句來和模式匹配連用。
let y = x => {
if (match('[...]')(x)) console.log('[]')
else if (match('{...}')(x)) console.log('{}')
else if (match('string')(x)) console.log('string')
}
y([1]) // print []
這個是沒有緩存的程序,每次調用y函數都會重新解析模式,對性能造成負面沖擊。所以,我們需要緩存。
let arr = match('[...]')
let obj = match('{...}')
let str = match('string')
let y = x => {
if (arr(x)) console.log('[]')
else if (obj(x)) console.log('{}')
else if (str(x)) console.log('string')
}
y([1]) // print []
上面程序成功解決了性能問題,避免重復解析,但是引入中間變量導致代碼復雜難懂。我們使用另一種解決方案。
import { match, cond } from 'structural-comparison'
let y = cond([
[match('[...]'), x => { console.log('[]') }],
[match('{...}'), x => { console.log('{}') }],
[match('string'), x => { console.log('string') }],
x => {
console.log('no matched!')
}
])
y([1]) // print []
y({}) // print {}
y(1) // print no matched!
cond
函數是一個返回函數的組合子,用來模擬if else
語句。它接受一個數組,數組的每個元素代表條件語句的一個分支。分支分為兩種形式,第一種是斷言函數,和行為函數組成的數組,當斷言為真時,執行並返回行為,斷言為假時跳過行為函數,執行下一分支。第二種是一個函數,當函數為真時,返回函數的返回值,當函數為假時,丟棄函數返回值,執行下一分支。cond
函數依次執行每個分支,返回第一個為真的分支結果為整體的結果。忽略其后所有分支。