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 | function square(number) { |
函数 square
接收一个名为 number
的参数。这个函数只有一个语句,其表示该函数将函数的参数(即 number
)自乘后返回。函数的 return
语句指定了函数的返回值:number * number
。
参数本质上是按值传递给函数的——因此,即使函数体的代码为传递给函数的参数赋了新值,这个改变也不会反映到全局或调用该函数的代码中。
如果你将对象作为参数传递,而函数改变了这个对象的属性,这样的改变对函数外部是可见的,如下面的例子所示:
1 | function myFunc(theObject) { |
如果你将数组作为参数传递,而函数改变了这个数组的值,这样的改变对函数外部也同样可见,如下面的例子所示:
1 | function myFunc(theArr) { |
函数表达式
虽然上面的函数声明在语法上是一个语句,但函数也可以由函数表达式创建。
这样的函数可以是匿名的;它不必有一个名称。例如,函数 square
也可这样来定义:
1 | const square = function (number) { |
然而,也可以为函数表达式提供名称,并且可以用于在函数内部代指其本身,或者在调试器堆栈跟踪中识别该函数:
1 | const factorial = function fac(n) { |
当将函数作为参数传递给另一个函数时,函数表达式很方便。下面的例子演示了一个叫 map
的函数,该函数接收函数作为第一个参数,接收数组作为第二个参数:
1 | function map(f, a) { |
在以下代码中,该函数接收由函数表达式定义的函数,并对作为第二个参数接收的数组的每个元素执行该函数:
1 | function map(f, a) { |
在 JavaScript 中,可以根据条件来定义一个函数。比如下面的代码,当 num
等于 0
的时候才会定义 myFunc
:
1 | let myFunc; |
除了上述的定义函数方法外,你也可以在运行时用 Function
构造函数从一个字符串创建一个函数,很像 eval()
函数。
以上均摘自 我很喜欢的教学网站 MDN。
箭头函数
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this
,arguments
,super
或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
基础语法
1 | (param1, param2, …, paramN) => { statements } |
高级语法
1 | //加括号的函数体返回对象字面量表达式: |
没有单独的this
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的 this 值:
- 如果该函数是一个构造函数,this 指针指向一个新的对象
- 在严格模式下的函数调用下,this 指向
undefined
- 如果该函数是一个对象的方法,则它的 this 指针指向这个对象
- 等等
This
被证明是令人厌烦的面向对象风格的编程。
1 | function Person() { |
在 ECMAScript 3/5 中,通过将this
值分配给封闭的变量,可以解决this
问题。
1 | function Person() { |
或者,可以创建绑定函数,以便将预先分配的this
值传递到绑定的目标函数(上述示例中的growUp()
函数)。
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承 this
。因此,在下面的代码中,传递给setInterval
的函数内的this
与封闭函数中的this
值相同:
1 | function Person() { |
与严格模式的关系
鉴于 this
是词法层面上的,严格模式中与 this
相关的规则都将被忽略。
1 | var f = () => { 'use strict'; return this; }; |
严格模式的其他规则依然不变。
通过 call 或 apply 调用
由于 箭头函数没有自己的 this 指针,通过 call()
或 apply()
方法调用一个函数时,只能传递参数(不能绑定 this—译者注),他们的第一个参数会被忽略。(这种现象对于 bind 方法同样成立 — 译者注)
1 | var adder = { |
不绑定arguments
箭头函数不绑定Arguments 对象。因此,在本示例中,arguments
只是引用了封闭作用域内的 arguments:
1 | var arguments = [1, 2, 3]; |
在大多数情况下,使用剩余参数是相较使用arguments
对象的更好选择。
1 | function foo(arg) { |
使用箭头函数作为方法
如上所述,箭头函数表达式对非方法函数是最合适的。让我们看看当我们试着把它们作为方法时发生了什么。
1 | ; |
箭头函数没有定义 this 绑定。另一个涉及Object.defineProperty()
的示例:
1 | ; |
使用 new
操作符
箭头函数不能用作构造器,和 new
一起用会抛出错误。
1 | var Foo = () => {}; |
使用prototype
属性
箭头函数没有prototype
属性。
1 | var Foo = () => {}; |
使用 yield
关键字
yield
关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。
函数体
箭头函数可以有一个“简写体”或常见的“块体”。
在一个简写体中,只需要一个表达式,并附加一个隐式的返回值。在块体中,必须使用明确的return
语句。
1 | var func = (x) => x * x; |
返回对象字面量
记住用params => {object:literal}
这种简单的语法返回对象字面量是行不通的。
1 | var func = () => { foo: 1 }; |
这是因为花括号({}
)里面的代码被解析为一系列语句(即 foo
被认为是一个标签,而非对象字面量的组成部分)。
所以,记得用圆括号把对象字面量包起来:
1 | var func = () => ({ foo: 1 }); |
换行
箭头函数在参数和箭头之间不能换行。
1 | var func = () |
但是,可以通过在‘=>’之后换行,或者用‘( )’、’{ }’来实现换行,如下:
1 | var func = (a, b, c) => 1; |
解析顺序
虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。
1 | let callback; |
更多相关内容请前往 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 中用于操作对象的工具方法都是静态的。更具体地说:
valueOf()
、toString()
和toLocaleString()
存在的目的是为了多态化,你应该期望对象会定义自己的实现并具有合理的行为,因此你可以将它们作为实例方法调用。但是,valueOf()
和toString()
通常是通过强制类型转换隐式调用的,因此你不需要在代码中自己调用它们。__defineGetter__()
、__defineSetter__()
、__lookupGetter__()
和__lookupSetter__()
已被弃用,不应该再使用。请使用静态方法Object.defineProperty()
和Object.getOwnPropertyDescriptor()
作为替代。__proto__
属性已被弃用,不应该再使用。请使用静态方法Object.getPrototypeOf()
和Object.setPrototypeOf()
作为替代。propertyIsEnumerable()
和hasOwnProperty()
方法可以分别用静态方法Object.getOwnPropertyDescriptor()
和Object.hasOwn()
替换。- 如果你正在检查一个构造函数的
prototype
属性,通常可以用instanceof
代替isPrototypeOf()
方法。
如果不存在语义上等价的静态方法,或者你真的想使用 Object.prototype
方法,你应该通过 call()
直接在目标对象上调用 Object.prototype
方法,以防止因目标对象上原有方法被重写而产生意外的结果。
1 | const obj = { |
从对象中删除属性
一个对象本身没有任何方法可以(像 Map.prototype.delete()
一样)删除自己的属性。要删除一个对象的属性,必须使用 delete 运算符。
null 原型对象
几乎所有的 JavaScript 对象最终都继承自 Object.prototype
(参见继承和原型链)。然而,你可以使用 Object.create(null)
或定义了 __proto__: null
的对象字面量语法(注意:对象字面量中的 __proto__
键不同于已弃用的 Object.prototype.__proto__
属性)来创建 null
原型对象。你还可以通过调用 Object.setPrototypeOf(obj, null)
将现有对象的原型更改为 null
。
1 | const obj = Object.create(null); |
null
原型对象可能会有一些预期外的行为表现,因为它不会从 Object.prototype
继承任何对象方法。这在调试时尤其需要注意,因为常见的对象属性转换/检测实用方法可能会产生错误或丢失信息(特别是在使用了忽略错误的静默错误捕获机制的情况下)。
例如,Object.prototype.toString()
方法的缺失通常会使得调试变得困难:
1 | const normalObj = {}; // 创建一个普通对象 |
其他方法也会失败。
1 | normalObj.valueOf(); // 显示 {} |
我们可以通过为 null
原型对象分配属性的方式将 toString
方法添加回去:
1 | nullProtoObj.toString = Object.prototype.toString; // 由于新对象缺少 `toString` 方法,因此需要将原始的通用 `toString` 方法添加回来。 |
普通对象的 toString()
方法是在对象的原型上的,而与普通对象不同的是,这里的 toString()
方法是 nullProtoObj
的自有属性。这是因为 nullProtoObj
没有原型(即为 null
)。
在实践中,null
原型对象通常被用作 map 的简单替代品。由于存在 Object.prototype
属性,会导致一些错误:
1 | const ages = { alice: 18, bob: 27 }; |
使用一个 null
原型对象可以消除这种风险,同时不会令 hasPerson
和 getAge
函数变得复杂:
1 | const ages = Object.create(null, { |
在这种情况下,添加任何方法都应该慎重,因为它们可能会与存储为数据的其他键值对混淆。
让你的对象不继承自 Object.prototype
还可以防止原型污染攻击。如果恶意脚本向 Object.prototype
添加一个属性,程序中的每个对象上都可访问它,除了那些原型为 null
的对象。
1 | const user = {}; |
JavaScript 还具有内置的 API,用于生成 null
原型对象,特别是那些将对象用作临时键值对集合的 API。例如:
Object.groupBy()
方法的返回值RegExp.prototype.exec()
方法返回结果中的groups
和indices.groups
属性Array.prototype[@@unscopables]
属性(所有@@unscopables
对象原型都应该为null
)import.meta
对象- 通过
import * as ns from "module"
或import()
(en-US) 获取的模块命名空间对象
“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
方法这样的自定义代码。
构造函数
-
将输入转换为一个对象。
静态方法
-
将一个或多个源对象的所有可枚举自有属性的值复制到目标对象中。
-
使用指定的原型对象和属性创建一个新对象。
-
向对象添加多个由给定描述符描述的命名属性。
-
向对象添加一个由给定描述符描述的命名属性。
-
返回包含给定对象自有可枚举字符串属性的所有
[key, value]
数组。 -
冻结一个对象。其他代码不能删除或更改其任何属性。
-
从一个包含
[key, value]
对的可迭代对象中返回一个新的对象(Object.entries
的反操作)。 Object.getOwnPropertyDescriptor()
返回一个对象的已命名属性的属性描述符。
Object.getOwnPropertyDescriptors()
返回一个包含对象所有自有属性的属性描述符的对象。
-
返回一个包含给定对象的所有自有可枚举和不可枚举属性名称的数组。
Object.getOwnPropertySymbols()
返回一个数组,它包含了指定对象所有自有 symbol 属性。
-
返回指定对象的原型(内部的
[[Prototype]]
属性)。 -
如果指定属性是指定对象的自有属性,则返回
true
,否则返回false
。如果该属性是继承的或不存在,则返回false
。 -
比较两个值是否相同。所有
NaN
值都相等(这与==
使用的IsLooselyEqual
和===
使用的IsStrictlyEqual
不同)。 -
判断对象是否可扩展。
-
判断对象是否已经冻结。
-
判断对象是否已经封闭。
-
返回一个包含所有给定对象自有可枚举字符串属性名称的数组。
-
防止对象的任何扩展。
-
防止其他代码删除对象的属性。
-
设置对象的原型(即内部
[[Prototype]]
属性)。 -
返回包含给定对象所有自有可枚举字符串属性的值的数组。
实例属性
这些属性在 Object.prototype
上定义,被所有 Object
实例所共享。
Object.prototype.__proto__
已弃用 指向实例对象在实例化时使用的原型对象。
-
创建该实例对象的构造函数。对于普通的
Object
实例,初始值为Object
构造函数。其它构造函数的实例都会从它们各自的Constructor.prototype
对象中继承constructor
属性。
实例方法
Object.prototype.__defineGetter__()
已弃用 将一个属性与一个函数相关联,当该属性被访问时,执行该函数,并且返回函数的返回值。
Object.prototype.__defineSetter__()
已弃用 将一个属性与一个函数相关联,当该属性被设置时,执行该函数,执行该函数去修改某个属性。
Object.prototype.__lookupGetter__()
已弃用 返回绑定在指定属性上的 getter 函数。
Object.prototype.__lookupSetter__()
已弃用 返回绑定在指定属性上的 setter 函数。
Object.prototype.hasOwnProperty()
返回一个布尔值,用于表示一个对象自身是否包含指定的属性,该方法并不会查找原型链上继承来的属性。
Object.prototype.isPrototypeOf()
返回一个布尔值,用于表示该方法所调用的对象是否在指定对象的原型链中。
Object.prototype.propertyIsEnumerable()
返回一个布尔值,指示指定属性是否是对象的可枚举自有属性。
Object.prototype.toLocaleString()
调用
toString()
方法。-
返回一个代表该对象的字符串。
-
返回指定对象的基本类型值。
示例
构造空对象
以下示例使用带有不同参数的 new
关键字创建空对象:
1 | const o1 = new Object(); |
使用 Object
生成布尔对象
下面的例子将 Boolean
对象存到 o
中:
1 | // 等价于 const o = new Boolean(true); |
1 | // 等价于 const o = new Boolean(false); |
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 | const fruits = []; |
当在 JavaScript 数组上设置一个属性时,如果该属性是一个有效的数组索引并且该索引在数组的当前边界之外,引擎将相应地更新数组的 length
属性:
1 | fruits[5] = "mango"; |
增加 length
。
1 | fruits.length = 10; |
但是,减少 length
属性会删除元素。
1 | fruits.length = 2; |
这将在 Array/length
页中进一步解释
解构赋值
解构赋值语法是一种 Javascript 表达式。可以将数组中的值或对象的属性取出,赋值给其他变量。
语法
1 | const [a, b] = array; |
描述
对象和数组字面量表达式提供了一种简单的方法来创建特别的数据包。
1 | const x = [1, 2, 3, 4, 5]; |
解构赋值使用类似的语法,但在赋值的左侧定义了要从原变量中取出哪些值。
1 | const x = [1, 2, 3, 4, 5]; |
同样,你可以在赋值语句的左侧解构对象。
1 | const obj = { a: 1, b: 2 }; |
这种功能类似于 Perl 和 Python 等语言中存在的特性。
绑定与赋值
对于对象和数组的解构,有两种解构模式:绑定模式和赋值模式,它们的语法略有不同。
在绑定模式中,模式以声明关键字(var
、let
或 const
)开始。然后,每个单独的属性必须绑定到一个变量或进一步解构。
1 | const obj = { a: 1, b: { c: 2 } }; |
所有变量共享相同的声明,因此,如果你希望某些变量可重新分配,而其他变量是只读的,则可能需要解构两次——一次使用 let
,一次使用 const
。
1 | const obj = { a: 1, b: { c: 2 } }; |
在赋值模式中,模式不以关键字开头。每个解构属性都被赋值给一个赋值目标——这个赋值目标可以事先用 var
或 let
声明,也可以是另一个对象的属性——一般来说,可以是任何可以出现在赋值表达式左侧的东西。
1 | const numbers = []; |
备注: 当使用对象文字解构赋值而不带声明时,在赋值语句周围必须添加括号 ( ... )
。
{ a, b } = { a: 1, b: 2 }
不是有效的独立语法,因为左侧的 {a, b}
被视为块而不是对象字面量。但是,({ a, b } = { a: 1, b: 2 })
是有效的,const { a, b } = { a: 1, b: 2 }
也是有效的。
如果你的编码风格不包括尾随分号,则 ( ... )
表达式前面需要有一个分号,否则它可能用于执行前一行的函数。
请注意,上述代码在等效的绑定模式中不是有效的语法:
1 | const numbers = []; |
默认值
每个解构属性都可以有一个默认值。当属性不存在或值为 undefined
时,将使用默认值。如果属性的值为 null
,则不使用它。
1 | const [a = 1] = []; // a is 1 |
默认值可以是任何表达式。仅在必要时对其进行评估。
1 | const { b = console.log("hey") } = { b: 2 }; |
剩余属性
你可以使用剩余属性(...rest
)结束解构模式。此模式会将对象或数组的所有剩余属性存储到新的对象或数组中。
1 | const { a, ...others } = { a: 1, b: 2, c: 3 }; |
剩余属性必须是模式中的最后一个,并且不能有尾随逗号。
js
1 | const [a, ...b,] = [1, 2, 3]; |
使用其他语法解构模式
在许多语法中,语言为你绑定变量,你也可以使用解构模式。其中包括:
有关特定于数组或对象解构的功能,请参阅下面的各个示例。
示例
解构数组
基本变量赋值
1 | const foo = ["one", "two", "three"]; |
解构比源更多的元素
在从赋值语句右侧指定的长度为 N 的数组解构的数组中,如果赋值语句左侧指定的变量数量大于 N,则只有前 N 个变量被赋值。其余变量的值将是未定义。
1 | const foo = ["one", "two"]; |
交换变量
可以在一个解构表达式中交换两个变量的值。
没有解构赋值的情况下,交换两个变量需要一个临时变量(或者用低级语言中的异或交换技巧)。
1 | let a = 1; |
解析一个从函数返回的数组
从一个函数返回一个数组是十分常见的情况。解构使得处理返回值为数组时更加方便。
在下面例子中,要让 f()
返回值 [1, 2]
作为其输出,可以使用解构在一行内完成解析。
1 | function f() { |
忽略某些返回值
你可以忽略你不感兴趣的返回值:
1 | function f() { |
你也可以忽略全部返回值:
1 | [, ,] = f(); |
使用绑定模式作为剩余属性
数组解构赋值的剩余属性可以是另一个数组或对象绑定模式。这允许你同时提取数组的属性和索引。
1 | const [a, b, ...{ pop, push }] = [1, 2]; |
1 | const [a, b, ...[c, d]] = [1, 2, 3, 4]; |
这些绑定模式甚至可以嵌套,只要每个剩余属性都在列表的最后。
1 | const [a, b, ...[c, d, ...[e, f]]] = [1, 2, 3, 4, 5, 6]; |
另一方面,对象解构只能有一个标识符作为剩余属性。
1 | const { a, ...{ b } } = { a: 1, b: 2 }; |
从正则表达式匹配项中提取值
当正则表达式的 exec()
方法找到匹配项时,它将返回一个数组,该数组首先包含字符串的整个匹配部分,然后返回与正则表达式中每个括号组匹配的字符串部分。解构赋值允许你轻易地提取出需要的部分,如果不需要,则忽略完整匹配。
1 | function parseProtocol(url) { |
在任何可迭代对象上使用数组解构
数组解构调用右侧的迭代协议。因此,任何可迭代对象(不一定是数组)都可以解构。
1 | const [a, b] = new Map([ |
不可迭代对象不能解构为数组。
1 | const obj = { 0: "a", 1: "b", length: 2 }; |
只有在分配所有绑定之前,才会迭代可迭代对象。
1 | const obj = { |
其余的绑定会提前求值并创建一个新数组,而不是使用旧的迭代器。
1 | const obj = { |
解构对象
基本赋值
1 | const user = { |
赋值给新的变量名
可以从对象中提取属性,并将其赋值给名称与对象属性不同的变量。
1 | const o = { p: 42, q: true }; |
举个例子,const { p: foo } = o
从对象 o
中获取名为 p
的属性,并将其赋值给名为 foo
的局部变量。
赋值到新的变量名并提供默认值
一个属性可以同时是两者:
- 从对象提取并分配给具有不同名称的变量。
- 指定一个默认值,以防获取的值为
undefined
。
1 | const { a: aa = 10, b: bb = 5 } = { a: 3 }; |
从作为函数参数传递的对象中提取属性
传递给函数参数的对象也可以提取到变量中,然后可以在函数体内访问这些变量。至于对象赋值,解构语法允许新变量具有与原始属性相同或不同的名称,并为原始对象未定义属性的情况分配默认值。
请考虑此对象,其中包含有关用户的信息。
1 | const user = { |
在这里,我们展示了如何将传递对象的属性提取到具有相同名称的变量。参数值 { id }
表示传递给函数的对象的 id
属性应该被提取到一个同名变量中,然后可以在函数中使用。
1 | function userId({ id }) { |
你可以定义提取变量的名称。在这里,我们提取名为 displayName
的属性,并将其重命名为 dname
,以便在函数体内使用。
1 | function userDisplayName({ displayName: dname }) { |
嵌套对象也可以提取。下面的示例展示了属性 fullname.firstName
被提取到名为 name
的变量中。
1 | function whois({ displayName, fullName: { firstName: name } }) { |
设置函数参数的默认值
默认值可以使用 =
指定,如果指定的属性在传递的对象中不存在,则将其用作变量值。
下面我们展示了一个默认大小为 big
的函数,默认坐标为 x: 0, y: 0
,默认半径为 25。
1 | function drawChart({ |
在上面 drawChart
的函数签名中,解构的左侧具有空对象 = {}
的默认值。
你也可以在没有该默认值的情况下编写该函数。但是,如果你省略该默认值,该函数将在调用时寻找至少一个参数来提供,而在当前形式下,你可以在不提供任何参数的情况下调用 drawChart()
。否则,你至少需要提供一个空对象字面量。
有关详细信息,请参阅默认参数值 > 有默认值的解构参数。
解构嵌套对象和数组
1 | const metadata = { |
For of 迭代和解构
1 | const people = [ |
对象属性计算名和解构
计算属性名,如对象字面量,可以被解构。
1 | const key = "z"; |
无效的 JavaScript 标识符作为属性名称
通过提供有效的替代标识符,解构可以与不是有效的 JavaScript 标识符的属性名称一起使用。
1 | const foo = { "fizz-buzz": true }; |
解构基本类型
对象解构几乎等同于属性访问。这意味着,如果尝试解构基本类型的值,该值将被包装到相应的包装器对象中,并且在包装器对象上访问该属性。
1 | const { a, toFixed } = 1; |
与访问属性相同,解构 null
或 undefined
会抛出 TypeError
。
1 | const { a } = undefined; // TypeError: Cannot destructure property 'a' of 'undefined' as it is undefined. |
即使模式为空,也会发生这种情况。
1 | const {} = null; // TypeError: Cannot destructure 'null' as it is null. |
组合数组和对象解构
数组和对象解构可以组合使用。假设你想要下面 props
数组中的第三个元素,然后你想要对象中的 name
属性,你可以执行以下操作:
1 | const props = [ |
解构对象时查找原型链
当解构一个对象时,如果属性本身没有被访问,它将沿着原型链继续查找。
1 | const obj = { |
规范
| Specification | | ———————————————————— | | ECMAScript Language Specification # sec-destructuring-assignment | | ECMAScript Language Specification # sec-destructuring-binding-patterns |
模板字符串
模板字面量是用反引号(```)分隔的字面量,允许多行字符串、带嵌入表达式的字符串插值和一种叫带标签的模板的特殊结构。
模板字面量有时被非正式地叫作模板字符串,因为它们最常被用作字符串插值(通过替换占位符来创建字符串)。然而,带标签的模板字面量可能不会产生字符串——它可以与自定义标签函数一起使用,来对模板字面量的不同部分执行任何操作。
语法
1 | `string text` |
参数
string text
将成为模板字面量的一部分的字符串文本。几乎允许所有字符,包括换行符和其他空白字符。但是,除非使用了标签函数,否则无效的转义序列将导致语法错误。
expression
要插入当前位置的表达式,其值被转换为字符串或传递给
tagFunction
。tagFunction
如果指定,将使用模板字符串数组和替换表达式调用它,返回值将成为模板字面量的值。见带标签的模板。
描述
模板字面量用反引号(```)括起来,而不是双引号("
)或单引号('
)。 除了普通字符串外,模板字面量还可以包含*占位符*——一种由美元符号和大括号分隔的嵌入式表达式:${expression}
。字符串和占位符被传递给一个函数(要么是默认函数,要么是自定义函数)。默认函数(当未提供自定义函数时)只执行字符串插值来替换占位符,然后将这些部分拼接到一个字符串中。
若要提供自定义函数,需在模板字面量之前加上函数名(结果被称为带标签的模板)。此时,模板字面量被传递给你的标签函数,然后就可以在那里对模板文本的不同部分执行任何操作。
若要转义模板字面量中的反引号(```),需在反引号之前加一个反斜杠(\
)。
1 | `\`` === "`"; // true |
美元符号 $
也可以被转义,来阻止插值。
1 | `\${1}` === "${1}"; // true |
多行字符串
在源码中插入的任何换行符都是模板字面量的一部分。
使用普通字符串,可以通过下面的方式得到多行字符串:
1 | console.log("string text line 1\n" + "string text line 2"); |
使用模板字面量,下面的代码同样可以做到:
1 | console.log(`string text line 1 |
字符串插值
如果没有模板字面量,当你想组合表达式的输出与字符串时,可以使用加法运算符 +
连接它们:
1 | const a = 5; |
这可能很难阅读,尤其是当存在多个表达式时。
有了模板字面量,就可以通过使用占位符 ${expression}
嵌入待替换的表达式,从而避免串联运算符,并提高代码的可读性:
1 | const a = 5; |
注意,这两种语法有一点小区别:模板字面量直接将其表达式强制转换为字符串,而加法则会先强制转换为原语类型。更多相关信息,参见加法(+
)运算符。
嵌套模板
在某些情况下,嵌套模板是具有可配置字符串的最简单的(也许还是更可读的)方法。在反引号分隔的模板中,允许在占位符 ${expression}
中使用内层的反引号。
例如,不用模板字面量的情况下,如果你想根据特定条件返回某个值,可以执行以下操作:
1 | let classes = "header"; |
用模板字面量但不嵌套时,你可以这么做:
1 | const classes = `header ${ |
用嵌套模板字面量时,你可以这么做:
1 | const classes = `header ${ |
带标签的模板
带标签的模板是模板字面量的一种更高级的形式,它允许你使用函数解析模板字面量。标签函数的第一个参数包含一个字符串数组,其余的参数与表达式相关。你可以用标签函数对这些参数执行任何操作,并返回被操作过的字符串(或者,也可返回完全不同的内容,见下面的示例)。用作标签的函数名没有限制。
1 | const person = "Mike"; |
标签不必是普通的标识符,你可以使用任何优先级大于 16 的表达式,包括属性访问、函数调用、new 表达式,甚至其他带标签的模板字面量。
1 | console.log`Hello`; // [ 'Hello' ] |
虽然语法从技术上允许这么做,但不带标签的模板字面量是字符串,并且在链式调用时会抛出 TypeError
。
1 | console.log(`Hello``World`); // TypeError: "Hello" is not a function |
唯一的例外是可选链,这将抛出语法错误。
1 | console.log?.`Hello`; // SyntaxError: Invalid tagged template on optional chain |
请注意,这两个表达式仍然是可解析的。这意味着它们将不受自动分号补全的影响,其只会插入分号来修复无法解析的代码。
1 | // 仍是语法错误 |
标签函数甚至不需要返回字符串!
1 | function template(strings, ...keys) { |
标签函数接收到的第一个参数是一个字符串数组。对于任何模板字面量,其长度等于替换次数(${…}
出现次数)加一,因此总是非空的。对于任何特定的带标签的模板字面量表达式,无论对字面量求值多少次,都将始终使用完全相同的字面量数组调用标签函数。
1 | const callHistory = []; |
这允许标签函数以其第一个参数作为标识来缓存结果。为了进一步确保数组值不变,第一个参数及其 raw
属性都会被冻结,因此你将无法改变它们。
原始字符串
在标签函数的第一个参数中,存在一个特殊的属性 raw
,我们可以通过它来访问模板字符串的原始字符串,而无需转义特殊字符。
1 | function tag(strings) { |
另外,使用 String.raw()
方法创建原始字符串和使用默认模板函数和字符串连接创建是一样的。
1 | let str = String.raw`Hi\n${2+3}!`; |
如果字面量不包含任何转义序列,String.raw
函数就像一个“identity”标签。如果你想要一个始终像不带标签的字面量那样的实际标识标签,可以用自定义函数,将“cooked”(例如,经转义序列处理过的)字面量数组传递给 String.raw
,将它们当成原始字符串。
1 | const identity = (strings, ...values) => |
这对于许多工具来说很有用,它们要对以特定名称为标签的字面量作特殊处理。
1 | const html = (strings, ...values) => String.raw({ raw: strings }, ...values); |
带标签的模板字面量及转义序列
在普通模板字面量中,字符串字面量中的转义序列都是允许的,任何其他格式不正确的转义序列都是语法错误,包括:
\
后跟0
以外的任何十进制数字,或\0
后跟一个十进制数字,例如\9
和\07
(这是一种已弃用的语法)\x
后跟两位以下十六进制数字,例如\xz
\u
后不跟{
,并且后跟四个以下十六进制数字,例如\uz
\u{}
包含无效的 Unicode 码点——包含一个非十六进制数字,或者它的值大于 10FFFF,例如\u{110000}
和\u{z}
备注: \
后面跟着其他字符,虽然它们可能没有用,因为没有转义,但它们不是语法错误。
然而,这对于带标签的模板来说是有问题的,除了“cooked”字面量外,这些模板还可以访问原始字面量(转义序列按原样保留)。带标签的模板应该允许嵌入语言(例如 DSL 或 LaTeX),在这些语言里其他转义序列是常见的。因此,从带标签的模板中删除了转义序列诸多格式的语法限制。
不过,非法转义序列在“cooked”当中仍然会体现出来。它们将以 undefined
元素的形式存在于“cooked”数组之中:
1 | function latex(str) { |
值得注意的是,这一转义序列限制只对带标签的模板字面量移除,而不包括不带标签的模板字面量:
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 | const age = 26; |
处理 null 值
一个常见的用法是处理可能为 null
的值:
1 | const greeting = (person) => { |
条件链
三元运算符是右结合的,这意味着它可以按以下方式“链接”起来,类似于 if … else if … else if … else
链:
1 | function example() { |
这等价于以下 if...else
链。
1 | function example() { |
规范
| Specification | | ———————————————————— | | ECMAScript Language Specification # sec-conditional-operator |
JavaScript 模块
这篇指南会给你入门 JavaScript 模块的全部信息。
模块化的背景
JavaScript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 JavaScript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。
因此,近年来,有必要开始考虑提供一种将 JavaScript 程序拆分为可按需导入的单独模块的机制。Node.js 已经提供这个能力很长时间了,还有很多的 JavaScript 库和框架已经开始了模块的使用(例如,CommonJS 和基于 AMD 的其他模块系统 如 RequireJS,以及最新的 Webpack 和 Babel)。
好消息是,最新的浏览器开始原生支持模块功能了,这是本文要重点讲述的。这会是一个好事情 —- 浏览器能够最优化加载模块,使它比使用库更有效率:使用库通常需要做额外的客户端处理。