JavaScript 权威指南第七版(GPT 重译)(一)(3)https://developer.aliyun.com/article/1485288
3.9 类型转换
JavaScript 对所需值的类型非常灵活。我们已经看到了布尔值的情况:当 JavaScript 需要一个布尔值时,您可以提供任何类型的值,JavaScript 将根据需要进行转换。一些值(“真值”)转换为 true
,而其他值(“假值”)转换为 false
。其他类型也是如此:如果 JavaScript 需要一个字符串,它将把您提供的任何值转换为字符串。如果 JavaScript 需要一个数字,它将尝试将您提供的值转换为数字(或者如果无法执行有意义的转换,则转换为 NaN
)。
一些例子:
10 + " objects" // => "10 objects": 数字 10 转换为字符串 "7" * "4" // => 28: 两个字符串都转换为数字 let n = 1 - "x"; // n == NaN; 字符串"x"无法转换为数字 n + " objects" // => "NaN objects": NaN 转换为字符串"NaN"
表 3-2 总结了 JavaScript 中值从一种类型转换为另一种类型的方式。表中的粗体条目突出显示了您可能会感到惊讶的转换。空单元格表示不需要转换,也不执行任何转换。
表 3-2. JavaScript 类型转换
值 | 转为字符串 | 转为数字 | 转为布尔值 |
undefined |
"undefined" |
NaN |
false |
null |
"null" |
0 |
false |
true |
"true" |
1 |
|
false |
"false" |
0 |
|
"" (空字符串) |
0 |
false |
|
"1.2" (非空,数值) |
1.2 |
true |
|
"one" (非空,非数字) |
NaN |
true |
|
0 |
"0" |
false |
|
-0 |
"0" |
false |
|
1 (有限的,非零) |
"1" |
true |
|
Infinity |
"Infinity" |
true |
|
-Infinity |
"-Infinity" |
true |
|
NaN |
"NaN" |
false |
|
{} (任何对象) |
见 §3.9.3 | 见 §3.9.3 | true |
[] (空数组) |
"" |
0 |
true |
[9] (一个数值元素) |
"9" |
9 |
true |
['a'] (任何其他数组) |
使用 join() 方法 | NaN |
true |
function(){} (任何函数) |
见 §3.9.3 | NaN |
true |
表中显示的原始到原始的转换相对简单。布尔值转换已在第 3.4 节中讨论过。对于所有原始值,字符串转换是明确定义的。转换为数字稍微棘手一点。可以解析为数字的字符串将转换为这些数字。允许前导和尾随空格,但任何不是数字文字的前导或尾随非空格字符会导致字符串到数字的转换产生 NaN
。一些数字转换可能看起来令人惊讶:true
转换为 1,false
和空字符串转换为 0。
对象到原始值的转换有点复杂,这是第 3.9.3 节的主题。
3.9.1 转换和相等性
JavaScript 有两个操作符用于测试两个值是否相等。“严格相等操作符”===
在不同类型的操作数时不认为它们相等,这几乎总是编码时应该使用的正确操作符。但是因为 JavaScript 在类型转换方面非常灵活,它还定义了==
操作符,具有灵活的相等定义。例如,以下所有比较都是真的:
null == undefined // => true: 这两个值被视为相等。 "0" == 0 // => true: 在比较之前,字符串转换为数字。 0 == false // => true: 在比较之前,布尔值转换为数字。 "0" == false // => true: 在比较之前,两个操作数都转换为 0!
§4.9.1 解释了==
操作符执行的转换,以确定两个值是否应被视为相等。
请记住,一个值转换为另一个值并不意味着这两个值相等。例如,如果在期望布尔值的地方使用undefined
,它会转换为false
。但这并不意味着undefined == false
。JavaScript 操作符和语句期望各种类型的值,并对这些类型进行转换。if
语句将undefined
转换为false
,但==
操作符从不尝试将其操作数转换为布尔值。
3.9.2 显式转换
尽管 JavaScript 会自动执行许多类型转换,但有时你可能需要执行显式转换,或者你可能更喜欢使转换明确以保持代码更清晰。
执行显式类型转换的最简单方法是使用Boolean()
、Number()
和String()
函数:
Number("3") // => 3 String(false) // => "false": 或者使用 false.toString() Boolean([]) // => true
除了null
或undefined
之外的任何值都有一个toString()
方法,而这个方法的结果通常与String()
函数返回的结果相同。
顺便提一下,注意Boolean()
、Number()
和String()
函数也可以被调用——带有new
——作为构造函数。如果以这种方式使用它们,你将得到一个行为就像原始布尔值、数字或字符串值的“包装”对象。这些包装对象是 JavaScript 最早期的历史遗留物,实际上从来没有任何好理由使用它们。
某些 JavaScript 操作符执行隐式类型转换,有时会明确用于类型转换的目的。如果+
操作符的一个操作数是字符串,则它会将另一个操作数转换为字符串。一元+
操作符将其操作数转换为数字。一元!
操作符将其操作数转换为布尔值并对其取反。这些事实导致以下类型转换习语,你可能在一些代码中看到:
x + "" // => String(x) +x // => Number(x) x-0 // => Number(x) !!x // => Boolean(x): 注意双重!
在计算机程序中,格式化和解析数字是常见的任务,JavaScript 有专门的函数和方法,可以更精确地控制数字到字符串和字符串到数字的转换。
Number 类定义的toString()
方法接受一个可选参数,指定转换的基数或进制。如果不指定参数,转换将以十进制进行。但是,你也可以将数字转换为其他进制(介于 2 和 36 之间)。例如:
let n = 17; let binary = "0b" + n.toString(2); // 二进制 == "0b10001" let octal = "0o" + n.toString(8); // 八进制 == "0o21" let hex = "0x" + n.toString(16); // hex == "0x11"
在处理财务或科学数据时,您可能希望以控制输出中小数位数或有效数字位数的方式将数字转换为字符串,或者您可能希望控制是否使用指数表示法。Number 类定义了三种用于这种数字到字符串转换的方法。toFixed()
将数字转换为一个字符串,小数点后有指定数量的数字。它永远不使用指数表示法。toExponential()
将数字转换为一个使用指数表示法的字符串,小数点前有一个数字,小数点后有指定数量的数字(这意味着有效数字的数量比您指定的值大一个)。toPrecision()
将数字转换为一个具有您指定的有效数字数量的字符串。如果有效数字的数量不足以显示整数部分的全部内容,则使用指数表示法。请注意,这三种方法都会四舍五入尾随数字或根据需要填充零。考虑以下示例:
let n = 123456.789; n.toFixed(0) // => "123457" n.toFixed(2) // => "123456.79" n.toFixed(5) // => "123456.78900" n.toExponential(1) // => "1.2e+5" n.toExponential(3) // => "1.235e+5" n.toPrecision(4) // => "1.235e+5" n.toPrecision(7) // => "123456.8" n.toPrecision(10) // => "123456.7890"
除了这里展示的数字格式化方法外,Intl.NumberFormat 类定义了一种更通用的、国际化的数字格式化方法。详细信息请参见§11.7.1。
如果将字符串传递给Number()
转换函数,它会尝试将该字符串解析为整数或浮点文字。该函数仅适用于十进制整数,并且不允许包含在文字中的尾随字符。parseInt()
和parseFloat()
函数(这些是全局函数,不是任何类的方法)更加灵活。parseInt()
仅解析整数,而parseFloat()
解析整数和浮点数。如果字符串以“0x”或“0X”开头,parseInt()
会将其解释为十六进制数。parseInt()
和parseFloat()
都会跳过前导空格,解析尽可能多的数字字符,并忽略其后的任何内容。如果第一个非空格字符不是有效的数字文字的一部分,它们会返回NaN
:
parseInt("3 blind mice") // => 3 parseFloat(" 3.14 meters") // => 3.14 parseInt("-12.34") // => -12 parseInt("0xFF") // => 255 parseInt("0xff") // => 255 parseInt("-0XFF") // => -255 parseFloat(".1") // => 0.1 parseInt("0.1") // => 0 parseInt(".1") // => NaN:整数不能以 "." 开头 parseFloat("$72.47") // => NaN:数字不能以 "$" 开头
parseInt()
接受一个可选的第二个参数,指定要解析的数字的基数(进制)。合法值介于 2 和 36 之间。例如:
parseInt("11", 2) // => 3:(1*2 + 1) parseInt("ff", 16) // => 255:(15*16 + 15) parseInt("zz", 36) // => 1295:(35*36 + 35) parseInt("077", 8) // => 63:(7*8 + 7) parseInt("077", 10) // => 77:(7*10 + 7)
3.9.3 对象到原始值的转换
前面的部分已经解释了如何显式将一种类型的值转换为另一种类型,并解释了 JavaScript 将值从一种原始类型转换为另一种原始类型的隐式转换。本节涵盖了 JavaScript 用于将对象转换为原始值的复杂规则。这部分内容很长,很晦涩,如果这是您第一次阅读本章,可以放心地跳到§3.10。
JavaScript 对象到原始值的转换复杂的一个原因是,某些类型的对象有多个原始表示。例如,日期对象可以被表示为字符串或数值时间戳。JavaScript 规范定义了三种基本算法来将对象转换为原始值:
优先选择字符串
这个算法返回一个原始值,如果可能的话,优先选择一个字符串值。
优先选择数字
这个算法返回一个原始值,如果可能的话,优先选择一个数字。
无偏好
这个算法不表达对所需原始值类型的偏好,类可以定义自己的转换。在内置的 JavaScript 类型中,除了日期类以优先选择字符串算法实现外,其他所有类都以优先选择数字算法实现。
这些对象到原始值的转换算法的实现在本节末尾有解释。然而,首先我们解释一下这些算法在 JavaScript 中是如何使用的。
对象到布尔值的转换
对象到布尔值的转换是微不足道的:所有对象都转换为true
。请注意,这种转换不需要使用前述的对象到原始值的算法,并且它确实适用于所有对象,包括空数组甚至包装对象new Boolean(false)
。
对象到字符串的转换
当一个对象需要被转换为字符串时,JavaScript 首先使用优先选择字符串算法将其转换为一个原始值,然后根据表 3-2 中的规则将得到的原始值转换为字符串,如果需要的话。
这种转换会发生在例如,如果你将一个对象传递给一个内置函数,该函数期望一个字符串参数,如果你调用String()
作为一个转换函数,以及当你将对象插入到模板字面量中时。
对象到数字的转换
当一个对象需要被转换为数字时,JavaScript 首先使用优先选择数字算法将其转换为一个原始值,然后根据表 3-2 中的规则将得到的原始值转换为数字,如果需要的话。
内置的 JavaScript 函数和方法期望数字参数时,将对象参数转换为数字的方式,大多数(参见下面的例外情况)期望数字操作数的 JavaScript 操作符也以这种方式将对象转换为数字。
特殊情况的操作符转换
操作符在第四章中有详细介绍。在这里,我们解释一下那些不使用前述基本对象到字符串和对象到数字转换的特殊情况操作符。
JavaScript 中的+
运算符执行数字加法和字符串连接。如果其操作数中有一个是对象,则 JavaScript 会使用no-preference算法将它们转换为原始值。一旦有了两个原始值,它会检查它们的类型。如果任一参数是字符串,则将另一个转换为字符串并连接字符串。否则,将两个参数转换为数字并相加。
==
和!=
运算符以一种允许类型转换的宽松方式执行相等性和不相等性测试。如果一个操作数是对象,另一个是原始值,这些运算符会使用no-preference算法将对象转换为原始值,然后比较两个原始值。
最后,关系运算符<
、<=
、>
和>=
比较它们的操作数的顺序,可用于比较数字和字符串。如果任一操作数是对象,则会使用prefer-number算法将其转换为原始值。但请注意,与对象到数字的转换不同,prefer-number转换返回的原始值不会再转换为数字。
请注意,Date 对象的数字表示可以有意义地使用<
和>
进行比较,但字符串表示则不行。对于 Date 对象,no-preference算法会转换为字符串,因此 JavaScript 对这些运算符使用prefer-number算法意味着我们可以使用它们来比较两个 Date 对象的顺序。
toString()和 valueOf()方法
所有对象都继承了两个用于对象到原始值转换的转换方法,在我们解释prefer-string、prefer-number和no-preference转换算法之前,我们必须解释这两个方法。
第一个方法是toString()
,它的作用是返回对象的字符串表示。默认的toString()
方法并不返回一个非常有趣的值(尽管我们会在§14.4.3 中发现它很有用):
({x: 1, y: 2}).toString() // => "[object Object]"
许多类定义了更具体版本的toString()
方法。例如,Array 类的toString()
方法将每个数组元素转换为字符串,并用逗号将结果字符串连接在一起。Function 类的toString()
方法将用户定义的函数转换为 JavaScript 源代码的字符串。Date 类定义了一个toString()
方法,返回一个可读的(且可被 JavaScript 解析)日期和时间字符串。RegExp 类定义了一个toString()
方法,将 RegExp 对象转换为类似 RegExp 字面量的字符串:
[1,2,3].toString() // => "1,2,3" (function(x) { f(x); }).toString() // => "function(x) { f(x); }" /\d+/g.toString() // => "/\\d+/g" let d = new Date(2020,0,1); d.toString() // => "Wed Jan 01 2020 00:00:00 GMT-0800 (Pacific Standard Time)"
另一个对象转换函数称为valueOf()
。这个方法的作用定义较少:它应该将对象转换为表示该对象的原始值,如果存在这样的原始值。对象是复合值,大多数对象实际上不能用单个原始值表示,因此默认的valueOf()
方法只返回对象本身,而不是返回原始值。包装类如 String、Number 和 Boolean 定义了简单返回包装的原始值的valueOf()
方法。数组、函数和正则表达式只是继承了默认方法。对于这些类型的实例调用valueOf()
只会返回对象本身。Date 类定义了一个valueOf()
方法,返回其内部表示的日期:自 1970 年 1 月 1 日以来的毫秒数:
let d = new Date(2010, 0, 1); // 2010 年 1 月 1 日(太平洋时间) d.valueOf() // => 1262332800000
对象到原始值转换算法
通过解释toString()
和valueOf()
方法,我们现在可以大致解释三种对象到原始值的算法是如何工作的(完整细节将延迟到§14.4.7):
- prefer-string算法首先尝试
toString()
方法。如果该方法被定义并返回一个原始值,那么 JavaScript 使用该原始值(即使它不是字符串!)。如果toString()
不存在或者返回一个对象,那么 JavaScript 尝试valueOf()
方法。如果该方法存在并返回一个原始值,那么 JavaScript 使用该值。否则,转换将失败并抛出 TypeError。 - prefer-number算法类似于prefer-string算法,只是它首先尝试
valueOf()
,然后尝试toString()
。 - no-preference算法取决于要转换的对象的类。如果对象是一个 Date 对象,那么 JavaScript 使用prefer-string算法。对于任何其他对象,JavaScript 使用prefer-number算法。
这里描述的规则适用于所有内置的 JavaScript 类型,并且是您自己定义的任何类的默认规则。§14.4.7 解释了如何为您定义的类定义自己的对象到原始值转换算法。
在我们离开这个主题之前,值得注意的是prefer-number转换的细节解释了为什么空数组转换为数字 0,而单元素数组也可以转换为数字:
Number([]) // => 0:这是意外的! Number([99]) // => 99:真的吗?
对象到数字的转换首先使用prefer-number算法将对象转换为原始值,然后将得到的原始值转换为数字。prefer-number算法首先尝试valueOf()
,然后退而求其次使用toString()
。但是 Array 类继承了默认的valueOf()
方法,它不会返回原始值。因此,当我们尝试将数组转换为数字时,实际上调用了数组的toString()
方法。空数组转换为空字符串。空字符串转换为数字 0。包含单个元素的数组转换为该元素的字符串。如果数组包含单个数字,则该数字被转换为字符串,然后再转换为数字。
3.10 变量声明和赋值
计算机编程中最基本的技术之一是使用名称或标识符来表示值。将名称绑定到值可以让我们引用该值并在我们编写的程序中使用它。当我们这样做时,通常说我们正在为变量赋值。术语“变量”意味着可以分配新值:与变量关联的值可能会随着程序运行而变化。如果我们永久地为一个名称分配一个值,那么我们称该名称为常量而不是变量。
在 JavaScript 程序中使用变量或常量之前,必须声明它。在 ES6 及更高版本中,可以使用let
和const
关键字来声明,我们将在下面解释。在 ES6 之前,变量使用var
声明,这更具特殊性,稍后在本节中解释。
3.10.1 使用 let 和 const 进行声明
在现代 JavaScript(ES6 及更高版本)中,变量使用let
关键字声明,如下所示:
let i; let sum;
也可以在单个let
语句中声明多个变量:
let i, sum;
在声明变量时给变量赋予初始值是一个良好的编程实践,如果可能的话:
let message = "hello"; let i = 0, j = 0, k = 0; let x = 2, y = x*x; // 初始化器可以使用先前声明的变量
如果使用let
语句时没有指定变量的初始值,那么变量会被声明,但其值为undefined
,直到你的代码为其赋值。
若要声明常量而不是变量,请使用const
代替let
。const
的工作方式与let
相同,只是在声明时必须初始化常量:
const H0 = 74; // 哈勃常数(km/s/Mpc) const C = 299792.458; // 真空中的光速(km/s) const AU = 1.496E8; // 天文单位:到太阳的距离(km)
如其名称所示,常量的值不能被更改,任何尝试这样做都会导致抛出 TypeError。
通常(但不是普遍)约定使用全大写字母的名称来声明常量,例如H0
或HTTP_NOT_FOUND
,以区分它们与变量。
何时使用 const
关于使用const
关键字有两种思路。一种方法是仅将const
用于基本上不变的值,比如所示的物理常数,或程序版本号,或用于识别文件类型的字节序列等。另一种方法认识到我们程序中许多所谓的变量实际上在程序运行时根本不会改变。在这种方法中,我们用const
声明所有内容,然后如果发现我们实际上想要允许值变化,我们将声明切换为let
。这可能有助于通过排除我们不打算的变量的意外更改来防止错误。
在一种方法中,我们仅将const
用于绝对不改变的值。在另一种方法中,我们将const
用于任何偶然不改变的值。在我的代码中,我更喜欢前一种方法。
在第五章,我们将学习 JavaScript 中的for
、for/in
和for/of
循环语句。每个循环都包括一个循环变量,在循环的每次迭代中都会被分配一个新值。JavaScript 允许我们将循环变量声明为循环语法的一部分,这是另一种常见的使用let
的方式:
for(let i = 0, len = data.length; i < len; i++) console.log(data[i]); for(let datum of data) console.log(datum); for(let property in object) console.log(property);
也许令人惊讶的是,你也可以使用const
来声明for/in
和for/of
循环的循环“变量”,只要循环体不重新分配新值。在这种情况下,const
声明只是表示该值在一个循环迭代期间是常量:
for(const datum of data) console.log(datum); for(const property in object) console.log(property);
变量和常量作用域
变量的作用域是定义它的程序源代码区域。使用let
和const
声明的变量和常量是块作用域。这意味着它们仅在let
或const
语句出现的代码块内定义。JavaScript 类和函数定义是块,if/else
语句的主体,while
循环,for
循环等也是块。粗略地说,如果一个变量或常量在一对花括号内声明,那么这些花括号限定了变量或常量定义的代码区域(尽管在声明变量的let
或const
语句之前执行的代码行中引用变量或常量是不合法的)。作为for
、for/in
或for/of
循环的一部分声明的变量和常量具有循环体作为它们的作用域,尽管它们在技术上出现在花括号外部。
当一个声明出现在顶层,不在任何代码块内时,我们称之为全局变量或常量,并具有全局作用域。在 Node 和客户端 JavaScript 模块(见第十章)中,全局变量的作用域是定义它的文件。然而,在传统的客户端 JavaScript 中,全局变量的作用域是定义它的 HTML 文档。也就是说:如果一个 <script>
声明了一个全局变量或常量,那么该变量或常量将在该文档中的所有 <script>
元素中定义(或至少在 let
或 const
语句执行后执行的所有脚本中定义)。
重复声明
在同一作用域内使用多个 let
或 const
声明相同名称是语法错误。在嵌套作用域中声明具有相同名称的新变量是合法的(尽管最好避免这种做法):
const x = 1; // 将 x 声明为全局常量 if (x === 1) { let x = 2; // 在一个块内,x 可能指向不同的值 console.log(x); // 打印 2 } console.log(x); // 打印 1:我们现在回到了全局范围 let x = 3; // 错误!尝试重新声明 x 的语法错误
声明和类型
如果你习惯于像 C 或 Java 这样的静态类型语言,你可能会认为变量声明的主要目的是指定可以分配给变量的值的类型。但是,正如你所见,JavaScript 的变量声明没有与之关联的类型。² JavaScript 变量可以保存任何类型的值。例如,在 JavaScript 中将一个数字赋给一个变量,然后稍后将一个字符串赋给该变量是完全合法的(但通常是不良的编程风格):
let i = 10; i = "ten";
3.10.2 使用 var 声明变量
在 ES6 之前的 JavaScript 版本中,声明变量的唯一方式是使用 var
关键字,没有办法声明常量。var
的语法与 let
的语法完全相同:
var x; var data = [], count = data.length; for(var i = 0; i < count; i++) console.log(data[i]);
尽管 var
和 let
具有相同的语法,但它们的工作方式有重要的区别:
- 使用
var
声明的变量没有块级作用域。相反,它们的作用域是包含函数的主体,无论它们在该函数内嵌套多深。 - 如果在函数体外部使用
var
,它会声明一个全局变量。但是用var
声明的全局变量与用let
声明的全局变量有一个重要的区别。用var
声明的全局变量被实现为全局对象的属性(§3.7)。全局对象可以被引用为globalThis
。因此,如果你在函数外部写var x = 2;
,就像你写了globalThis.x = 2;
。但请注意,这个类比并不完美:用全局var
声明创建的属性不能被delete
运算符删除(§4.13.4)。用let
和const
声明的全局变量和常量不是全局对象的属性。 - 与使用
let
声明的变量不同,使用var
可以多次声明同一个变量是合法的。由于var
变量具有函数作用域而不是块作用域,这种重新声明实际上是很常见的。变量i
经常用于整数值,尤其是作为for
循环的索引变量。在具有多个for
循环的函数中,每个循环通常以for(var i = 0; ...
开始。因为var
不将这些变量限定在循环体内,所以每个循环都会(无害地)重新声明和重新初始化相同的变量。 var
声明中最不寻常的特性之一被称为提升。当使用var
声明变量时,声明会被提升(或“提升”)到封闭函数的顶部。变量的初始化仍然在你编写的位置,但变量的定义移动到函数的顶部。因此,使用var
声明的变量可以在封闭函数的任何地方使用,而不会出错。如果初始化代码尚未运行,则变量的值可能是undefined
,但在变量初始化之前使用变量不会出错。(这可能是一个错误的来源,也是let
纠正的重要缺陷之一:如果使用let
声明变量但在let
语句运行之前尝试使用它,你将收到一个实际的错误,而不仅仅是看到一个undefined
值。)
使用未声明的变量
在严格模式(§5.6.3)中,如果尝试使用未声明的变量,在运行代码时会收到一个引用错误。然而,在非严格模式下,如果给一个未用let
、const
或var
声明的名称赋值,你将创建一个新的全局变量。无论你的代码嵌套多深,它都将是一个全局变量,这几乎肯定不是你想要的,容易出错,这也是使用严格模式的最好理由之一!
以这种意外方式创建的全局变量类似于用var
声明的全局变量:它们定义了全局对象的属性。但与由正确的var
声明定义的属性不同,这些属性可以使用delete
运算符(§4.13.4)删除。
3.10.3 解构赋值
ES6 实现了一种称为解构赋值的复合声明和赋值语法。在解构赋值中,等号右侧的值是一个数组或对象(一个“结构化”值),而左侧指定一个或多个变量名,使用一种模仿数组和对象字面量语法的语法。当发生解构赋值时,一个或多个值从右侧的值中被提取(“解构”)并存储到左侧命名的变量中。解构赋值可能最常用于作为const
、let
或var
声明语句的一部分初始化变量,但也可以在常规赋值表达式中进行(使用已经声明的变量)。正如我们将在§8.3.5 中看到的,解构也可以在定义函数参数时使用。
这里是使用值数组的简单解构赋值:
let [x,y] = [1,2]; // 同 let x=1, y=2 [x,y] = [x+1,y+1]; // 同 x = x + 1, y = y + 1 [x,y] = [y,x]; // 交换两个变量的值 [x,y] // => [3,2]:递增和交换的值
注意解构赋值如何使处理返回值数组的函数变得简单:
// 将[x,y]坐标转换为[r,theta]极坐标 function toPolar(x, y) { return [Math.sqrt(x*x+y*y), Math.atan2(y,x)]; } // 将极坐标转换为直角坐标 function toCartesian(r, theta) { return [r*Math.cos(theta), r*Math.sin(theta)]; } let [r,theta] = toPolar(1.0, 1.0); // r == Math.sqrt(2); theta == Math.PI/4 let [x,y] = toCartesian(r,theta); // [x, y] == [1.0, 1,0]
我们看到变量和常量可以作为 JavaScript 的各种for
循环的一部分声明。在这种情况下,也可以在此上下文中使用变量解构。以下是一个代码,循环遍历对象的所有属性的名称/值对,并使用解构赋值将这些对从两个元素数组转换为单独的变量:
let o = { x: 1, y: 2 }; // 我们将循环的对象 for(const [name, value] of Object.entries(o)) { console.log(name, value); // 打印 "x 1" 和 "y 2" }
解构赋值的左侧变量数量不必与右侧数组元素数量匹配。左侧的额外变量将被设置为undefined
,右侧的额外值将被忽略。左侧变量列表可以包含额外的逗号以跳过右侧的某些值:
let [x,y] = [1]; // x == 1; y == undefined [x,y] = [1,2,3]; // x == 1; y == 2 [,x,,y] = [1,2,3,4]; // x == 2; y == 4
如果要在解构数组时将所有未使用或剩余的值收集到一个变量中,请在左侧最后一个变量名之前使用三个点(...
):
let [x, ...y] = [1,2,3,4]; // y == [2,3,4]
我们将在§8.3.2 中再次看到这种方式使用三个点,用于指示所有剩余的函数参数应该被收集到一个单独的数组中。
解构赋值可以与嵌套数组一起使用。在这种情况下,赋值的左侧应该看起来像一个嵌套数组字面量:
let [a, [b, c]] = [1, [2,2.5], 3]; // a == 1; b == 2; c == 2.5
数组解构的一个强大特性是它实际上并不需要一个数组!您可以在赋值的右侧使用任何可迭代对象(第十二章);任何可以与for/of
循环(§5.4.4)一起使用的对象也可以被解构:
let [first, ...rest] = "Hello"; // first == "H"; rest == ["e","l","l","o"]
当右侧是对象值时,也可以执行解构赋值。在这种情况下,赋值的左侧看起来像一个对象字面量:在花括号内用逗号分隔的变量名列表:
let transparent = {r: 0.0, g: 0.0, b: 0.0, a: 1.0}; // 一个 RGBA 颜色 let {r, g, b} = transparent; // r == 0.0; g == 0.0; b == 0.0
下一个示例将全局函数Math
对象的函数复制到变量中,这可能简化了大量三角函数的代码:
// 同 const sin=Math.sin, cos=Math.cos, tan=Math.tan const {sin, cos, tan} = Math;
在这里的代码中请注意,Math
对象除了被解构为单独变量的三个属性外,还有许多其他属性。那些未命名的属性将被简单地忽略。如果这个赋值的左侧包含一个不是Math
属性的变量,那么该变量将被简单地赋值为undefined
。
在这些对象解构示例中,我们选择了与要解构的对象的属性名匹配的变量名。这保持了语法的简单和易于理解,但并非必须。在对象解构赋值的左侧,每个标识符也可以是一个以冒号分隔的标识符对,第一个是要赋值的属性名,第二个是要赋给它的变量名:
// 同 const cosine = Math.cos, tangent = Math.tan; const { cos: cosine, tan: tangent } = Math;
我发现当变量名和属性名不同时,对象解构语法变得过于复杂,不太实用,我倾向于在这种情况下避免使用简写。如果你选择使用它,请记住属性名始终位于冒号的左侧,无论是在对象字面量中还是在对象解构赋值的左侧。
当与嵌套对象、对象数组或数组对象一起使用时,解构赋值变得更加复杂,但是是合法的:
let points = [{x: 1, y: 2}, {x: 3, y: 4}]; // 一个包含两个点对象的数组 let [{x: x1, y: y1}, {x: x2, y: y2}] = points; // 解构成 4 个变量 (x1 === 1 && y1 === 2 && x2 === 3 && y2 === 4) // => true
或者,我们可以对一个包含数组的对象进行解构:
let points = { p1: [1,2], p2: [3,4] }; // 一个具有 2 个数组属性的对象 let { p1: [x1, y1], p2: [x2, y2] } = points; // 解构成 4 个变量 (x1 === 1 && y1 === 2 && x2 === 3 && y2 === 4) // => true
像这样复杂的解构语法可能很难编写和阅读,你可能最好还是用传统的代码明确地写出你的赋值,比如let x1 = points.p1[0];
。
3.11 总结
本章需要记住的一些关键点:
- 如何在 JavaScript 中编写和操作数字和文本字符串。
- 如何处理 JavaScript 的其他基本类型:布尔值、符号、
null
和undefined
。 - 不可变的基本类型和可变的引用类型之间的区别。
- JavaScript 如何隐式地将值从一种类型转换为另一种类型,以及你如何在程序中显式地进行转换。
- 如何声明和初始化常量和变量(包括解构赋值),以及你声明的变量和常量的词法作用域。
¹ 这是 Java、C++和大多数现代编程语言中double
类型的数字的格式。
² 有一些 JavaScript 的扩展,比如 TypeScript 和 Flow (§17.8),允许在变量声明中指定类型,语法类似于let x: number = 0;
。