中卷(1.2)
内容大纲:
js 中的数组是通过数字索引的一组任意类型的值。字符串和数组相似,但是它们的行为特征不同, 将字符串作为数组来处理时需要特别小心。js 中的数字包括“整数”和“浮点型”。
基本类型中定义了几个特殊的值。
null 类型只有一个值 null,undefined 类型也只有一个值 undefined。所有变量在赋值之前默认值都是 undefined。void 运算符返回 undefined。
数字类型有几个特殊的值,包括 NaN、+Infinity、-Infinity 和 -0。
简单标量基本类型值通过值复制来赋值 / 传递,而复合值通过引用复制来赋值 / 传递。
数组
JS 数组可以容纳任何类型的值,而且不需要预先设定大小
var a = [];
a.length; // 0
a[0] = 1;
a[1] = "2";
a[2] = [ 3 ];
a.length; // 3
数组通过数字进行索引,但有趣的是他们也是 对象,所以也可以包含字符串键值和属性(但这些并不计算在数组的长度内):
var a = [];
a[0] - 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
这里有个问题需要特别注意,如果字符串键值能够被强制类型转换为十进制数字的话,它就会被当作数字索引来处理。
var a = [];
a["13"] = 42;
a.length; // 14
在数组中加入字符串键值 / 属性并不是一个好主意,建议使用对象来存放键值 / 属性值,用数组来存放数字索引值。
类数组
有时需要将类数组(一组通过数字索引的值)转换为真正的数组,这一般通过数组工具函数(如 indexOf(..)、concat(..)、forEach(..) 等)来实现。
字符串
字符串经常成为字符数组。字符串的内部究竟有没有使用数组并不好说,但 js 中的字符串和字符数组并不是一回事,最多只是看上去相似而已。
字符串和数组的确很相似,他们都是类数组,都有 length 属性以及 indexOf(..) 和 concat(..) 方法
var a = "foo";
var b = ["f","o","o"];
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"]
但这并不意味着它们都是“字符数组”,比如:
a[1] = "0";
b[1] = "0";
a; // "foo"
b; // ["f","0","o"]
Js 中字符串是不可变的,而数组是可变的。并且 a[1] 在 js 中并非总是会和法语发,在老版本的 IE 中就不允许(现在可以了)。正确的方法应该是 a.charAt(1)。
字符串不可变是指字符串的成员函数不会改变其原始值,而是创建并返回一个新的字符串。而数组的成员函数都是在其原始值上进行操作。
c = a.toUpperCase();
a === c; //false
a; // "foo"
c; // "FOO"
许多数组函数用来处理字符串很方便。虽然字符串没有这些函数,但可以通过“借用”
数组中的非变更方法来处理字符串:
a.join; // undefined
a.map; //undefined
var c = Array.prototype.join.call( a, "-" );
var d = Array.prototype.map.call( a, function(v){
return v.toUpperCase() + ".";
} ).join( "" );
c; // "f-o-o"
d; // "F.O.O."
另一个不同点在于字符串反转( js 常见面试题)。数组有一个字符串没有的可变更成员函数 reverse();
a.reverse; // undefined
b.reverse(); // ["o"."0","f"]
b; // ["f","0","o"]
可惜我们无法“借用”数组的可变更成员函数,因为字符串是不可变的。
一个变通的方法是先将字符串转换为数组,待处理完后再将其结果转换回字符串:
var c = a.split("").reverse.join("");
c; // "oof";
这种方法对简单的字符串完全适用,但对复杂字符(Unicode,如星号、多字节字符等)的字符串不适用。
数字
特别大和特别小的数字默认用指数格式显示,与 toExponential() 函数的输出结果相同。
由于数字只可以使用 Number 对象进行封装,因此数字值可以调用 Number.prototype 中的方法。例如,toFixed(..) 方法可指定小数部分的显示位数:
var a = 42.59;
a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); //"42.59"
a.toFixed( 3 ); //"42.590"
请注意上面的输出结果实际上是给定数字的字符串形式。
而 toPrecision(..) 方法用来指定有效数位的显示位数:
var a = 42.59;
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
上面的方法不仅适用于数字变量,也适用于数字字面量。不过对于 . 运算符要特别注意,因为它是一个有效的数字字符,会被优先识别为数字字面量的一部分,然后才是对象属性访问运算符。
//无效语法:
42.toFixed( 3 ); // SyntaxError
//下面的有效:
(42).toFixed( 3 ); // "42.000"
42.toFixed( 3 ) 是无效语法,因为 . 被视为常量 42. 的一部分,所以没有 . 属性访问运算符来调用 toFixed 方法。
42..toFixed( 3 ) 或 42 .toFixed( 3 ) 则没有问题,但也比较少见。
比较较小的数值
二进制浮点数最大的问题(不仅 js ,所有遵循 IEEE 754 规范的语言都是如此),是会出现如下的情况:
0.1 + 0.2 === 0.3; // false
简单来说,二进制浮点数中的 0.1 和 0.2 并不是十分精确,他们相加的结果并非刚好等于 0.3。
问题是,如果一些数字无法做到十分精确,是否意味着数字类型毫无作用呢?答案是否定的。
在处理带有小数的数字时需要特别注意。很多程序只需要处理整数,最大不超过百万或者百亿,此时使用 js 的数字类型时绝对安全的。
那么应该怎么判断 0.1 + 0.2 和 0.3 是否相等呢?
最常见的方法时设置一个误差范围值,通常成为“机器精度”(machine epsilon),对 js 的数字来说,这个值通常是 2^-52。
从 ES6 开始,该值定义在 Number.EPSILON 中,我们可以直接拿来用,也可以为 ES6 之前的版本写 polyfill:
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
可以使用 Number.EPSILON 来比较两个数字是否相等(在指定的误差范围内):
function numbersCloseEnoughToEqual(n1,n2){
return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b); // true
整数的安全范围
有时 js 程序需要处理一些比较大的数字,如数据库中的64位 ID 等。由于 js 的数字类型无法精确呈现64位数值,所以必须将它们保存(转换)位字符串。
整数检测
可以使用 ES6 的 Number.isInteger(..) 方法,也可以为 ES6 之前的版本写 polyfill:
if (!Number.isInteger){
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
}
}
要检测一个值是否是安全的整数,可以使用 ES6 中的 Number.isSafeInteger(..) 方法,也可以为 ES6 之前的版本写 polyfill:
if(!Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) &&
Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
}
}
特殊数值
两个不是值的值: undefined 和 null。
它们的名称既是类型也是值。
两者之间有一些细微的差别,例如:
- null 指控制(empty value)
- undefined 指没有值(missing value)
或者:
- undefined 指从未赋过值
- null 指曾赋过值,但是目前没有值
null 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 undefined 却是一个标识符,可以被当作变量来使用和赋值。
void 运算符
undefined 是一个内置标识符,它的值为 undefined,通过void运算符可以得到该值。
表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值:
var a = 42;
console.log( void a, a ); // undefined 42
void运算符在某些地方上也能配上用场,比如不让表达式返回任何结果(即使其有副作用)。
例如:
function doSomethiing() {
//注: APP.ready 由程序自己定义
if(!APP.ready){
//稍后再试
return void setTimeout( doSomething,100 );
}
var result;
// 其他
return result;
}
// 现在可以了么
if (doSomething()){
//立即执行下一任务
}
这里 setTimeout(..) 函数返回一个数值(计时器间隔的唯一标识符,用于取消计时器),但是为了确保 if 语句不产生误报,我们要 void 掉它。
很多开发人员喜欢分开操作,效果都是一样的,只是没有使用void运算符:
if (!APP.ready){
//稍后再试
setTimout( doSomthing,100 );
return;
}
特殊的数字
1.不是数字的数字:NaN
NaN 意指“不是一个数字”(not a number),这个名字容易引起误会。将它理解为“无效数值”可能更准确些。
NaN是一个特殊的之,它和自身不相等,是唯一一个非自反的值。而 NaN != NaN 为 true。
那么我们该如何判断它呢?
var a = 2 / "foo";
isNaN( a ); // true
然而 isNaN(..) 有一个严重的缺陷,它的检查方式过于死板,例如:
var b = "foo";
window.isNaN( b ); // true———晕!
这个 bug 自 js 问世以来一直存在。
从 ES6 开始我们可以使用工具函数 Number.isNaN(..)。ES6 之前的浏览器的 polyfill 如下:
if( !Number.isNaN){
Number.isNaN = function(n){
return (
typeof n === "number" &&
window.isNaN( n )
);
}
}
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false———好!
实际上还有一个更简单的方法,即利用 NaN 不等于自身这个特点。NaN 是 js 中唯一一个不等于自身的值。
if(!Number.isNaN){
Number.isNaN = function(n){
return n !== n;
}
}
2.无穷数
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity
js 使用有限数字表示法,所以和纯粹的数字运算不同,js 的运算结果有可能溢出,此时结果为 Infinity 或者 -Infinity。例如:
var a = Number.MAX_VALUE;
a + a; // Infinity
那么无穷除以无穷呢?因为从数学运算和 js 语言的角度来说, Infinity/Infinity 是一个未定义操作,结果为 NaN。
有穷正数除以 Infinity 呢?结果为 0。有穷负数除以 Infinity 呢?答案是 -0。
3.零值
var a = 0 / -3; // -0
var b = 0 * -3; // -0
加法和减法运算不会得到负零
根据规范,对负零进行字符串化会返回 “0”;
var a = 0 / -3;
// 至少在某些浏览器的控制台中显示是正确的
a; // -0
// 但是规范定义的返回结果是这样!
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
// JSON也如此
JSON.stringify(a); // "0"
有意思的是,如果反过来将其字符串转换为数字,得到的结果是准确的:
+"-0"; // -0
Numebr( "-0" ); // -0
JSON.parse(" -0 "); // -0
-0 的比较操作:
-0 == 0; // true
-0 === 0; // true
0 > -0; // false
如何判断 -0:
function isNegZero(n){
n = Number( n );
return (n === 0) && (1 / n === -Infinity);
}
isNegZero( -0 ); // true
isNegZero( 0 / -3); // true
isNegZero( 0 ); // false
我们为什么需要 -0?
有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号为用来表示其他信息(比如移动的方向)。此时如果一个值为 0 的变量失去了他的符号位,它的方向信息就会丢失。所以保留 0 值得符号位可以防止此情况发生。
特殊等式
ES6 中新加入了一个工具方法 Object.is(..) 来判断两个值是否相等,可以用来处理 NaN 和负零的情况:
Object.is( NaN, NaN ); // true
Object.is( -0, -0 ); // true
Object.si( 0, -0 ); // false
对于 ES6 之前的版本,有一个简单的 polyfill:
if(!Object.is) {
Object.is = function(v1, v2){
// 判断是否为-0
if(v1 === 0 && v2 === 0){
return 1 / v1 === 1 / v2;
}
// 判断是否是 NaN
if(v1 !== v1){
return v2 !== v2;
}
// 其他情况
return v1 === v2;
}
}
能使用 == 和 === 时尽量不要使用 Object.is(..),因为前者效率更高,更为通用,后者主要用来处理那些特殊的相等比较。
值和引用
var a = 2;
var b = a; // b是a的值的一个复本
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // d是[1,2,3]的一个引用
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
- 简单值(即标量基本类型值)总是通过值赋值的方式来赋值 / 传递,包括 null、undefined、字符串、数字、布尔和 ES6 中的 symbol。
- 复合值 ——对象(包括数组和封装对象)和函数,则总是通过引用复制的方式来赋值 / 传递
由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。
var a = [1,2,3]
var b = a;
a; // [1,2,3]
b; // [1,2,3]
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
此外,还要注意函数传参:
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
x = [4,5,6]
x.push( 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo(a);
a; // 是[1,2,3,4]
请记住:我们无法自行决定使用值复制还是引用复制,一切由值的类型来决定。
如果通过值复制的方式来传递复合值(如数组),就需要为其创建一个复本,这样传递的就不是原始值。例如:
foo( a.slice() );
slice(..) 不带参数会返回当前数组的一个浅复本。由于传递给函数的是指向该复本的引用,所以 foo(..) 中的操作不会影响 a 指向的数组。
相反,如果要将标量基本类型值传递到函数内并进行更改,就需要将该值封装到一个复合值(对象、数组等)中,然后通过引用复制的方式传递。
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a : 2
}
foo(obj);
obj.a; // 42;
这样看来,如果需要传递指向标量基本类型值的引用,就可以将其封装到对应的数字封装对象中。
与预期不同的是,虽然传递的是指向数字对象的引用复本,但我们并不能通过它来更改其中的基本类型值:
function foo(x) {
x = x + 1;
x; // 3
}
var b = new Number( 2 ); // Object(a) 也一样
foo( b );
b; // 是2,不是3!
原因是标量基本类型值是不可更改的(字符串和布尔也是如此)。如果一个数字对象的标量基本类型值是2,那么该值就不能更改,除非创建一个包含新值的数字对象。