大话 JavaScript(Speaking JavaScript):第十一章到第十五章

发布时间 2024-01-11 12:28:43作者: 绝不原创的飞龙

第十一章:数字

原文: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
布尔值 false0
true1
数字 与输入相同(无需转换)
字符串 解析字符串中的数字(忽略前导和尾随空格);空字符串转换为 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()

全局函数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 有几个特殊的数字值:

  • 两个错误值,NaNInfinity

  • 两个零值,+0-0。JavaScript 有两个零,一个正零和一个负零,因为数字的符号和大小存储在不同的位置。在本书的大部分内容中,我假设只有一个零,并且您几乎从不在 JavaScript 中看到有两个零。

NaN

错误值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 === 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是一个错误值,指示两个问题中的一个:一个数字无法表示,因为其大小太大,或者发生了除以零。

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“中和”一个Infinity,则会得到错误结果NaN

> Infinity - Infinity
NaN
> Infinity / Infinity
NaN

如果您尝试超出Infinity,您仍然会得到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 用于NaNInfinity等错误值。

  • −1023 用于:

  • 零(如果分数为 0,如刚才解释的那样)

  • 靠近零的小数字(如果分数不为 0)。

为了同时启用两个应用程序,使用了不同的所谓非标准化表示:

(–1)^(sign) × %0.fraction × 2^(–1022)

要比较,标准化表示中最小(即“最接近零”的)数字是:

(–1)^(sign) × %1.fraction × 2^(–1022)

非标准化的数字更小,因为没有前导数字 1。

处理舍入误差

JavaScript 的数字通常以十进制浮点数输入,但在内部表示为二进制浮点数。这导致了不精确。为了理解原因,让我们忘记 JavaScript 的内部存储格式,来看看十进制浮点数和二进制浮点数可以很好地表示哪些分数。在十进制系统中,所有分数都是一个底数 m 除以 10 的幂:

因此,在分母中只有十。这就是为什么无法将精确表示为十进制浮点数的原因——无法将 3 放入分母。二进制浮点数中只有二。让我们看看哪些十进制浮点数可以很好地表示为二进制浮点数,哪些不能。如果分母中只有二,那么可以表示十进制数:

  • 0.5[dec] = = = 0.1[bin]

  • 0.75[dec] = = = 0.11[bin]

  • 0.125[dec] = = = 0.001[bin]

其他分数无法精确表示,因为分母中有 2 以外的数字(经过质因数分解):

  • 0.1[dec] = =

  • 0.2[dec] = =

通常看不到 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 只有浮点数。整数在内部以两种方式出现。首先,大多数 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 中的定义

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“最接近”的整数(“最接近”的含义取决于如何进行转换)。您有几种选项可以执行此转换:

  1. Math函数Math.floor()Math.ceil()Math.round()(参见Integers via Math.floor(), Math.ceil(), and Math.round()

  2. 自定义函数ToInteger()(参见Integers via the Custom Function ToInteger()

  3. 二进制位运算符(参见[通过位运算符实现 32 位整数](ch11.html#integers_via_bitwise_operators "通过位运算符实现 32 位整数"))

  4. 全局函数parseInt()(参见[通过 parseInt()实现整数](ch11.html#parseInt "通过 parseInt()实现整数"))

结论:#1 通常是最佳选择,#2 和#3 有特定应用,#4 适用于解析字符串,但不适用于将数字转换为整数。

通过 Math.floor()、Math.ceil()和 Math.round()实现整数

以下三个函数通常是将数字转换为整数的最佳方式:

  • 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);
}

通过位运算符实现 32 位整数

二进制位运算符(参见[二进制位运算符](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()函数:

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 位整数

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]false2[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?)

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?)

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?)

对于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?)

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)

如果numberNaN,则返回true。详情请参见Pitfall: checking whether a value is NaN

parseFloat(str)

str转换为浮点数。详情请参见parseFloat()

parseInt(str, radix?)

str解析为以radix为基数的整数(2-36)。详情请参阅通过 parseInt()获取整数

本章的来源

在编写本章时,我参考了以下来源:


¹² 来源: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表示。

十六进制转义序列

\xHHHH是两个十六进制数字)指定了一个 ASCII 码的字符。例如:

> '\x4D'
'M'

Unicode 转义序列

\uHHHHHHHH是四个十六进制数字)指定了一个 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() | (对于undefinednull不起作用!) |

我更喜欢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

对于undefinednull,我们面临类似的问题。

比较字符串

有两种比较字符串的方法。首先,您可以使用比较运算符:<>===<=>=。它们有以下缺点:

  • 它们区分大小写:

    > '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 位无符号整数codeUnit1codeUnit2等指定的 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?, ...)

返回接收者和str1str2等的连接:

> '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»⟧

停止当前循环迭代,并立即继续下一个。

标签

标签是一个标识符,后面跟着一个冒号。在循环前,标签允许您即使从嵌套在其中的循环中也可以中断或继续该循环。在块的前面,您可以跳出该块。在这两种情况下,标签的名称成为breakcontinue的参数。这是一个打破块的例子:

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-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在循环之前执行一次,只要conditiontrue,循环就会继续。您可以在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

不要使用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

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-then-else语句中:

if («condition»)
    «then_branch»
⟦else
    «else_branch»⟧

then_branchelse_branch可以是单个语句或语句块(参见循环和条件的主体)。

链接 if 语句

您可以链接几个if语句:

if (s1 > s2) {
    return 1;
} else if (s1 < s2) {
    return -1;
} else {
    return 0;
}

请注意,在前面的例子中,所有的else分支都是单个语句(if语句)。只允许else分支为块的编程语言需要一些类似else-if分支的东西来进行链接。

陷阱:悬空的 else

以下示例的else分支被称为“悬空”,因为不清楚它属于两个if语句中的哪一个:

if («cond1») if («cond2») «stmt1» else «stmt2»

这是一个简单的规则:使用大括号。前面的片段等同于以下代码(在这里很明显else属于谁):

if («cond1») {
    if («cond2») {
        «stmt1»
    } else {
        «stmt2»
    }
}

switch

一个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。但是returnthrow也可以工作,尽管它们通常不仅仅离开switch语句。

以下示例说明了如果使用throwreturn,则不需要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. 如果在发生错误的地方无法有意义地处理问题,请抛出异常。

  2. 找到可以处理错误的地方:捕获异常。

在(1)处,以下结构是活动的:

    processFile()
        extractAllEntries(...)
            fileNames.forEach(...)
                function (fileName) { ... }
                    try { ... } catch (exception) { ... }
                        extractOneEntry(...)
                            openFile(...)

在(1)处的throw语句沿着树向上走,并离开所有结构,直到遇到一个活动的try语句。然后调用该语句的catch块并将异常值传递给它。

JavaScript 中的异常处理

JavaScript 中的异常处理与大多数编程语言一样:try语句将语句分组,并允许您拦截这些语句中的异常。

throw

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是必需的,catchfinally至少有一个也必须存在:

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...

finallyreturn语句之后执行:

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。

堆栈跟踪

错误的常见来源要么是外部的(错误的输入,丢失的文件等),要么是内部的(程序中的错误)。特别是在后一种情况下,您将收到意外的异常并需要进行调试。通常情况下,您没有运行调试器。对于“手动”调试,有两条信息是有帮助的:

  1. 数据:变量具有什么值?

  2. 执行:异常发生在哪一行,活动的函数调用是什么?

您可以将第一项(数据)的一些内容放入消息或异常对象的属性中。第二项(执行)在许多 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

本节仅展示了定义函数的一种方式和调用函数的一种方式。其他方式将在后面描述。

JavaScript 中函数的三种角色

一旦您像刚才所示那样定义了一个函数,它可以扮演多种角色:

非方法函数(“普通函数”)

您可以直接调用函数。然后它将作为普通函数工作。以下是一个示例调用:

id('hello')

按照惯例,普通函数的名称以小写字母开头。

构造函数

您可以通过new运算符调用函数。然后它变成一个构造函数,一个对象的工厂。以下是一个示例调用:

new Date()

按照惯例,构造函数的名称以大写字母开头。

方法

您可以将函数存储在对象的属性中,这将使其成为一个方法,您可以通过该对象调用它。以下是一个示例调用:

obj.method()

按照惯例,方法的名称以小写字母开头。

非方法函数在本章中有解释;构造函数和方法在第十七章中有解释。

术语:“参数”与“参数”

术语参数参数通常可以互换使用,因为上下文通常可以清楚地表明所需的含义。以下是区分它们的一个经验法则。

  • 参数用于定义函数。它们也被称为形式参数和形式参数。在下面的例子中,param1param2是参数:

    function foo(param1, param2) {
        ...
    }
    
  • 参数用于调用函数。它们也被称为实际参数和实际参数。在下面的例子中,37是参数:

    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()

call()apply()bind()是所有函数都具有的方法(请记住函数是对象,因此具有方法)。它们可以在调用方法时提供this的值,因此主要在面向对象的上下文中很有趣(参见[调用函数时设置 this:call(),apply()和 bind()](ch17_split_000.html#oop_call_apply_bind "调用函数时设置 this:call(),apply()和 bind()"))。本节解释了非方法的两种用法。

func.apply(thisValue, argArray)

此方法在调用函数func时使用argArray的元素作为参数;也就是说,以下两个表达式是等价的:

func(arg1, arg2, arg3)
func.apply(null, [arg1, arg2, arg3])

thisValue是在执行functhis的值。在非面向对象的设置中不需要它,因此在这里是null

apply()在函数以类似数组的方式接受多个参数时很有用,但不是一个数组。

由于apply(),我们可以使用Math.max()(参见[其他函数](ch21.html#Math_max "其他函数"))来确定数组的最大元素:

> Math.max(17, 33, 2)
33
> Math.max.apply(null, [17, 33, 2])
33

func.bind(thisValue, arg1, ..., argN)

这执行部分函数应用 - 创建一个新函数,该函数使用thisValue调用func,并使用以下参数:从arg1argN,然后是新函数的实际参数。在以下非面向对象的设置中,不需要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

特殊变量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 中模拟命名参数

JavaScript 不像 Python 和许多其他语言那样原生支持命名参数。但是有一个相当优雅的模拟方法:通过对象字面量命名参数,作为单个实际参数传递。当您使用这种技术时,selectEntries()的调用看起来像:

selectEntries({ start: 3, end: 20, step: 2 });

该函数接收一个具有属性startendstep的对象。您可以省略其中任何一个:

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 文档)。