这是当时面阿里云遇到的一个手撕代码题,而且是三道里面最简单的一道……现在看来仍然很有难度。现在一步一步解决它。
1 题目
实现一个 render
函数,函数传入一个字符串和一个对象,将字符串里用 {{ }}
标记的占位符用对象里的属性值替换,并返回新字符串。
示例:
1 | let template = "你好,我们是{{company}},我们来自{{group}},我们有{{business[0]}}、{{business[1]}}等。" |
返回:
1 | "你好,我们是阿里,我们来自蚂蚁,我们有支付宝、蚂蚁金服等。" |
题目保证占位符一定是对象里的属性名。
2 暴力解法
最简单的方法就是直接使用 split
将字符串分割开,然后使用 switch
循环挨个替换。
1 | function render(template, obj) { |
这样做很不优雅,而且只能解决示例,无法解决其他判例。
3 从简单例子开始
既然上面的 split
为了同时分割 {{ }}
使用了正则,那么我们为什么不用正则来做呢!
我们暂时不考虑数组的形式,从一个简单的例子开始:
1 | let template = "我叫{{name}},我今年{{age}}岁了。"; |
然后来看看几个使用正则的方法:
3.1 方法 1
1 | function render(template, obj) { |
这个方法的思路是,先把所有 {{*}}
格式的占位符全部去掉大括号(使用圆括号捕获的 $1
),然后尝试与 obj
的属性名匹配。
这个方法的问题在于:
属性名不一定是
\w
(即字母、数字或下划线)。如果出现了不必要替换的字符串,就会出现错误替换,比如:
1
let template = "我的name是{{name}},我今年{{age}}岁了。";
因此我们最好把占位符做个整体替换,而非去掉大括号后再替换。
3.2 方法 2
1 | function render(template, obj) { |
这个方法是整体替换的思路,先拿出所有 {{*}}
格式的占位符,放到一个数组里,然后通过 replace
去掉大括号,并替换原数组中的占位符。
但是我们发现要多次匹配 {{*}}
,并且属性名也不一定是 [a-zA-Z\d]+
。
3.3 方法 3
1 | function render(template, obj) { |
由于 replace
本身就做了一步正则匹配,遍历所有的键,再匹配替换一步走就行了。
这个方法还是不够精炼,我们希望直接拿到 template
里的占位符 {{*}}
,而不要通过 obj
的 key
。
3.4 方法 4
1 | function render(template, obj) { |
这里补充几个知识:
.*?
是正则固定用法,表示非贪婪匹配,防止从第一个{{` 到最后一个 `}}
都被匹配replace
第二个参数支持一个回调函数,回调函数第一个参数是匹配结果,第二个参数是正则中的子表达式匹配结果(可以有 0 个或多个该参数),接下来的参数是匹配位置,最后的参数是原串本身
我们用 .*?
匹配了任何 {{*}}
格式的占位符并用圆括号捕获了内部的属性值,然后使用函数接收捕获结果并替换。
4 更复杂一点
我们现在打算替换 {{business[0]}}
这样的占位符。这给我们带来了两个问题:
- 如何匹配?可以试试改一下正则表达式
- 如何替换?如果还使用
obj[key]
就不行了,我们必须分开字母与后面的索引
4.1 改写方法 2
我们改写方法 2 的正则表达式:
1 | function render(template, obj) { |
这里使用 ?
判断 [*]
表达式存在与否,使用 \[\d+\]
匹配 [*]
表达式。
注意这里使用了 eval
函数,它将传入它的 JS 字符串作为 JS 语句执行。这是一个非常危险的方法,我们一般不使用它。
在例子中,eval
得到的 JS 语句 obj.business[0]
等同于 obj["business"][0]
,因此得到 "支付宝"
。
4.2 改写方法 4
方法 4 的正则表达式无需修改,只要把 eval
加上就行了。
1 | function render(template, obj) { |
我们还是不要用 eval
好了,那就在匹配时分别拿到前后两部分。
5 不要 eval
1 | function render(template, obj) { |
通过这个正则表达式,我们在三个圆括号表达式里分别捕获了属性值、索引和索引内的数字,那就拿到函数里操作就行了。
完美!