# stringToPath
# Description
stringToPath
作用是将深层嵌套属性字符串转换成路径数组,即将类似于 a.b.c
这样的字符串,转换成 ['a', 'b', 'c'] 这样的数组,方便 lodash
从数组中将属性一个一个取出,然后取值。
# Params
string
# Return
Array
# Depend
import memoizeCapped from './memoizeCapped.js'
# Code
const charCodeOfDot = '.'.charCodeAt(0)
const reEscapeChar = /\\(\\)?/g
const rePropName = RegExp(
// Match anything that isn't a dot or bracket.
'[^.[\\]]+' + '|' +
// Or match property names within brackets.
'\\[(?:' +
// Match a non-string expression.
'([^"\'][^[]*)' + '|' +
// Or match strings (supports escaping characters).
'(["\'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2' +
')\\]'+ '|' +
// Or match "" as the space between consecutive dots or empty brackets.
'(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|$))'
, 'g')
const stringToPath = memoizeCapped((string) => {
const result = []
if (string.charCodeAt(0) === charCodeOfDot) {
result.push('')
}
string.replace(rePropName, (match, expression, quote, subString) => {
let key = match
if (quote) {
key = subString.replace(reEscapeChar, '$1')
}
else if (expression) {
key = expression.trim()
}
result.push(key)
})
return result
})
# Analyze
# Regexp
const rePropName = /[^.[\]]+|\[(?:([^"'][^[]*)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/
正则可以分为三块来看
[^.[\]]+
匹配除 .
, [
, ]
意外的字符一次或者更多次
a.b.c
[^xyz]
一个反向字符集。也就是说, 它匹配任何没有包含在方括号中的字符。你可以使用破折号(-)来指定一个字符范围。任何普通字符在这里都是起作用的。
例如,[^abc] 和 [^a-c] 是一样的。他们匹配 "brisket" 中的‘r’,也匹配 “chop” 中的‘h’
\[(?:([^"'][^[]*)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]
a[0].b
第二部分可以接着进行拆分
?:x
匹配
'x'
但是不记住匹配项。这种括号叫作非捕获括号,使得你能够定义与正则表达式运算符一起使用的子表达式。看看这个例子/(?:foo){1,2}/
。如果表达式是/foo{1,2}/
,{1,2}
将只应用于'foo'
的最后一个字符'o'
。如果使用非捕获括号,则{1,2}
会应用于整个'foo'
单词。
([^"'][^[]*)
匹配 "空字符串"(非字符串表达式)
(["'])
匹配 "
, '
[xyz]
一个字符集合。匹配方括号中的任意字符,包括转义序列。你可以使用破折号(-)来指定一个字符范围。对于点(.)和星号(*)这样的特殊符号在一个字符集中没有特殊的意义。他们不必进行转义,不过转义也是起作用的。 例如,[abcd] 和 [a-d] 是一样的。他们都匹配 "brisket" 中的‘b’, 也都匹配 “city” 中的‘c’。/[a-z.]+/ 和 /[\w.]+/ 与字符串 “test.i.ng” 匹配。
(["'])((?:(?!\2)[^\\]|\\.)*?)\2
匹配字符串,支持转移字符
\2 代表第二个 ()
x(?!y)
仅仅当 'x' 后面不跟着 'y' 时匹配 'x',这被称为正向否定查找。
例如,仅仅当这个数字后面没有跟小数点的时候,/\d+(?!.)/ 匹配一个数字。正则表达式 /\d+(?!.)/.exec ("3.141") 匹配‘141’而不是‘3.141’
(?=(?:\.|\[\])(?:\.|\[\]|$))
....... [][][][]
获取连续的 .
或者连续的 []
中的间隔
x(?=y)
匹配 'x' 仅仅当 'x' 后面跟着 'y'. 这种叫做先行断言。
例如,/Jack (?=Sprat)/ 会匹配到 'Jack' 仅当它后面跟着 'Sprat'。/Jack (?=Sprat|Frost)/ 匹配‘Jack’仅当它后面跟着 'Sprat' 或者是‘Frost’。但是‘Sprat ’和‘Frost’都不是匹配结果的一部分。
# 处理以 .
开头的字符串
const charCodeOfDot = '.'.charCodeAt(0)
const result = []
if (string.charCodeAt(0) === charCodeOfDot) {
result.push('')
}
以 .
开头的字符串,lodash
会 push
一个空字符串。
# 处理常规路径,如 a.b.c
a.b.c
会匹配到 [^.[\]]+
正则,匹配3次,拿到 a
、b
、c
,代码逻辑简化如下
string.replace(rePropName, (match) => {
let key = match
result.push(key)
})
# 处理中括号取值,如 a['b'].c
、a[0].b
a[0].b
和 a['b'].c
会匹配到 \[(?:([^"'][^[]*)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]
, 代码逻辑简化如下
string.replace(rePropName, (match, expression, quote) => {
let key = match
if (quote) {
key = subString.replace(reEscapeChar, '$1')
}
else if (expression) {
key = expression.trim()
}
result.push(key)
})
a[0].b
匹配时
- 此时
key
匹配到match
为[0]
- 但是
expression
(触发([^"'][^[]*)
正则) 为0
- 所以最终结果为
0
,返回a
、0
、b
a['b'].c
匹配时
- 此时
key
匹配到match
为 ['b'] - 触发
quote
(触发(["'])
正则) 判断 - 此时
subString
(触发((?:(?!\2)[^\\]|\\.)*?)
正则) 为'b'
- 如果是转义字符 如:
a[\'\\b\'].d"
,此时subString
就为\b
,此时调用replace
方法,去除掉转义,返回b
# 最终会返回结果数组
# Remark
- String.prototype.replace() MDN (opens new window)
- 正则表达式 MDN (opens new window)
- 老姚的正则表达式mini GitHub (opens new window)
- JavaScript正则表达式mini 1.1
# Example
stringToPath("a[b].c") // [ 'a', 'b', 'c' ]
stringToPath("a[b][c].d") // [ 'a', 'b', 'c', 'd' ]
stringToPath("a[\'b\'][c].d") // [ 'a', 'b', 'c', 'd' ]
stringToPath("a[\'\\b\']['c'].d") // [ 'a', 'b', 'c', 'd' ]
stringToPath("a.b.c[d]") // [ 'a', 'b', 'c', 'd' ]
stringToPath("['a']['b'].c['d']") // [ 'a', 'b', 'c', 'd' ]
← stringToArray toKey →