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

Convert a string to a template string

是否可以像通常的字符串一样创建模板字符串,

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

然后转换成模板字符串,

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

没有evalnew Function等动态代码生成方式?

由于您的模板字符串必须动态地(在运行时)获取对 b 变量的引用,所以答案是:不,如果没有动态代码生成是不可能做到的。

但是,使用 eval 就很简单了:

let tpl = eval('`'+a+'`');

你在这里问的是什么:

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

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

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


当您听说 "template strings" 出现在 JavaScript 中时,很自然地认为它是一个内置模板库,例如 Mustache。它不是。它主要只是 string interpolation 和 JS 的多行字符串。不过,我认为这在一段时间内会成为一种普遍的误解。 :(

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

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

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。

这里的问题是有一个函数可以访问其调用者的变量。这就是我们看到直接 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));

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

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

但是原题的答案是没办法

我目前无法对现有答案发表评论,因此我无法直接评论 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;
    })();

在我的项目中,我用 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);

此解决方案无需 ES6 即可工作:

function render(template, opts) {
  return new Function(
    'return new Function (' + Object.keys(opts).reduce((args, arg) => args += '\'' + arg + '\',', '') + '\'return `' + template.replace(/(^|[^\])'/g, '\\'') + '`;\'' +
    ').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'});

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

我需要这种支持 Internet Explorer 的方法。事实证明,即使 IE11 也不支持反引号。还;使用 eval 或等效的 Function 感觉不对。

对于注意到的人;我也使用反引号,但这些反引号会被像 babel 这样的编译器删除。其他人建议的方法取决于run-time。如前所述;这是 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 -

我喜欢 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
            )
    );
}

@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 回购中提出了一个问题,https://github.com/facebook/react-native/issues/14107,所以一旦他们可以 fix/guide 我差不多 :)

仍然是动态的,但似乎比只使用裸评估更受控制:

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

类似于 Daniel 的回答(以及 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} 之类的东西(正则表达式只允许 ${ 和 [ 中的非大括号字符=14=]).


更新:我被要求提供一个使用这个的例子,所以给你:

const replacements = {
    name: 'Bob',
    age: 37
}

interpolate('My name is ${name}, and I am ${age}.', replacements)

由于我们正在重新发明轮子,这将是 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.

如果您没有使用正则表达式的经验,一个非常安全的规则是转义每个非字母数字字符,并且不要 永远 不必要地转义字母,因为许多转义字母都有对几乎所有类型的正则表达式都有特殊意义。

您应该试试这个微型 JS 模块,作者是 Andrea Giammarchi,来自 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);
};

Demo(以下所有测试return为真):

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});

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

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');

这里发布了很多好的解决方案,但是 none 还利用了 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. 将字符串拆分为非参数文本部分。参见
    parts: ["Hello, ", "! Are you ", " years old?"]
  2. 将字符串拆分为 属性 个名称。如果匹配失败则为空数组。
    args: ["name", "age"]
  3. 通过 属性 名称映射来自 obj 的参数。解决方案受浅层映射的限制。未定义的值被替换为空字符串,但接受其他虚假值。
    parameters: ["John Doe", 18]
  4. 利用 String.raw(...) 和 return 结果。

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

const a =  (b) => `b:${b}`;

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

这样做(这样):

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>

我想到了这个实现,它非常有效。

function interpolateTemplate(template: string, args: any): string {
  return Object.entries(args).reduce(
    (result, [arg, val]) => result.replace(`$\{${arg}}`, `${val}`),
    teamplte,
  )
}

const teamplte = 'This is an example: ${name}, ${age} ${email}'

console.log(interpolateTemplate(teamplte,{name:'Med', age:'20', email:'example@abc.com'}))

如果在模板中找不到 arg,您可能会报错