中卷(1.2)

2018-08-12  本文已影响10人  风声233

内容大纲:
js 中的数组是通过数字索引的一组任意类型的值。字符串和数组相似,但是它们的行为特征不同, 将字符串作为数组来处理时需要特别小心。js 中的数字包括“整数”和“浮点型”。
基本类型中定义了几个特殊的值。
null 类型只有一个值 nullundefined 类型也只有一个值 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 是一个特殊关键字,不是标识符,我们不能将其当作变量来使用和赋值。然而 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]

由于引用指向的是值本身而非变量,所以一个引用无法更改另一个引用的指向。

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,那么该值就不能更改,除非创建一个包含新值的数字对象。

阅读下一篇

上一篇下一篇

猜你喜欢

热点阅读