一行代码实现一个简单的模板字符串替换

一行代码实现一个简单的模板字符串替换

作者:微醺岁月

http://segmentfault.com/a/1190000013516128



起始

同许多初学

Javascript

的菜鸟一样,起初,我也是采用拼接字符串的形式,将

JSON

数据嵌入

HTML

中。开始时代码量较少,暂时还可以接受。但当页面结构复杂起来后,其弱点开始变得无法忍受起来:

  • 书写不连贯。每写一个变量就要断一下,插入一个 + 和 "。十分容易出错。

  • 无法重用。

    HTML

     片段都是离散化的数据,难以对其中重复的部分进行提取。

  • 无法很好地利用 

    <template>

     标签。这是 

    HTML5

     中新增的一个标签,标准极力推荐将 

    HTML

     模板放入 

    <template>

     标签中,使代码更简洁。

  • 当时我的心情就是这样的:

    这TMD是在逗我吗。

    于是出来了后来的

    ES6

    ,ES6的模板字符串用起来着实方便,对于比较老的项目,项目没

    webpack

    gulp

    等构建工具,无法使用

    ES6

    的语法,但是想也借鉴这种优秀的处理字符串拼接的方式,我们不妨可以试着自己写一个,主要是思路,可以使用

    ES6

    语法模拟 ES6的模板字符串的这个功能。

    后端返回的一般都是

    JSON

    的数据格式,所以我们按照下面的规则进行模拟。

    需求描述

    实现一个 render(template, context) 方法,将 template 中的占位符用 context 填充。

    要求:

    不需要有控制流成分(如 循环、条件 等等),只要有变量替换功能即可;

    级联的变量也可以展开;

    被转义的的分隔符 { 和 } 不应该被渲染,分隔符与变量之间允许有空白字符。

    var

     obj 

    =

     

    {

    name

    :

    "二月"

    ,

    age

    :

    "15"

    }

    ;

    var

     str 

    =

     

    "{{name}}很厉害,才{{age}}岁"

    ;

    输出:二月很厉害,才

    15

    岁。

    PS:本文需要对正则表达式有一定的了解,如果还不了解正则表达式,建议先去学习一下,正则也是面试笔试必备的技能,上面链接末尾有不少正则学习的链接。

    如果是你,你会怎么实现?可以先尝试自己写写,实现也不难。

    先不说我的实现,我把这个题给其他好友做的时候,实现的不尽相同,我们先看几位童鞋的实现,然后在他们的基础上找到常见的误区以及实现不够优雅的地方。

    二月童鞋:

    let

     str 

    =

     

    "{{name}}很厉害,才{{age}}岁"

    let

     obj 

    =

     

    {

    name

    :

     

    "二月"

    ,

     age

    :

     

    15

    }

    function

     

    test

    (

    str

    ,

     obj

    )

    {

        

    let

     _s 

    =

     str

    .

    replace

    (

    /\{\{(\w+)\}\}/g

    ,

     

    "$1"

    )

        

    let

     result

        

    for

    (

    let

     k 

    in

     obj

    )

     

    {

          _s 

    =

     _s

    .

    replace

    (

    new

     

    RegExp

    (

    k

    ,

     

    "g"

    )

    ,

     obj

    [

    k

    ]

    )

        

    }

      

    return

     _s

    }

    const

     s 

    =

     

    test

    (

    str

    ,

     obj

    )

    最基本的是实现了,但是代码还是有很多问题没考虑到,首先 Object 的 key 值不一定只是\w,

    还有就是如果字符串是这种的:

    let

     str 

    =

     

    "{{name}}很name厉害,才{{age}}岁"

    `

    会输出 :二月很厉害二月害,才

    15

    此处你需要了解正则的分组才会明白

    $1

    的含义,错误很明显,把本来就是字符串不要替换的

    name

    也给替换了,从代码我们可以看出二月的思路。

  • 代码的作用目标是 

    str

    ,先用正则匹配出 

    {{name}}

     和 

    {{age}}

    ,然后用分组获取括号的 

    name

    ,

    age

    ,最后用 

    replace

     方法把 

    {{name}}

     和 

    {{age}}

     替换成 

    name

     和 

    age

    ,最后字符串就成了 name很name厉害,才age岁,最后 

    for in

     循环的时候才导致一起都被替换掉了。

  • 用 

    for in

     循环完全没必要,能不用 

    for in

     尽量不要用 

    for in

    for in

     会遍历自身以及原型链所有的属性。

  • 志钦童鞋:

    var

     str 

    =

     

    "{{name}}很厉害,才{{age}}岁"

    ;

    var

     str2 

    =

     

    "{{name}}很厉name害,才{{age}}岁{{name}}"

    ;



    var

     obj 

    =

     

    {

    name

    :

     

    "周杰伦"

    ,

     age

    :

     

    15

    }

    ;

    function

     

    fun

    (

    str

    ,

     obj

    )

     

    {

        

    var

     arr

    ;

        arr 

    =

     str

    .

    match

    (

    /{{[a-zA-Z\d]+}}/g

    )

    ;

        

    for

    (

    var

     i

    =

    0

    ;

    i

    <

    arr

    .

    length

    ;

    i

    ++

    )

    {

            arr

    [

    i

    ]

     

    =

     arr

    [

    i

    ]

    .

    replace

    (

    /{{|}}/g

    ,

    ""

    )

    ;

            str 

    =

     str

    .

    replace

    (

    "{{"

    +

    arr

    [

    i

    ]

    +

    "}}"

    ,

    obj

    [

    arr

    [

    i

    ]

    ]

    )

    ;

        

    }

        

    return

     str

    ;

    }

    console

    .

    log

    (

    fun

    (

    str

    ,

    obj

    )

    )

    ;

    console

    .

    log

    (

    fun

    (

    str2

    ,

    obj

    )

    )

    ;

    思路是正确的,知道最后要替换的是

    {{name}}

    {{age}}

    整体,而不是像二月童鞋那样最后去替换

    name

    ,所有跑起来肯定没问题,实现是实现了但是感觉有点那个,我们要探讨的是一行代码也就是代码越少越好。

    小维童鞋:

    function

     

    a

    (

    str

    ,

     obj

    )

     

    {

      

    var

     str1 

    =

     str

    ;

      

    for

     

    (

    var

     key 

    in

     obj

    )

     

    {

        

    var

     re 

    =

     

    new

     

    RegExp

    (

    "{{"

     

    +

     key 

    +

     

    "}}"

    ,

     

    "g"

    )

    ;

        str1 

    =

     str1

    .

    replace

    (

    re

    ,

     obj

    [

    key

    ]

    )

    ;

      

    }

      console

    .

    log

    (

    str1

    )

    ;

    }

    const

     str 

    =

     

    "{{name}}很厉name害{{name}},才{{age}}岁"

    ;

    const

     obj 

    =

     

    {

     name

    :

     

    "jawil"

    ,

     age

    :

     

    "15"

     

    }

    ;

    a

    (

    str

    ,

     obj

    )

    ;

    实现的已经简单明了了,就是把

    obj

    key

    值遍历,然后拼成

    {{key}}

    ,最后用

    obj[key]

    也就是

    value

    {{key}}

    整个给替换了,思路很好,跟我最初的版本一个样。

    我的实现:

    function

     

    parseString

    (

    str

    ,

     obj

    )

     

    {

      Object

    .

    keys

    (

    obj

    )

    .

    forEach

    (

    key 

    =

    >

     

    {

        str 

    =

     str

    .

    replace

    (

    new

     

    RegExp

    (

    `{{

    ${

    key

    }

    }}`

    ,

    "g"

    )

    ,

     obj

    [

    key

    ]

    )

    ;

      

    }

    )

    ;

      

    return

     str

    ;

    }

    const

     str 

    =

     

    "{{name}}很厉name害{{name}},才{{age}}岁"

    ;

    const

     obj 

    =

     

    {

     name

    :

     

    "jawil"

    ,

     age

    :

     

    "15"

     

    }

    ;

    console

    .

    log

    (

    parseString

    (

    str

    ,

     obj

    )

    )

    ;

    其实这里还是有些问题的,首先我没用

    for...in

    循环就是为了考虑不必要的循环,因为

    for...in

    循环会遍历原型链所有的可枚举属性,造成不必要的循环。

    我们可以简单看一个例子,看看

    for...in

    的可怕性。

    // Chrome v63

    const

     div 

    =

     document

    .

    createElement

    (

    "div"

    )

    ;

    let

     m 

    =

     

    0

    ;

    for

     

    (

    let

     k 

    in

     div

    )

     

    {

      m

    ++

    ;

    }

    let

     n 

    =

     

    0

    ;

    console

    .

    log

    (

    m

    )

    ;

     

    // 231

    console

    .

    log

    (

    Object

    .

    keys

    (

    div

    )

    .

    length

    )

    ;

     

    // 0

    一个 DOM 节点属性竟然有这么多的属性,列举这个例子只是让大家看到

    for in

    遍历的效率问题,不要轻易用

    for in

    循环,通过这个

    DOM

    节点之多也可以一定程度了解到

    React

    Virtual DOM

    的思想和优越性。

    除了用

    for in

    循环获取

    obj

    key

    值,还可以用

    Object.key()

    获取,

    Object.getOwnPropertyNames()

    以及

    Reflect.ownKeys()

    也可以获取,那么这几种有啥区别呢?这里就简单说一下他们的一些区别。

    for...in

    循环:会遍历对象自身的属性,以及原型属性,

    for...in

    循环只遍历可枚举(不包括

    enumerable

    false

    )属性。像

    Array

    Object

    使用内置构造函数所创建的对象都会继承自

    Object.prototype

    String.prototype

    的不可枚举属性;

    上面说的可能比较抽象,不够直观。可以看个我写的

    DEMO

    ,一切简单明鸟。

    const

     parent 

    =

     

    {

      a

    :

     

    1

    ,

      b

    :

     

    2

    ,

      c

    :

     

    3

    }

    ;

    const

     child 

    =

     

    {

      d

    :

     

    4

    ,

      e

    :

     

    5

    ,

      

    [

    Symbol

    (

    )

    ]

    :

     

    6

    }

    ;

    child

    .

    __proto__ 

    =

     parent

    ;

    Object

    .

    defineProperty

    (

    child

    ,

     

    "d"

    ,

     

    {

     enumerable

    :

     

    false

     

    }

    )

    ;



    for

     

    (

    var

     attr 

    in

     child

    )

     

    {

      console

    .

    log

    (

    "for...in:"

    ,

     attr

    )

    ;

    // a,b,c,e

    }

    console

    .

    log

    (

    "Object.keys:"

    ,

     Object

    .

    keys

    (

    child

    )

    )

    ;

    // [ "e" ]

    console

    .

    log

    (

    "Object.getOwnPropertyNames:"

    ,

     Object

    .

    getOwnPropertyNames

    (

    child

    )

    )

    ;

     

    // [ "d", "e" ]

    console

    .

    log

    (

    "Reflect.ownKeys:"

    ,

     Reflect

    .

    ownKeys

    (

    child

    )

    )

    ;

     

    // [ "d", "e", Symbol() ]

    最后实现

    上面的实现其实已经很简洁了,但是还是有些不完美的地方,通过 MDN 首先我们先了解一下 replace 的用法。

    通过文档里面写的

    str.replace(regexp|substr, newSubStr|function)

    ,我们可以发现 replace 方法可以传入

    function

    回调函数,

    function (replacement)

    一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。参考这个指定一个函数作为参数。

    有了这句话,其实就很好实现了,先看看具体代码再做下一步分析。

    function

     

    render

    (

    template

    ,

     context

    )

     

    {

      

    return

     template

    .

    replace

    (

    /\{\{(.*?)\}\}/g

    ,

     

    (

    match

    ,

     key

    )

     

    =

    >

     context

    [

    key

    ]

    )

    ;

    }

    const

     template 

    =

     

    "{{name}}很厉name害,才{{age}}岁"

    ;

    const

     context 

    =

     

    {

     name

    :

     

    "jawil"

    ,

     age

    :

     

    "15"

     

    }

    ;

    console

    .

    log

    (

    render

    (

    template

    ,

     context

    )

    )

    ;

    可以对照上面文档的话来做分析:该函数的返回值(

    obj[key]=jawil

    )将替换掉第一个参数(

    match=={{name}}

    )匹配到的结果。

    简单分析一下:

    .*?

    是正则固定搭配用法,表示非贪婪匹配模式,尽可能匹配少的,什么意思呢?举个简单的例子。

    先看一个例子:

    源字符串:aa

    <

    div

    >

    test1

    <

    /

    div

    >

    bb

    <

    div

    >

    test2

    <

    /

    div

    >

    cc



    正则表达式一:

    <

    div

    >

    .

    *

    <

    /

    div

    >



    匹配结果一:

    <

    div

    >

    test1

    <

    /

    div

    >

    bb

    <

    div

    >

    test2

    <

    /

    div

    >



    正则表达式二:

    <

    div

    >

    .

    *

    ?

    <

    /

    div

    >



    匹配结果二:

    <

    div

    >

    test1

    <

    /

    div

    >

    (这里指的是一次匹配结果,不使用

    /

    g,所以没包括

    <

    div

    >

    test2

    <

    /

    div

    >

    根据上面的例子,从匹配行为上分析一下,什是贪婪与非贪婪匹配模式。

    利用非贪婪匹配模就能匹配到所有的

    {{name}}

    {{age}}

    ,上面的也说到过正则分组,分组匹配到的就是

    name

    ,也就是

    function

    的第二个参数

    key

    所以这行代码的意思就很清楚,正则匹配到

    {{name}}

    ,分组获取

    name

    ,然后把

    {{name}}

    替换成

    obj[name](jawil)

    当然后来发现还有一个小问题,如果有空格的话就会匹配失败,类似这种写法:

    const

     template 

    =

     

    "{{name }}很厉name害,才{{age }}岁"

    ;

    所以在上面的基础上还要去掉空格,其实也很简单,用正则或者

    String.prototype.trim()

    方法都行。

    function

     

    render

    (

    template

    ,

     context

    )

     

    {

      

    return

     template

    .

    replace

    (

    /\{\{(.*?)\}\}/g

    ,

     

    (

    match

    ,

     key

    )

     

    =

    >

     context

    [

    key

    .

    trim

    (

    )

    ]

    )

    ;

    }

    const

     template 

    =

     

    "{{name }}很厉name害,才{{age }}岁"

    ;

    const

     context 

    =

     

    {

     name

    :

     

    "jawil"

    ,

     age

    :

     

    "15"

     

    }

    ;

    console

    .

    log

    (

    render

    (

    template

    ,

     context

    )

    )

    ;

    将函数挂到 String 的原型链,得到最终版本

    甚至,我们可以通过修改原型链,实现一些很酷的效果:

    String

    .

    prototype

    .

    render 

    =

     

    function

     

    (

    context

    )

     

    {

      

    return

     

    this

    .

    replace

    (

    /\{\{(.*?)\}\}/g

    ,

     

    (

    match

    ,

     key

    )

     

    =

    >

     context

    [

    key

    .

    trim

    (

    )

    ]

    )

    ;

    }

    ;

    如果{}中间不是数字,则{}本身不需要转义,所以最终最简洁的代码:

    String

    .

    prototype

    .

    render 

    =

     

    function

     

    (

    context

    )

     

    {

      

    return

     

    this

    .

    replace

    (

    /{{(.*?)}}/g

    ,

     

    (

    match

    ,

     key

    )

     

    =

    >

     context

    [

    key

    .

    trim

    (

    )

    ]

    )

    ;

    }

    ;

    之后,我们便可以这样调用啦:

    "{{name}}很厉name害,才{{ age }}岁"

    .

    render

    (

    {

     name

    :

     

    "jawil"

    ,

     age

    :

     

    "15"

     

    }

    )

    ;

    收获

    通过一个小小的模板字符串的实现,领悟到要把一个功能实现不难,要把做到完美真是难上加难,需要对基础掌握牢固,有一定的沉淀,然后不断地打磨才能比较优雅的实现,通过由一个很小的点往往可以拓展出很多的知识点。

    一张图快速入门正则表达式:

    一行代码实现一个简单的模板字符串替换

    一行代码实现一个简单的模板字符串替换