大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!
什么是 Tagged Template Literals模板文字(template literals)或模板字符串(template strings)附带的一个好处是能够对它们进行标记。
这意味着开发者可以通过函数运行模板字符串,而不是让浏览器立即将该值分配给变量,从而可以控制实际字符串的生成方式。
标签模板(tag template)的工作方式是开发者只需创建一个函数,然后获取要针对字符串运行的函数的名称,最后只需将该函数名称放在模板前面即可:
function highlight() { // 可以是任意逻辑}const name = 'Snickers';const age = '100';const sentence = highlight`My dog's name is ${name} and he is ${age} years old`;console.log(sentence);上面示例使用函数 highlight 来标记字符串,浏览器将运行这个函数,向其提供有关字符串的所有信息、用户输入的所有片段以及传入的变量。
在 highlight 函数实际被放入句子之前,开发者可以以任何给定的方式修改。假如有以下代码:
function highlight() { return 'cool';}如果 highlight 函数只简单输入 return 'cool' ,函数将接受整个字符串作为参数,然后只返回单词“cool”,此时控制台将仅仅输出字符串cool。
const sentence = highlight`My dog's name is ${name} and he is ${age} years old`;console.log(sentence);// 输出 'cool'显然,这不是我们实际想要的结果,但这个函数可以接受一些参数,这样开发者就可以自己创建字符串。
首先,highlight 函数将得到所有字符串片段的数组,主要包括:
My dog's name isand he isyears old所有字符串片段数组之后的参数表示正在插值的值,上面例子则表示 name 和 age 参数。当然,好的方式是使用 ES6 的 Rest 运算符,比如下面的示例:
// 以上示例使用 ES6 Rest运算符function highlight(strings, ...values) { return 'cool';}这里的 strings 是一个由三个元素组成的数组:
"My dog's name is ""and he is ""years old "上面函数的 values 参数则是另外一个数组,代表实际传递的两个值,这里是字符串 Snickers 和 100。
因此,标记模板的意义在于可以自己创建字符串。 下面的代码将所有的字符串和变量值一起组合成最终的字符串,与浏览器的运行机制完全相同。值得注意的是,strings 数组的长度永远会比 values 多 1,因此最后做了一个判空处理。
function highlight(strings, ...values) { let str = ''; strings.forEach((string, i) => { str += string + (values[i] || ''); // 因为长度问题判空处理 }); return str;}const name = 'Snickers';const age = '100';const sentence = highlight`My dog's name is ${name} and he is ${age} years old`;console.log(sentence);Tagged Template 其他用法标签函数的第一个参数包含一个字符串数组(字符串常量),其余的参数与表达式相关。
标签不必是普通的标识符,开发者可以使用任何优先级大于 16 的表达式,包括:属性访问、函数调用、new 表达式,甚至其他带标签的模板字面量。
console.log`Hello`;//输出 [ 'Hello' ]console.log.bind(1, 2)`Hello`;// 输出 2 [ 'Hello' ]new Function('console.log(arguments)')`Hello`;//输出 [Arguments] { '0': [ 'Hello' ] }function recursive(strings, ...values) { console.log(strings, values); return recursive;}recursive`Hello``World`;// 输出 [ 'Hello' ] []// 输出 [ 'World' ] []虽然语法从技术上允许这么做,但不带标签的模板字面量是字符串,并且在链式调用时会抛出 TypeError。
console.log(`Hello``World`); // TypeError: "Hello" is not a function唯一的例外是可选链,这将抛出语法错误
console.log?.`Hello`;// SyntaxError: Invalid tagged template on optional chainconsole?.log`Hello`;// SyntaxError: Invalid tagged template on optional chain请注意,这两个表达式仍然是可解析的。这意味着它们将不受自动分号补全的影响,其只会插入分号来修复无法解析的代码。
// 仍是语法错误const a = console?.log`Hello`标签函数接收到的第一个参数是一个字符串数组,对于任何模板字面量,其长度等于替换次数(${…} 出现次数)加一,因此总是非空的。对于任何特定的带标签的模板字面量表达式,无论对字面量求值多少次,都将始终使用完全相同的字面量数组调用标签函数。
const callHistory = [];function tag(strings, ...values) { callHistory.push(strings); // 返回一个新的对象 return {};}function evaluateLiteral() { return tag`Hello, ${'world'}!`;}console.log(evaluateLiteral() === evaluateLiteral());// 输出 false; 每次`tag`被调用都返回一个全新的对象console.log(callHistory[0] === callHistory[1]);// 输出 true。注意:相同标记文字(tagged literal)的所有计算都将传递到相同的字符串数组中标签函数以其第一个参数作为标识来缓存结果。为了进一步确保数组值不变,第一个参数及其 raw 属性都会被冻结,因此将无法改变。
Tagged Template 能干什么目前 Tagged Template 遇到的一个典型用例是字符串的解析,以下面的 html 方法为例,开发者希望将字符中的所有变量替换为真实的值,然后通过 DOM 操作渲染在屏幕中。
function render(state) { return html` <div>此时看看如何设计 html 方法能满足以上需求,这里就离不开借用 Tagged Template 的魔力。重点关注下面的 render 方法,该方法用于动态替换字符串中的变量并返回最终的 HTML 字符串。const tokensToTemplate = new WeakMap();/** * 创建<template>元素 */function parseTemplate(htmlString) { const template = document.createElement('template'); template.innerHTML = htmlString; return template;}/** * 这里的tokens就是文字字符串数组,expressions是变量集合 * expressions: ['blue', 'Blue!'] */function html(tokens, ...expressions) { const replaceStubs = (string) => string.replaceAll(/__stub-(\d+)__/g, (_, i) => expressions[i]); let template = tokensToTemplate.get(tokens); if (!template) { // WeakMap没有数据才创建 const stubs = expressions.map((_, i) => `__stub-${i}__`); const allTokens = tokens.map((token, i) => (stubs[i - 1] ?? '') + token); const htmlString = allTokens.join(''); template = parseTemplate(htmlString); tokensToTemplate.set(tokens, template); } // 克隆和更新绑定 const cloned = template.content.cloneNode(true); // 克隆元素 const element = cloned.firstElementChild; // 这里是所有的属性,比如name为`class`,而 value 为 '__stub-0__' 字符串 for (const { name, value } of element.attributes) { element.setAttribute(name, replaceStubs(value)); } element.textContent = replaceStubs(element.textContent); return element;}接下来使用 html 方法将生成的 DOM 插入到浏览器中:
function render(state) { return html` <div>https://wesbos.com/tagged-template-literalshttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals
https://www.youtube.com/watch?v=arOXcy7K4XQ
https://www.youtube.com/watch?v=PgQ3wobT1_U