0 Views 前端 with
本文字数:3,657 字 | 阅读时长 ≈ 14 min

0 Views 前端 with
本文字数:3,657 字 | 阅读时长 ≈ 14 min

数组(array)、字符串(string)和数字(number)是一个程序最基本的组成部分,但在
JavaScript 中,它们可谓让人喜忧掺半.

数组

和其他强类型语言不同,在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串、数字、对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的):

1
2
3
4
var a = [1, "2", [3]];
a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true

对数组声明后即可向其中加入值,不需要预先设定大小

1
2
3
4
5
6
var a = [];
a.length; // 0
a[0] = 1;
a[1] = "2";
a[2] = [3];
a.length; // 3

使用 delete 运算符可以将单元从数组中删除,但是请注意,单元删除后,数组的 length 属性并不会发生变化。

在创建“稀疏”数组(sparse array,即含有空白或空缺单元的数组)时要特别注意:

1
2
3
4
5
6
var a = [];
a[0] = 1;
// 此处没有设置a[1]单元
a[2] = [3];
a[1]; // undefined
a.length; // 3

上面的代码可以正常运行,但其中的“空白单元”(empty slot)可能会导致出人意料的结果。 a[1] 的值为 undefined,但这与将其显式赋值为 undefined(a[1] = undefined)还是有所区别。

数组通过数字进行索引,但有趣的是它们也是对象,所以也可以包含字符串键值和属性(但这些并不计算在数组长度内)

1
2
3
4
5
6
var a = [];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2

这里有个问题需要特别注意,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理。

1
2
3
var a = [];
a["13"] = 42;
a.length; // 14

在数组中加入字符串键值 / 属性并不是一个好主意。建议使用对象来存放键值 / 属性值,用数组来存放数字索引值。

类数组

有时需要将类数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数(如 indexOf(..)、 concat(..)、 forEach(..) 等)来实现。

例如,一些 DOM 查询操作会返回 DOM 元素列表,它们并非真正意义上的数组,但十分类似。另一个例子是通过 arguments 对象(类数组)将函数的参数当作列表来访问(从 ES6 开始已废止)。

工具函数 slice(..) 经常被用于这类转换

1
2
3
4
5
6
function foo() {
var arr = Array.prototype.slice.call(arguments);
arr.push("bam");
console.log(arr);
}
foo("bar", "baz"); // ["bar","baz","bam"]

如上所示, slice() 返回参数列表(上例中是一个类数组)的一个数组复本。

字符串

字符串经常被当成字符数组。字符串的内部实现究竟有没有使用数组并不好说,但 JavaScript 中的字符串和字符数组并不是一回事,最多只是看上去相似而已。

例如下面两个值:

1
2
var a = "foo";
var b = ["f", "o", "o"];

字符串和数组的确很相似,它们都是类数组,都有 length 属性以及 indexOf(..)(从 ES5 开始数组支持此方法)和 concat(..) 方法:

1
2
3
4
5
6
7
8
9
10
11
[source, js]
a.length; // 3
b.length; // 3
a.indexOf( "o" ); // 1
b.indexOf( "o" ); // 1
var c = a.concat( "bar" ); // "foobar"
var d = b.concat( ["b","a","r"] ); // ["f","o","o","b","a","r"]
a === c; // false
b === d; // false
a; // "foo"
b; // ["f","o","o"]

但这并不意味着它们都是“字符数组”,比如:

1
2
3
4
a[1] = "O";
b[1] = "O";
a; // "foo"
b; // ["f","O","o"]

JavaScript 中字符串是不可变的,而数组是可变的。并且 a[1] 在 JavaScript 中并非总是合法语法,在老版本的 IE 中就不被允许(现在可以了)。 正确的方法应该是 a.charAt(1)。

字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。

1
2
3
4
5
6
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
b.push("!");
b; // ["f","O","o","!"]

许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”数组的非变更方法来处理字符串

另一个不同点在于字符串反转(JavaScript 面试常见问题)。数组有一个字符串没有的可变更成员函数 reverse()

1
2
3
a.reverse; // undefined
b.reverse(); // ["!","o","O","f"]
b; // ["f","O","o","!"]

可惜我们无法“借用”数组的可变更成员函数,因为字符串是不可变的

一个变通(破解)的办法是先将字符串转换为数组,待处理完后再将结果转换回字符串:

1
2
3
4
5
6
7
8
var c = a
// 将a的值转换为字符数组
.split("")
// 将数组中的字符进行倒转
.reverse()
// 将数组中的字符拼接回字符串
.join("");
c; // "oof"

这种方法的确简单粗暴,但对简单的字符串却完全适用。

如果需要经常以字符数组的方式来处理字符串的话,倒不如直接使用数组。这样就不用在字符串和数组之间来回折腾。可以在需要时使用 join(“”) 将字符数组转换为字符串。

数字

JavaScript 只有一种数值类型: number(数字),包括“整数”和带小数的十进制数。此处“整数”之所以加引号是因为和其他语言不同, JavaScript 没有真正意义上的整数,这也是它一直以来为人诟病的地方。这种情况在将来或许会有所改观,但目前只有数字类型

JavaScript 中的“整数”就是没有小数的十进制数。所以 42.0 即等同于“整数” 42

数字的语法

JavaScript 中的数字常量一般用十进制表示。例如:

1
2
var a = 42;
var b = 42.3;

数字前面的 0 可以省略:

1
2
var a = 0.42;
var b = .42;

小数点后小数部分最后面的 0 也可以省略

1
2
var a = 42.0;
var b = 42.;

42. 这种写法没问题,只是不常见,但从代码的可读性考虑,不建议这样写。

默认情况下大部分数字都以十进制显示,小数部分最后面的 0 被省略,如:

1
2
3
4
var a = 42.300;
var b = 42.0;
a; // 42.3
b; // 42

特别大和特别小的数字默认用指数格式显示,与 toExponential() 函数的输出结果相同。

例如:

1
2
3
4
5
6
7
var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11

由于数字值可以使用 Number 对象进行封装,因此数字值可以调用 Number.prototype 中的方法。例如, tofixed(..) 方法可指定小数部分的显示位数:

1
2
3
4
5
6
var a = 42.59;
a.toFixed(0); // "43"
a.toFixed(1); // "42.6"
a.toFixed(2); // "42.59"
a.toFixed(3); // "42.590"
a.toFixed(4); // "42.5900

上面的方法不仅适用于数字变量,也适用于数字常量

不过对于.运算符需要给予特别注意,因为它是一个有效的数字字符,会被优先识别为数字常量的一部分,然后才是对象属性访问运算符。

1
2
3
4
5
6
// 无效语法:
42. toFixed(3); // SyntaxError
// 下面的语法都有效:
(42).toFixed(3); // "42.000"
0.42.toFixed(3); // "0.420"
42..toFixed(3); // "42.000"

42.tofixed(3) 是无效语法,因为 . 被视为常量 42. 的一部分(如前所述),所以没有 . 属性访问运算符来调用 tofixed 方法。

42..tofixed(3) 则没有问题,因为第一个 . 被视为 number 的一部分,第二个 . 是属性访问运算符。只是这样看着奇怪,实际情况中也很少见。在基本类型值上直接调用的方法并不多见,不过这并不代表不好或不对。

下面的语法也是有效的(请注意其中的空格)

1
42.toFixed(3); // "42.000

然而对数字常量而言,这样的语法很容易引起误会,不建议使用。

数字常量还可以用其他格式来表示,如二进制、八进制和十六进制

当前的 JavaScript 版本都支持这些格式

1
2
3
0xf3; // 243的十六进制
0Xf3; // 同上
0363; // 243的八进制

较小的数值

二进制浮点数最大的问题(不仅 JavaScript,所有遵循 IEEE 754 规范的语言都是如此),是会出现如下情况:

1
0.1 + 0.2 === 0.3; // fal

从数学角度来说,上面的条件判断应该为 true,可结果为什么是 false 呢?

简单来说,二进制浮点数中的 0.1 和 0.2 并不是十分精确,它们相加的结果并非刚好等于 0.3,而是一个比较接近的数字 0.30000000000000004,所以条件判断结果为 false。

有人认为, JavaScript 应该采用一种可以精确呈现数字的实现方式。一直以来出现过很多替代方案,只是都没能成为标准,以后大概也不会。这个问题看似简单,实则不然,否则早就解决了。

问题是,如果一些数字无法做到完全精确,是否意味着数字类型毫无用处呢?答案当然是否定的。

在处理带有小数的数字时需要特别注意。很多(也许是绝大多数)程序只需要处理整数,最大不超过百万或者万亿,此时使用 JavaScript 的数字类型是绝对安全的

整数的安全范围

数字的呈现方式决定了“整数”的安全值范围远远小于 Number.MAX_VALUE

能够被“安全”呈现的最大整数是 2^53 - 1,即 9007199254740991

有时 JavaScript 程序需要处理一些比较大的数字,如数据库中的 64 位 ID 等。由于 JavaScript的数字类型无法精确呈现 64 位数值,所以必须将它们保存(转换)为字符串。

好在大数值操作并不常见(它们的比较操作可以通过字符串来实现)。如果确实需要对大数值进行数学运算,目前还是需要借助相关的工具库。将来 JavaScript 也许会加入对大数值的支持。

整数检测

要检测一个值是否是整数,可以使用 ES6 中的 Number.isInteger(..) 方法:

1
2
3
Number.isInteger(42); // true
Number.isInteger(42.000); // true
Number.isInteger(42.3); // false

特殊数值

不是值的值

undefined 类型只有一个值,即 undefined。 null 类型也只有一个值,即 null。它们的名称既是类型也是值

undefined 和 null 常被用来表示“空的”值或“不是值”的值。二者之间有一些细微的差别。例如:

或者:

null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。

特殊的数字

数字类型中有几个特殊的值,下面将详细介绍。

不是数字的数字

如果数学运算的操作数不是数字类型(或者无法解析为常规的十进制或十六进制数字),就无法返回一个有效的数字,这种情况下返回值为 NaN。

NaN 意指“不是一个数字”(not a number),这个名字容易引起误会,后面将会提到。将它理解为“无效数值”“失败数值”或者“坏数值”可能更准确些。

例如:

1
2
var a = 2 / "foo"; // NaN
typeof a === "number"; // true

换句话说,“不是数字的数字”仍然是数字类型。这种说法可能有点绕。

NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

有人也许认为如果要检查变量的值是否为 NaN,可以直接和 NaN 进行比较,就像比较 null 和undefined 那样。实则不然。

1
2
3
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反, reflexive,即 x === x 不
成立)的值。而 NaN != NaN 为 true,很奇怪吧?

既然我们无法对 NaN 进行比较(结果永远为 false),那应该怎样来判断它呢?

1
2
var a = 2 / "foo";
isNaN(a); // true

很简单,可以使用内建的全局工具函数 isNaN(..) 来判断一个值是否是 NaN。

然而操作起来并非这么容易。 isNaN(..) 有一个严重的缺陷,它的检查方式过于死板,就是“检查参数是否不是 NaN,也不是数字”。但是这样做的结果并不太准确:

1
2
3
4
5
6
7
var a = 2 / "foo";
var b = "foo";
a; // NaN
b;
"foo"
window.isNaN(a); // true
window.isNaN(b); // true——晕!

很明显 “foo” 不是一个数字,但是它也不是 NaN。

无穷数

熟悉传统编译型语言(如 C)的开发人员可能都遇到过编译错误(compiler error)或者运行时错误(runtime exception),例如“除以 0”:

1
var a = 1 / 0;

然而在 JavaScript 中上例的结果为 Infinity(即 Number.POSITIVE_INfiNITY)。同样:

1
2
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity

如果除法运算中的一个操作数为负数,则结果为-Infinity( 即Number.NEGATIVE_INfiNITY)。

JavaScript 使用有限数字表示法(finite numeric representation,即之前介绍过的 IEEE 754 浮点数),所以和纯粹的数学运算不同, JavaScript 的运算结果有可能溢出,此时结果为 Infinity 或者 -Infinity。

例如:

1
2
3
4
var a = Number.MAX_VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow(2, 970); // Infinity
a + Math.pow(2, 969); // 1.7976931348623157e+308

零值

这部分内容对于习惯数学思维的读者可能会带来困惑, JavaScript 有一个常规的 0(也叫作 +0)和一个 -0。在解释为什么会有 -0 之前,我们先来看看 JavaScript 是如何来处理它的。

-0 除了可以用作常量以外,也可以是某些数学运算的返回值。例如:

1
2
var a = 0 / -3; // -0
var b = 0 * -3; // -0

加法和减法运算不会得到负零(negative zero)

负零在开发调试控制台中通常显示为 -0,但在一些老版本的浏览器中仍然会显示为 0。

Sep 02, 2018