代码之家  ›  专栏  ›  技术社区  ›  KOLANICH

将字符串转换为模板字符串

  •  211
  • KOLANICH  · 技术社区  · 11 年前

    是否可以将模板字符串创建为普通字符串,

    let a = "b:${b}";
    

    然后将其转换为模板字符串,

    let b = 10;
    console.log(a.template()); // b:10
    

    没有 eval , new Function 以及其他动态代码生成方法?

    22 回复  |  直到 4 年前
        1
  •  148
  •   Peter Mortensen Pieter Jan Bonestroo    4 年前

    在我的项目中,我用ES6创建了这样的东西:

    String.prototype.interpolate = function(params) {
      const names = Object.keys(params);
      const vals = Object.values(params);
      return new Function(...names, `return \`${this}\`;`)(...vals);
    }
    
    const template = 'Example text: ${text}';
    const result = template.interpolate({
      text: 'Foo Boo'
    });
    console.log(result);
        2
  •  114
  •   ataravati    5 年前

    因为模板字符串必须获得对 b 变量动态(在运行时),因此答案是: 不,没有动态代码生成是不可能的。

    但是,随着 eval 这很简单:

    let tpl = eval('`'+a+'`');
    
        3
  •  31
  •   aWebDeveloper    10 年前

    您在这里的要求:

    //non working code quoted from the question
    let b=10;
    console.log(a.template());//b:10
    

    完全等同于(在功率和呃安全性方面) eval :获取包含代码的字符串并执行该代码的能力;以及执行的代码在调用方环境中查看本地变量的能力。

    在JS中,函数无法在其调用者中看到局部变量,除非该函数 eval() 即使 Function() 做不到。


    当你听到JavaScript中有一种叫做“模板字符串”的东西时,很自然会假设它是一个内置的模板库,比如Mustache。它不是。主要是 string interpolation 以及JS的多行字符串。不过,我认为这将是一段时间内的普遍误解(

        4
  •  31
  •   Matt Browne    8 年前

    不,没有动态代码生成就无法做到这一点。

    然而,我已经创建了一个函数,它可以将一个常规字符串转换为一个可以在内部使用模板字符串提供值映射的函数。

    Generate Template String Gist

    /**
     * Produces a function which uses template strings to do simple interpolation from objects.
     * 
     * Usage:
     *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
     * 
     *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
     *    // Logs 'Bryan is now the king of Scotland!'
     */
    var generateTemplateString = (function(){
        var cache = {};
    
        function generateTemplate(template){
            var fn = cache[template];
    
            if (!fn){
                // Replace ${expressions} (etc) with ${map.expressions}.
    
                var sanitized = template
                    .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                        return `\$\{map.${match.trim()}\}`;
                        })
                    // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                    .replace(/(\$\{(?!map\.)[^}]+\})/g, '');
    
                fn = Function('map', `return \`${sanitized}\``);
            }
    
            return fn;
        }
    
        return generateTemplate;
    })();
    

    用法:

    var kingMaker = generateTemplateString('${name} is king!');
    
    console.log(kingMaker({name: 'Bryan'}));
    // Logs 'Bryan is king!' to the console.
    

    希望这对某人有所帮助。如果您发现代码有问题,请及时更新Gist。

        5
  •  17
  •   pekaaw    6 年前

    这里有很多好的解决方案,但还没有一个利用 ES6 String.raw method 这是我的忏悔。它有一个重要的限制,即它只接受传入对象的属性,这意味着模板中的代码执行将不起作用。

    function parseStringTemplate(str, obj) {
        let parts = str.split(/\$\{(?!\d)[\wæøåÆØÅ]*\}/);
        let args = str.match(/[^{\}]+(?=})/g) || [];
        let parameters = args.map(argument => obj[argument] || (obj[argument] === undefined ? "" : obj[argument]));
        return String.raw({ raw: parts }, ...parameters);
    }
    
    let template = "Hello, ${name}! Are you ${age} years old?";
    let values = { name: "John Doe", age: 18 };
    
    parseStringTemplate(template, values);
    // output: Hello, John Doe! Are you 18 years old?
    
    1. 将字符串拆分为非参数文本部分。看见 regex .
      parts: ["Hello, ", "! Are you ", " years old?"]
    2. 将字符串拆分为属性名称。如果匹配失败,则为空数组。
      args: ["name", "age"]
    3. 映射参数来自 obj 按属性名称。解决方案受到浅层一级映射的限制。未定义的值将替换为空字符串,但接受其他错误的值。
      parameters: ["John Doe", 18]
    4. 使用 String.raw(...) 并返回结果。
        6
  •  13
  •   Peter Mortensen Pieter Jan Bonestroo    4 年前

    TLDR: https://jsfiddle.net/bj89zntu/1/

    每个人似乎都担心访问变量。为什么不直接通过呢?我确信在调用者中获取变量上下文并传递它不会太困难。使用 ninjagecko's answer 从obj那里得到道具。

    function renderString(str,obj){
        return str.replace(/\$\{(.+?)\}/g,(match,p1)=>{return index(obj,p1)})
    }
    

    以下是完整代码:

    function index(obj,is,value) {
        if (typeof is == 'string')
            is=is.split('.');
        if (is.length==1 && value!==undefined)
            return obj[is[0]] = value;
        else if (is.length==0)
            return obj;
        else
            return index(obj[is[0]],is.slice(1), value);
    }
    
    function renderString(str,obj){
        return str.replace(/\$\{.+?\}/g,(match)=>{return index(obj,match)})
    }
    
    renderString('abc${a}asdas',{a:23,b:44}) //abc23asdas
    renderString('abc${a.c}asdas',{a:{c:22,d:55},b:44}) //abc22asdas
    
        7
  •  10
  •   didinko    10 年前

    这里的问题是有一个函数可以访问其调用者的变量。这就是为什么我们看到直接 eval 用于模板处理。一种可能的解决方案是生成一个函数,该函数采用字典属性命名的形式参数,并以相同的顺序调用相应的值。另一种方法是这样简单:

    var name = "John Smith";
    var message = "Hello, my name is ${name}";
    console.log(new Function('return `' + message + '`;')());
    

    对于任何使用Babel编译器的人,我们需要创建一个闭包,它可以记住创建它的环境:

    console.log(new Function('name', 'return `' + message + '`;')(name));
    
        8
  •  9
  •   Daniel    9 年前

    我喜欢s.meijer的回答,并根据他的回答撰写了自己的版本:

    function parseTemplate(template, map, fallback) {
        return template.replace(/\$\{[^}]+\}/g, (match) => 
            match
                .slice(2, -1)
                .trim()
                .split(".")
                .reduce(
                    (searchObject, key) => searchObject[key] || fallback || match,
                    map
                )
        );
    }
    
        9
  •  9
  •   Matt Browne    8 年前

    类似于丹尼尔的回答(和s.meijer的 gist )但更可读:

    const regex = /\${[^{]+}/g;
    
    export default function interpolate(template, variables, fallback) {
        return template.replace(regex, (match) => {
            const path = match.slice(2, -1).trim();
            return getObjPath(path, variables, fallback);
        });
    }
    
    //get the specified property or nested property of an object
    function getObjPath(path, obj, fallback = '') {
        return path.split('.').reduce((res, key) => res[key] || fallback, obj);
    }
    

    注意:这稍微改进了s.meijer的原始版本,因为它与以下内容不匹配 ${foo{bar} (正则表达式只允许内部包含非大括号字符 ${ } ).


    更新:我被要求提供一个使用这个的示例,所以这里是:

    const replacements = {
        name: 'Bob',
        age: 37
    }
    
    interpolate('My name is ${name}, and I am ${age}.', replacements)
    
        10
  •  6
  •   Mohit Pandey    9 年前

    @Mateusz Moska,解决方案很好,但当我在React Native(构建模式)中使用它时,它会抛出一个错误: 无效字符“” ,但当我在调试模式下运行它时,它会工作。

    所以我用正则表达式写下了自己的解决方案。

    String.prototype.interpolate = function(params) {
      let template = this
      for (let key in params) {
        template = template.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), params[key])
      }
      return template
    }
    
    const template = 'Example text: ${text}',
      result = template.interpolate({
        text: 'Foo Boo'
      })
    
    console.log(result)
    

    演示: https://es6console.com/j31pqx1p/

    注: 由于我不知道问题的根本原因,我在react native repo中提出了一个问题, https://github.com/facebook/react-native/issues/14107 ,以便一旦他们能够修复/指导我:)

        11
  •  5
  •   sarkiroka    10 年前

    例如,可以使用字符串原型

    String.prototype.toTemplate=function(){
        return eval('`'+this+'`');
    }
    //...
    var a="b:${b}";
    var b=10;
    console.log(a.toTemplate());//b:10
    

    但最初问题的答案是不可能的。

        12
  •  4
  •   s.meijer    9 年前

    我需要支持Internet Explorer的此方法。事实证明,即使是IE11也不支持反引号。而且使用 eval 或者等效的 Function 感觉不对劲。

    对于发出通知的人;我也使用倒勾,但这些倒勾会被babel等编译器删除。其他人建议的方法依赖于运行时的方法。如前所述;这是IE11及更低版本中的一个问题。

    这就是我想到的:

    function get(path, obj, fb = `$\{${path}}`) {
      return path.split('.').reduce((res, key) => res[key] || fb, obj);
    }
    
    function parseTpl(template, map, fallback) {
      return template.replace(/\$\{.+?}/g, (match) => {
        const path = match.substr(2, match.length - 3).trim();
        return get(path, map, fallback);
      });
    }
    

    示例输出:

    const data = { person: { name: 'John', age: 18 } };
    
    parseTpl('Hi ${person.name} (${person.age})', data);
    // output: Hi John (18)
    
    parseTpl('Hello ${person.name} from ${person.city}', data);
    // output: Hello John from ${person.city}
    
    parseTpl('Hello ${person.name} from ${person.city}', data, '-');
    // output: Hello John from -
    
        13
  •  3
  •   user2501097    9 年前

    我目前无法评论现有的答案,因此我无法直接评论Bryan Raynor的出色回应。因此,这一回应将对他的回答进行轻微修正。

    简而言之,他的函数无法实际缓存创建的函数,因此无论之前是否看到过模板,它都会重新创建。以下是更正的代码:

        /**
         * Produces a function which uses template strings to do simple interpolation from objects.
         * 
         * Usage:
         *    var makeMeKing = generateTemplateString('${name} is now the king of ${country}!');
         * 
         *    console.log(makeMeKing({ name: 'Bryan', country: 'Scotland'}));
         *    // Logs 'Bryan is now the king of Scotland!'
         */
        var generateTemplateString = (function(){
            var cache = {};
    
            function generateTemplate(template){
                var fn = cache[template];
    
                if (!fn){
                    // Replace ${expressions} (etc) with ${map.expressions}.
    
                    var sanitized = template
                        .replace(/\$\{([\s]*[^;\s\{]+[\s]*)\}/g, function(_, match){
                            return `\$\{map.${match.trim()}\}`;
                        })
                        // Afterwards, replace anything that's not ${map.expressions}' (etc) with a blank string.
                        .replace(/(\$\{(?!map\.)[^}]+\})/g, '');
    
                    fn = cache[template] = Function('map', `return \`${sanitized}\``);
                }
    
                return fn;
            };
    
            return generateTemplate;
        })();
    
        14
  •  3
  •   Robert Moskal    8 年前

    仍然是动态的,但似乎比仅使用裸求值更可控:

    const vm = require('vm')
    const moment = require('moment')
    
    
    let template = '### ${context.hours_worked[0].value} \n Hours worked \n #### ${Math.abs(context.hours_worked_avg_diff[0].value)}% ${fns.gt0(context.hours_worked_avg_diff[0].value, "more", "less")} than usual on ${fns.getDOW(new Date())}'
    let context = {
      hours_worked:[{value:10}],
      hours_worked_avg_diff:[{value:10}],
    
    }
    
    
    function getDOW(now) {
      return moment(now).locale('es').format('dddd')
    }
    
    function gt0(_in, tVal, fVal) {
      return _in >0 ? tVal: fVal
    }
    
    
    
    function templateIt(context, template) {
      const script = new vm.Script('`'+template+'`')
      return script.runInNewContext({context, fns:{getDOW, gt0 }})
    }
    
    console.log(templateIt(context, template))
    

    https://repl.it/IdVt/3

        15
  •  2
  •   cesarmarinhorj    6 年前

    我制作了自己的解决方案,将描述作为函数

    export class Foo {
    ...
    description?: Object;
    ...
    }
    
    let myFoo:Foo = {
    ...
      description: (a,b) => `Welcome ${a}, glad to see you like the ${b} section`.
    ...
    }
    

    这样做:

    let myDescription = myFoo.description('Bar', 'bar');
    
        16
  •  2
  •   darrachequesne Mohamad Abdel Rida    4 年前

    我想出了这个实现,它工作起来很有魅力。

    function interpolateTemplate(template: string, args: any): string {
      return Object.entries(args).reduce(
        (result, [arg, val]) => result.replace(`$\{${arg}}`, `${val}`),
        template,
      )
    }
    
    const template = 'This is an example: ${name}, ${age} ${email}'
    
    console.log(interpolateTemplate(template,{name:'Med', age:'20', email:'example@abc.com'}))
    

    如果在模板中找不到arg,则可能会引发错误

        17
  •  1
  •   cruzanmo    9 年前

    此解决方案不使用ES6:

    function render(template, opts) {
      return new Function(
        'return new Function (' + Object.keys(opts).reduce((args, arg) => args += '\'' + arg + '\',', '') + '\'return `' + template.replace(/(^|[^\\])'/g, '$1\\\'') + '`;\'' +
        ').apply(null, ' + JSON.stringify(Object.keys(opts).reduce((vals, key) => vals.push(opts[key]) && vals, [])) + ');'
      )();
    }
    
    render("hello ${ name }", {name:'mo'}); // "hello mo"
    

    注: Function 构造函数总是在全局范围内创建的,这可能会导致全局变量被模板覆盖,例如。 render("hello ${ someGlobalVar = 'some new value' }", {name:'mo'});

        18
  •  1
  •   colxi    8 年前

    你应该试试这个小小的JS模块,由Andrea Giammarci,来自github: https://github.com/WebReflection/backtick-template

    /*! (C) 2017 Andrea Giammarchi - MIT Style License */
    function template(fn, $str, $object) {'use strict';
      var
        stringify = JSON.stringify,
        hasTransformer = typeof fn === 'function',
        str = hasTransformer ? $str : fn,
        object = hasTransformer ? $object : $str,
        i = 0, length = str.length,
        strings = i < length ? [] : ['""'],
        values = hasTransformer ? [] : strings,
        open, close, counter
      ;
      while (i < length) {
        open = str.indexOf('${', i);
        if (-1 < open) {
          strings.push(stringify(str.slice(i, open)));
          open += 2;
          close = open;
          counter = 1;
          while (close < length) {
            switch (str.charAt(close++)) {
              case '}': counter -= 1; break;
              case '{': counter += 1; break;
            }
            if (counter < 1) {
              values.push('(' + str.slice(open, close - 1) + ')');
              break;
            }
          }
          i = close;
        } else {
          strings.push(stringify(str.slice(i)));
          i = length;
        }
      }
      if (hasTransformer) {
        str = 'function' + (Math.random() * 1e5 | 0);
        if (strings.length === values.length) strings.push('""');
        strings = [
          str,
          'with(this)return ' + str + '([' + strings + ']' + (
            values.length ? (',' + values.join(',')) : ''
          ) + ')'
        ];
      } else {
        strings = ['with(this)return ' + strings.join('+')];
      }
      return Function.apply(null, strings).apply(
        object,
        hasTransformer ? [fn] : []
      );
    }
    
    template.asMethod = function (fn, object) {'use strict';
      return typeof fn === 'function' ?
        template(fn, this, object) :
        template(this, fn);
    };
    

    演示(以下所有测试均返回true):

    const info = 'template';
    // just string
    `some ${info}` === template('some ${info}', {info});
    
    // passing through a transformer
    transform `some ${info}` === template(transform, 'some ${info}', {info});
    
    // using it as String method
    String.prototype.template = template.asMethod;
    
    `some ${info}` === 'some ${info}'.template({info});
    
    transform `some ${info}` === 'some ${info}'.template(transform, {info});
    
        19
  •  0
  •   Regular Jo    8 年前

    因为我们正在重新发明javascript中一个可爱的功能。

    我使用 eval() ,这不安全,但javascript不安全。我很乐意承认我对javascript并不擅长,但我有一个需要,我需要一个答案,所以我做了一个。

    我选择使用 @ 而不是 $ ,特别是因为我想使用文字的多行特性 没有 评估直到准备就绪。所以变量语法是 @{OptionalObject.OptionalObjectN.VARIABLE_NAME}

    我不是javascript专家,所以我很乐意接受改进建议,但。。。

    var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
    for(i = 0; i < myResultSet.length; i++) {
        prsLiteral = rt.replace(prsRegex,function (match,varname) {
            return eval(varname + "[" + i + "]");
            // you could instead use return eval(varname) if you're not looping.
        })
        console.log(prsLiteral);
    }
    

    下面是一个非常简单的实现

    myResultSet = {totalrecords: 2,
    Name: ["Bob", "Stephanie"],
    Age: [37,22]};
    
    rt = `My name is @{myResultSet.Name}, and I am @{myResultSet.Age}.`
    
    var prsLiteral, prsRegex = /\@\{(.*?)(?!\@\{)\}/g
    for(i = 0; i < myResultSet.totalrecords; i++) {
        prsLiteral = rt.replace(prsRegex,function (match,varname) {
            return eval(varname + "[" + i + "]");
            // you could instead use return eval(varname) if you're not looping.
        })
        console.log(prsLiteral);
    }

    在我的实际实现中,我选择使用 @{{variable}} 。再来一套大括号。不太可能意外地遇到这种情况。其正则表达式如下 /\@\{\{(.*?)(?!\@\{\{)\}\}/g

    为了更容易阅读

    \@\{\{    # opening sequence, @{{ literally.
    (.*?)     # capturing the variable name
              # ^ captures only until it reaches the closing sequence
    (?!       # negative lookahead, making sure the following
              # ^ pattern is not found ahead of the current character
      \@\{\{  # same as opening sequence, if you change that, change this
    )
    \}\}      # closing sequence.
    

    如果您不熟悉正则表达式,一个非常安全的规则是转义每个非字母数字字符,而不要 曾经 不必要的转义字母,因为许多转义字母对几乎所有类型的正则表达式都有特殊意义。

        20
  •  0
  •   Thiago Lagden    4 年前

    Faz assim(这边):

    let a = 'b:${this.b}'
    let b = 10
    
    function template(templateString, templateVars) {
        return new Function('return `' + templateString + '`').call(templateVars)
    }
    
    result.textContent = template(a, {b})
    <b id=result></b>
        21
  •  0
  •   Giang Thanh Dat    3 年前

    您可以参考此解决方案

    const interpolate = (str) =>
      new Function(`return \`${new String(str)}\`;`)();
    
    const foo = 'My';
    
    const obj = {
      text: 'Hanibal Lector',
      firstNum: 1,
      secondNum: 2
    }
     
    const str = "${foo} name is : ${obj.text}. sum = ${obj.firstNum} + ${obj.secondNum} = ${obj.firstNum + obj.secondNum}";
    
    console.log(interpolate(str));
        22
  •  -3
  •   joehep    4 年前

    我意识到我比赛迟到了,但你可以:

    const a =  (b) => `b:${b}`;
    
    let b = 10;
    console.log(a(b)); // b:10