开始阅读JavaScript高级程序设计(第5版)学习JS,总共有1000+页,非常全面,短期看完不太现实,找到了一篇博客,花些时间跟着这篇博客过一下红宝书。
红宝书《JavaScript高级程序设计(第5版)》学习大纲 - 大前端全栈开发 - SegmentFault 思否
语言基础:
10年前,主流浏览器对某些ECMAScript特性的支持还存在很大分歧。今天,每年发布一次的ECMAScript版本中所包含的特性通常用不了一年时间就能得到所有主流浏览器支持。
ECMAScript中的基本数据类型包括Undefined、Null、Boolean、Number、BigInt、String和Symbol。
Object是一种复杂数据类型,它是这门语言中所有对象的基类。
严格模式为这门语言中某些容易出错的部分施加了限制。
ECMAScript提供了C语言和类C语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
这门语言中的流控制语句大多是从其他语言中借鉴而来的,比如if语句、for语句和switch语句等。
不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
不指定返回值的函数实际上会返回特殊值undefined。
标识符:
第一个字符必须是字母、下划线(_)或美元符号($);
其他字符可以是字母、下划线、美元符号或数字。
标识符中的字母可以是扩展ASCII(Extended ASCII)中的字母,也可以是Unicode的字母字符,如 À 和 Æ(但不推荐使用)。按照惯例,ECMAScript标识符使用驼峰大小写形式。这种形式跟ECMAScript内置函数和对象的命名方式一致,所以算是最佳实践。
严格模式:
ECMAScript 5增加了严格模式(strict mode)的概念。严格模式是一种不同的JavaScript解析和执行模型,在这种模型下ECMAScript 3的一些不规范写法会被指出来,而不安全的操作将抛出错误。
要对整个脚本启用严格模式,在脚本开头加上这一行:
1
"use strict";
虽然看起来像没有赋值给任何变量的字符串,但它其实是一个编译指令(pragma)。任何支持的JavaScript引擎看到它都会切换到严格模式。
ES6模块和类不需要”use strict” 指令就自动应用严格模式。另外,Webpack等现代的代码编译器和打包器也会自动插入这个指令。
语句:
即使语句末尾的分号不是必需的,也应该加上。加分号有助于防止省略造成的问题,比如不会补足你输入的内容,从而便于开发者通过删除空白符来压缩代码(如果没有结尾的分号,则删除空行可能会导致语法错误)。加分号也有助于在某些情况下提升性能,因为否则解析器会尝试在合适的位置补上分号以纠正语法错误。
关键字和保留字:
ECMA-262规定的所有关键字如下:
1
2
3
4
5
6
await break case catch class const continue
debugger default delete do else export
extends false finally for function if import
in instanceof new null return super switch
this throw true try typeof var void while
with yield
规范中也描述了一组未来的保留字,同样不能用作标识符或属性名。虽然保留字在语言中没有特定用途,但它们是保留给将来做关键字用的。
1
2
3
4
5
6
7
8
始终保留:
enum
严格模式下保留:
arguments eval implements interface package private protected public static
变量:
有3个关键字可以声明变量:var、const和let。
var声明作用域:
使用var操作符定义的变量会成为包含它的函数的局部变量。比如,使用var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
1
2
3
4
5
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
在函数内定义变量时省略var操作符是可以创建全局变量的,只要调用一次函数test(),就会定义这个变量,并且可以在函数外部访问它:
1
2
3
4
5
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
虽然可以通过省略var操作符定义全局变量,但不推荐这么做。在局部作用域中定义的全局变量很难维护,也会造成困惑。这是因为不能一下子断定省略var是不是有意为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出ReferenceError。
如果需要定义多个变量,可以在一条语句中用逗号分隔每个变量,因为ECMAScript是松散类型的,所以使用不同数据类型初始化的变量可以用一条语句来声明。
1
2
3
var message = "hi",
found = false,
age = 29;
var声明提升:
使用var时,下面的代码不会报错。这是因为使用这个关键字声明的变量会自动提升到函数作用域顶部:
1
2
3
4
5
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
之所以不会报错,是因为ECMAScript运行时把它看成等价于如下代码:
1
2
3
4
5
6
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined,声明后却未及时赋值或赋类,会默认给予undefined的值
这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用var声明同一个变量也没有问题:
1
2
3
4
5
6
7
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
ES6引入后,在目前的开发中默认使用const,需要重新赋值时则使用let,var几乎不使用。var会使变量默认提升至函数作用域(例如在一个函数的不同块内用var声明几个同名变量,会出现覆盖),而let的块级作用域更符合直觉。
例如像下面的这个问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 经典闭包问题
var buttons = document.querySelectorAll('button');
// 使用var - 所有按钮都显示5,不符合预期的闭包,5个闭包都捕获了同一个变量
for (var i = 0; i < 5; i++) {
buttons[i].onclick = function() {
console.log('按钮 ' + i + ' 被点击'); // 总是"按钮 5 被点击"
};
}
// 使用let - 每个按钮显示自己的索引,符合预期的闭包
for (let i = 0; i < 5; i++) {
buttons[i].onclick = function() {
console.log('按钮 ' + i + ' 被点击'); // 正确显示0,1,2,3,4
};
}
由于var的提升,执行过程会如下:
1
2
3
4
5
6
7
8
9
10
11
// 实际执行过程相当于:
var i; // 变量提升到函数/全局作用域顶部
for (i = 0; i < 5; i++) {
buttons[i].onclick = function() {
console.log('按钮 ' + i + ' 被点击');
};
}
// 循环结束后,i的值是5
console.log('循环结束后的i:', i); // 5
因为闭包捕获的是i的引用,而不是i的当前值,循环开始后,外部的i会自增,点击事件也随之被设置。但当用户开始点击按钮时,循环早就结束了,i的值已经为5,每次点击都只能引用值为5的i。
当使用let时,每次循环都会根据产生的新的块作用域的不同而创建一个新的i:
1
2
3
4
5
6
7
8
9
10
11
// let的循环可以理解为:
{
let i = 0;
if (i < 5) {
buttons[i].onclick = function() {
console.log('按钮 ' + i + ' 被点击'); // 捕获的是这个作用域的i
};
i++;
}
// 每次迭代都进入一个新的块作用域
}
let声明:
let声明的范围是块作用域,而var声明的范围是函数作用域。
1
2
3
4
5
6
7
8
9
10
11
if (true) {
var name = 'Bob';
console.log(name); // Bob
}
console.log(name); // Bob
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age没有定义
let也不允许同一个块作用域中出现冗余声明。这样会导致报错:
1
2
3
4
5
var name;
var name;
let age;
let age; // SyntaxError;标识符age已经声明过了
在不同块中,嵌套使用同名的let声明的标识符不会报错:
1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'Alice';
console.log(name); // 'Alice'
if (true) {
var name = 'Bob';
console.log(name); // 'Bob'
}
let age = 30;
console.log(age); // 30
if (true) {
let age = 26;
console.log(age); // 26
}
暂时性死区:
let与var的另一个重要的区别,就是let声明的变量不会在作用域中被提升。
1
2
3
4
5
6
7
// name会被提升
console.log(name); // undefined
var name = 'Bob';
// age不会被提升
console.log(age); // ReferenceError:age没有定义
let age = 26;
在解析代码时,JavaScript引擎也会注意出现在块后面的let声明,只不过在此之前不能以任何方式来使用未声明的变量。在let声明之前的执行瞬间被称为“暂时性死区”(temporal dead zone),在此阶段使用任何后面才声明的变量都会抛出ReferenceError。
全局声明:
与var关键字不同,使用let在全局作用域中声明的变量不会像var声明的变量那样成为window对象的属性。不过,let声明仍然是在全局作用域中发生的,相应变量会在页面的生命周期内存续。
1
2
3
4
5
var name = 'Bob';
console.log(window.name); // 'Bob'
let age = 26;
console.log(window.age); // undefined
条件声明:
var可以使用条件声明,而let由于作用域被块限定,不能使用条件声明。
const声明:
const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
const age = 26;
age = 36; // TypeError: 给常量赋值
// const也不允许重复声明
const name = 'Bob';
const name = 'Alice'; // SyntaxError
// const声明的作用域也是块
const name = 'Bob';
if (true) {
const name = 'Alice';
}
console.log(name); // Bob
const声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制。
1
2
const person = {};
person.name = 'Bob'; // ok
数据类型:
ECMAScript有7种简单数据类型(也称为原始类型):Undefined、Null、Boolean、Number、BigInt、String和Symbol。还有一种复杂数据类型叫Object(对象),是一个无序名值对的集合。
typeof操作符:
对一个值使用typeof操作符会返回下列字符串之一:
下面是使用typeof操作符的例子,因为typeof是一个操作符而不是函数,所以不需要参数(但可以使用参数):
1
2
3
4
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"
调用typeof null返回的是 “object”。这是因为特殊值null被认为是一个对空对象的引用。
严格来讲,函数在ECMAScript中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过typeof操作符来区分函数和其他对象。
Undefined类型:
Undefined类型只有一个值,就是特殊值undefined。当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值。
一般来说,永远不用显式地给某个变量设置undefined值。字面值undefined主要用于比较,而且在ECMA-262第3版之前是不存在的。增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。
包含undefined值的变量跟未定义变量是有区别的。
1
2
3
4
5
6
7
let message; // 这个变量被声明了,但值为undefined
// 确保没有声明过这个变量
// let age
console.log(message); // "undefined"
console.log(age); // 报错
在上面的例子中,第一个console.log会输出变量message的值,即 “undefined”。而第二个console.log要输出一个未声明的变量age的值,因此会导致报错。对未声明的变量,只能执行一个有用的操作就是对它调用typeof。
如果使用typeof,无论变量是否已经声明,返回的结果都会是undefined。逻辑上讲这是对的,因为虽然严格来讲这两个变量存在根本性差异,但对它们都无法执行实际操作。
1
2
3
4
5
6
7
let message; // 这个变量被声明了,只是值为undefined
// 确保没有声明过这个变量
// let age
console.log(typeof message); // "undefined"
console.log(typeof age); // "undefined"
即使未初始化的变量会被自动赋予undefined值,但我们仍然建议在声明变量的同时进行初始化。这样,当typeof返回 “undefined” 时,你就会知道那是因为给定的变量尚未声明,而不是声明了但未初始化。
undefined是一个假值,但也有很多其他可能的值同样是假值。所以一定要明确自己想检测的就是undefined这个字面值,而不仅仅是假值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let message; // 这个变量被声明了,但值为undefined
// age没有声明
if (message) {
// 这个块不会执行
}
if (!message) {
// 这个块会执行
}
if (age) {
// 这里会报错
}
Null类型:
Null类型同样只有一个值,即特殊值null。逻辑上讲,null值表示一个空对象指针。在定义将来要保存对象值的变量时,建议使用null来初始化,不要使用其他值(可以永远不显式地将变量值设置为undefined,但如果一个变量将来要存储对象,而目前又还未存入,就应主动将变量设为null)。这样,只要检查这个变量的值是不是null就可以知道这个变量是否在后来被重新赋予了一个对象的引用。
undefined值是由null值派生而来的,因此ECMA-262将它们定义为表面上相等:
1
console.log(null == undefined); // true
用等于操作符(==)比较null和undefined始终返回true,null也同样是一个假值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let message = null;
let age;
if (message) {
// 这个块不会执行
}
if (!message) {
// 这个块会执行
}
if (age) {
// 这个块不会执行
}
if (!age) {
// 这个块会执行
}
Boolean类型:
虽然布尔值只有两个,但所有其他ECMAScript类型的值都有相应布尔值的等价形式。要将一个其他类型的值转换为布尔值,可以调用特定的Boolean()转型函数,Boolean()转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值:
1
2
let message = "Hello world!";
let messageAsBoolean = Boolean(message);
这种转化非常常见,例如像if等流控制语句会自动执行其他类型值到布尔值的转换。
Number类型:
最基本的数值字面量格式是十进制整数,整数也可以用二进制(以2为基数)、八进制(以8为基数)或十六进制(以16为基数)字面量表示。二进制字面量的前缀0b后面必须是一系列1和0:
1
2
let binaryNum1 = 0b110; // 二进制的6
let binaryNum2 = 0b333; // 无效的二进制值,SyntaxError
八进制字面量可以隐式或显式地定义。隐式声明的第一个数字必须是零(0),然后是相应的八进制数字(数值0~7)。如果字面量中包含的数字超出了范围,就会忽略前缀的零,后面的数字序列会被当成十进制数:
1
2
let octalNum1 = 070; // 八进制的56
let octalNum2 = 079; // 无效的八进制值,当成79处理
八进制字面量也可以显式定义,此时要加上0o前缀:
1
2
let octalNum3 = 0o70; // 八进制的56
let octalNum4 = 0o79; // 无效的八进制值,SyntaxError
八进制字面量的隐式声明在严格模式下是无效的,会导致JavaScript引擎抛出语法错误。要创建十六进制字面量,必须使用前缀0x(区分大小写),然后跟十六进制数字(0~9以及A~F)。十六进制数字中的字母大小写均可。
1
2
let hexNum1 = 0xA; // 十六进制10
let hexNum2 = 0x1f; // 十六进制31
使用二进制、八进制和十六进制格式创建的数值在所有数学操作中都被视为十进制数值。
浮点值:
要定义浮点值,数值中必须包含小数点,而且小数点后面必须至少有一个数字。虽然小数点前面不是必须有整数,但推荐加上。
1
2
3
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
ECMAScript中科学记数法的格式要求是一个数值(整数或浮点数)后跟一个大写或小写的字母e,再加上一个要乘的10的多少次幂。
1
let floatNum = 3.125e7; // 等于31250000
默认情况下,ECMAScript会将小数点后至少包含6个零的浮点值转换为科学记数法(例如,0.000 000 3会被转换为3e-7)。
浮点值的精确度最高可达17位小数,但在算术计算中远不如整数精确。例如,0.1加0.2得到的不是0.3,而是0.300 000 000 000 000 04。由于这种微小的舍入错误,导致很难测试特定的浮点值。
1
2
3
if (a + b == 0.3) { // 别这么干!
console.log("You got 0.3.");
}
这里检测两个数值之和是否等于0.3。如果两个数值分别是0.05和0.25,或者0.15和0.15,那没问题。但如果是0.1和0.2,如前所述,测试将失败。因此永远不要测试某个特定的浮点值。
数字分隔符:
所有数值都可以使用下划线作为数字分隔符以增进可读性。下划线在数值字面量中可以出现任意次,解释器会静默地忽略它们。
1
2
3
let oneMillion = 1_000_000;
let binary = 0b0100_0000;
let float = 1_000.000_001;
作为分隔符的下划线不能出现在数值字面量的开头或末尾,不能紧挨着小数点,前面也不能有打头儿的0:
1
2
3
4
let invalid1 = _101; // ReferenceError,_101会被当成变量名
let invalid2 = 101_; // SyntaxError
let invalid3 = 0_01; // SyntaxError
let invalid4 = 1._4; // SyntaxError
数值转换:
有3个函数可以将非数值转换为数值:Number()、parseInt()和parseFloat()。Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
1
2
3
4
let num1 = Number("Hello world!"); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
用Number()函数转换字符串时相对复杂且有点反常规,通常在需要得到整数时可以优先使用parseInt()函数。
1
2
3
4
5
6
7
8
9
10
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
let num1 = parseInt("10", 2); // 2,按二进制解析
let num2 = parseInt("10", 8); // 8,按八进制解析
let num3 = parseInt("10", 10); // 10,按十进制解析
let num4 = parseInt("10", 16); // 16,按十六进制解析
因为不传底数参数相当于让parseInt()自己决定如何解析,所以为避免解析出错,建议始终传给它第二个参数。
parseFloat()函数的工作方式跟parseInt()函数类似,都是从位置0开始检测每个字符。同样,它也是解析到字符串末尾或者解析到一个无效的浮点数值字符为止。这意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了。
parseFloat()函数的另一个不同之处在于,它始终忽略字符串开头的零。这个函数能识别前面讨论的所有浮点格式,以及十进制格式(开头的零始终被忽略)。十六进制数值始终会返回0。由于parseFloat()只解析十进制值,因此不能指定底数。
1
2
3
4
5
6
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
String类型:
String(字符串)数据类型表示零或多个16位Unicode字符序列。字符串可以使用双引号(“)、单引号(‘)或反引号(`)标示,ECMAScript语法中表示字符串的引号没有区别。
1
2
3
let firstName = "John";
let middleName = 'Jacob';
let lastName = `Jingleheimerschmidt`
字符字面量:
这些字符字面量可以出现在字符串中的任意位置,且可以作为单个字符被解释:
1
let text = "This is the letter sigma: \u03a3.";
在这个例子中,即使包含6个字符长的转义序列,变量text仍然是28个字符长。因为转义序列表示一个字符,所以只算一个字符。
字符串的长度可以通过其length属性获取,这个属性返回字符串中16位字符的个数:
1
console.log(text.length); // 28
如果字符串中包含双字节字符,那么length属性返回的值可能不是准确的字符数。第5章将具体讨论如何解决这个问题。
字符串的特点:
ECMAScript中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量:
1
2
let lang = "Java";
lang = lang + "Script";
变量lang一开始包含字符串 “Java”。紧接着,lang被重新定义为包含 “Java” 和”Script” 的组合,也就是 “JavaScript”。整个过程首先会分配一个足够容纳10个字符的空间,然后填充上 “Java” 和 “Script”。最后销毁原始的字符串 “Java” 和字符串”Script”。
转换为字符串:
首先是toString()方法。这个方法唯一的用途就是返回当前值的字符串等价物。
1
2
3
4
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
toString()方法可用于数值、布尔值、对象和字符串值(简单地返回自身的一个副本)。null和undefined值没有toString()方法。
多数情况下,toString()不接收任何参数。不过,在对数值调用这个方法时,toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示。默认情况下,toString()返回数值的十进制字符串表示。而通过传入参数,可以得到数值的二进制、八进制、十六进制,或者其他任何有效基数的字符串表示。
1
2
3
4
5
6
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
如果不确定一个值是不是null或undefined,可以使用String()转型函数,它始终会返回表示相应类型值的字符串。
1
2
3
4
5
6
7
8
9
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
模板字面量:
模板字面量是 ES6(ES2015)引入的一种新的字符串语法,使用反引号(```)包裹字符串内容,可以更方便地创建多行字符串、嵌入表达式和执行字符串插值。与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串。
主要有两种用法,一种是反引号的基本用法,可以包含插值${};一种是标签函数,可以在反引号前带有用以处理模板字面量的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 这个模板字面量在换行符之后有25个空格符
let myTemplateLiteral = `first line
second line`;
console.log(myTemplateLiteral.length); // 47
// 这个模板字面量以一个换行符开头
let secondTemplateLiteral = `
first line
second line`;
console.log(secondTemplateLiteral[0] === '\n'); // true
// 这个模板字面量没有意料之外的字符
let thirdTemplateLiteral = `first line
second line`;
console.log(thirdTemplateLiteral);
// first line
// second line
模板字面量最常用的一个特性是支持字符串插值,也就是可以在一个连续定义中插入一个或多个值。技术上讲,模板字面量不是字符串,而是一种特殊的JavaScript句法表达式,只不过求值后得到的是字符串。模板字面量在定义时立即求值并转换为字符串实例,任何插入的变量也会从它们最接近的作用域中取值。
1
2
3
4
5
6
7
8
9
10
11
12
13
let value = 5;
let exponent = 'second';
// 以前,字符串插值是这样实现的:
let interpolatedString =
value + ' to the ' + exponent + ' power is ' + (value * value);
// 现在,可以用模板字面量这样实现:
let interpolatedTemplateLiteral =
`${ value } to the ${ exponent } power is ${ value * value }`;
console.log(interpolatedString); // 5 to the second power is 25
console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
将表达式转换为字符串时会调用toString():
1
2
let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`); // Hello, World!
模板字面量标签函数:
模板字面量也支持定义标签函数(tag function),而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板(即去除了插值的原始字符串数组片段,这些片段组成数组作为第一个参数传入)和对每个表达式求值(即插值,作为后续参数依次传入)的结果。
标签函数本身是一个常规函数,通过前缀到模板字面量来应用自定义行为,如下例所示。标签函数接收到的参数依次是原始字符串数组和对每个表达式求值的结果。这个函数的返回值是对模板字面量求值得到的字符串。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let a = 6;
let b = 9;
function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
console.log(strings);
console.log(aValExpression);
console.log(bValExpression);
console.log(sumExpression);
return 'foobar';
}
let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""] 开头和末尾即使是空字符串也会用""占位
// 元素数量 = 插值数量 + 1
// 6
// 9
// 15
console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult); // "foobar"
1
2
3
4
5
6
7
// 实际上会这样调用:
simpleTag(
["", "+ ", " = ", ""], // 字符串数组
6, // 第一个插值表达式的结果
9, // 第二个插值表达式的结果
15 // 第三个插值表达式的结果
);
原始字符串:
使用模板字面量也可以直接获取原始的模板字面量内容(如换行符或Unicode字符),而不是被转换后的字符表示。为此,可以使用默认的String.raw标签函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Unicode示例
// \u00A9是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9
// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line"
// 对实际的换行符来说是不行的
// 它们不会被转换成转义序列的形式
console.log(`first line
second line`);
// first line
// second line
console.log(String.raw`first line
second line`);
// first line
// second line
另外,也可以通过标签函数的第一个参数,即字符串数组的 .raw属性取得每个字符串的原始内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters:');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\u00A9${ 'and' }\n`;
// Actual characters:
// ©
//(换行符)
// Escaped characters:
// \u00A9
// \n




