原文:11. Numbers
译者:飞龙
协议:CC BY-NC-SA 4.0
JavaScript 对所有数字都使用单一类型:它将它们全部视为浮点数。但是,如果小数点后没有数字,则不显示小数点:
> 5.000
5
在内部,大多数 JavaScript 引擎都会优化并区分浮点数和整数(详情请参见JavaScript 中的整数)。但这是程序员看不到的东西。
JavaScript 数字是基于 IEEE 浮点算术标准(IEEE 754)的double
(64 位)值。该标准被许多编程语言使用。
数字文字可以是整数、浮点数或(整数)十六进制:
> 35 // integer
35
> 3.141 // floating point
3.141
> 0xFF // hexadecimal
255
指数eX
是“乘以 10^X”的缩写:
> 5e2
500
> 5e-2
0.05
> 0.5e2
50
对于数字文字,访问属性的点必须与小数点区分开。如果要在数字文字123
上调用toString()
,则有以下选项:
123..toString()
123 .toString() // space before the dot
123.0.toString()
(123).toString()
将值转换为数字的方式如下:
值 | 结果 |
---|---|
undefined |
NaN |
null |
0 |
布尔值 | false → 0 |
true → 1 |
|
数字 | 与输入相同(无需转换) |
字符串 | 解析字符串中的数字(忽略前导和尾随空格);空字符串转换为 0。示例:'3.141' → 3.141 |
对象 | 调用ToPrimitive(value, Number) (参见算法:ToPrimitive()—将值转换为原始值)并转换生成的原始值。 |
将空字符串转换为数字时,NaN
可能是更好的结果。选择结果 0 是为了帮助处理空的数字输入字段,符合 1990 年代中期其他编程语言的做法。¹²
将任何值转换为数字的两种最常见方法是:
Number(value) |
(作为函数调用,而不是作为构造函数调用) |
---|---|
+value |
我更喜欢Number()
,因为它更具描述性。以下是一些示例:
> Number('')
0
> Number('123')
123
> Number('\t\v\r12.34\n ') // ignores leading and trailing whitespace
12.34
> Number(false)
0
> Number(true)
1
全局函数parseFloat()
提供了另一种将值转换为数字的方法。但是,Number()
通常是更好的选择,我们稍后将看到。这段代码:
parseFloat(str)
将str
转换为字符串,修剪前导空格,然后解析最长的浮点数前缀。如果不存在这样的前缀(例如,在空字符串中),则返回NaN
。
比较parseFloat()
和Number()
:
将parseFloat()
应用于非字符串的效率较低,因为它在解析之前将其参数强制转换为字符串。因此,Number()
转换为实际数字的许多值被parseFloat()
转换为NaN
:
> parseFloat(true) // same as parseFloat('true')
NaN
> Number(true)
1
> parseFloat(null) // same as parseFloat('null')
NaN
> Number(null)
0
parseFloat()
将空字符串解析为NaN
:
> parseFloat('')
NaN
> Number('')
0
parseFloat()
解析到最后一个合法字符,这意味着您可能会得到一个您不想要的结果:
> parseFloat('123.45#')
123.45
> Number('123.45#')
NaN
parseFloat()
忽略前导空格,并在非法字符之前停止(其中包括空格):
> parseFloat('\t\v\r12.34\n ')
12.34
Number()
忽略前导和尾随空格(但其他非法字符会导致NaN
)。
JavaScript 有几个特殊的数字值:
两个错误值,NaN
和Infinity
。
两个零值,+0
和-0
。JavaScript 有两个零,一个正零和一个负零,因为数字的符号和大小存储在不同的位置。在本书的大部分内容中,我假设只有一个零,并且您几乎从不在 JavaScript 中看到有两个零。
错误值NaN
(“不是一个数字”的缩写)是一个数字值,具有讽刺意味:
> typeof NaN
'number'
它是由以下错误产生的:
无法解析数字:
> Number('xyz')
NaN
> Number(undefined)
NaN
操作失败:
> Math.acos(2)
NaN
> Math.log(-1)
NaN
> Math.sqrt(-1)
NaN
操作数之一是NaN
(这可以确保在较长的计算过程中发生错误时,您可以在最终结果中看到它):
> NaN + 3
NaN
> 25 / NaN
NaN
NaN
是唯一不等于自身的值:
> NaN === NaN
false
严格相等(===
)也被Array.prototype.indexOf
使用。因此,您不能通过该方法在数组中搜索NaN
:
> [ NaN ].indexOf(NaN)
-1
如果要检查值是否为NaN
,则必须使用全局函数isNaN()
:
> isNaN(NaN)
true
> isNaN(33)
false
但是,isNaN
不能正确处理非数字,因为它首先将它们转换为数字。该转换可能产生NaN
,然后该函数错误地返回true
:
> isNaN('xyz')
true
因此,最好将isNaN
与类型检查结合使用:
function myIsNaN(value) {
return typeof value === 'number' && isNaN(value);
}
或者,您可以检查值是否不等于自身(因为NaN
是唯一具有此特性的值)。但这不够自解释:
function myIsNaN(value) {
return value !== value;
}
请注意,此行为由 IEEE 754 规定。如第 7.11 节“比较谓词的详细信息”中所述:¹³
每个 NaN 都将与任何东西(包括自身)比较无序。
Infinity
是一个错误值,指示两个问题中的一个:一个数字无法表示,因为其大小太大,或者发生了除以零。
Infinity
大于任何其他数字(除了NaN
)。同样,-Infinity
小于任何其他数字(除了NaN
)。这使它们在默认值方面非常有用,例如,当您正在寻找最小值或最大值时。
一个数字的大小取决于其内部表示(如数字的内部表示中所讨论的),即:
尾数(一个二进制数 1.f[1]f[2]…)
指数的 2 次幂
指数必须在(不包括)-1023 和 1024 之间。如果指数太小,数字变为 0。如果指数太大,它变为Infinity
。2¹⁰²³仍然可以表示,但 2¹⁰²⁴不能:
> Math.pow(2, 1023)
8.98846567431158e+307
> Math.pow(2, 1024)
Infinity
除以零会产生Infinity
作为错误值:
> 3 / 0
Infinity
> 3 / -0
-Infinity
如果您尝试用另一个Infinity
“中和”一个Infinity
,则会得到错误结果NaN
:
> Infinity - Infinity
NaN
> Infinity / Infinity
NaN
如果您尝试超出Infinity
,您仍然会得到Infinity
:
> Infinity + Infinity
Infinity
> Infinity * Infinity
Infinity
严格和宽松的相等对Infinity
也适用:
> var x = Infinity;
> x === Infinity
true
此外,全局函数isFinite()
允许您检查一个值是否是一个实际的数字(既不是无穷大也不是NaN
):
> isFinite(5)
true
> isFinite(Infinity)
false
> isFinite(NaN)
false
因为 JavaScript 的数字保持大小和符号分开,每个非负数都有一个负数,包括0
。
这是因为当您以数字的方式表示数字时,它可能变得非常小,以至于无法与 0 区分,因为编码不够精确以表示差异。然后,有符号零允许您记录“从哪个方向”接近零;也就是说,在被视为零之前,数字具有什么符号。维基百科很好地总结了有符号零的利弊:
据称,IEEE 754 中包含有符号零使得在一些关键问题中更容易实现数值精度,特别是在计算复杂的初等函数时。另一方面,有符号零的概念与大多数数学领域(以及大多数数学课程)中的一般假设相矛盾,即负零和零是相同的。允许负零的表示可以成为程序中的错误源,因为软件开发人员没有意识到(或可能忘记了),虽然这两个零表示在数值比较下行为相等,但它们是不同的位模式,并在一些操作中产生不同的结果。
JavaScript 竭尽全力隐藏有两个零这一事实。鉴于通常并不重要它们是不同的,建议您配合单个零的幻觉。让我们看看这个幻觉是如何维持的。
在 JavaScript 中,通常写为0
,这意味着+0
。但-0
也显示为简单的0
。这是您在使用浏览器命令行或 Node.js REPL 时看到的情况:
> -0
0
这是因为标准的toString()
方法将这两个零都转换为相同的'0'
:
> (-0).toString()
'0'
> (+0).toString()
'0'
相等也无法区分零。甚至===
也不行:
> +0 === -0
true
Array.prototype.indexOf
使用===
搜索元素,维持了这个幻觉:
> [ -0, +0 ].indexOf(+0)
0
> [ +0, -0 ].indexOf(-0)
0
排序运算符也认为这两个零是相等的:
> -0 < +0
false
> +0 < -0
false
您如何实际观察到这两个零是不同的?您可以除以零(-Infinity
和+Infinity
可以通过===
进行区分):
> 3 / -0
-Infinity
> 3 / +0
Infinity
通过Math.pow()
(参见数值函数)进行除以零的另一种方法:
> Math.pow(-0, -1)
-Infinity
> Math.pow(+0, -1)
Infinity
Math.atan2()
(参见[三角函数](ch21.html#Math.atan2 “Trigonometric Functions”))还显示了这两个零是不同的:
> Math.atan2(-0, -1)
-3.141592653589793
> Math.atan2(+0, -1)
3.141592653589793
区分这两个零的规范方法是除以零。因此,用于检测负零的函数如下:
function isNegativeZero(x) {
return x === 0 && (1/x < 0);
}
以下是使用的函数:
> isNegativeZero(0)
false
> isNegativeZero(-0)
true
> isNegativeZero(33)
false
JavaScript 数字具有 64 位精度,也称为双精度(某些编程语言中的double
类型)。内部表示基于 IEEE 754 标准。64 位分布在数字的符号、指数和分数之间,如下所示:
符号 | 指数 ∈ [−1023, 1024] | 分数 |
---|---|---|
1 位 | 11 位 | 52 位 |
位 63 | 位 62–52 | 位 51–0 |
数字的值由以下公式计算:
(–1)^(sign) × %1.fraction × 2^(exponent)
前缀百分号(%
)表示中间的数字以二进制表示:1,后跟二进制点,后跟二进制分数,即分数的二进制数字(自然数)。以下是此表示的一些示例:
+0 | (符号:0,小数:0,指数:−1023) | |
---|---|---|
–0 | (符号:1,小数:0,指数:−1023) | |
1 | = (−1)⁰ × %1.0 × 2⁰ | (符号:0,小数:0,指数:0) |
2 | = (−1)⁰ × %1.0 × 2¹ | |
3 | = (−1)⁰ × %1.1 × 2¹ | (符号:0,小数:2⁵¹,指数:0) |
0.5 | = (−1)⁰ × %1.0 × 2^(−1) | |
−1 | = (−1)¹ × %1.0 × 2⁰ |
+0、−0 和 3 的编码可以解释如下:
±0:鉴于分数始终以 1 为前缀,因此无法使用它来表示 0。因此,JavaScript 通过分数 0 和特殊指数−1023 来编码零。符号可以是正数或负数,这意味着 JavaScript 有两个零(参见两个零)。
3:位 51 是分数的最高有效位。该位为 1。
前面提到的数字表示称为标准化。在这种情况下,指数 e 在范围内 −1023 < e < 1024(不包括下限和上限)。−1023 和 1024 是特殊指数:
1024 用于NaN
和Infinity
等错误值。
−1023 用于:
零(如果分数为 0,如刚才解释的那样)
靠近零的小数字(如果分数不为 0)。
为了同时启用两个应用程序,使用了不同的所谓非标准化表示:
(–1)^(sign) × %0.fraction × 2^(–1022)
要比较,标准化表示中最小(即“最接近零”的)数字是:
(–1)^(sign) × %1.fraction × 2^(–1022)
非标准化的数字更小,因为没有前导数字 1。
JavaScript 的数字通常以十进制浮点数输入,但在内部表示为二进制浮点数。这导致了不精确。为了理解原因,让我们忘记 JavaScript 的内部存储格式,来看看十进制浮点数和二进制浮点数可以很好地表示哪些分数。在十进制系统中,所有分数都是一个底数 m 除以 10 的幂:
因此,在分母中只有十。这就是为什么无法将精确表示为十进制浮点数的原因——无法将 3 放入分母。二进制浮点数中只有二。让我们看看哪些十进制浮点数可以很好地表示为二进制浮点数,哪些不能。如果分母中只有二,那么可以表示十进制数:
其他分数无法精确表示,因为分母中有 2 以外的数字(经过质因数分解):
通常看不到 JavaScript 内部并未精确存储 0.1。但是,通过将其乘以足够高的 10 的幂,可以使其可见:
> 0.1 * Math.pow(10, 24)
1.0000000000000001e+23
如果将两个不精确表示的数字相加,结果有时会不精确到足以使不精确性变得可见:
> 0.1 + 0.2
0.30000000000000004
另一个例子:
> 0.1 + 1 - 1
0.10000000000000009
由于舍入误差,最好的做法是不直接比较非整数。而是考虑舍入误差的上限。这样的上限称为机器 epsilon。双精度标准 epsilon 值为 2^(−53):
var EPSILON = Math.pow(2, -53);
function epsEqu(x, y) {
return Math.abs(x - y) < EPSILON;
}
epsEqu()
确保正确的结果,普通比较会不足以满足要求:
> 0.1 + 0.2 === 0.3
false
> epsEqu(0.1+0.2, 0.3)
true
如前所述,JavaScript 只有浮点数。整数在内部以两种方式出现。首先,大多数 JavaScript 引擎将足够小的没有小数部分的数字存储为整数(例如,31 位),并尽可能长时间地保持该表示。如果数字的大小增长太大或出现小数部分,则必须切换回浮点表示。
其次,ECMAScript 规范具有整数运算符:即所有按位运算符。这些运算符将其操作数转换为 32 位整数并返回 32 位整数。对于规范,整数只意味着数字没有小数部分,32 位意味着它们在某个范围内。对于引擎,32 位整数意味着通常可以引入或保持实际整数(非浮点)表示。
在 JavaScript 中,以下整数范围很重要:
安全整数(参见安全整数),JavaScript 支持的最大实用整数范围:
53 位加上一个符号,范围(−2⁵³, 2⁵³)
数组索引(参见数组索引):
32 位,无符号
最大长度:2³²−1
索引范围:[0, 2³²−1)(不包括最大长度!)
按位操作数(参见按位运算符):
无符号右移运算符(>>>
):32 位,无符号,范围[0, 2³²)
所有其他按位运算符:32 位,包括符号,范围[−2³¹, 2³¹]
“字符代码”,UTF-16 代码单元作为数字:
被String.fromCharCode()
接受(参见字符串构造方法)
由String.prototype.charCodeAt()
返回(参见提取子字符串)
16 位,无符号
JavaScript 只能处理最大为 53 位的整数值(52 位的小数部分加上 1 个间接位,通过指数; 有关详细信息,请参见数字的内部表示)。
以下表格解释了 JavaScript 如何将 53 位整数表示为浮点数:
位 | 范围 | 编码 |
---|---|---|
1 位 | 0 | (参见数字的内部表示) |
1 位 | 1 | %1 × 2⁰ |
2 位 | 2–3 | %1.f[51] × 2¹ |
3 位 | 4–7 = 2²–(2³−1) | %1.f[51]f[50] × 2² |
4 位 | 2³–(2⁴−1) | %1.f[51]f[50]f[49] × 2³ |
⋯ | ⋯ | ⋯ |
53 位 | 2⁵²–(2⁵³−1) | %1.f[51]⋯f[0] × 2⁵² |
没有固定的位序列表示整数。相反,尾数%1.f 被指数移位,以便领先的数字 1 位于正确的位置。在某种程度上,指数计算出分数中活跃使用的数字的数量(其余数字为 0)。这意味着对于 2 位,我们使用分数的一位数字,对于 53 位,我们使用分数的所有数字。此外,我们可以将 2⁵³表示为%1.0 × 2⁵³,但是对于更高的数字,我们会遇到问题:
位 | 范围 | 编码 |
---|---|---|
54 位 | 2⁵³–(2⁵⁴−1) | %1.f[51]⋯f[0]0 × 2⁵³ |
55 位 | 2⁵⁴–(2⁵⁵−1) | %1.f[51]⋯f[0]00 × 2⁵⁴ |
⋯ |
对于 54 位,最低有效位始终为 0,对于 55 位,最低的两位始终为 0,依此类推。这意味着对于 54 位,我们只能表示每第二个数字,对于 55 位,只能表示每第四个数字,依此类推。例如:
> Math.pow(2, 53) - 1 // OK
9007199254740991
> Math.pow(2, 53) // OK
9007199254740992
> Math.pow(2, 53) + 1 // can't be represented
9007199254740992
> Math.pow(2, 53) + 2 // OK
9007199254740994
如果您使用的整数的大小不超过 53 位,那么就没问题。不幸的是,在编程中经常会遇到 64 位无符号整数(Twitter ID、数据库等)。这些必须以字符串形式存储在 JavaScript 中。如果要对这样的整数执行算术运算,就需要特殊的库。有计划将更大的整数引入 JavaScript,但这需要一些时间。
JavaScript 只能安全地表示范围在−2⁵³ < i < 2⁵³的整数。本节将探讨这意味着什么以及其后果。它基于 Mark S. Miller 发送给 es-discuss 邮件列表的一封邮件。
安全整数的概念集中在 JavaScript 中如何表示数学整数上。在范围(−2⁵³, 2⁵³)(不包括下限和上限)内,JavaScript 整数是安全的:数学整数与它们在 JavaScript 中的表示之间存在一对一的映射。
超出此范围后,JavaScript 整数是不安全的:两个或更多数学整数被表示为相同的 JavaScript 整数。例如,从 2⁵³开始,JavaScript 只能表示每第二个数学整数(前一节解释了原因)。因此,安全的 JavaScript 整数是可以明确表示单个数学整数的整数。
ECMAScript 6 将提供以下常量:
Number.MAX_SAFE_INTEGER = Math.pow(2, 53)-1;
Number.MIN_SAFE_INTEGER = -Number.MAX_SAFE_INTEGER;
它还将提供一个用于确定整数是否安全的函数:
Number.isSafeInteger = function (n) {
return (typeof n === 'number' &&
Math.round(n) === n &&
Number.MIN_SAFE_INTEGER <= n &&
n <= Number.MAX_SAFE_INTEGER);
}
对于给定值n
,此函数首先检查n
是否为数字和整数。如果两个检查都成功,则如果n
大于或等于MIN_SAFE_INTEGER
且小于或等于MAX_SAFE_INTEGER
,则n
是安全的。
我们如何确保算术计算的结果是正确的?例如,以下结果显然是不正确的:
> 9007199254740990 + 3
9007199254740992
我们有两个安全的操作数,但是一个不安全的结果:
> Number.isSafeInteger(9007199254740990)
true
> Number.isSafeInteger(3)
true
> Number.isSafeInteger(9007199254740992)
false
以下结果也是不正确的:
> 9007199254740995 - 10
9007199254740986
这次结果是安全的,但其中一个操作数不是:
> Number.isSafeInteger(9007199254740995)
false
> Number.isSafeInteger(10)
true
> Number.isSafeInteger(9007199254740986)
true
因此,只有当所有操作数和结果都是安全的时,才能保证应用整数运算符op
的结果是正确的。更正式地说:
isSafeInteger(a) && isSafeInteger(b) && isSafeInteger(a op b)
意味着a op b
是正确的结果。
在 JavaScript 中,所有数字都是浮点数。整数是没有小数部分的浮点数。将数字n
转换为整数意味着找到与n
“最接近”的整数(“最接近”的含义取决于如何进行转换)。您有几种选项可以执行此转换:
Math
函数Math.floor()
、Math.ceil()
和Math.round()
(参见Integers via Math.floor(), Math.ceil(), and Math.round())
自定义函数ToInteger()
(参见Integers via the Custom Function ToInteger())
二进制位运算符(参见[通过位运算符实现 32 位整数](ch11.html#integers_via_bitwise_operators “通过位运算符实现 32 位整数”))
全局函数parseInt()
(参见[通过 parseInt()实现整数](ch11.html#parseInt “通过 parseInt()实现整数”))
结论:#1 通常是最佳选择,#2 和#3 有特定应用,#4 适用于解析字符串,但不适用于将数字转换为整数。
以下三个函数通常是将数字转换为整数的最佳方式:
Math.floor()
将其参数转换为最接近的较低整数:
> Math.floor(3.8)
3
> Math.floor(-3.8)
-4
Math.ceil()
将其参数转换为最接近的更高整数:
> Math.ceil(3.2)
4
> Math.ceil(-3.2)
-3
Math.round()
将其参数转换为最接近的整数:
> Math.round(3.2)
3
> Math.round(3.5)
4
> Math.round(3.8)
4
四舍五入-3.5
的结果可能会让人惊讶:
> Math.round(-3.2)
-3
> Math.round(-3.5)
-3
> Math.round(-3.8)
-4
```
因此,`Math.round(x)`与以下相同:
```js
Math.ceil(x + 0.5)
```
### 通过自定义函数 ToInteger()实现整数
将任何值转换为整数的另一个好选择是内部 ECMAScript 操作`ToInteger()`,它去除了浮点数的小数部分。如果它在 JavaScript 中可用,它将像这样工作:
```js
> ToInteger(3.2)
3
> ToInteger(3.5)
3
> ToInteger(3.8)
3
> ToInteger(-3.2)
-3
> ToInteger(-3.5)
-3
> ToInteger(-3.8)
-3
ECMAScript 规范将ToInteger(number)
的结果定义为:
sign(number) × floor(abs(number))
这个公式相对复杂,因为floor
寻找最接近的大整数;如果你想去掉负整数的小数部分,你必须寻找最接近的小整数。以下代码在 JavaScript 中实现了这个操作。如果数字是负数,我们避免使用sign
操作,而是使用ceil
:
function ToInteger(x) {
x = Number(x);
return x < 0 ? Math.ceil(x) : Math.floor(x);
}
二进制位运算符(参见[二进制位运算符](ch11.html#binary_bitwise_operators “二进制位运算符”)将(至少)一个操作数转换为 32 位整数,然后对其进行操作以产生也是 32 位整数的结果。因此,如果你适当选择另一个操作数,你可以快速地将任意数字转换为 32 位整数(有符号或无符号)。
如果掩码,第二个操作数,为 0,则不改变任何位,结果是第一个操作数,强制转换为有符号 32 位整数。这是执行这种强制转换的规范方式,例如,asm.js(参见[JavaScript 足够快吗?](ch02.html#asm.js “JavaScript 足够快吗?”))中使用:
// Convert x to a signed 32-bit integer
function ToInt32(x) {
return x | 0;
}
ToInt32()
去除小数并应用模 2³²:
> ToInt32(1.001)
1
> ToInt32(1.999)
1
> ToInt32(1)
1
> ToInt32(-1)
-1
> ToInt32(Math.pow(2, 32)+1)
1
> ToInt32(Math.pow(2, 32)-1)
-1
对移位运算符也适用与按位或相同的技巧:如果你移动零位,移位操作的结果是第一个操作数,强制转换为 32 位整数。以下是通过移位运算符实现 ECMAScript 规范操作的一些示例:
// Convert x to a signed 32-bit integer
function ToInt32(x) {
return x << 0;
}
// Convert x to a signed 32-bit integer
function ToInt32(x) {
return x >> 0;
}
// Convert x to an unsigned 32-bit integer
function ToUint32(x) {
return x >>> 0;
}
这是ToUint32()
的实际操作:
> ToUint32(-1)
4294967295
> ToUint32(Math.pow(2, 32)-1)
4294967295
> ToUint32(Math.pow(2, 32))
0
你必须自己决定,稍微提高效率是否值得让你的代码更难理解。另外要注意,位运算符人为地限制自己在 32 位,这通常既不必要也不实用。使用Math
函数之一,可能还加上Math.abs()
,是一个更易于理解且可能更好的选择。
parseInt()
函数:
parseInt(str, radix?)
解析字符串str
(非字符串被强制转换)为整数。该函数忽略前导空格,并考虑尽可能多的连续合法数字。
基数的范围是 2 ≤ radix
≤ 36。它确定要解析的数字的基数。如果基数大于 10,则除了 0-9,还使用字母作为数字(不区分大小写)。
如果radix
缺失,则假定为 10,除非str
以“0x”或“0X”开头,此时radix
设置为 16(十六进制):
> parseInt('0xA')
10
如果radix
已经是 16,则十六进制前缀是可选的:
> parseInt('0xA', 16)
10
> parseInt('A', 16)
10
到目前为止,我已经描述了parseInt()
的行为,符合 ECMAScript 规范。此外,一些引擎如果str
以零开头,则将基数设置为 8:
> parseInt('010')
8
> parseInt('0109') // ignores digits ≥ 8
8
因此,最好总是明确指定基数,始终使用两个参数调用parseInt()
。
以下是一些例子:
> parseInt('')
NaN
> parseInt('zz', 36)
1295
> parseInt(' 81', 10)
81
> parseInt('12**', 10)
12
> parseInt('12.34', 10)
12
> parseInt(12.34, 10)
12
不要使用parseInt()
将数字转换为整数。最后一个例子让我们希望我们可以使用parseInt()
将数字转换为整数。然而,这里有一个转换不正确的例子:
> parseInt(1000000000000000000000.5, 10)
1
首先将参数转换为字符串:
> String(1000000000000000000000.5)
'1e+21'
parseInt
不认为“e”是一个整数数字,因此在 1 之后停止解析。这里是另一个例子:
> parseInt(0.0000008, 10)
8
> String(0.0000008)
'8e-7'
parseInt()
不应该用于将数字转换为整数:强制转换为字符串是一个不必要的绕道,即使这样,结果也不总是正确的。
parseInt()
用于解析字符串很有用,但你必须意识到它会在第一个非法数字处停止。通过Number()
(参见[函数 Number](ch11.html#function_number “函数 Number”))解析字符串不太宽容,但可能会产生非整数。
以下运算符适用于数字:
number1 + number2
数值相加,除非其中一个操作数是字符串。然后两个操作数都会被转换为字符串并连接在一起(参见[加号运算符(+)](ch09.html#plus_operator “加号运算符(+)”)):
> 3.1 + 4.3
7.4
> 4 + ' messages'
'4 messages'
number1 - number2
减法。
number1 * number2
乘法。
number1 / number2
除法。
number1 % number2
余数:
> 9 % 7
2
> -9 % 7
-2
这个操作不是模运算。它返回一个与第一个操作数相同符号的值(稍后会有更多细节)。
-number
否定其参数。
+number
将其参数保持不变;非数字被转换为数字。
++variable
, --variable
在增加(或减少)1 之后返回变量的当前值:
> var x = 3;
> ++x
4
> x
4
variable++
, variable--
通过 1 来增加(或减少)变量的值并返回它:
> var x = 3;
> x++
3
> x
4
操作数的位置可以帮助你记住它是在增加(或减少)之前还是之后返回的。如果操作数在增加运算符之前,它在增加之前返回。如果操作数在运算符之后,它会增加然后返回。(减量运算符的工作方式类似。)
陷阱:余数运算符(%)不是模运算
余数运算符的结果始终具有第一个操作数的符号(对于模运算,它是第二个操作数的符号):
> -5 % 2
-1
这意味着以下函数不起作用:
// Wrong!
function isOdd(n) {
return n % 2 === 1;
}
console.log(isOdd(-5)); // false
console.log(isOdd(-4)); // false
正确的版本是:
function isOdd(n) {
return Math.abs(n % 2) === 1;
}
console.log(isOdd(-5)); // true
console.log(isOdd(-4)); // false
JavaScript 有几个位运算符,可以处理 32 位整数。也就是说,它们将操作数转换为 32 位整数,并产生一个 32 位整数的结果。这些运算符的用例包括处理二进制协议、特殊算法等。
本节解释了一些概念,这些概念将帮助你理解位运算符。
计算二进制补码(或反码)的两种常见方法是:
补码
通过反转 32 位数字来计算数字x
的补码~x
。让我们通过四位数字来说明补码。1100
的补码是0011
。将一个数字加上它的补码会得到一个所有数字都是 1 的数字:
1 + ~1 = 0001 + 1110 = 1111
二进制补码
数字x
的二进制补码-x
是补码加一。将一个数字加上它的二进制补码会得到0
(忽略最高位之外的溢出)。以下是一个使用四位数字的例子:
1 + -1 = 0001 + 1111 = 0000
32 位整数没有显式的符号,但你仍然可以编码负数。例如,-1 可以编码为 1 的补码:将结果加 1 得到 0(在 32 位内)。正数和负数之间的边界是流动的;4294967295(2³²−1)和-1 在这里是相同的整数。但是,当你将这样的整数从 JavaScript 数字转换到 JavaScript 数字时,你必须决定一个符号,这个符号与隐式符号相对。因此,有符号的 32 位整数被分成两组:
最高位为 0:数字为零或正数。
最高位为 1:数字为负数。
最高位通常称为符号位。因此,4294967295,解释为有符号 32 位整数,当转换为 JavaScript 数字时变为-1:
> ToInt32(4294967295)
-1
ToInt32()
在通过按位操作获取 32 位整数中有解释。
只有无符号右移操作符(>>>
)适用于无符号 32 位整数;所有其他按位操作符适用于有符号 32 位整数。
在以下示例中,我们通过以下两个操作使用二进制数:
parseInt(str, 2)
(参见[通过 parseInt()获取整数](ch11.html#parseInt “Integers via parseInt()”))解析二进制表示法(基数为 2)的字符串str
。例如:
> parseInt('110', 2)
6
num.toString(2)
(参见[Number.prototype.toString(radix?)](ch11.html#Number.prototype.toString “Number.prototype.toString(radix?)”)将数字num
转换为二进制表示的字符串。例如:
> 6..toString(2)
'110'
~number
计算number
的补码:
> (~parseInt('11111111111111111111111111111111', 2)).toString(2)
'0'
JavaScript 有三个二进制按位操作符:
number1 & number2
(按位与):
> (parseInt('11001010', 2) & parseInt('1111', 2)).toString(2)
'1010'
number1 | number2
(按位或):
> (parseInt('11001010', 2) | parseInt('1111', 2)).toString(2)
'11001111'
number1 ^ number2
(按位异或):
> (parseInt('11001010', 2) ^ parseInt('1111', 2)).toString(2)
'11000101'
直观理解二进制按位操作符有两种方式:
每位一个布尔操作。
在以下公式中,n[i]
表示将数字n
的第i
位解释为布尔值(0 为false
,1 为true
)。例如,2[0]
为false
;2[1]
为true
:
And:result[i] = number1[i] && number2[i]
或:result[i] = number1[i] || number2[i]
Xor:result[i] = number1[i] ^^ number2[i]
操作符^^
不存在。如果存在,它将按照以下方式工作(如果操作数中恰好有一个为true
,则结果为true
):
x ^^ y === (x && !y) ||(!x && y)
```
通过`number2`改变`number1`的位
+ And:仅保留`number1`中设置的那些位。这个操作也被称为*掩码*,`number2`是*掩码*。
+ 或:设置`number1`中设置的所有位,并保持所有其他位不变。
+ Xor:反转`number1`中设置的所有位,并保持所有其他位不变。
### 按位移动操作符
JavaScript 有三个按位移动操作符:
+ `number << digitCount`(左移):
```js
> (parseInt('1', 2) << 1).toString(2)
'10'
```
+ `number >> digitCount`(有符号右移):
32 位二进制数被解释为有符号数(参见前面的部分)。向右移动时,符号被保留:
```js
> (parseInt('11111111111111111111111111111110', 2) >> 1).toString(2)
'-1'
```
我们已经右移了-2。结果-1 等同于一个 32 位整数,其所有数字都是 1(1 的补码)。换句话说,向右移动一个数字,负数和正数都会除以 2。
+ `number >>> digitCount`(无符号右移):
```js
> (parseInt('11100', 2) >>> 1).toString(2)
'1110'
```
正如你所看到的,这个操作符从左边补零。
## 函数数字
`Number`函数可以以两种方式调用:
`Number(value)`
作为普通函数,它将`value`转换为原始数字(参见[转换为数字](ch11.html#tonumber "Converting to Number")):
```js
> Number('123')
123
> typeof Number(3) // no change
'number'
new Number(num)
作为构造函数,它创建一个Number
的新实例(参见[原始值的包装对象](ch08.html#wrapper_objects “Wrapper Objects for Primitives”)),一个将num
(在转换为数字后)包装的对象。例如:
> typeof new Number(3)
'object'
前一种调用是常见的。
对象Number
具有以下属性:
Number.MAX_VALUE
可以表示的最大正数。在内部,其分数的所有数字都是 1,指数是最大的,为 1023。如果尝试通过将指数乘以 2 来增加指数,结果将是错误值Infinity
(参见Infinity):
> Number.MAX_VALUE
1.7976931348623157e+308
> Number.MAX_VALUE * 2
Infinity
Number.MIN_VALUE
最小的可表示正数(大于零,一个微小的分数):
> Number.MIN_VALUE
5e-324
Number.NaN
与全局NaN
相同的值。
Number.NEGATIVE_INFINITY
与“-无穷大”相同的值:
> Number.NEGATIVE_INFINITY === -Infinity
true
Number.POSITIVE_INFINITY
与Infinity
相同的值:
> Number.POSITIVE_INFINITY === Infinity
true
所有原始数字的方法都存储在Number.prototype
中(参见Primitives Borrow Their Methods from Wrappers)。
Number.prototype.toFixed(fractionDigits?)
返回一个不带指数的数字表示,四舍五入到fractionDigits
位。如果省略参数,则使用值 0:
> 0.0000003.toFixed(10)
'0.0000003000'
> 0.0000003.toString()
'3e-7'
如果数字大于或等于 10²¹,那么这个方法的工作方式与toString()
相同。您会得到一个用指数表示的数字:
> 1234567890123456789012..toFixed()
'1.2345678901234568e+21'
> 1234567890123456789012..toString()
'1.2345678901234568e+21'
Number.prototype.toPrecision(precision?)
在使用类似于toString()
的转换算法之前,将尾数修剪为precision
位数字。如果没有给出精度,则直接使用toString()
:
> 1234..toPrecision(3)
'1.23e+3'
> 1234..toPrecision(4)
'1234'
> 1234..toPrecision(5)
'1234.0'
> 1.234.toPrecision(3)
'1.23'
您需要指数表示法来显示 1234,精度为三位。
对于Number.prototype.toString(radix?)
,参数radix
表示要显示数字的系统的基数。最常见的基数是 10(十进制)、2(二进制)和 16(十六进制):
> 15..toString(2)
'1111'
> 65535..toString(16)
'ffff'
基数必须至少为 2,最多为 36。任何大于 10 的基数都会导致字母字符被用作数字,这解释了最大 36,因为拉丁字母表有 26 个字符:
> 1234567890..toString(36)
'kf12oi'
全局函数parseInt
(参见Integers via parseInt())允许您将这些表示法转换回数字:
> parseInt('kf12oi', 36)
1234567890
对于基数 10,toString()
在两种情况下使用指数表示法(小数点前有一个数字)。首先,如果小数点前有超过 21 位数字:
> 1234567890123456789012
1.2345678901234568e+21
> 123456789012345678901
123456789012345680000
其次,如果一个数字以0.
开头,后面跟着超过五个零和一个非零数字:
> 0.0000003
3e-7
> 0.000003
0.000003
在所有其他情况下,使用固定表示法。
Number.prototype.toExponential(fractionDigits?)
强制一个数字以指数表示。fractionDigits
是一个介于 0 和 20 之间的数字,用于确定小数点后应显示多少位数字。如果省略,则包括尽可能多的有效数字以唯一指定数字。
在这个例子中,当toString()
也使用指数表示时,我们强制更多的精度。结果是混合的,因为当将二进制数字转换为十进制表示时,我们达到了可以实现的精度限制:
> 1234567890123456789012..toString()
'1.2345678901234568e+21'
> 1234567890123456789012..toExponential(20)
'1.23456789012345677414e+21'
在这个例子中,数字的数量级不够大,无法通过toString()
显示指数。然而,toExponential()
确实显示了一个指数:
> 1234..toString()
'1234'
> 1234..toExponential(5)
'1.23400e+3'
> 1234..toExponential()
'1.234e+3'
在这个例子中,当分数不够小时,我们得到指数表示法:
> 0.003.toString()
'0.003'
> 0.003.toExponential(4)
'3.0000e-3'
> 0.003.toExponential()
'3e-3'
以下函数操作数字:
isFinite(number)
检查number
是否是实际数字(既不是Infinity
也不是NaN
)。详情请参见Checking for Infinity。
isNaN(number)
如果number
是NaN
,则返回true
。详情请参见Pitfall: checking whether a value is NaN。
parseFloat(str)
将str
转换为浮点数。详情请参见parseFloat()。
parseInt(str, radix?)
将str
解析为以radix
为基数的整数(2-36)。详情请参阅通过 parseInt()获取整数。
在编写本章时,我参考了以下来源:
“IEEE 标准 754 浮点数” 由 Steve Hollasch
“数据类型和缩放(固定点块集)” 在 MATLAB 文档中
“IEEE 浮点” 在维基百科上
¹² 来源:Brendan Eich,bit.ly/1lKzQeC
。
¹³ Béla Varga(@netzzwerg)指出 IEEE 754 规定 NaN 不等于自身。
原文:12. Strings
译者:飞龙
协议:CC BY-NC-SA 4.0
字符串是 JavaScript 字符的不可变序列。每个字符都是一个 16 位的 UTF-16 代码单元。这意味着一个 Unicode 字符由一个或两个 JavaScript 字符表示。当您计算字符数或拆分字符串时,您主要需要考虑两个字符的情况(参见第二十四章)。
单引号和双引号都可以用来界定字符串文字:
'He said: "Hello"'
"He said: \"Hello\""
'Everyone\'s a winner'
"Everyone's a winner"
因此,您可以自由地使用任何一种引号。不过,有几点需要考虑:
社区中最常见的风格是在 HTML 中使用双引号,在 JavaScript 中使用单引号。
另一方面,某些语言(例如 C 和 Java)中双引号专门用于字符串。因此,在多语言代码库中使用它们可能是有意义的。
对于 JSON(在第二十二章中讨论),您必须使用双引号。
如果您一贯使用引号,您的代码看起来会更整洁。但有时,不同的引号意味着您不必转义,这可以证明您不那么一致是合理的(例如,您可能通常使用单引号,但暂时切换到双引号来编写前面例子的最后一个)。
字符串文字中的大多数字符只是代表它们自己。反斜杠用于转义并启用了一些特殊功能:
行继续
您可以通过用反斜杠转义行尾(行终止字符,行终止符)来将字符串分布在多行上:
var str = 'written \
over \
multiple \
lines';
console.log(str === 'written over multiple lines'); // true
另一种方法是使用加号运算符进行连接:
var str = 'written ' +
'over ' +
'multiple ' +
'lines';
字符转义序列
这些序列以反斜杠开头:
控制字符:\b
是一个退格,\f
是一个换页符,\n
是一个换行符(新行),\r
是一个回车,\t
是一个水平制表符,\v
是一个垂直制表符。
转义字符代表它们自己:\'
是一个单引号,\"
是一个双引号,\\
是一个反斜杠。除了b f n r t v x u
和十进制数字之外,所有字符也代表它们自己。以下是两个例子:
> '\"'
'"'
> '\q'
'q'
NUL 字符(Unicode 代码点 0)
这个字符由\0
表示。
十六进制转义序列
\xHH
(HH
是两个十六进制数字)指定了一个 ASCII 码的字符。例如:
> '\x4D'
'M'
Unicode 转义序列
\uHHHH
(HHHH
是四个十六进制数字)指定了一个 UTF-16 代码单元(参见第二十四章)。以下是两个例子:
> '\u004D'
'M'
> '\u03C0'
'π'
有两个操作可以返回字符串的第n个字符。请注意,JavaScript 没有专门的字符数据类型;这些操作返回字符串:
> 'abc'.charAt(1)
'b'
> 'abc'[1]
'b'
一些较旧的浏览器不支持通过方括号进行类似数组的字符访问。
值将按以下方式转换为字符串:
值 | 结果 |
---|---|
undefined → 'undefined' |
|
null → 'null' |
|
布尔值 | false → 'false' |
true → 'true' |
|
数字 | 作为字符串的数字(例如,3.141 → '3.141' ) |
字符串 | 与输入相同(无需转换) |
对象 | 调用ToPrimitive(value, String) (请参阅算法:ToPrimitive()——将值转换为原始值)并转换生成的原始值。 |
将三种将任何值转换为字符串的最常见方法是:
| String(value)
| (作为函数调用,而不是作为构造函数) |
| ''+value
| |
| value.toString()
| (对于undefined
和null
不起作用!) |
我更喜欢String()
,因为它更具描述性。以下是一些示例:
> String(false)
'false'
> String(7.35)
'7.35'
> String({ first: 'John', last: 'Doe' })
'[object Object]'
> String([ 'a', 'b', 'c' ])
'a,b,c'
请注意,对于显示数据,JSON.stringify()
(JSON.stringify(value, replacer?, space?))通常比规范的字符串转换效果更好:
> console.log(JSON.stringify({ first: 'John', last: 'Doe' }))
{"first":"John","last":"Doe"}
> console.log(JSON.stringify([ 'a', 'b', 'c' ]))
["a","b","c"]
当然,您必须意识到JSON.stringify()
的局限性——它并不总是显示所有内容。例如,它隐藏了它无法处理的属性的值(函数等!)。另一方面,它的输出可以被eval()
解析,并且可以将深度嵌套的数据显示为格式良好的树。
考虑到 JavaScript 自动转换的频率,遗憾的是转换并不总是可逆的,特别是在布尔值方面:
> String(false)
'false'
> Boolean('false')
true
对于undefined
和null
,我们面临类似的问题。
有两种比较字符串的方法。首先,您可以使用比较运算符:<
,>
,===
,<=
,>=
。它们有以下缺点:
它们区分大小写:
> 'B' > 'A' // ok
true
> 'B' > 'a' // should be true
false
它们不能很好地处理变音符和重音符号:
> 'ä' < 'b' // should be true
false
> 'é' < 'f' // should be true
false
其次,您可以使用String.prototype.localeCompare(other)
,这往往更好,但并不总是受支持(有关详细信息,请参阅搜索和比较)。以下是 Firefox 控制台中的交互:
> 'B'.localeCompare('A')
2
> 'B'.localeCompare('a')
2
> 'ä'.localeCompare('b')
-2
> 'é'.localeCompare('f')
-2
小于零的结果意味着接收器“小于”参数。大于零的结果意味着接收器“大于”参数。
有两种主要的字符串连接方法。
运算符+
在其操作数之一是字符串时进行字符串连接。如果要在变量中收集字符串片段,则复合赋值运算符+=
很有用:
> var str = '';
> str += 'Say hello ';
> str += 7;
> str += ' times fast!';
> str
'Say hello 7 times fast!'
似乎以前的方法每次添加一个片段到str
时都会创建一个新的字符串。旧的 JavaScript 引擎是这样做的,这意味着您可以通过首先将所有片段收集到一个数组中,然后作为最后一步连接它们来提高字符串连接的性能:
> var arr = [];
> arr.push('Say hello ');
> arr.push(7);
> arr.push(' times fast');
> arr.join('')
'Say hello 7 times fast'
然而,较新的引擎通过+
优化字符串连接,并在内部使用类似的方法。因此,在这些引擎上,加号运算符的速度更快。
函数String
可以以两种方式调用:
String(value)
作为普通函数,它将value
转换为原始字符串(请参阅转换为字符串):
> String(123)
'123'
> typeof String('abc') // no change
'string'
new String(str)
作为构造函数,它创建String
的新实例(请参阅原始值的包装对象),一个包装str
的对象(非字符串被强制转换为字符串)。例如:
> typeof new String('abc')
'object'
前一种调用是常见的。
String.fromCharCode(codeUnit1, codeUnit2, ...)
生成一个字符串,其字符由 16 位无符号整数codeUnit1
,codeUnit2
等指定的 UTF-16 代码单元组成。例如:
> String.fromCharCode(97, 98, 99)
'abc'
如果要将数字数组转换为字符串,可以通过apply()
(请参阅func.apply(thisValue, argArray))来实现:
> String.fromCharCode.apply(null, [97, 98, 99])
'abc'
String.fromCharCode()
的反函数是String.prototype.charCodeAt()
。
length
属性指示字符串中的 JavaScript 字符数,并且是不可变的:
> 'abc'.length
3
原始字符串的所有原始字符串方法都存储在String.prototype
中(参见原始通过包装器借用其方法)。接下来,我描述了它们如何用于原始字符串,而不是String
的实例。
以下方法从接收者中提取子字符串:
String.prototype.charAt(pos)
返回位置pos
处的字符。例如:
> 'abc'.charAt(1)
'b'
以下两个表达式返回相同的结果,但一些较旧的 JavaScript 引擎只支持使用charAt()
来访问字符:
str.charAt(n)
str[n]
String.prototype.charCodeAt(pos)
返回 JavaScript 字符(UTF-16 代码单元;参见第二十四章)在位置pos
处的代码(一个 16 位无符号整数)。
这是如何创建字符代码数组的:
> 'abc'.split('').map(function (x) { return x.charCodeAt(0) })
[ 97, 98, 99 ]
charCodeAt()
的反函数是String.fromCharCode()
。
String.prototype.slice(start, end?)
返回从位置start
开始到位置end
之前的子字符串。这两个参数都可以是负数,然后它们的长度将被添加到它们中:
> 'abc'.slice(2)
'c'
> 'abc'.slice(1, 2)
'b'
> 'abc'.slice(-2)
'bc'
String.prototype.substring(start, end?)
应该避免使用slice()
,它类似,但可以处理负位置,并且在各个浏览器中实现更一致。
String.prototype.split(separator?, limit?)
提取由separator
分隔的接收者的子字符串,并将它们作为数组返回。该方法有两个参数:
separator
:要么是一个字符串,要么是一个正则表达式。如果缺失,将返回完整的字符串,包裹在一个数组中。
limit
:如果给定,返回的数组最多包含limit
个元素。
以下是一些示例:
> 'a, b,c, d'.split(',') // string
[ 'a', ' b', 'c', ' d' ]
> 'a, b,c, d'.split(/,/) // simple regular expression
[ 'a', ' b', 'c', ' d' ]
> 'a, b,c, d'.split(/, */) // more complex regular expression
[ 'a', 'b', 'c', 'd' ]
> 'a, b,c, d'.split(/, */, 2) // setting a limit
[ 'a', 'b' ]
> 'test'.split() // no separator provided
[ 'test' ]
如果有一个组,那么匹配项也会作为数组元素返回:
> 'a, b , '.split(/(,)/)
[ 'a', ',', ' b ', ',', ' ' ]
> 'a, b , '.split(/ *(,) */)
[ 'a', ',', 'b', ',', '' ]
使用''
(空字符串)作为分隔符,以产生一个包含字符串字符的数组:
> 'abc'.split('')
[ 'a', 'b', 'c' ]
前一节是关于提取子字符串,而这一节是关于将给定的字符串转换为新字符串。这些方法通常如下使用:
var str = str.trim();
换句话说,原始字符串在(非破坏性地)转换后被丢弃:
String.prototype.trim()
从字符串的开头和结尾删除所有空格:
> '\r\nabc \t'.trim()
'abc'
String.prototype.concat(str1?, str2?, ...)
返回接收者和str1
、str2
等的连接:
> 'hello'.concat(' ', 'world', '!')
'hello world!'
String.prototype.toLowerCase()
创建一个新字符串,其中包含所有原始字符串的字符转换为小写:
> 'MJÖLNIR'.toLowerCase()
'mjölnir'
String.prototype.toLocaleLowerCase()
与toLowerCase()
相同,但遵守当前区域设置的规则。根据 ECMAScript 规范:“只有在少数情况下(如土耳其语)语言的规则与常规 Unicode 大小写映射冲突时才会有差异。”
String.prototype.toUpperCase()
创建一个新字符串,其中包含所有原始字符串的字符转换为大写:
> 'mjölnir'.toUpperCase()
'MJÖLNIR'
String.prototype.toLocaleUpperCase()
与toUpperCase()
相同,但遵守当前区域设置的规则。
以下方法用于搜索和比较字符串:
String.prototype.indexOf(searchString, position?)
从position
(默认为 0)开始搜索searchString
。它返回searchString
被找到的位置,或者-1(如果找不到):
> 'aXaX'.indexOf('X')
1
> 'aXaX'.indexOf('X', 2)
3
请注意,当涉及在字符串中查找文本时,正则表达式同样有效。例如,以下两个表达式是等价的:
str.indexOf('abc') >= 0
/abc/.test(str)
String.prototype.lastIndexOf(searchString, position?)
从position
(默认为末尾)开始向后搜索searchString
。它返回searchString
被找到的位置,或者-1(如果找不到):
> 'aXaX'.lastIndexOf('X')
3
> 'aXaX'.lastIndexOf('X', 2)
1
String.prototype.localeCompare(other)
对字符串与other
进行区域敏感比较。它返回一个数字:
< 0 如果字符串在other
之前
= 0 如果字符串等同于other
如果字符串在
other
之后
例如:
> 'apple'.localeCompare('banana')
-2
> 'apple'.localeCompare('apple')
0
并非所有 JavaScript 引擎都正确实现了这种方法。有些只是基于比较运算符。然而,ECMAScript 国际化 API(参见ECMAScript 国际化 API)提供了一个基于 Unicode 的实现。也就是说,如果引擎中有这个 API,localeCompare()
将起作用。
如果支持,localeCompare()
比比较运算符更适合比较字符串。请参阅比较字符串了解更多信息。
以下方法适用于正则表达式:
String.prototype.search(regexp)
(在字符串原型搜索:有匹配的索引是什么?中更详细地解释)
返回regexp
在接收者中匹配的第一个索引(如果没有匹配,则返回-1):
> '-yy-xxx-y-'.search(/x+/)
4
String.prototype.match(regexp)
(在字符串原型匹配:捕获组或返回所有匹配的子字符串中更详细地解释)
匹配给定的正则表达式与接收者。如果未设置regexp
的标志/g
,则返回第一个匹配的匹配对象:
> '-abb--aaab-'.match(/(a+)b/)
[ 'ab',
'a',
index: 1,
input: '-abb--aaab-' ]
如果标志/g
被设置,那么所有完整的匹配(第 0 组)将以数组的形式返回:
> '-abb--aaab-'.match(/(a+)b/g)
[ 'ab', 'aaab' ]
String.prototype.replace(search, replacement)
(在字符串原型替换:搜索和替换中更详细地解释)
搜索search
并用replacement
替换它。search
可以是一个字符串或一个正则表达式,replacement
可以是一个字符串或一个函数。除非您使用一个设置了标志/g
的正则表达式作为search
,否则只会替换第一个出现的:
> 'iixxxixx'.replace('i', 'o')
'oixxxixx'
> 'iixxxixx'.replace(/i/, 'o')
'oixxxixx'
> 'iixxxixx'.replace(/i/g, 'o')
'ooxxxoxx'
替换字符串中的美元符号($
)允许您引用完整的匹配或捕获的组:
> 'iixxxixx'.replace(/i+/g, '($&)') // complete match
'(ii)xxx(i)xx'
> 'iixxxixx'.replace(/(i+)/g, '($1)') // group 1
'(ii)xxx(i)xx'
您还可以通过函数计算替换:
> function repl(all) { return '('+all.toUpperCase()+')' }
> 'axbbyyxaa'.repl(/a+|b+/g, replacement)
'(A)x(BB)yyx(AA)'
¹⁴ 严格来说,JavaScript 字符串由一系列 UTF-16 代码单元组成。也就是说,JavaScript 字符是 Unicode 代码单元(参见第二十四章)。
原文:13. Statements
译者:飞龙
协议:CC BY-NC-SA 4.0
本章涵盖了 JavaScript 的语句:变量声明、循环、条件语句等。
var
用于声明一个变量,它创建变量并使您能够使用它。等号(=
)用于给它赋值:
var foo;
foo = 'abc';
var
还允许您将前面的两个语句合并为一个:
var foo = 'abc';
最后,您还可以将多个var
语句合并为一个:
var x, y=123, z;
了解有关变量如何工作的更多信息,请阅读第十六章。
复合语句,如循环和条件语句,嵌入了一个或多个“主体”——例如,while
循环:
while («condition»)
«statement»
对于«statement»
主体,您有选择。您可以使用单个语句:
while (x >= 0) x--;
或者您可以使用一个块(它算作一个单独的语句):
while (x > 0) {
x--;
}
如果要使主体包含多个语句,您需要使用一个块。除非完整的复合语句可以写在一行中,否则我建议使用一个块。
本节探讨了 JavaScript 的循环语句。
以下机制可以与所有循环一起使用:
break ⟦«label»⟧
退出循环。
continue ⟦«label»⟧
停止当前循环迭代,并立即继续下一个。
标签
标签是一个标识符,后面跟着一个冒号。在循环前,标签允许您即使从嵌套在其中的循环中也可以中断或继续该循环。在块的前面,您可以跳出该块。在这两种情况下,标签的名称成为break
或continue
的参数。这是一个打破块的例子:
function findEvenNumber(arr) {
loop: { // label
for (var i=0; i<arr.length; i++) {
var elem = arr[i];
if ((elem % 2) === 0) {
console.log('Found: ' + elem);
break loop;
}
}
console.log('No even number found.');
}
console.log('DONE');
}
一个while
循环:
while («condition»)
«statement»
只要condition
成立,就执行statement
。如果condition
始终为true
,则会得到一个无限循环:
while (true) { ... }
在以下示例中,我们删除数组的所有元素并将它们记录到控制台:
var arr = [ 'a', 'b', 'c' ];
while (arr.length > 0) {
console.log(arr.shift());
}
这是输出:
a
b
c
一个do-while
循环:
do «statement»
while («condition»);
至少执行statement
一次,然后只要condition
成立。例如:
var line;
do {
line = prompt('Enter a number:');
} while (!/^[0-9]+$/.test(line));
在for
循环中:
for (⟦«init»⟧; ⟦«condition»⟧; ⟦«post_iteration»⟧)
«statement»
init
在循环之前执行一次,只要condition
为true
,循环就会继续。您可以在init
中使用var
声明变量,但是这些变量的作用域始终是完整的周围函数。post_iteration
在循环的每次迭代之后执行。考虑到所有这些,前面的循环等同于以下while
循环:
«init»;
while («condition») {
«statement»
«post_iteration»;
}
以下示例是迭代数组的传统方法(其他可能性在最佳实践:迭代数组中描述):
var arr = [ 'a', 'b', 'c' ];
for (var i=0; i<arr.length; i++) {
console.log(arr[i]);
}
如果您省略头部的所有部分,for
循环将变得无限:
for (;;) {
...
}
一个for-in
循环:
for («variable» in «object»)
«statement»
遍历object
的所有属性键,包括继承的属性。但是,标记为不可枚举的属性将被忽略(参见属性属性和属性描述符)。以下规则适用于for-in
循环:
您可以使用var
声明变量,但是这些变量的作用域始终是完整的周围函数。
在迭代期间可以删除属性。
不要使用for-in
来遍历数组。首先,它遍历索引,而不是值:
> var arr = [ 'a', 'b', 'c' ];
> for (var key in arr) { console.log(key); }
0
1
2
其次,它还遍历所有(非索引)属性键。以下示例说明了当您向数组添加属性foo
时会发生什么:
> var arr = [ 'a', 'b', 'c' ];
> arr.foo = true;
> for (var key in arr) { console.log(key); }
0
1
2
foo
因此,最好使用普通的for
循环或数组方法forEach()
(参见最佳实践:迭代数组)。
for-in
循环遍历所有(可枚举)属性,包括继承的属性。这可能不是您想要的。让我们使用以下构造函数来说明问题:
function Person(name) {
this.name = name;
}
Person.prototype.describe = function () {
return 'Name: '+this.name;
};
Person
的实例从Person.prototype
继承了属性describe
,这是由for-in
看到的:
var person = new Person('Jane');
for (var key in person) {
console.log(key);
}
这是输出:
name
describe
通常,使用for-in
的最佳方法是通过hasOwnProperty()
跳过继承的属性:
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
这是输出:
name
还有一个最后的警告:person
可能有一个hasOwnProperty
属性,这将阻止检查起作用。为了安全起见,您必须直接引用通用方法(参见通用方法:从原型中借用方法)Object.prototype.hasOwnProperty
:
for (var key in person) {
if (Object.prototype.hasOwnProperty.call(person, key)) {
console.log(key);
}
}
还有其他更舒适的方法可以遍历属性键,这些方法在最佳实践:遍历自有属性中有描述。
这个循环只存在于 Firefox 上。不要使用它。
本节涵盖了 JavaScript 的条件语句。
在if-then-else
语句中:
if («condition»)
«then_branch»
⟦else
«else_branch»⟧
then_branch
和else_branch
可以是单个语句或语句块(参见循环和条件的主体)。
您可以链接几个if
语句:
if (s1 > s2) {
return 1;
} else if (s1 < s2) {
return -1;
} else {
return 0;
}
请注意,在前面的例子中,所有的else
分支都是单个语句(if
语句)。只允许else
分支为块的编程语言需要一些类似else-if
分支的东西来进行链接。
以下示例的else
分支被称为“悬空”,因为不清楚它属于两个if
语句中的哪一个:
if («cond1») if («cond2») «stmt1» else «stmt2»
这是一个简单的规则:使用大括号。前面的片段等同于以下代码(在这里很明显else
属于谁):
if («cond1») {
if («cond2») {
«stmt1»
} else {
«stmt2»
}
}
一个switch
语句:
switch («expression») {
case «label1_1»:
case «label1_2»:
...
«statements1»
⟦break;⟧
case «label2_1»:
case «label2_2»:
...
«statements2»
⟦break;⟧
...
⟦default:
«statements_default»
⟦break;⟧⟧
}
评估expression
,然后跳转到与结果匹配的case
子句。如果没有匹配的标签,switch
会跳转到default
子句(如果存在)或者不执行任何操作。
case
后的“操作数”可以是任何表达式;它通过===
与switch
的参数进行比较。
如果不使用终止语句结束子句,执行将继续到下一个子句。最常用的终止语句是break
。但是return
和throw
也可以工作,尽管它们通常不仅仅离开switch
语句。
以下示例说明了如果使用throw
或return
,则不需要break
:
function divide(dividend, divisor) {
switch (divisor) {
case 0:
throw 'Division by zero';
default:
return dividend / divisor;
}
}
在这个例子中,没有default
子句。因此,如果fruit
不匹配任何case
标签,则什么也不会发生:
function useFruit(fruit) {
switch (fruit) {
case 'apple':
makeCider();
break;
case 'grape':
makeWine();
break;
// neither apple nor grape: do nothing
}
}
在这里,有多个连续的case
标签:
function categorizeColor(color) {
var result;
switch (color) {
case 'red':
case 'yellow':
case 'blue':
result = 'Primary color: '+color;
break;
case 'or':
case 'green':
case 'violet':
result = 'Secondary color: '+color;
break;
case 'black':
case 'white':
result = 'Not a color';
break;
default:
throw 'Illegal argument: '+color;
}
console.log(result);
}
这个例子演示了case
后面的值可以是任意表达式:
function compare(x, y) {
switch (true) {
case x < y:
return -1;
case x === y:
return 0;
default:
return 1;
}
}
前面的switch
语句通过遍历case
子句来寻找其参数true
的匹配项。如果其中一个case
表达式求值为true
,则执行相应的case
主体。因此,前面的代码等同于以下if
语句:
function compare(x, y) {
if (x < y) {
return -1;
} else if (x === y) {
return 0;
} else {
return 1;
}
}
通常应该更喜欢后一种解决方案;它更加自解释。
with
语句本节解释了with
语句在 JavaScript 中的工作原理以及为什么不鼓励使用它。
with
语句的语法如下:
with («object»)
«statement»
它将object
的属性转换为statement
的局部变量。例如:
var obj = { first: 'John' };
with (obj) {
console.log('Hello '+first); // Hello John
}
它的预期用途是在多次访问对象时避免冗余。以下是一个带有冗余的代码示例:
foo.bar.baz.bla = 123;
foo.bar.baz.yadda = 'abc';
with
使这更短:
with (foo.bar.baz) {
bla = 123;
yadda = 'abc';
}
with
语句已被弃用通常不鼓励使用with
语句(下一节解释了原因)。例如,在严格模式下是禁止的:
> function foo() { 'use strict'; with ({}); }
SyntaxError: strict mode code may not contain 'with' statements
with
语句的技巧避免这样的代码:
// Don't do this:
with (foo.bar.baz) {
console.log('Hello '+first+' '+last);
}
而是使用一个短名称的临时变量:
var b = foo.bar.baz;
console.log('Hello '+b.first+' '+b.last);
如果您不想将临时变量b
暴露给当前作用域,可以使用 IIFE(参见通过 IIFE 引入新作用域):
(function () {
var b = foo.bar.baz;
console.log('Hello '+b.first+' '+b.last);
}());
您还可以选择将要访问的对象作为 IIFE 的参数:
(function (b) {
console.log('Hello '+b.first+' '+b.last);
}(foo.bar.baz));
要理解为什么with
被弃用,请看下面的例子,并注意函数的参数如何完全改变了它的工作方式:
function logit(msg, opts) {
with (opts) {
console.log('msg: '+msg); // (1)
}
}
如果opts
有一个msg
属性,那么第(1)行的语句不再访问参数msg
。它访问属性:
> logit('hello', {}) // parameter msg
msg: hello
> logit('hello', { msg: 'world' }) // property opts.msg
msg: world
with
语句引起了三个问题:
性能下降
变量查找变慢,因为对象被临时插入到作用域链中。
代码变得不太可预测
您无法通过查看其语法环境(其词法上下文)来确定标识符指的是什么。根据Brendan Eich的说法,这才是with
被弃用的实际原因,而不是性能考虑:
with
违反了词法作用域,使程序分析(例如安全性)变得困难或不可行。
缩小器(在第三十二章中描述)无法缩短变量名
在with
语句内部,无法静态确定名称是指变量还是属性。缩小器只能重命名变量。
以下是with
使代码变得脆弱的示例:
function foo(someArray) {
var values = ...; // (1)
with (someArray) {
values.someMethod(...); // (2)
...
}
}
foo(myData); // (3)
即使您无法访问数组myData
,也可以阻止行(3)中的函数调用起作用。
如何?通过向Array.prototype
添加一个属性values
。例如:
Array.prototype.values = function () {
...
};
现在,行(2)中的代码调用someArray.values.someMethod()
而不是values.someMethod()
。原因是,在with
语句内,values
现在指的是someArray.values
,而不再是行(1)中的局部变量。
这不仅仅是一个思想实验:数组方法values()
已添加到 Firefox 并破坏了 TYPO3 内容管理系统。Brandon Benvie 找出了问题所在。
debugger
语句的语法如下:
debugger;
如果调试器处于活动状态,此语句将作为断点;如果没有,它没有可观察的效果。
原文:14. Exception Handling
译者:飞龙
协议:CC BY-NC-SA 4.0
本章描述了 JavaScript 的异常处理工作原理。它从异常处理的一般解释开始。
在异常处理中,通常会将紧密耦合的语句分组在一起。如果在执行这些语句时,其中一个导致错误,那么继续执行剩余的语句就没有意义了。相反,您尝试尽可能优雅地从错误中恢复。这在某种程度上类似于事务(但没有原子性)。
让我们来看一下没有异常处理的代码:
function processFiles() {
var fileNames = collectFileNames();
var entries = extractAllEntries(fileNames);
processEntries(entries);
}
function extractAllEntries(fileNames) {
var allEntries = new Entries();
fileNames.forEach(function (fileName) {
var entry = extractOneEntry(fileName);
allEntries.add(entry); // (1)
});
}
function extractOneEntry(fileName) {
var file = openFile(fileName); // (2)
...
}
...
在(2)处的openFile()
中,对错误做出反应的最佳方法是什么?显然,不应再执行语句(1)。但我们也不想中止extractAllEntries()
。相反,足够的是跳过当前文件并继续下一个。为此,我们在先前的代码中添加异常处理:
function extractAllEntries(fileNames) {
var allEntries = new Entries();
fileNames.forEach(function (fileName) {
try {
var entry = extractOneEntry(fileName);
allEntries.add(entry);
} catch (exception) { // (2)
errorLog.log('Error in '+fileName, exception);
}
});
}
function extractOneEntry(fileName) {
var file = openFile(fileName);
...
}
function openFile(fileName) {
if (!exists(fileName)) {
throw new Error('Could not find file '+fileName); // (1)
}
...
}
异常处理有两个方面:
如果在发生错误的地方无法有意义地处理问题,请抛出异常。
找到可以处理错误的地方:捕获异常。
在(1)处,以下结构是活动的:
processFile()
extractAllEntries(...)
fileNames.forEach(...)
function (fileName) { ... }
try { ... } catch (exception) { ... }
extractOneEntry(...)
openFile(...)
在(1)处的throw
语句沿着树向上走,并离开所有结构,直到遇到一个活动的try
语句。然后调用该语句的catch
块并将异常值传递给它。
JavaScript 中的异常处理与大多数编程语言一样:try
语句将语句分组,并允许您拦截这些语句中的异常。
throw
的语法如下:
throw «value»;
任何 JavaScript 值都可以被抛出。为了简单起见,许多 JavaScript 程序只抛出字符串:
// Don't do this
if (somethingBadHappened) {
throw 'Something bad happened';
}
不要这样做。JavaScript 有专门的异常对象构造函数(参见错误构造函数)。使用它们或对其进行子类化(参见第二十八章)。它们的优势是 JavaScript 会自动添加堆栈跟踪(在大多数引擎上),并且它们有额外的上下文特定属性的空间。最简单的解决方案是使用内置构造函数Error()
:
if (somethingBadHappened) {
throw new Error('Something bad happened');
}
try-catch-finally
的语法如下。try
是必需的,catch
和finally
至少有一个也必须存在:
try {
«try_statements»
}
⟦catch («exceptionVar») {
«catch_statements»
}⟧
⟦finally {
«finally_statements»
}⟧
它是如何工作的:
catch
捕获在try_statements
中抛出的任何异常,无论是直接抛出还是在它们调用的函数中。提示:如果要区分不同类型的异常,可以使用constructor
属性来切换异常的构造函数(请参阅构造函数属性的用例)。
finally
总是被执行,无论try_statements
中发生了什么(或者它们调用的函数中发生了什么)。用它来进行应该始终执行的清理操作,无论try_statements
中发生了什么:
var resource = allocateResource();
try {
...
} finally {
resource.deallocate();
}
如果try_statements
中有一个return
,则try
块会在之后执行(在离开函数或方法之前立即执行;请参阅接下来的示例)。
任何值都可以被抛出:
function throwIt(exception) {
try {
throw exception;
} catch (e) {
console.log('Caught: '+e);
}
}
以下是交互:
> throwIt(3);
Caught: 3
> throwIt('hello');
Caught: hello
> throwIt(new Error('An error happened'));
Caught: Error: An error happened
finally
总是被执行:
function throwsError() {
throw new Error('Sorry...');
}
function cleansUp() {
try {
throwsError();
} finally {
console.log('Performing clean-up');
}
}
以下是交互:
> cleansUp();
Performing clean-up
Error: Sorry...
finally
在return
语句之后执行:
function idLog(x) {
try {
console.log(x);
return 'result';
} finally {
console.log("FINALLY");
}
}
以下是交互:
> idLog('arg')
arg
FINALLY
'result'
在执行finally
之前,返回值已排队:
var count = 0;
function countUp() {
try {
return count;
} finally {
count++; // (1)
}
}
在执行语句(1)时,count
的值已经排队返回:
> countUp()
0
> count
1
ECMAScript 标准化以下错误构造函数。描述摘自 ECMAScript 5 规范:
Error
是错误的通用构造函数。这里提到的所有其他错误构造函数都是子构造函数。
EvalError
“在本规范中当前未使用。此对象保留用于与本规范先前版本的兼容性。”
RangeError
“表示数字值超出了允许的范围。”例如:
> new Array(-1)
RangeError: Invalid array length
ReferenceError
“表示检测到无效引用值。”通常,这是一个未知的变量。例如:
> unknownVariable
ReferenceError: unknownVariable is not defined
SyntaxError
“表示发生了解析错误”——例如,通过eval()
解析代码时:
> eval('3 +')
SyntaxError: Unexpected end of file
TypeError
“表示操作数的实际类型与预期类型不同。”例如:
> undefined.foo
TypeError: Cannot read property 'foo' of undefined
URIError
“表示以与其定义不兼容的方式使用了全局 URI 处理函数之一。”例如:
> decodeURI('%2')
URIError: URI malformed
以下是错误的属性:
message
错误消息。
name
错误的名称。
stack
堆栈跟踪。这是非标准的,但在许多平台上都可用,例如 Chrome,Node.js 和 Firefox。
错误的常见来源要么是外部的(错误的输入,丢失的文件等),要么是内部的(程序中的错误)。特别是在后一种情况下,您将收到意外的异常并需要进行调试。通常情况下,您没有运行调试器。对于“手动”调试,有两条信息是有帮助的:
数据:变量具有什么值?
执行:异常发生在哪一行,活动的函数调用是什么?
您可以将第一项(数据)的一些内容放入消息或异常对象的属性中。第二项(执行)在许多 JavaScript 引擎上通过堆栈跟踪得到支持,这是在创建异常对象时调用堆栈的快照。以下示例打印堆栈跟踪:
function catchit() {
try {
throwit();
} catch(e) {
console.log(e.stack); // print stack trace
}
}
function throwit() {
throw new Error('');
}
以下是交互:
> catchit()
Error
at throwit (~/examples/throwcatch.js:9:11)
at catchit (~/examples/throwcatch.js:3:9)
at repl:1:5
如果您想要堆栈跟踪,您需要内置错误构造函数的服务。您可以使用现有构造函数并将自己的数据附加到其中。或者您可以创建一个子构造函数,其实例可以通过instanceof
与其他错误构造函数的实例区分开来。然而,这样做(对于内置构造函数)是复杂的;请参阅第二十八章以了解如何做到这一点。
原文:15. Functions
译者:飞龙
协议:CC BY-NC-SA 4.0
函数是可以调用的值。定义函数的一种方式称为函数声明。例如,以下代码定义了具有单个参数x
的函数id
:
function id(x) {
return x;
}
return
语句从id
返回一个值。您可以通过提及其名称,后跟括号中的参数来调用函数:
> id('hello')
'hello'
如果您从函数中不返回任何内容,则返回undefined
(隐式):
> function f() { }
> f()
undefined
本节仅展示了定义函数的一种方式和调用函数的一种方式。其他方式将在后面描述。
一旦您像刚才所示那样定义了一个函数,它可以扮演多种角色:
非方法函数(“普通函数”)
您可以直接调用函数。然后它将作为普通函数工作。以下是一个示例调用:
id('hello')
按照惯例,普通函数的名称以小写字母开头。
构造函数
您可以通过new
运算符调用函数。然后它变成一个构造函数,一个对象的工厂。以下是一个示例调用:
new Date()
按照惯例,构造函数的名称以大写字母开头。
方法
您可以将函数存储在对象的属性中,这将使其成为一个方法,您可以通过该对象调用它。以下是一个示例调用:
obj.method()
按照惯例,方法的名称以小写字母开头。
非方法函数在本章中有解释;构造函数和方法在第十七章中有解释。
术语参数和参数通常可以互换使用,因为上下文通常可以清楚地表明所需的含义。以下是区分它们的一个经验法则。
参数用于定义函数。它们也被称为形式参数和形式参数。在下面的例子中,param1
和param2
是参数:
function foo(param1, param2) {
...
}
参数用于调用函数。它们也被称为实际参数和实际参数。在下面的例子中,3
和7
是参数:
foo(3, 7);
本节描述了创建函数的三种方法:
通过函数表达式
通过函数声明
通过构造函数Function()
所有函数都是对象,是Function
的实例:
function id(x) {
return x;
}
console.log(id instanceof Function); // true
因此,函数从Function.prototype
获取它们的方法。
函数表达式产生一个值 - 一个函数对象。例如:
var add = function (x, y) { return x + y };
console.log(add(2, 3)); // 5
前面的代码将函数表达式的结果分配给变量add
,并通过该变量调用它。函数表达式产生的值可以分配给一个变量(如最后一个例子中所示),作为另一个函数的参数传递,等等。因为普通函数表达式没有名称,它们也被称为匿名函数表达式。
您可以给函数表达式一个名称。命名函数表达式允许函数表达式引用自身,这对于自我递归很有用:
var fac = function me(n) {
if (n > 0) {
return n * me(n-1);
} else {
return 1;
}
};
console.log(fac(3)); // 6
命名函数表达式的名称只能在函数表达式内部访问:
var repeat = function me(n, str) {
return n > 0 ? str + me(n-1, str) : '';
};
console.log(repeat(3, 'Yeah')); // YeahYeahYeah
console.log(me); // ReferenceError: me is not defined
以下是一个函数声明:
function add(x, y) {
return x + y;
}
前面的代码看起来像一个函数表达式,但它是一个语句(参见表达式与语句)。它大致相当于以下代码:
var add = function (x, y) {
return x + y;
};
换句话说,函数声明声明一个新变量,创建一个函数对象,并将其分配给变量。
构造函数Function()
评估存储在字符串中的 JavaScript 代码。例如,以下代码等同于前面的例子:
var add = new Function('x', 'y', 'return x + y');
然而,这种定义函数的方式很慢,并且将代码保留在字符串中(无法访问工具)。因此,最好尽可能使用函数表达式或函数声明。使用 new Function()评估代码更详细地解释了Function()
;它的工作方式类似于eval()
。
提升意味着“移动到作用域的开头”。函数声明完全提升,变量声明只部分提升。
函数声明完全被提升。这允许您在声明之前调用函数:
foo();
function foo() { // this function is hoisted
...
}
前面的代码之所以有效是因为 JavaScript 引擎将foo
的声明移动到作用域的开头。它们执行代码,就好像它看起来是这样的:
function foo() {
...
}
foo();
var
声明也会被提升,但只有声明,而不是使用它们进行的赋值。因此,类似于前面的例子使用var
声明和函数表达式会导致错误:
foo(); // TypeError: undefined is not a function
var foo = function foo() {
...
};
只有变量声明被提升。引擎执行前面的代码如下:
var foo;
foo(); // TypeError: undefined is not a function
foo = function foo() {
...
};
大多数 JavaScript 引擎支持函数对象的非标准属性name
。函数声明具有它:
> function f1() {}
> f1.name
'f1'
匿名函数表达式的名称是空字符串:
> var f2 = function () {};
> f2.name
''
然而,命名函数表达式确实有一个名称:
> var f3 = function myName() {};
> f3.name
'myName'
函数的名称对于调试很有用。有些人总是给他们的函数表达式命名。
您是否更喜欢以下的函数声明?
function id(x) {
return x;
}
或者等效的var
声明加上函数表达式的组合?
var id = function (x) {
return x;
};
它们基本上是相同的,但是函数声明比函数表达式有两个优点:
它们被提升(参见提升),因此您可以在它们出现在源代码中之前调用它们。
它们有一个名称(请参见[函数的名称](ch15.html#function_names “函数的名称”))。但是,JavaScript 引擎正在更好地推断匿名函数表达式的名称。
call()
,apply()
和bind()
是所有函数都具有的方法(请记住函数是对象,因此具有方法)。它们可以在调用方法时提供this
的值,因此主要在面向对象的上下文中很有趣(参见[调用函数时设置 this:call(),apply()和 bind()](ch17_split_000.html#oop_call_apply_bind “调用函数时设置 this:call(),apply()和 bind()”))。本节解释了非方法的两种用法。
此方法在调用函数func
时使用argArray
的元素作为参数;也就是说,以下两个表达式是等价的:
func(arg1, arg2, arg3)
func.apply(null, [arg1, arg2, arg3])
thisValue
是在执行func
时this
的值。在非面向对象的设置中不需要它,因此在这里是null
。
apply()
在函数以类似数组的方式接受多个参数时很有用,但不是一个数组。
由于apply()
,我们可以使用Math.max()
(参见[其他函数](ch21.html#Math_max “其他函数”))来确定数组的最大元素:
> Math.max(17, 33, 2)
33
> Math.max.apply(null, [17, 33, 2])
33
这执行部分函数应用 - 创建一个新函数,该函数使用thisValue
调用func
,并使用以下参数:从arg1
到argN
,然后是新函数的实际参数。在以下非面向对象的设置中,不需要thisValue
,这就是为什么它在这里是null
。
在这里,我们使用bind()
创建一个新函数plus1()
,它类似于add()
,但只需要参数y
,因为x
始终为 1:
function add(x, y) {
return x + y;
}
var plus1 = add.bind(null, 1);
console.log(plus1(5)); // 6
换句话说,我们已经创建了一个等效于以下代码的新函数:
function plus1(y) {
return add(1, y);
}
JavaScript 不强制函数的 arity:您可以使用任意数量的实际参数调用它,而不受已定义的形式参数的限制。因此,实际参数和形式参数的数量可以以两种方式不同:
实际参数比形式参数多
额外的参数将被忽略,但可以通过特殊的类数组变量arguments
检索(稍后讨论)。
实际参数比形式参数少
所有缺失的形式参数都具有值undefined
。
特殊变量arguments
仅存在于函数内(包括方法)。它是一个类似数组的对象,保存当前函数调用的所有实际参数。以下代码使用它:
function logArgs() {
for (var i=0; i<arguments.length; i++) {
console.log(i+'. '+arguments[i]);
}
}
以下是交互:
> logArgs('hello', 'world')
0\. hello
1\. world
arguments
具有以下特点:
length
属性,可以通过索引读取和写入单个参数。另一方面,arguments
不是一个数组,它只是类似于数组。它没有任何数组方法(slice()
,forEach()
等)。幸运的是,您可以借用数组方法或将arguments
转换为数组,如类数组对象和通用方法中所述。
它是一个对象,因此所有对象方法和运算符都是可用的。例如,你可以使用in
运算符(迭代和属性检测)来检查arguments
是否“有”给定的索引:
> function f() { return 1 in arguments }
> f('a')
false
> f('a', 'b')
true
你可以以类似的方式使用hasOwnProperty()
(迭代和属性检测):
> function g() { return arguments.hasOwnProperty(1) }
> g('a', 'b')
true
```
#### 已弃用的`arguments`特性
严格模式下会取消`arguments`的一些更不寻常的特性:
+ `arguments.callee`指的是当前函数。它主要用于在匿名函数中进行自递归,并且在严格模式下是不允许的。作为一种解决方法,可以使用命名函数表达式(参见[命名函数表达式](ch15.html#named_function_expression "Named function expressions")),它可以通过其名称引用自身。
+ 在非严格模式下,如果更改参数,`arguments`会保持最新:
```js
function sloppyFunc(param) {
param = 'changed';
return arguments[0];
}
console.log(sloppyFunc('value')); // changed
```
但是在严格模式下不会进行这种更新:
```js
function strictFunc(param) {
'use strict';
param = 'changed';
return arguments[0];
}
console.log(strictFunc('value')); // value
```
+ 严格模式禁止对变量`arguments`进行赋值(例如通过`arguments++`)。仍然允许对元素和属性进行赋值。
### 强制参数,强制最小数量
有三种方法可以找出参数是否缺失。首先,你可以检查它是否为`undefined`:
```js
function foo(mandatory, optional) {
if (mandatory === undefined) {
throw new Error('Missing parameter: mandatory');
}
}
其次,你可以将参数解释为布尔值。然后undefined
被视为false
。但是,有一个警告:其他几个值也被视为false
(参见真值和假值),因此检查无法区分,比如0
和缺少的参数:
if (!mandatory) {
throw new Error('Missing parameter: mandatory');
}
第三,你也可以检查arguments
的长度以强制最小 arity:
if (arguments.length < 1) {
throw new Error('You need to provide at least 1 argument');
}
最后一种方法与其他方法不同:
前两种方法不区分foo()
和foo(undefined)
。在这两种情况下,都会抛出异常。
第三种方法对foo()
抛出异常,并对foo(undefined)
将optional
设置为undefined
。
如果参数是可选的,这意味着如果缺少参数,则给它一个默认值。与强制参数类似,有四种替代方案。
首先,检查undefined
:
function bar(arg1, arg2, optional) {
if (optional === undefined) {
optional = 'default value';
}
}
其次,将optional
解释为布尔值:
if (!optional) {
optional = 'default value';
}
第三,你可以使用或运算符||
(参见逻辑或(||)),如果左操作数不是假值,则返回左操作数。否则,返回右操作数:
// Or operator: use left operand if it isn't falsy
optional = optional || 'default value';
第四,你可以通过arguments.length
检查函数的 arity:
if (arguments.length < 3) {
optional = 'default value';
}
再次,最后一种方法与其他方法不同:
前三种方法不区分bar(1, 2)
和bar(1, 2, undefined)
。在这两种情况下,optional
都是'default value'
。
第四种方法为bar(1, 2)
设置optional
为'default value'
,并且对于bar(1, 2, undefined)
保持undefined
(即不变)。
另一种可能性是将可选参数作为命名参数传递,作为对象字面量的属性(参见命名参数)。
在 JavaScript 中,你不能通过引用传递参数;也就是说,如果你将一个变量传递给一个函数,它的值会被复制并传递给函数(按值传递)。因此,函数无法更改变量。如果需要这样做,必须将变量的值封装在数组中。
这个例子演示了一个增加变量的函数:
function incRef(numberRef) {
numberRef[0]++;
}
var n = [7];
incRef(n);
console.log(n[0]); // 8
如果将函数c
作为参数传递给另一个函数f
,则必须了解两个签名:
f
期望其参数具有的签名。f
可能提供多个参数,而c
可以决定使用其中的多少(如果有的话)。
c
的实际签名。例如,它可能支持可选参数。
如果两者不一致,那么您可能会得到意想不到的结果:c
可能具有您不知道的可选参数,并且会错误地解释f
提供的附加参数。
例如,考虑数组方法map()
(参见[转换方法](ch18.html#Array.prototype.map “转换方法”)),其参数通常是一个带有单个参数的函数:
> [ 1, 2, 3 ].map(function (x) { return x * x })
[ 1, 4, 9 ]
您可以将parseInt()
作为参数传递给一个函数(参见[通过 parseInt()获取整数](ch11.html#parseInt “通过 parseInt()获取整数”)):
> parseInt('1024')
1024
您可能(错误地)认为map()
只提供了一个参数,而parseInt()
只接受了一个参数。然后您会对以下结果感到惊讶:
> [ '1', '2', '3' ].map(parseInt)
[ 1, NaN, NaN ]
map()
期望具有以下签名的函数:
function (element, index, array)
但是parseInt()
具有以下签名:
parseInt(string, radix?)
因此,map()
不仅填充了string
(通过element
),还填充了radix
(通过index
)。这意味着前面数组的值是这样产生的:
> parseInt('1', 0)
1
> parseInt('2', 1)
NaN
> parseInt('3', 2)
NaN
总之,对于您不确定其签名的函数和方法要小心。如果使用它们,明确指定接收了哪些参数并传递了哪些参数通常是有意义的。这是通过回调函数实现的:
> ['1', '2', '3'].map(function (x) { return parseInt(x, 10) })
[ 1, 2, 3 ]
在调用编程语言中的函数(或方法)时,您必须将实际参数(由调用者指定)映射到函数定义的形式参数。有两种常见的方法来实现这一点:
位置参数按位置进行映射。第一个实际参数映射到第一个形式参数,第二个实际参数映射到第二个形式参数,依此类推。
命名参数使用名称(标签)执行映射。名称与函数定义中的形式参数相关联,并标记函数调用中的实际参数。命名参数出现的顺序并不重要,只要它们被正确标记。
命名参数有两个主要好处:它们为函数调用中的参数提供描述,并且对于可选参数也很有效。我将首先解释这些好处,然后向您展示如何通过对象字面量在 JavaScript 中模拟命名参数。
一旦函数有多个参数,您可能会对每个参数的用途感到困惑。例如,假设您有一个名为selectEntries()
的函数,它从数据库中返回条目。给定以下函数调用:
selectEntries(3, 20, 2);
这两个数字代表什么?Python 支持命名参数,这使得很容易弄清楚发生了什么:
selectEntries(start=3, end=20, step=2) # Python syntax
可选位置参数仅在末尾省略时才有效。在其他任何地方,您必须插入占位符,例如null
,以便剩余参数具有正确的位置。对于可选命名参数,这不是问题。您可以轻松地省略其中任何一个。以下是一些示例:
# Python syntax
selectEntries(step=2)
selectEntries(end=20, start=3)
selectEntries()
JavaScript 不像 Python 和许多其他语言那样原生支持命名参数。但是有一个相当优雅的模拟方法:通过对象字面量命名参数,作为单个实际参数传递。当您使用这种技术时,selectEntries()
的调用看起来像:
selectEntries({ start: 3, end: 20, step: 2 });
该函数接收一个具有属性start
、end
和step
的对象。您可以省略其中任何一个:
selectEntries({ step: 2 });
selectEntries({ end: 20, start: 3 });
selectEntries();
您可以将selectEntries()
实现如下:
function selectEntries(options) {
options = options || {};
var start = options.start || 0;
var end = options.end || getDbLength();
var step = options.step || 1;
...
}
您还可以将位置参数与命名参数结合使用。后者通常出现在最后:
selectEntries(posArg1, posArg2, { namedArg1: 7, namedArg2: true });
在 JavaScript 中,这里显示的命名参数模式有时被称为选项或选项对象(例如,由 jQuery 文档)。