将ES6模板字面量的执行推迟

60

我正在使用新的ES6模板字面量功能进行开发,脑海中浮现出的第一件事是为JavaScript创建一个String.format,因此我开始实现一个原型:

String.prototype.format = function() {
  var self = this;
  arguments.forEach(function(val,idx) {
    self["p"+idx] = val;
  });
  return this.toString();
};
console.log(`Hello, ${p0}. This is a ${p1}`.format("world", "test"));

ES6Fiddle

然而,模板字面量在传递到我的原型方法之前进行评估。有没有办法编写上述代码以推迟结果,直到我动态创建元素?


你在哪里执行这个程序?我认为最新的JS实现都没有实现这个功能。 - thefourtheye
1
@thefourtheye 在问题中链接的ES6Fiddle中 - CodingIntrigue
1
我认为对于 .format() 方法,你不应该使用模板字符串,而是应该使用普通的字符串字面量。 - Bergi
@Bergi 这并不是真正意义上的问题,更多是一个带有例子的假设情况。似乎将预处理输出传递给函数可能是一个频繁使用的情况。 - CodingIntrigue
2
值得指出的是,反引号字符串只是字符串连接和表达式求值的语法糖。 \foo ${5+6}`会被计算为"foo 11"`将格式方法附加到字符串原型上,可以让您做一些愚蠢的事情,比如:`\`My ${5+6}th token is {0}\`.format(11)`这应该被计算为 "My 11th token is 11" - Hovis Biddle
相关:将字符串转换为模板字符串 - Sebastian Simon
8个回答

72

我可以看到三种解决方法:

  • Use template strings like they were designed to be used, without any format function:

    console.log(`Hello, ${"world"}. This is a ${"test"}`);
    // might make more sense with variables:
    var p0 = "world", p1 = "test";
    console.log(`Hello, ${p0}. This is a ${p1}`);
    

    or even function parameters for actual deferral of the evaluation:

    const welcome = (p0, p1) => `Hello, ${p0}. This is a ${p1}`;
    console.log(welcome("world", "test"));
    
  • Don't use a template string, but a plain string literal:

    String.prototype.format = function() {
        var args = arguments;
        return this.replace(/\$\{p(\d)\}/g, function(match, id) {
            return args[id];
        });
    };
    console.log("Hello, ${p0}. This is a ${p1}".format("world", "test"));
    
  • Use a tagged template literal. Notice that the substitutions will still be evaluated without interception by the handler, so you cannot use identifiers like p0 without having a variable named so. This behavior may change if a different substitution body syntax proposal is accepted (Update: it was not).

    function formatter(literals, ...substitutions) {
        return {
            format: function() {
                var out = [];
                for(var i=0, k=0; i < literals.length; i++) {
                    out[k++] = literals[i];
                    out[k++] = arguments[substitutions[i]];
                }
                out[k] = literals[i];
                return out.join("");
            }
        };
    }
    console.log(formatter`Hello, ${0}. This is a ${1}`.format("world", "test"));
    // Notice the number literals: ^               ^
    

我喜欢这个功能,因为它允许您在插值之前操纵值。例如,如果您传入一个名称数组,您可以聪明地将它们组合成字符串,如“James”,“James&Mary”或“James,Mary和William”,具体取决于数组中有多少个名称。 - Andrew Hedges
这也可以被添加为String的静态方法,如String.formatter - Quentin Engles
非常详细。请参考@rodrigorodrigues的答案,特别是他的第一个代码块,以获取最简洁的解决方案。 - cage rattler
不错。后面的版本几乎与我自己的解决方案完全相同: https://github.com/spikesagal/es6interpolate/blob/main/src/interpolate.js (也已经粘贴到了这个线程中)。 - Spike Sagal

10

扩展@Bergi的回答,当你意识到你不仅可以返回纯字符串,还可以返回任何结果时,标记模板字符串的威力就显现出来了。在他的示例中,标签构造并返回一个带有闭包和函数属性format的对象。

在我最喜欢的方法中,我仅返回一个函数值,您可以稍后调用它并传递新参数以填充模板。像这样:

function fmt([fisrt, ...rest], ...tags) {
  return values => rest.reduce((acc, curr, i) => {
    return acc + values[tags[i]] + curr;
  }, fisrt);
}

或者,对于代码高手来说:
let fmt=([f,...r],...t)=>v=>r.reduce((a,c,i)=>a+v[t[i]]+c,f)

然后,您需要构建模板并延迟替换:
> fmt`Test with ${0}, ${1}, ${2} and ${0} again`(['A', 'B', 'C']);
// 'Test with A, B, C and A again'
> template = fmt`Test with ${'foo'}, ${'bar'}, ${'baz'} and ${'foo'} again`
> template({ foo:'FOO', bar:'BAR' })
// 'Test with FOO, BAR, undefined and FOO again'

另一个选择,更接近于您所写的内容,是返回一个继承自字符串的对象,以获得开箱即用的鸭子类型并遵守接口。扩展String.prototype将无法工作,因为您需要模板标签的闭包来稍后解析参数。
class FormatString extends String {
  // Some other custom extensions that don't need the template closure
}

function fmt([fisrt, ...rest], ...tags) {
  const str = new FormatString(rest.reduce((acc, curr, i) => `${acc}\${${tags[i]}}${curr}`, fisrt));
  str.format = values => rest.reduce((acc, curr, i) => {
    return acc + values[tags[i]] + curr;
  }, fisrt);
  return str;
}

接着,在调用现场:

> console.log(fmt`Hello, ${0}. This is a ${1}.`.format(["world", "test"]));
// Hello, world. This is a test.
> template = fmt`Hello, ${'foo'}. This is a ${'bar'}.`
> console.log(template)
// { [String: 'Hello, ${foo}. This is a ${bar}.'] format: [Function] }
> console.log(template.format({ foo: true, bar: null }))
// Hello, true. This is a null.

您可以在这个答案中查看更多信息和应用。

1
那个小的reducer非常强大。创建了两个Codepens来展示使用示例,一个使用value objects一个使用value arrays - cage rattler
最近我一直在尝试学习JavaScript和现代Web开发,当我探索编写一个类似于functor的东西来动态生成基于API端点和参数值的fetch hooks时,我偶然发现了这个回复,并对这个reducer技巧印象深刻。我只有一个问题,我无法通过谷歌找到答案。这里的values是什么?它是否与Object.values()有关?我已经在dev console中尝试了你的第一个解决方案,但并没有弄清楚这个值是如何工作的或者它来自哪里。 - zaile
1
fmt 是一个函数,当被执行时,会返回另一个函数。在该代码片段中,它返回一个匿名函数,该函数只有一个参数,命名为 values。请注意语法:return values => ...。在这个返回的函数中,期望使用参数 values 传递一个查找列表或包含替换内容的对象。 - Rodrigo Rodrigues

3
您可以使用以下函数将值注入到字符串中。
let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);

let inject = (str, obj) => str.replace(/\${(.*?)}/g, (x,g)=> obj[g]);


// --- Examples ---

// parameters in object
let t1 = 'My name is ${name}, I am ${age}. My brother name is also ${name}.';
let r1 = inject(t1, {name: 'JOHN',age: 23} );
console.log("OBJECT:", r1);


// parameters in array
let t2 = "Today ${0} saw ${2} at shop ${1} times - ${0} was haapy."
let r2 = inject(t2, {...['JOHN', 6, 'SUsAN']} );
console.log("ARRAY :", r2);


3
据我所知,有用的“字符串模板延迟执行”特性仍然不可用。但使用lambda表达式是一种表达清晰、易读且简洁的解决方案:
var greetingTmpl = (...p)=>`Hello, ${p[0]}. This is a ${p[1]}`;

console.log( greetingTmpl("world","test") );
console.log( greetingTmpl("@CodingIntrigue","try") );

1
我也喜欢 String.format 函数的想法,以及能够明确定义变量以进行解析。
这是我想到的...基本上是一个具有 deepObject 查找功能的 String.replace 方法。

const isUndefined = o => typeof o === 'undefined'

const nvl = (o, valueIfUndefined) => isUndefined(o) ? valueIfUndefined : o

// gets a deep value from an object, given a 'path'.
const getDeepValue = (obj, path) =>
  path
    .replace(/\[|\]\.?/g, '.')
    .split('.')
    .filter(s => s)
    .reduce((acc, val) => acc && acc[val], obj)

// given a string, resolves all template variables.
const resolveTemplate = (str, variables) => {
  return str.replace(/\$\{([^\}]+)\}/g, (m, g1) =>
            nvl(getDeepValue(variables, g1), m))
}

// add a 'format' method to the String prototype.
String.prototype.format = function(variables) {
  return resolveTemplate(this, variables)
}

// setup variables for resolution...
var variables = {}
variables['top level'] = 'Foo'
variables['deep object'] = {text:'Bar'}
var aGlobalVariable = 'Dog'

// ==> Foo Bar <==
console.log('==> ${top level} ${deep object.text} <=='.format(variables))

// ==> Dog Dog <==
console.log('==> ${aGlobalVariable} ${aGlobalVariable} <=='.format(this))

// ==> ${not an object.text} <==
console.log('==> ${not an object.text} <=='.format(variables))

或者,如果您想要更多的内容,而不仅仅是变量分辨率(例如模板文字的行为),则可以使用以下内容。

N.B. eval 被认为是“邪恶的” - 考虑使用 safe-eval 替代方案。

// evalutes with a provided 'this' context.
const evalWithContext = (string, context) => function(s){
    return eval(s);
  }.call(context, string)

// given a string, resolves all template variables.
const resolveTemplate = function(str, variables) {
  return str.replace(/\$\{([^\}]+)\}/g, (m, g1) => evalWithContext(g1, variables))
}

// add a 'format' method to the String prototype.
String.prototype.format = function(variables) {
  return resolveTemplate(this, variables)
}

// ==> 5Foobar <==
console.log('==> ${1 + 4 + this.someVal} <=='.format({someVal: 'Foobar'}))


0

我曾经回答过一个类似的问题,提供了两种延迟执行模板字面量的方法。当模板字面量在函数中时,只有在调用函数时才会对其进行评估,并且使用函数的作用域进行评估。

https://dev59.com/N5vga4cB1Zd3GeqP9OyO#49539260


0
尽管这个问题已经被回答过了,但是在这里我有一个简单的实现,当我加载配置文件时使用(代码是typescript,但是转换为JS非常容易,只需删除类型声明):
/**
 * This approach has many limitations:
 *   - it does not accept variable names with numbers or other symbols (relatively easy to fix)
 *   - it does not accept arbitrary expressions (quite difficult to fix)
 */
function deferredTemplateLiteral(template: string, env: { [key: string]: string | undefined }): string {
  const varsMatcher = /\${([a-zA-Z_]+)}/
  const globalVarsmatcher = /\${[a-zA-Z_]+}/g

  const varMatches: string[] = template.match(globalVarsmatcher) ?? []
  const templateVarNames = varMatches.map(v => v.match(varsMatcher)?.[1] ?? '')
  const templateValues: (string | undefined)[] = templateVarNames.map(v => env[v])

  const templateInterpolator = new Function(...[...templateVarNames, `return \`${template}\`;`])

  return templateInterpolator(...templateValues)
}

// Usage:
deferredTemplateLiteral("hello ${thing}", {thing: "world"}) === "hello world"

尽管可以使这些东西更强大和灵活,但它会引入太多复杂性和风险,而且收益不大。
这里有一个链接到要点:https://gist.github.com/castarco/94c5385539cf4d7104cc4d3513c14f55

0
(请参见@Bergi上面非常相似的答案)

function interpolate(strings, ...positions) {
  var errors = positions.filter(pos=>~~pos!==pos);
  if (errors.length) {
    throw "Invalid Interpolation Positions: " + errors.join(', ');
  }
  return function $(...vals) {
    var output = '';
    for (let i = 0; i < positions.length; i ++) {
      output += (strings[i] || '') + (vals[positions[i] - 1] || '');
    }
    output += strings[strings.length - 1];
    return output;
  };
}

var iString = interpolate`This is ${1}, which is pretty ${2} and ${3}. Just to reiterate, ${1} is ${2}! (nothing ${0} ${100} here)`;
// Sets iString to an interpolation function

console.log(iString('interpolation', 'cool', 'useful', 'extra'));
// Substitutes the values into the iString and returns:
//   'This is interpolation, which is pretty cool and useful.
//   Just to reiterate, interpolation is cool! (nothing  here)'

这个答案与@Bergi的答案之间的主要区别在于如何处理错误(静默 vs 非静默)。

将这个想法扩展到接受命名参数的语法中应该很容易:

interpolate`This is ${'foo'}, which is pretty ${'bar'}.`({foo: 'interpolation', bar: 'cool'});

https://github.com/spikesagal/es6interpolate/blob/main/src/interpolate.js


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接