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

是否可以创建一个模板字符串作为一个普通的字符串,

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

然后把它转换成一个模板字符串,

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

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

100720 次浏览

因为你的模板字符串必须动态地(在运行时)引用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()也做不到。


当您听说JavaScript中出现了所谓的“模板字符串”时,很自然地认为这是一个内置的模板库,就像Mustache一样。它不是。它主要是字符串插值和JS的多行字符串。不过,我认为这在一段时间内将是一个普遍的误解。:(

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

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

生成模板字符串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

但是最初问题的答案是没有办法。

我目前不能评论现有的答案,所以我无法直接评论布莱恩·雷诺的出色回答。因此,这个回答将会更新他的答案,稍微修正一下。

简而言之,他的函数实际上没有缓存创建的函数,所以它总是重新创建,不管之前是否见过模板。以下是修正后的代码:

    /**
* 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, '$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'});

< p > TLDR: https://jsfiddle.net/bj89zntu/1/ < / p > 每个人似乎都担心访问变量。为什么不直接通过呢?我相信在调用者中获取变量context并将其传递下去不会太难。使用 ninjagecko的回答从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这样的编译器删除了。其他方法建议的方法取决于运行时。如前所述;这是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.m ijer的回答,并在他的基础上写了自己的版本:

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 repo中提出了一张票,https://github.com/facebook/react-native/issues/14107,这样一旦他们能够修复/指导我大致相同:)

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

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

< a href = " https://repl。it/IdVt/3" rel="nofollow noreferrer">https://repl.it/IdVt/3 .it/IdVt/3 " rel="nofollow noreferrer">https://repl.it/IdVt/3

类似于Daniel的答案(和s.m ijer的要点),但更易于阅读:

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.m ijer的原始版本,因为它不会匹配${foo{bar}这样的东西(正则表达式只允许在${}内的非花括号字符)。


更新:我被要求使用这个例子,所以你去:

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 < / p >
/*! (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});

我制定了自己的解决方案,用描述作为函数来处理类型

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

这里有许多好的解决方案,但还没有一个使用ES6字符串。原始的方法。这是我的贡献。它有一个重要的限制,它只接受传入对象的属性,这意味着模板中的代码执行将不起作用。

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. 将字符串分割为非参数的文本部分。看到正则表达式。< br > 李parts: ["Hello, ", "! Are you ", " years old?"] < / >
  2. 将字符串拆分为属性名。如果匹配失败,则为空数组 李args: ["name", "age"] < / > 根据属性名映射obj中的参数。解决方案受限于浅的一级映射。未定义的值用空字符串代替,但接受其他假值 李parameters: ["John Doe", 18] < / >
  3. 使用String.raw(...)并返回结果。

我知道我来晚了,但你可以:

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


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

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>

我提出了这个实现,它的工作就像一个魅力。

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,可能会引发错误