JavaScript
标签函数

前言

在看 Next.js 新特性时,看到处理表单方式介绍 (opens in a new tab)的时候,看到了这种使用 sql 的方法。

tagFn_1

后面就去了解了一下这是什么。

什么是标签函数?

JS 中有一类特殊的函数 —— (字符串)标签函数,标签函数是一种特殊的函数调用语法,它赋予开发人员处理模板字面量的能力,从而以更加灵活和控制的方式生成和操作字符串,也就是用于自定义模版字符串的处理逻辑。标签函数是 ES6 引入的,因此不用担心兼容性问题! 标签函数除了可以作为普通函数,通过 fn()调用之外,标签函数还可以直接跟在模版字符串前面使用模板字符串 fn`` 来调用。换句话说,当一个函数使用模板字符串的方式调用时,这个函数就可以被称为标签函数,所以我们不妨把标签函数理解为新增的一种函数调用方式。比如:

function tagFn() {
  return "我是返回值";
}
let res1 = tagFn();
let res2 = tagFn`一个模板字符串`;
console.log({ res1, res2 }); //{ res1: '我是返回值', res2: '我是返回值' }

内置的标签函数

JS 中只有一个内置标签函数 —— String.raw ,用于获取模字符串的原始字符串形式,即:

  • 处理替换(例如替换${name}为变量实际的值)
  • 不处理转义序列(例如 \n)
String.raw
const name = "xh";
 
const s1 = `Hello \n ${name}`;
const s2 = String.raw`Hello \n ${name}`;
 
console.log(s1);
console.log(s2);

tagFn_2

标签函数的参数

当标签函数存在参数时,它的参数是什么?

function tagFn(...args) {
  console.log(...args);
}
 
tagFn`一个普通的模板字符串`;
// [ '一个普通的模板字符串' ]
tagFn`一个有插值的模板字符串:${"var"}`;
//[ '一个有插值的模板字符串:', '' ] var
tagFn`一个有插值的模板字符串:${"var1"}-${"var2"}`;
//[ '一个有插值的模板字符串:', '-', '' ] var1 var2

从上面可以看出,标签函数调用时,接收到的一个参数总是一个数组,数组中的元素就是模板字符串中的字符串部分;从第二个参数开始的剩余参数接收的是模板字符串的插值变量,这些变量的数目是任意的。换种方式声明的话,可能更直观一些:

function tagFn(templateStrings, ...insertVars) {
  console.log({ templateStrings, insertVars });
}
tagFn`一个普通的模板字符串`;
//{ templateStrings: [ '一个普通的模板字符串' ], insertVars: [] }
tagFn`一个有插值的模板字符串:${"var"}`;
//{ templateStrings: [ '一个有插值的模板字符串:', '' ], insertVars: [ 'var' ] }
tagFn`一个有插值的模板字符串:${"var1"},${"var2"}`;
//{ templateStrings: [ '一个有插值的模板字符串:', ',', '' ], insertVars: [ 'var1', 'var2' ] }
tagFn`${"var"}一个有插值的模板字符串:${"var1"}!`;
//{ templateStrings: [ '', '一个有插值的模板字符串:', '!' ], insertVars: [ 'var', 'var1' ] }

可以看出来,templateStrings 中的每两个元素之间,都应该有一个 insertVars 中插入的变量。两个数组中元素的顺序是有对应关系的。当 templateStrings 开头和结尾有 insertVars 时会有个空字符串。

自定义标签函数

实现简易 String.raw

function myRaw(strings, ...values) {
  let result = "";
  for (let i = 0; i < strings.length; i++) {
    result += strings.raw[i] || strings[i];
    if (i < values.length) {
      result += values[i];
    }
  }
  return result;
}
console.log(`Hello \n world!`);
console.log(String.raw`Hello \n world!`);
console.log(myRaw`Hello \n world!`);

tagFn_3

标签函数的应用场景

1. 语法校验

比如,对于 HTML 字符串,可以使用标签函数来自动转义模板字符串中的特殊字符,以防止 XSS (跨站脚本攻击)。下面是一个代码示例:

function safeHtml(strings, ...values) {
  let result = strings[0];
  for (let i = 1; i < strings.length; i++) {
    let val = String(values[i - 1]);
    result += val.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    result += strings[i];
  }
  return result;
}
 
let userSuppliedInput = "<img src=x onerror=alert('XSS')>";
const result = safeHtml`<div>${userSuppliedInput}</div>`;
// <div>&lt;img src=x onerror=alert('XSS')&gt;</div>

2. i18n(国际化和本地化)

标签函数可以用于处理模板字符串中的文本,使其根据用户的语言和地区进行适当的转换。

function translate(strings, ...values) {
  const lang = "en"; // 假设当前语言为英语
  const translations = {
    Hello: "你好",
    world: "世界",
  };
 
  let result = "";
  strings.forEach((string, index) => {
    result += string;
    if (values[index] !== undefined) {
      result += translations[values[index]] || values[index];
    }
  });
 
  return result;
}
 
const greeting = translate`${"Hello"}, ${"world"}!`;
 
console.log(greeting);
// 输出: "你好, 世界!"

3. 特殊使用 (styled-components (opens in a new tab))

const Button = styled.a`
  /* This renders the buttons above... Edit me! */
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  margin: 0.5rem 1rem;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;

  /* The GitHub button is a primary button
   * edit this to target it specifically! */
  ${(props) =>
    props.primary &&
    css`
      background: white;
      color: black;
    `}
`;

tagFn_4

得到的 Button 就是一个 React 组件。通过 styled-components ,我们可以在 JS 中写 css 样式了!

总结

标签函数属于 ES6 特性,所以整体兼容性很不错,当我们需要处理模版字符串时,可以考虑自定义一个标签函数。

tagFn_caniuse