Allureluoli 二月FEBRUARY

Next学习手册(三)- Essential JavaScript

基本JavaScript

熟悉JavaScript可以帮助我们更好的学习React,本章主要讲一些基本的JavaScript,以防止因为缺少JavaScript知识导致对后面的内容难以理解。

如果你很熟悉JavaScript可以跳过此节。

JavaScript的用途非常广泛,也非常的庞大,我们从以下几个角度开始学习基本的JavaScript:

  • Functions and Arrow Functions:函数和箭头函数

  • Object:对象

  • Arrays and array methods:数组和数组方法

  • Destructuring:解构函数

  • Template literals:模板文字

  • Ternary Operators:三元运算符

  • ES Modules and Import / Export Syntax:ES 模块和导入/导出 语法

函数和箭头函数

MDN:

  • 函数是 JavaScript 中的基本组件之一。JavaScript 中的函数类似于过程——一组执行任务或计算值的语句。但要成为函数,这个过程应该接受输入并返回与输入存在某些明显关系的输出。要使用一个函数,你必须将其定义在你希望调用它的作用域内。

函数声明

一个函数定义(也称为函数声明,或函数语句)由 function 关键字,并跟随以下部分组成:

  • 函数名称。
  • 函数参数列表,包围在括号中并由逗号分隔。
  • 定义函数的 JavaScript 语句,用大括号括起来,{ /* … */ }。

例如,以下的代码定义了一个简单的名为 square 的函数:

1
2
3
function square(number) {
return number * number;
}

函数 square 接收一个名为 number 的参数。这个函数只有一个语句,其表示该函数将函数的参数(即 number)自乘后返回。函数的 return 语句指定了函数的返回值:number * number。

参数本质上是按值传递给函数的——因此,即使函数体的代码为传递给函数的参数赋了新值,这个改变也不会反映到全局或调用该函数的代码中。

如果你将对象作为参数传递,而函数改变了这个对象的属性,这样的改变对函数外部是可见的,如下面的例子所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
function myFunc(theObject) {
theObject.make = "Toyota";
}

const mycar = {
make: "Honda",
model: "Accord",
year: 1998,
};

console.log(mycar.make); // "Honda"
myFunc(mycar);
console.log(mycar.make); // "Toyota"

如果你将数组作为参数传递,而函数改变了这个数组的值,这样的改变对函数外部也同样可见,如下面的例子所示:

1
2
3
4
5
6
7
8
9
function myFunc(theArr) {
theArr[0] = 30;
}

const arr = [45];

console.log(arr[0]); // 45
myFunc(arr);
console.log(arr[0]); // 30

函数表达式

虽然上面的函数声明在语法上是一个语句,但函数也可以由函数表达式创建。

这样的函数可以是匿名的;它不必有一个名称。例如,函数 square 也可这样来定义:

1
2
3
4
5
const square = function (number) {
return number * number;
};

console.log(square(4)); // 16

然而,也可以为函数表达式提供名称,并且可以用于在函数内部代指其本身,或者在调试器堆栈跟踪中识别该函数:

1
2
3
4
5
const factorial = function fac(n) {
return n < 2 ? 1 : n * fac(n - 1);
};

console.log(factorial(3)); // 6

当将函数作为参数传递给另一个函数时,函数表达式很方便。下面的例子演示了一个叫 map 的函数,该函数接收函数作为第一个参数,接收数组作为第二个参数:

1
2
3
4
5
6
7
function map(f, a) {
const result = new Array(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = f(a[i]);
}
return result;
}

在以下代码中,该函数接收由函数表达式定义的函数,并对作为第二个参数接收的数组的每个元素执行该函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function map(f, a) {
const result = new Array(a.length);
for (let i = 0; i < a.length; i++) {
result[i] = f(a[i]);
}
return result;
}

const cube = function (x) {
return x * x * x;
};

const numbers = [0, 1, 2, 5, 10];
console.log(map(cube, numbers)); // [0, 1, 8, 125, 1000]

在 JavaScript 中,可以根据条件来定义一个函数。比如下面的代码,当 num 等于 0 的时候才会定义 myFunc:

1
2
3
4
5
6
let myFunc;
if (num === 0) {
myFunc = function (theObject) {
theObject.make = "Toyota";
};
}

除了上述的定义函数方法外,你也可以在运行时用 Function 构造函数从一个字符串创建一个函数,很像 eval() 函数。

以上均摘自 我很喜欢的教学网站 MDN。

箭头函数

箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。

基础语法

1
2
3
4
5
6
7
8
9
10
(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
//相当于:(param1, param2, …, paramN) =>{ return expression; }

// 当只有一个参数时,圆括号是可选的:
(singleParam) => { statements }
singleParam => { statements }

// 没有参数的函数应该写成一对圆括号。
() => { statements }

高级语法

1
2
3
4
5
6
7
8
9
10
11
//加括号的函数体返回对象字面量表达式:
params => ({foo: bar})

//支持剩余参数和默认参数
(param1, param2, ...rest) => { statements }
(param1 = defaultValue1, param2, …, paramN = defaultValueN) => {
statements }

//同样支持参数列表解构
let f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f(); // 6

没有单独的this

在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的 this 值:

  • 如果该函数是一个构造函数,this 指针指向一个新的对象
  • 在严格模式下的函数调用下,this 指向undefined
  • 如果该函数是一个对象的方法,则它的 this 指针指向这个对象
  • 等等

This被证明是令人厌烦的面向对象风格的编程。

1
2
3
4
5
6
7
8
9
10
11
12
function Person() {
// Person() 构造函数定义 `this`作为它自己的实例。
this.age = 0;

setInterval(function growUp() {
// 在非严格模式,growUp() 函数定义 `this`作为全局对象,
// 与在 Person() 构造函数中定义的 `this`并不相同。
this.age++;
}, 1000);
}

var p = new Person();

在 ECMAScript 3/5 中,通过将this值分配给封闭的变量,可以解决this问题。

1
2
3
4
5
6
7
8
9
function Person() {
var that = this;
that.age = 0;

setInterval(function growUp() {
// 回调引用的是`that`变量,其值是预期的对象。
that.age++;
}, 1000);
}

或者,可以创建绑定函数,以便将预先分配的this值传递到绑定的目标函数(上述示例中的growUp()函数)。

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承 this。因此,在下面的代码中,传递给setInterval的函数内的this与封闭函数中的this值相同:

1
2
3
4
5
6
7
8
9
function Person() {
this.age = 0;

setInterval(() => {
this.age++; // |this| 正确地指向 p 实例
}, 1000);
}

var p = new Person();

与严格模式的关系

鉴于 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。

1
2
var f = () => { 'use strict'; return this; };
f() === window; // 或者 global

严格模式的其他规则依然不变。

通过 call 或 apply 调用

由于 箭头函数没有自己的 this 指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定 this—译者注),他们的第一个参数会被忽略。(这种现象对于 bind 方法同样成立 — 译者注)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var adder = {
base: 1,

add: function (a) {
var f = (v) => v + this.base;
return f(a);
},

addThruCall: function (a) {
var f = (v) => v + this.base;
var b = {
base: 2,
};

return f.call(b, a);
},
};

console.log(adder.add(1)); // 输出 2
console.log(adder.addThruCall(1)); // 仍然输出 2

不绑定arguments

箭头函数不绑定Arguments 对象。因此,在本示例中,arguments只是引用了封闭作用域内的 arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var arguments = [1, 2, 3];
var arr = () => arguments[0];

arr(); // 1

function foo(n) {
var f = () => arguments[0] + n; // 隐式绑定 foo 函数的 arguments 对象。arguments[0] 是 n,即传给 foo 函数的第一个参数
return f();
}

foo(1); // 2
foo(2); // 4
foo(3); // 6
foo(3, 2); //6

在大多数情况下,使用剩余参数是相较使用arguments对象的更好选择。

1
2
3
4
5
6
7
8
9
10
11
function foo(arg) {
var f = (...args) => args[0];
return f(arg);
}
foo(1); // 1

function foo(arg1, arg2) {
var f = (...args) => args[1];
return f(arg1, arg2);
}
foo(1, 2); //2

使用箭头函数作为方法

如上所述,箭头函数表达式对非方法函数是最合适的。让我们看看当我们试着把它们作为方法时发生了什么。

1
2
3
4
5
6
7
8
9
10
11
12
"use strict";
var obj = {
i: 10,
b: () => console.log(this.i, this),
c: function () {
console.log(this.i, this);
},
};
obj.b();
// undefined, Window{...}
obj.c();
// 10, Object {...}

箭头函数没有定义 this 绑定。另一个涉及Object.defineProperty()的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"use strict";
var obj = {
a: 10,
};

Object.defineProperty(obj, "b", {
get: () => {
console.log(this.a, typeof this.a, this);
return this.a + 10;
// 代表全局对象 'Window', 因此 'this.a' 返回 'undefined'
},
});

obj.b; // undefined "undefined" Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}

使用 new 操作符

箭头函数不能用作构造器,和 new一起用会抛出错误。

1
2
var Foo = () => {};
var foo = new Foo(); // TypeError: Foo is not a constructor

使用prototype属性

箭头函数没有prototype属性。

1
2
var Foo = () => {};
console.log(Foo.prototype); // undefined

使用 yield 关键字

yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。

函数体

箭头函数可以有一个“简写体”或常见的“块体”。

在一个简写体中,只需要一个表达式,并附加一个隐式的返回值。在块体中,必须使用明确的return语句。

1
2
3
4
5
6
7
var func = (x) => x * x;
// 简写函数 省略 return

var func = (x, y) => {
return x + y;
};
//常规编写 明确的返回值

返回对象字面量

记住用params => {object:literal}这种简单的语法返回对象字面量是行不通的。

1
2
3
4
5
var func = () => { foo: 1 };
// Calling func() returns undefined!

var func = () => { foo: function() {} };
// SyntaxError: function statement requires a name

这是因为花括号({} )里面的代码被解析为一系列语句(即 foo 被认为是一个标签,而非对象字面量的组成部分)。

所以,记得用圆括号把对象字面量包起来:

1
var func = () => ({ foo: 1 });

换行

箭头函数在参数和箭头之间不能换行。

1
2
3
var func = ()
=> 1;
// SyntaxError: expected expression, got '=>'

但是,可以通过在‘=>’之后换行,或者用‘( )’、’{ }’来实现换行,如下:

1
2
3
4
5
6
7
8
9
10
11
var func = (a, b, c) => 1;

var func = (a, b, c) => 1;

var func = (a, b, c) => {
return 1;
};

var func = (a, b, c) => 1;

// 不会有语法错误

解析顺序

虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。

1
2
3
4
5
6
7
8
let callback;

callback = callback || function() {}; // ok

callback = callback || () => {};
// SyntaxError: invalid arrow-function arguments

callback = callback || (() => {}); // ok

更多相关内容请前往 MDN 进行查阅和学习。

Object(对象)

Object 是 JavaScript 的一种数据类型。它用于存储各种键值集合和更复杂的实体。可以通过 Object() 构造函数或者使用对象字面量的方式创建对象。

描述

在 JavaScript 中,几乎所有的对象都是 Object 的实例;一个典型的对象从 Object.prototype 继承属性(包括方法),尽管这些属性可能被覆盖(或者说重写)。唯一不从 Object.prototype 继承的对象是那些 null 原型对象,或者是从其他 null 原型对象继承而来的对象。

通过原型链,所有对象都能观察到 Object.prototype 对象的改变,除非这些改变所涉及的属性和方法沿着原型链被进一步重写。尽管有潜在的危险,但这为覆盖或扩展对象的行为提供了一个非常强大的机制。为了使其更加安全,Object.prototype 是核心 JavaScript 语言中唯一具有不可变原型的对象——Object.prototype 的原型始终为 null 且不可更改。

对象原型属性

你应该避免调用任何 Object.prototype 方法,特别是那些不打算多态化的方法(即只有其初始行为是合理的,且无法被任何继承的对象以合理的方式重写)。所有从 Object.prototype 继承的对象都可以自定义一个具有相同名称但语义可能与你的预期完全不同的自有属性。此外,这些属性不会被 null 原型对象继承。现代 JavaScript 中用于操作对象的工具方法都是静态的。更具体地说:

如果不存在语义上等价的静态方法,或者你真的想使用 Object.prototype 方法,你应该通过 call() 直接在目标对象上调用 Object.prototype 方法,以防止因目标对象上原有方法被重写而产生意外的结果。

1
2
3
4
5
6
7
8
9
10
11
const obj = {
foo: 1,
// 如果可能的话,你不应该在自己的对象上定义这样的方法,
// 但是如果你从外部输入接收对象,可能无法防止这种情况的发生
propertyIsEnumerable() {
return false;
},
};

obj.propertyIsEnumerable("foo"); // false;预期外的结果
Object.prototype.propertyIsEnumerable.call(obj, "foo"); // true;预期的结果

从对象中删除属性

一个对象本身没有任何方法可以(像 Map.prototype.delete() 一样)删除自己的属性。要删除一个对象的属性,必须使用 delete 运算符。

null 原型对象

几乎所有的 JavaScript 对象最终都继承自 Object.prototype(参见继承和原型链)。然而,你可以使用 Object.create(null) 或定义了 __proto__: null 的对象字面量语法(注意:对象字面量中的 __proto__ 键不同于已弃用的 Object.prototype.__proto__ 属性)来创建 null 原型对象。你还可以通过调用 Object.setPrototypeOf(obj, null) 将现有对象的原型更改为 null。

1
2
const obj = Object.create(null);
const obj2 = { __proto__: null };

null 原型对象可能会有一些预期外的行为表现,因为它不会从 Object.prototype 继承任何对象方法。这在调试时尤其需要注意,因为常见的对象属性转换/检测实用方法可能会产生错误或丢失信息(特别是在使用了忽略错误的静默错误捕获机制的情况下)。

例如,Object.prototype.toString() 方法的缺失通常会使得调试变得困难:

1
2
3
4
5
6
7
8
const normalObj = {}; // 创建一个普通对象
const nullProtoObj = Object.create(null); // 创建一个 "null" 原型对象

console.log(`normalObj 是:${normalObj}`); // 显示 "normalObj 是:[object Object]"
console.log(`nullProtoObj 是:${nullProtoObj}`); // 抛出错误:Cannot convert object to primitive value

alert(normalObj); // 显示 [object Object]
alert(nullProtoObj); // 抛出错误:Cannot convert object to primitive value

其他方法也会失败。

1
2
3
4
5
6
7
8
normalObj.valueOf(); // 显示 {}
nullProtoObj.valueOf(); // 抛出错误:nullProtoObj.valueOf is not a function

normalObj.hasOwnProperty("p"); // 显示 "true"
nullProtoObj.hasOwnProperty("p"); // 抛出错误:nullProtoObj.hasOwnProperty is not a function

normalObj.constructor; // 显示 "Object() { [native code] }"
nullProtoObj.constructor; // 显示 "undefined"

我们可以通过为 null 原型对象分配属性的方式将 toString 方法添加回去:

1
2
3
4
nullProtoObj.toString = Object.prototype.toString; // 由于新对象缺少 `toString` 方法,因此需要将原始的通用 `toString` 方法添加回来。

console.log(nullProtoObj.toString()); // 显示 "[object Object]"
console.log(`nullProtoObj 是:${nullProtoObj}`); // 显示 "nullProtoObj 是:[object Object]"

普通对象的 toString() 方法是在对象的原型上的,而与普通对象不同的是,这里的 toString() 方法是 nullProtoObj 的自有属性。这是因为 nullProtoObj 没有原型(即为 null)。

在实践中,null 原型对象通常被用作 map 的简单替代品。由于存在 Object.prototype 属性,会导致一些错误:

1
2
3
4
5
6
7
8
9
10
11
12
const ages = { alice: 18, bob: 27 };

function hasPerson(name) {
return name in ages;
}

function getAge(name) {
return ages[name];
}

hasPerson("hasOwnProperty"); // true
getAge("toString"); // [Function: toString]

使用一个 null 原型对象可以消除这种风险,同时不会令 hasPerson 和 getAge 函数变得复杂:

1
2
3
4
5
6
7
const ages = Object.create(null, {
alice: { value: 18, enumerable: true },
bob: { value: 27, enumerable: true },
});

hasPerson("hasOwnProperty"); // false
getAge("toString"); // undefined

在这种情况下,添加任何方法都应该慎重,因为它们可能会与存储为数据的其他键值对混淆。

让你的对象不继承自 Object.prototype 还可以防止原型污染攻击。如果恶意脚本向 Object.prototype 添加一个属性,程序中的每个对象上都可访问它,除了那些原型为 null 的对象。

1
2
3
4
5
6
7
8
9
const user = {};

// 恶意脚本:
Object.prototype.authenticated = true;

// 意外允许未经身份验证的用户通过
if (user.authenticated) {
// 访问机密数据
}

JavaScript 还具有内置的 API,用于生成 null 原型对象,特别是那些将对象用作临时键值对集合的 API。例如:

“null 原型对象”这个术语通常也包括其原型链中没有 Object.prototype 的任何对象。当使用类时,可以通过 extends null 来创建这样的对象。

对象强制转换

许多内置操作首先将它们的参数强制转换为对象。该过程可以概括如下:

在 JavaScript 中实现相同效果的最佳方式是使用 Object() 构造函数。Object(x) 可以将 x 转换为对象,对于 undefined 或 null,它会返回一个普通对象而不是抛出 TypeError 异常。

使用对象强制转换的地方包括:

  • for...in 循环的 object 参数。
  • Array 方法的 this 值。
  • Object 方法的参数,如 Object.keys()。
  • 当访问基本类型的属性时进行自动转换,因为基本类型没有属性。
  • 在调用非严格函数时的 this 值。基本类型值被封装为对象,而 null 和 undefined 被替换为全局对象。

与转换为基本类型不同,对象强制转换过程本身无法以任何方式被观察到,因为它不会调用像 toString 或 valueOf 方法这样的自定义代码。

构造函数

  • Object()

    ​ 将输入转换为一个对象。

静态方法

实例属性

这些属性在 Object.prototype 上定义,被所有 Object 实例所共享。

  • Object.prototype.__proto__ 已弃用

    ​ 指向实例对象在实例化时使用的原型对象。

  • Object.prototype.constructor

    ​ 创建该实例对象的构造函数。对于普通的 Object 实例,初始值为 Object 构造函数。其它构造函数的实例都会从它们各自的 Constructor.prototype 对象中继承 constructor 属性。

实例方法

示例

构造空对象

以下示例使用带有不同参数的 new 关键字创建空对象:

1
2
3
const o1 = new Object();
const o2 = new Object(undefined);
const o3 = new Object(null);

使用 Object 生成布尔对象

下面的例子将 Boolean 对象存到 o 中:

1
2
// 等价于 const o = new Boolean(true);
const o = new Object(true);
1
2
// 等价于 const o = new Boolean(false);
const o = new Object(Boolean());

Array(数组)

与其他编程语言中的数组一样,**Array** 对象支持在单个变量名下存储多个元素,并具有执行常见数组操作的成员。

描述

在 JavaScript 中,数组不是基本类型,而是具有以下核心特征的 Array 对象:

  • **JavaScript 数组是可调整大小的,并且可以包含不同的数据类型**。(当不需要这些特征时,可以使用类型化数组。)
  • JavaScript 数组不是关联数组,因此,不能使用任意字符串作为索引访问数组元素,但必须使用非负整数(或它们各自的字符串形式)作为索引访问。
  • **JavaScript 数组的索引从 0 开始**:数组的第一个元素在索引 0 处,第二个在索引 1 处,以此类推,最后一个元素是数组的 length 属性减去 1 的值。
  • **JavaScript 数组复制操作创建浅拷贝*。(所有* JavaScript 对象的标准内置复制操作都会创建浅拷贝,而不是深拷贝)。

数组下标

Array 对象不能使用任意字符串作为元素索引(如关联数组),必须使用非负整数(或它们的字符串形式)。通过非整数设置或访问不会设置或从数组列表本身检索元素,但会设置或访问与该数组的对象属性集合相关的变量。数组的对象属性和数组元素列表是分开的,数组的遍历和修改操作不能应用于这些命名属性。

数组元素是对象属性,就像 toString 是属性一样(具体来说,toString() 是一种方法)。然而,尝试按以下方式访问数组的元素会抛出语法错误,因为属性名无效:

1
console.log(arr.0); // 语法错误

JavaScript 语法要求使用方括号表示法而不是点号表示法来访问以数字开头的属性。也可以用引号包裹数组下标(例如,years['2'] 而不是 years[2]),尽管通常没有必要。

JavaScript 引擎通过隐式的 toString,将 years[2] 中的 2 强制转换为字符串。因此,'2' 和 '02' 将指向 years 对象上的两个不同的槽位,下面的例子可能是 true:

1
console.log(years["2"] !== years["02"]);

只有 years['2'] 是一个实际的数组索引。years['02'] 是一个在数组迭代中不会被访问的任意字符串属性。

长度与数值属性的关系

JavaScript 数组的 length 属性和数值属性是连接的。

一些内置数组方法(例如 join()、slice()、indexOf() 等)在被调用时会考虑到数组的 length 属性的值。

其他方法(例如,push()、splice() 等)也会更新数组的 length 属性。

1
2
3
const fruits = [];
fruits.push("banana", "apple", "peach");
console.log(fruits.length); // 3

当在 JavaScript 数组上设置一个属性时,如果该属性是一个有效的数组索引并且该索引在数组的当前边界之外,引擎将相应地更新数组的 length 属性:

1
2
3
4
fruits[5] = "mango";
console.log(fruits[5]); // 'mango'
console.log(Object.keys(fruits)); // ['0', '1', '2', '5']
console.log(fruits.length); // 6

增加 length。

1
2
3
4
5
fruits.length = 10;
console.log(fruits); // ['banana', 'apple', 'peach', empty x 2, 'mango', empty x 4]
console.log(Object.keys(fruits)); // ['0', '1', '2', '5']
console.log(fruits.length); // 10
console.log(fruits[8]); // undefined

但是,减少 length 属性会删除元素。

1
2
3
fruits.length = 2;
console.log(Object.keys(fruits)); // ['0', '1']
console.log(fruits.length); // 2

这将在 Array/length 页中进一步解释

解构赋值

解构赋值语法是一种 Javascript 表达式。可以将数组中的值或对象的属性取出,赋值给其他变量。

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const [a, b] = array;
const [a, , b] = array;
const [a = aDefault, b] = array;
const [a, b, ...rest] = array;
const [a, , b, ...rest] = array;
const [a, b, ...{ pop, push }] = array;
const [a, b, ...[c, d]] = array;

const { a, b } = obj;
const { a: a1, b: b1 } = obj;
const { a: a1 = aDefault, b = bDefault } = obj;
const { a, b, ...rest } = obj;
const { a: a1, b: b1, ...rest } = obj;
const { [key]: a } = obj;

let a, b, a1, b1, c, d, rest, pop, push;
[a, b] = array;
[a, , b] = array;
[a = aDefault, b] = array;
[a, b, ...rest] = array;
[a, , b, ...rest] = array;
[a, b, ...{ pop, push }] = array;
[a, b, ...[c, d]] = array;

({ a, b } = obj); // brackets are required
({ a: a1, b: b1 } = obj);
({ a: a1 = aDefault, b = bDefault } = obj);
({ a, b, ...rest } = obj);
({ a: a1, b: b1, ...rest } = obj);

描述

对象和数组字面量表达式提供了一种简单的方法来创建特别的数据包。

1
const x = [1, 2, 3, 4, 5];

解构赋值使用类似的语法,但在赋值的左侧定义了要从原变量中取出哪些值。

1
2
3
4
const x = [1, 2, 3, 4, 5];
const [y, z] = x;
console.log(y); // 1
console.log(z); // 2

同样,你可以在赋值语句的左侧解构对象。

1
2
3
4
5
const obj = { a: 1, b: 2 };
const { a, b } = obj;
// is equivalent to:
// const a = obj.a;
// const b = obj.b;

这种功能类似于 Perl 和 Python 等语言中存在的特性。

绑定与赋值

对于对象和数组的解构,有两种解构模式:绑定模式和赋值模式,它们的语法略有不同。

在绑定模式中,模式以声明关键字(var、let 或 const)开始。然后,每个单独的属性必须绑定到一个变量或进一步解构。

1
2
3
4
5
6
const obj = { a: 1, b: { c: 2 } };
const {
a,
b: { c: d },
} = obj;
// Two variables are bound: `a` and `d`

所有变量共享相同的声明,因此,如果你希望某些变量可重新分配,而其他变量是只读的,则可能需要解构两次——一次使用 let,一次使用 const。

1
2
3
4
5
const obj = { a: 1, b: { c: 2 } };
const { a } = obj; // a is constant
let {
b: { c: d },
} = obj; // d is re-assignable

在赋值模式中,模式不以关键字开头。每个解构属性都被赋值给一个赋值目标——这个赋值目标可以事先用 var 或 let 声明,也可以是另一个对象的属性——一般来说,可以是任何可以出现在赋值表达式左侧的东西。

1
2
3
4
const numbers = [];
const obj = { a: 1, b: 2 };
({ a: numbers[0], b: numbers[1] } = obj);
// The properties `a` and `b` are assigned to properties of `numbers`

备注: 当使用对象文字解构赋值而不带声明时,在赋值语句周围必须添加括号 ( ... )。

{ a, b } = { a: 1, b: 2 } 不是有效的独立语法,因为左侧的 {a, b} 被视为块而不是对象字面量。但是,({ a, b } = { a: 1, b: 2 }) 是有效的,const { a, b } = { a: 1, b: 2 } 也是有效的。

如果你的编码风格不包括尾随分号,则 ( ... ) 表达式前面需要有一个分号,否则它可能用于执行前一行的函数。

请注意,上述代码在等效的绑定模式中不是有效的语法:

1
2
3
4
5
6
7
8
const numbers = [];
const obj = { a: 1, b: 2 };
const { a: numbers[0], b: numbers[1] } = obj;

// This is equivalent to:
// const numbers[0] = obj.a;
// const numbers[1] = obj.b;
// Which definitely is not valid.

默认值

每个解构属性都可以有一个默认值。当属性不存在或值为 undefined 时,将使用默认值。如果属性的值为 null,则不使用它。

1
2
3
const [a = 1] = []; // a is 1
const { b = 2 } = { b: undefined }; // b is 2
const { c = 2 } = { c: null }; // c is null

默认值可以是任何表达式。仅在必要时对其进行评估。

1
2
3
const { b = console.log("hey") } = { b: 2 };
// Does not log anything, because `b` is defined and there's no need
// to evaluate the default value.

剩余属性

你可以使用剩余属性(...rest)结束解构模式。此模式会将对象或数组的所有剩余属性存储到新的对象或数组中。

1
2
3
4
5
const { a, ...others } = { a: 1, b: 2, c: 3 };
console.log(others); // { b: 2, c: 3 }

const [first, ...others2] = [1, 2, 3];
console.log(others2); // [2, 3]

剩余属性必须是模式中的最后一个,并且不能有尾随逗号。

js

1
2
3
4
const [a, ...b,] = [1, 2, 3];

// SyntaxError: rest element may not have a trailing comma
// Always consider using rest operator as the last element

使用其他语法解构模式

在许多语法中,语言为你绑定变量,你也可以使用解构模式。其中包括:

有关特定于数组或对象解构的功能,请参阅下面的各个示例。

示例

解构数组

基本变量赋值

1
2
3
4
5
6
const foo = ["one", "two", "three"];

const [red, yellow, green] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // "three"

解构比源更多的元素

在从赋值语句右侧指定的长度为 N 的数组解构的数组中,如果赋值语句左侧指定的变量数量大于 N,则只有前 N 个变量被赋值。其余变量的值将是未定义。

1
2
3
4
5
6
7
const foo = ["one", "two"];

const [red, yellow, green, blue] = foo;
console.log(red); // "one"
console.log(yellow); // "two"
console.log(green); // undefined
console.log(blue); //undefined

交换变量

可以在一个解构表达式中交换两个变量的值。

没有解构赋值的情况下,交换两个变量需要一个临时变量(或者用低级语言中的异或交换技巧)。

1
2
3
4
5
6
7
8
9
10
let a = 1;
let b = 3;

[a, b] = [b, a];
console.log(a); // 3
console.log(b); // 1

const arr = [1, 2, 3];
[arr[2], arr[1]] = [arr[1], arr[2]];
console.log(arr); // [1, 3, 2]

解析一个从函数返回的数组

从一个函数返回一个数组是十分常见的情况。解构使得处理返回值为数组时更加方便。

在下面例子中,要让 f() 返回值 [1, 2] 作为其输出,可以使用解构在一行内完成解析。

1
2
3
4
5
6
7
function f() {
return [1, 2];
}

const [a, b] = f();
console.log(a); // 1
console.log(b); // 2

忽略某些返回值

你可以忽略你不感兴趣的返回值:

1
2
3
4
5
6
7
8
9
10
function f() {
return [1, 2, 3];
}

const [a, , b] = f();
console.log(a); // 1
console.log(b); // 3

const [c] = f();
console.log(c); // 1

你也可以忽略全部返回值:

1
[, ,] = f();

使用绑定模式作为剩余属性

数组解构赋值的剩余属性可以是另一个数组或对象绑定模式。这允许你同时提取数组的属性和索引。

1
2
3
const [a, b, ...{ pop, push }] = [1, 2];
console.log(a, b); // 1 2
console.log(pop, push); // [Function pop] [Function push]
1
2
const [a, b, ...[c, d]] = [1, 2, 3, 4];
console.log(a, b, c, d); // 1 2 3 4

这些绑定模式甚至可以嵌套,只要每个剩余属性都在列表的最后。

1
2
const [a, b, ...[c, d, ...[e, f]]] = [1, 2, 3, 4, 5, 6];
console.log(a, b, c, d, e, f); // 1 2 3 4 5 6

另一方面,对象解构只能有一个标识符作为剩余属性。

1
2
3
4
5
6
const { a, ...{ b } } = { a: 1, b: 2 };
// SyntaxError: `...` must be followed by an identifier in declaration contexts

let a, b;
({ a, ...{ b } } = { a: 1, b: 2 });
// SyntaxError: `...` must be followed by an assignable reference in assignment contexts

从正则表达式匹配项中提取值

当正则表达式的 exec() 方法找到匹配项时,它将返回一个数组,该数组首先包含字符串的整个匹配部分,然后返回与正则表达式中每个括号组匹配的字符串部分。解构赋值允许你轻易地提取出需要的部分,如果不需要,则忽略完整匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function parseProtocol(url) {
const parsedURL = /^(\w+):\/\/([^/]+)\/(.*)$/.exec(url);
if (!parsedURL) {
return false;
}
console.log(parsedURL);
// ["https://developer.mozilla.org/zh-CN/docs/Web/JavaScript",
// "https", "developer.mozilla.org", "zh-CN/docs/Web/JavaScript"]

const [, protocol, fullhost, fullpath] = parsedURL;
return protocol;
}

console.log(
parseProtocol("https://developer.mozilla.org/zh-CN/docs/Web/JavaScript"),
);
// "https"

在任何可迭代对象上使用数组解构

数组解构调用右侧的迭代协议。因此,任何可迭代对象(不一定是数组)都可以解构。

1
2
3
4
5
const [a, b] = new Map([
[1, 2],
[3, 4],
]);
console.log(a, b); // [1, 2] [3, 4]

不可迭代对象不能解构为数组。

1
2
3
const obj = { 0: "a", 1: "b", length: 2 };
const [a, b] = obj;
// TypeError: obj is not iterable

只有在分配所有绑定之前,才会迭代可迭代对象。

1
2
3
4
5
6
7
8
9
const obj = {
*[Symbol.iterator]() {
for (const v of [0, 1, 2, 3]) {
console.log(v);
yield v;
}
},
};
const [a, b] = obj; // Only logs 0 and 1

其余的绑定会提前求值并创建一个新数组,而不是使用旧的迭代器。

1
2
3
4
5
6
7
8
9
10
const obj = {
*[Symbol.iterator]() {
for (const v of [0, 1, 2, 3]) {
console.log(v);
yield v;
}
},
};
const [a, b, ...rest] = obj; // Logs 0 1 2 3
console.log(rest); // [2, 3] (an array)

解构对象

基本赋值

1
2
3
4
5
6
7
8
9
const user = {
id: 42,
isVerified: true,
};

const { id, isVerified } = user;

console.log(id); // 42
console.log(isVerified); // true

赋值给新的变量名

可以从对象中提取属性,并将其赋值给名称与对象属性不同的变量。

1
2
3
4
5
const o = { p: 42, q: true };
const { p: foo, q: bar } = o;

console.log(foo); // 42
console.log(bar); // true

举个例子,const { p: foo } = o 从对象 o 中获取名为 p 的属性,并将其赋值给名为 foo 的局部变量。

赋值到新的变量名并提供默认值

一个属性可以同时是两者:

  • 从对象提取并分配给具有不同名称的变量。
  • 指定一个默认值,以防获取的值为 undefined。
1
2
3
4
const { a: aa = 10, b: bb = 5 } = { a: 3 };

console.log(aa); // 3
console.log(bb); // 5

从作为函数参数传递的对象中提取属性

传递给函数参数的对象也可以提取到变量中,然后可以在函数体内访问这些变量。至于对象赋值,解构语法允许新变量具有与原始属性相同或不同的名称,并为原始对象未定义属性的情况分配默认值。

请考虑此对象,其中包含有关用户的信息。

1
2
3
4
5
6
7
8
const user = {
id: 42,
displayName: "jdoe",
fullName: {
firstName: "Jane",
lastName: "Doe",
},
};

在这里,我们展示了如何将传递对象的属性提取到具有相同名称的变量。参数值 { id } 表示传递给函数的对象的 id 属性应该被提取到一个同名变量中,然后可以在函数中使用。

1
2
3
4
5
function userId({ id }) {
return id;
}

console.log(userId(user)); // 42

你可以定义提取变量的名称。在这里,我们提取名为 displayName 的属性,并将其重命名为 dname,以便在函数体内使用。

1
2
3
4
5
function userDisplayName({ displayName: dname }) {
return dname;
}

console.log(userDisplayName(user)); // `jdoe`

嵌套对象也可以提取。下面的示例展示了属性 fullname.firstName 被提取到名为 name 的变量中。

1
2
3
4
5
function whois({ displayName, fullName: { firstName: name } }) {
return `${displayName} is ${name}`;
}

console.log(whois(user)); // "jdoe is Jane"

设置函数参数的默认值

默认值可以使用 = 指定,如果指定的属性在传递的对象中不存在,则将其用作变量值。

下面我们展示了一个默认大小为 big的函数,默认坐标为 x: 0, y: 0,默认半径为 25。

1
2
3
4
5
6
7
8
9
10
11
12
13
function drawChart({
size = "big",
coords = { x: 0, y: 0 },
radius = 25,
} = {}) {
console.log(size, coords, radius);
// do some chart drawing
}

drawChart({
coords: { x: 18, y: 30 },
radius: 30,
});

在上面 drawChart 的函数签名中,解构的左侧具有空对象 = {} 的默认值。

你也可以在没有该默认值的情况下编写该函数。但是,如果你省略该默认值,该函数将在调用时寻找至少一个参数来提供,而在当前形式下,你可以在不提供任何参数的情况下调用 drawChart()。否则,你至少需要提供一个空对象字面量。

有关详细信息,请参阅默认参数值 > 有默认值的解构参数。

解构嵌套对象和数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const metadata = {
title: "Scratchpad",
translations: [
{
locale: "de",
localization_tags: [],
last_edit: "2014-04-14T08:43:37",
url: "/de/docs/Tools/Scratchpad",
title: "JavaScript-Umgebung",
},
],
url: "/zh-CN/docs/Tools/Scratchpad",
};

let {
title: englishTitle, // rename
translations: [
{
title: localeTitle, // rename
},
],
} = metadata;

console.log(englishTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"

For of 迭代和解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const people = [
{
name: "Mike Smith",
family: {
mother: "Jane Smith",
father: "Harry Smith",
sister: "Samantha Smith",
},
age: 35,
},
{
name: "Tom Jones",
family: {
mother: "Norah Jones",
father: "Richard Jones",
brother: "Howard Jones",
},
age: 25,
},
];

for (const {
name: n,
family: { father: f },
} of people) {
console.log(`Name: ${n}, Father: ${f}`);
}

// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"

对象属性计算名和解构

计算属性名,如对象字面量,可以被解构。

1
2
3
4
const key = "z";
const { [key]: foo } = { z: "bar" };

console.log(foo); // "bar"

无效的 JavaScript 标识符作为属性名称

通过提供有效的替代标识符,解构可以与不是有效的 JavaScript 标识符的属性名称一起使用。

1
2
3
4
const foo = { "fizz-buzz": true };
const { "fizz-buzz": fizzBuzz } = foo;

console.log(fizzBuzz); // true

解构基本类型

对象解构几乎等同于属性访问。这意味着,如果尝试解构基本类型的值,该值将被包装到相应的包装器对象中,并且在包装器对象上访问该属性。

1
2
const { a, toFixed } = 1;
console.log(a, toFixed); // undefined ƒ toFixed() { [native code] }

与访问属性相同,解构 null 或 undefined 会抛出 TypeError。

1
2
const { a } = undefined; // TypeError: Cannot destructure property 'a' of 'undefined' as it is undefined.
const { a } = null; // TypeError: Cannot destructure property 'b' of 'null' as it is null.

即使模式为空,也会发生这种情况。

1
const {} = null; // TypeError: Cannot destructure 'null' as it is null.

组合数组和对象解构

数组和对象解构可以组合使用。假设你想要下面 props 数组中的第三个元素,然后你想要对象中的 name 属性,你可以执行以下操作:

1
2
3
4
5
6
7
8
9
const props = [
{ id: 1, name: "Fizz" },
{ id: 2, name: "Buzz" },
{ id: 3, name: "FizzBuzz" },
];

const [, , { name }] = props;

console.log(name); // "FizzBuzz"

解构对象时查找原型链

当解构一个对象时,如果属性本身没有被访问,它将沿着原型链继续查找。

1
2
3
4
5
6
7
8
9
const obj = {
self: "123",
__proto__: {
prot: "456",
},
};
const { self, prot } = obj;
// self "123"
// prot "456" (Access to the prototype chain)

规范

Specification
ECMAScript Language Specification # sec-destructuring-assignment
ECMAScript Language Specification # sec-destructuring-binding-patterns

模板字符串

模板字面量是用反引号(```)分隔的字面量,允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。

模板字面量有时被非正式地叫作模板字符串,因为它们最常被用作字符串插值(通过替换占位符来创建字符串)。然而,带标签的模板字面量可能不会产生字符串——它可以与自定义标签函数一起使用,来对模板字面量的不同部分执行任何操作。

语法

1
2
3
4
5
6
7
8
`string text`

`string text line 1
string text line 2`

`string text ${expression} string text`

tagFunction`string text ${expression} string text`

参数

  • string text

    ​ 将成为模板字面量的一部分的字符串文本。几乎允许所有字符,包括换行符和其他空白字符。但是,除非使用了标签函数,否则无效的转义序列将导致语法错误。

  • expression

    ​ 要插入当前位置的表达式,其值被转换为字符串或传递给 tagFunction。

  • tagFunction

    ​ 如果指定,将使用模板字符串数组和替换表达式调用它,返回值将成为模板字面量的值。见带标签的模板。

描述

模板字面量用反引号(```)括起来,而不是双引号(")或单引号(')。 除了普通字符串外,模板字面量还可以包含*占位符*——一种由美元符号和大括号分隔的嵌入式表达式:${expression}。字符串和占位符被传递给一个函数(要么是默认函数,要么是自定义函数)。默认函数(当未提供自定义函数时)只执行字符串插值来替换占位符,然后将这些部分拼接到一个字符串中。

若要提供自定义函数,需在模板字面量之前加上函数名(结果被称为带标签的模板)。此时,模板字面量被传递给你的标签函数,然后就可以在那里对模板文本的不同部分执行任何操作。

若要转义模板字面量中的反引号(```),需在反引号之前加一个反斜杠(\)。

1
`\`` === "`"; // true

美元符号 $ 也可以被转义,来阻止插值。

1
`\${1}` === "${1}"; // true

多行字符串

在源码中插入的任何换行符都是模板字面量的一部分。

使用普通字符串,可以通过下面的方式得到多行字符串:

1
2
3
console.log("string text line 1\n" + "string text line 2");
// "string text line 1
// string text line 2"

使用模板字面量,下面的代码同样可以做到:

1
2
3
4
console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"

字符串插值

如果没有模板字面量,当你想组合表达式的输出与字符串时,可以使用加法运算符 + 连接它们:

1
2
3
4
5
const a = 5;
const b = 10;
console.log("Fifteen is " + (a + b) + " and\nnot " + (2 * a + b) + ".");
// "Fifteen is 15 and
// not 20."

这可能很难阅读,尤其是当存在多个表达式时。

有了模板字面量,就可以通过使用占位符 ${expression} 嵌入待替换的表达式,从而避免串联运算符,并提高代码的可读性:

1
2
3
4
5
6
const a = 5;
const b = 10;
console.log(`Fifteen is ${a + b} and
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."

注意,这两种语法有一点小区别:模板字面量直接将其表达式强制转换为字符串,而加法则会先强制转换为原语类型。更多相关信息,参见加法(+)运算符。

嵌套模板

在某些情况下,嵌套模板是具有可配置字符串的最简单的(也许还是更可读的)方法。在反引号分隔的模板中,允许在占位符 ${expression} 中使用内层的反引号。

例如,不用模板字面量的情况下,如果你想根据特定条件返回某个值,可以执行以下操作:

1
2
3
4
5
6
let classes = "header";
classes += isLargeScreen()
? ""
: item.isCollapsed
? " icon-expander"
: " icon-collapser";

用模板字面量但不嵌套时,你可以这么做:

1
2
3
const classes = `header ${
isLargeScreen() ? "" : item.isCollapsed ? "icon-expander" : "icon-collapser"
}`;

用嵌套模板字面量时,你可以这么做:

1
2
3
const classes = `header ${
isLargeScreen() ? "" : `icon-${item.isCollapsed ? "expander" : "collapser"}`
}`;

带标签的模板

带标签的模板是模板字面量的一种更高级的形式,它允许你使用函数解析模板字面量。标签函数的第一个参数包含一个字符串数组,其余的参数与表达式相关。你可以用标签函数对这些参数执行任何操作,并返回被操作过的字符串(或者,也可返回完全不同的内容,见下面的示例)。用作标签的函数名没有限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const person = "Mike";
const age = 28;

function myTag(strings, personExp, ageExp) {
const str0 = strings[0]; // "That "
const str1 = strings[1]; // " is a "
const str2 = strings[2]; // "."

const ageStr = ageExp > 99 ? "centenarian" : "youngster";

// 我们甚至可以返回使用模板字面量构建的字符串
return `${str0}${personExp}${str1}${ageStr}${str2}`;
}

const output = myTag`That ${person} is a ${age}.`;

console.log(output);
// That Mike is a youngster.

标签不必是普通的标识符,你可以使用任何优先级大于 16 的表达式,包括属性访问、函数调用、new 表达式,甚至其他带标签的模板字面量。

1
2
3
4
5
6
7
8
9
10
11
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。

1
console.log(`Hello``World`); // TypeError: "Hello" is not a function

唯一的例外是可选链,这将抛出语法错误。

1
2
console.log?.`Hello`; // SyntaxError: Invalid tagged template on optional chain
console?.log`Hello`; // SyntaxError: Invalid tagged template on optional chain

请注意,这两个表达式仍然是可解析的。这意味着它们将不受自动分号补全的影响,其只会插入分号来修复无法解析的代码。

1
2
3
// 仍是语法错误
const a = console?.log
`Hello`

标签函数甚至不需要返回字符串!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function template(strings, ...keys) {
return (...values) => {
const dict = values[values.length - 1] || {};
const result = [strings[0]];
keys.forEach((key, i) => {
const value = Number.isInteger(key) ? values[key] : dict[key];
result.push(value, strings[i + 1]);
});
return result.join("");
};
}

const t1Closure = template`${0}${1}${0}!`;
// const t1Closure = template(["","","","!"],0,1,0);
t1Closure("Y", "A"); // "YAY!"

const t2Closure = template`${0} ${"foo"}!`;
// const t2Closure = template([""," ","!"],0,"foo");
t2Closure("Hello", { foo: "World" }); // "Hello World!"

const t3Closure = template`I'm ${"name"}. I'm almost ${"age"} years old.`;
// const t3Closure = template(["I'm ", ". I'm almost ", " years old."], "name", "age");
t3Closure("foo", { name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."
t3Closure({ name: "MDN", age: 30 }); // "I'm MDN. I'm almost 30 years old."

标签函数接收到的第一个参数是一个字符串数组。对于任何模板字面量,其长度等于替换次数(${…} 出现次数)加一,因此总是非空的。对于任何特定的带标签的模板字面量表达式,无论对字面量求值多少次,都将始终使用完全相同的字面量数组调用标签函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const callHistory = [];

function tag(strings, ...values) {
callHistory.push(strings);
// Return a freshly made object
return {};
}

function evaluateLiteral() {
return tag`Hello, ${"world"}!`;
}

console.log(evaluateLiteral() === evaluateLiteral()); // false; each time `tag` is called, it returns a new object
console.log(callHistory[0] === callHistory[1]); // true; all evaluations of the same tagged literal would pass in the same strings array

这允许标签函数以其第一个参数作为标识来缓存结果。为了进一步确保数组值不变,第一个参数及其 raw 属性都会被冻结,因此你将无法改变它们。

原始字符串

在标签函数的第一个参数中,存在一个特殊的属性 raw ,我们可以通过它来访问模板字符串的原始字符串,而无需转义特殊字符。

1
2
3
4
5
6
7
function tag(strings) {
console.log(strings.raw[0]);
}

tag`string text line 1 \n string text line 2`;
// logs "string text line 1 \n string text line 2" ,
// including the two characters '\' and 'n'

另外,使用 String.raw() 方法创建原始字符串和使用默认模板函数和字符串连接创建是一样的。

1
2
3
4
5
6
7
8
let str = String.raw`Hi\n${2+3}!`;
// "Hi\\n5!"

str.length;.
// 6

str.split('').join(',');.
// "H,i,\\,n,5,!"

如果字面量不包含任何转义序列,String.raw 函数就像一个“identity”标签。如果你想要一个始终像不带标签的字面量那样的实际标识标签,可以用自定义函数,将“cooked”(例如,经转义序列处理过的)字面量数组传递给 String.raw,将它们当成原始字符串。

1
2
3
4
5
const identity = (strings, ...values) =>
String.raw({ raw: strings }, ...values);
console.log(identity`Hi\n${2 + 3}!`);
// Hi
// 5!

这对于许多工具来说很有用,它们要对以特定名称为标签的字面量作特殊处理。

1
2
3
4
5
6
7
8
9
10
11
const html = (strings, ...values) => String.raw({ raw: strings }, ...values);
// 一些格式化程序会将此字面量的内容格式化为 HTML
const doc = html`<!doctype html>
<html lang="en-US">
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>`;

带标签的模板字面量及转义序列

在普通模板字面量中,字符串字面量中的转义序列都是允许的,任何其他格式不正确的转义序列都是语法错误,包括:

  • \ 后跟 0 以外的任何十进制数字,或 \0 后跟一个十进制数字,例如 \9 和 \07(这是一种已弃用的语法)
  • \x 后跟两位以下十六进制数字,例如\xz
  • \u 后不跟 {,并且后跟四个以下十六进制数字,例如 \uz
  • \u{} 包含无效的 Unicode 码点——包含一个非十六进制数字,或者它的值大于 10FFFF,例如 \u{110000} 和 \u{z}

备注: \ 后面跟着其他字符,虽然它们可能没有用,因为没有转义,但它们不是语法错误。

然而,这对于带标签的模板来说是有问题的,除了“cooked”字面量外,这些模板还可以访问原始字面量(转义序列按原样保留)。带标签的模板应该允许嵌入语言(例如 DSL 或 LaTeX),在这些语言里其他转义序列是常见的。因此,从带标签的模板中删除了转义序列诸多格式的语法限制。

不过,非法转义序列在“cooked”当中仍然会体现出来。它们将以 undefined 元素的形式存在于“cooked”数组之中:

1
2
3
4
5
6
7
function latex(str) {
return { cooked: str[0], raw: str.raw[0] };
}

latex`\unicode`;

// { cooked: undefined, raw: "\\unicode" }

值得注意的是,这一转义序列限制只对带标签的模板字面量移除,而不包括不带标签的模板字面量:

1
const bad = `bad escape sequence: \unicode`;

规范

Specification
ECMAScript Language Specification # sec-template-literals

条件(三元)运算符

条件(三元)运算符是 JavaScript 唯一使用三个操作数的运算符:一个条件后跟一个问号(?),如果条件为真值,则执行冒号(:)前的表达式;若条件为假值,则执行最后的表达式。该运算符经常当作 if...else 语句的简捷形式来使用。

语法

1
condition ? exprIfTrue : exprIfFalse

参数

  • condition

    ​ 计算结果用作条件的表达式。

  • exprIfTrue

    ​ 如果 condition 的计算结果为真值(等于或可以转换为 true 的值),则执行该表达式。

  • exprIfFalse

    ​ 如果 condition 为假值(等于或可以转换为 false 的值)时执行的表达式。

描述

除了 false,可能的假值表达式还有:null、NaN、0、空字符串("")和 undefined。如果 condition 是其中任何一个,那么条件表达式的结果就是 exprIfFalse 表达式执行的结果。

示例

简单的例子

1
2
3
const age = 26;
const beverage = age >= 21 ? "Beer" : "Juice";
console.log(beverage); // "Beer"

处理 null 值

一个常见的用法是处理可能为 null 的值:

1
2
3
4
5
6
7
const greeting = (person) => {
const name = person ? person.name : "stranger";
return `Howdy, ${name}`;
};

console.log(greeting({ name: "Alice" })); // "Howdy, Alice"
console.log(greeting(null)); // "Howdy, stranger"

条件链

三元运算符是右结合的,这意味着它可以按以下方式“链接”起来,类似于 if … else if … else if … else 链:

1
2
3
4
5
6
7
8
9
function example() {
return condition1
? value1
: condition2
? value2
: condition3
? value3
: value4;
}

这等价于以下 if...else 链。

1
2
3
4
5
6
7
8
9
10
11
function example() {
if (condition1) {
return value1;
} else if (condition2) {
return value2;
} else if (condition3) {
return value3;
} else {
return value4;
}
}

规范

Specification
ECMAScript Language Specification # sec-conditional-operator

JavaScript 模块

这篇指南会给你入门 JavaScript 模块的全部信息。

模块化的背景

JavaScript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 JavaScript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。

因此,近年来,有必要开始考虑提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。Node.js 已经提供这个能力很长时间了,还有很多的 JavaScript 库和框架已经开始了模块的使用(例如,CommonJS 和基于 AMD 的其他模块系统 如 RequireJS,以及最新的 Webpack 和 Babel)。

好消息是,最新的浏览器开始原生支持模块功能了,这是本文要重点讲述的。这会是一个好事情 —- 浏览器能够最优化加载模块,使它比使用库更有效率:使用库通常需要做额外的客户端处理。

评论COMMENTS