本章主要涉及的是在开发中最经常涉及的三种值:

  • 数组 Array
  • 字符串 String
  • 数字 Number

这三种值也是在其他语言中经常用到的,所以需要重点的了解一下这些值在JavaScript中的表现

一 数组

对比Java这种强类型语言(本人是做Java开发出身的,除了JS外比较熟的语言也就是Java了。),JavaScript在使用数组时候并不需要设定任何类型,其数组可以容纳任何JavaScript中合法类型的值

  1. // 甚至是在一个数组内容纳多个不同类型值,真正的"有容乃大"
  2. var youRongNaiDa = ["a",1,undefined,null,[1],{}]
  3. youRongNaiDa.length; // 6
  4. youRongNaiDa[1]; // 1
  5. youRongNaiDa[4][0]; // 1

且数组在声明的时候无需设定大小,添加新元素时也无需创建新数组进行扩容。

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

但注意的是,在创建”稀疏”数组时候(即含有空白或空缺单元的数组),使用时候要小心

  1. var a = [];
  2. a[3] = 2;
  3. a[1]; // undefined
  4. a.length; // 4

上面代码中,a[1]明显是空缺单元但程序显示的值为undefined。这和将undefined赋给a[1]还是有区别的。
其次,在 类型 一章说到,Array其实也是 object 下的一个子类型.
所以也可以包含字符串键值属性(但并不影响Array计算其数组长度)

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

但注意的是,如果使用的字符串能够强制转化为十进制的数字,则会被当做数字索引来用

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

所以通常不要在数组对象内去声明字符串的键值,要用就用真正的对象来。

二 字符串

很多时候字符串都会被当作字符数组来看待,它们确实很相似,都是类数组,都有length属性以及indexOf(…)(es5数组开始支持)和concat(…)方法;

  1. var a = "foo";
  2. var b = ["f","o","o"];
  3. a.length; // 3
  4. b.length; // 3
  5. a.indexOf("0"); // 1
  6. b.indexOf("0"); // 1
  7. a.concat("bar"); // "foobar"
  8. b.concat(["b","a","r"]); // ["f","o","o","b","a","r"]

这并不能意味这两者都是”字符数组”

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

上述例子中,字符串在JavaScript中是不可变的而数组是可以改变的,而且字符串a[1]并不属于合法语法。
字符串在JavaScript中是不可变的,当字符串创建后其字符串的成员函数不会改变其原始值,而是会创建一个新的字符串并返回。而数组的成员函数都是在其原始值上进行操作。
但我们可以借用数组的一些方法来处理字符串,非常好用~~~

  1. var a = "foo";
  2. var b = Array.prototype.join.call(a, "-");
  3. var c = Array.prototype.map.call(a, function(value){
  4. return value.toUpperCase() + ',';
  5. }).join("");
  6. b; // f-o-o
  7. c; // F,O,O

但其中有一个数组反转函数 reverse() 是字符串无法借用的,因为字符串是不可变的
但我们可以换个思路,这也是很经典的一道 JavaScript 面试题 —- 反转字符串

  1. var a = "abcd";
  2. var b = a.split("").reverse().join("");
  3. b; // "dcba"

注意!!!这仅仅针对简单的字符串数组,如果是比较复杂的Unicode字符可能不适用。
但大部分场景下,这种方式还是简单管用的!(至于复杂的情况再说,这里也不做多于表述)

三 数字

JavaScript中只有一种的数值类型:number,其又包含了”整数”和带小数的十进制数。
这里之所以”整数”加上引号,因为在JavaScript中并没有真正意义上的整数,其整数可以认为是没有显现小数的十进制数,即42.0等同于42”整数”。

  1. var a = 42;
  2. var b = 42.3;
  3. var c = 0.42;
  4. var d = .423; // 可以省略前面的0
  5. var e = 43.0;
  6. var f = 43.; // 后面的0也可以省略,但这种纯粹的没必要。为了可读性,做个人吧。

赋值特别大或特别小的数字时候可以使用指数格式显示,与toExponential()函数的输出结果一致

  1. var a = 1e10;
  2. a; // 10000000000;
  3. a.toExponential(); // "1e+10"
  4. var b = 1 / a;
  5. b; // 1e-10

数字值可以直接使用内置对象Number的方法

  1. var a = 42;
  2. // toFixed(): 保留几位小数,返回字符串
  3. a.toFixed(1); // "42.0"
  4. // toPrecision(): 保留几位有效数字,返回字符串
  5. a.toPrecision(1); // "4e+1"

上面方法不仅可以用于数字变量,也可以直接用在数字常量上。
但注意,. 运算符在识别优先级上,”被认为是数字常量部分”的优先级高于”认为是对象属性访问符”的优先级

  1. // 这里可以理解为:JavaScript认为,这边的.是包含在数字常量里
  2. // 即在识别时候,该语句被拆分为(42.)toFixed(1);
  3. // 明显toFixed方法前没有.运算符来调用,所以这边会被识别为语法错误
  4. 42.toFixed(1); // SyntaxError
  5. // 但加上一些语法处理即可
  6. (42).toFixed(1); // "42.0"
  7. 42..toFixed(1); // "42.0"

这样方式其实实际开发中并不多见,但了解一下也不是啥坏事,起码能装逼。
其次,JavaScript中也支持声明其他进制的数

  1. // es5
  2. 0xf3; // 243的十六进制
  3. 0Xf3; // 同上
  4. 0363; // 243的八进制,但严格模式下并不支持,建议也别用太容易混淆了。
  5. // es6下的新格式
  6. 0o363; // 243的八进制
  7. 0O363; // 同上
  8. 0b11110011; // 243的二进制
  9. 0B11110011; // 同上

为了避免混淆,建议使用时候,字母都小写。(毕竟0和大写O太像了,还是小写比较有友好性)。

经典浮点问题 - 0.1 + 0.2 === 0.3

在JavaScript开发或面试中肯定有碰到下面这种情况

  1. 0.1 + 0.2 === 0.3; // false

得出了神奇的结果,初学者很多都是摸不着头脑。
其实是因为JavaScript的数字值遵守的是IEEE754规范(采用了这个规范的语言都有这个毛病)
其转换原理大致是:小数部分乘以2取整数,若结果小数部分不为0,则继续乘2取整数直到小数部分为0

  1. // 过程大致如下
  2. 0.1 * 2 = 0.2 --------------- 取整数 0,小数 0.2
  3. 0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
  4. 0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
  5. 0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
  6. 0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
  7. 0.2 * 2 = 0.4 --------------- 取整数 0,小数 0.4
  8. 0.4 * 2 = 0.8 --------------- 取整数 0,小数 0.8
  9. 0.8 * 2 = 1.6 --------------- 取整数 1,小数 0.6
  10. 0.6 * 2 = 1.2 --------------- 取整数 1,小数 0.2
  11. ...

随便上网找个转换器算一下,就会发现后面是 0011 的无限循环,因此二进制无法精确保留这种0.1的小数。
在 IEEE 754 规范中,在双精确度 64 位下最多存储的有效整数位数为 52 位,会采用 就近舍入(round to nearest)模式(进一舍零) 进行存储。
这必然造成了精度丢失,导致计算结果的精度丢失

  1. 0.1 + 0.2 // 0.30000000000000004

这才是产生这个奇怪算式的原因。(只要是无法在52内算出来的小数,都会有这个问题,这是没办法的。)
ES6中提供了精度的常量值:Number.EPSILON,用来做数字计算中的精度丢失比对。
Number.EPSILON的值通常是 2-52。

  1. (0.1 + 0.2) - 0.3 < Number.EPSILON; // true

整数的安全范围

JavaScript中,Number类型能够安全表示的范围:
-9007199254740991 (-(2^53-1)) - 9007199254740991(2^53-1)
ES6中,上限值定义为:Number.MAX_SAFE_INTEGER,下限值定义为:Number.MIN_SAFE_INTEGER。
ES10中,新定义的基本数据类型:bigint,并不受到该范围限制。(爱写多少写多少,越多表现越好!)

整数检测

列举一些常用的整数检测方法

  1. // 检测是否为整数,后面小数都为0也是认为是整数
  2. Number.isInteger(42); // true
  3. Number.isInteger(42.0000000000000); // true
  4. Number.isInteger(42.1); // false
  5. // 检测是否是安全范围内的数
  6. Number.isSafeInteger(Number.MAX_SAFE_INTEGER); // true
  7. Number.isSafeInteger(Math.pow(2,53)); // false
  8. Number.isSafeInteger(Math.pow(2,53) - 1); // true

不是数字的数字 - NaN

这是个神奇的数字 - NaN!
其字面意思是:“Not a Number”,但在typeof下却识别它是个数字

  1. typeof NaN; // "number"

其NaN是一个内置的境界词,用于指出在数字类型中的错误情况,比如:使用了不是数字类型的值进行计算等。
这时候,肯定有聪明的小伙伴觉得可以直接判断计算的结果是否为NaN来判断计算结果的合法性。

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

然后就获得了上面这种神奇的结果!
NaN是一个特殊的值,是JavaScript中唯一一个自反的值,即自己不等于自己。

  1. NaN !== NaN; // true

这时候肯定有经验丰富的小伙伴说用 isNaN(…)来判断

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

但isNaN(…)方法过于死板 —— 检查参数是否不是NaN,也不是数字!

  1. var a = 1 / "foo"; // NaN
  2. var b = "NaN";
  3. window.isNaN(a); // true
  4. window.isNaN(b); // true - 啊,这......

明显b是个字符串,它也认为这是个NaN。
所以最简单的方法,就是让他自己和自己进行对比即可

  1. var a = 1 / "foo"; // NaN
  2. if(a !== a) {
  3. console.log("这是一个NaN.");
  4. }

无穷数

Java中有个很神奇的地方

  1. 1 / 0; // /by zero
  2. 1 / 0.0; // Infinity
  3. 1.0 / 0.0; // Infinity

明显1/0在java中是不允许的会报错,而1.0/0.0在程序中是运行的返回值为Infinity。
而在 JavaScript 中,因为只有number一种类型,所以1/0得出结果会是什么呢?

  1. 1 / 0; // Infinity

答案还是Infinity。java的原理我就不多概述了,毕竟这里是学JavaScript的文章。
在JavaScript中使用有限数字表示法(即IEEE754规范),当结果超出该安全范围时候,就会用Infinity/-Infinity表示

  1. 1 / 0; // Infinity
  2. -1 / 0; // -Infinity

零值

在JavaScript中其实有两个0:一个是0(也作+0),另一个是-0.

  1. 0 / -3; // -0
  2. 0 * -3; // -0

加减法是永远不会得到-0结果。
老旧版本的浏览器输出时候然而会显示为0。
根据规范,转换成字符串时候,其显示是为0.但”-0”转会数字却会是正常结果

  1. (-0).toString(); // 0
  2. Number("-0"); // -0

在实际开发中,其实这些细节都不会有影响。那为啥要有-0呢?
因为在有些程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字符号位用来表示其他信息(比如运动的方向),所以当0失去了它的符号位,导致它的方向信息也就会丢失。故这里保留了0的符号位防止此类情况。
说白了,保留0的符号位是保证了数字值的方向完整性,连续性。

赋值/传递

首先我们将JavaScript中数据类型进行区分:

  • 简单值,即基本类型值,包括:null,undefined,字符串,数字,boolean,ES6中的Symbol以及ES10中的bigint
  • 复合值,包括对象和函数(对象下的子类:数组,封装对象等等)。

其简单值的赋值/传递都是通过 复制 的方式,将新的值传给下一个变量。
而复合值的赋值/传递都是通过 引用复制 的方式,传递给下一个变量。

  1. var a = 1;
  2. var b = a;
  3. a = 2;
  4. a; // 2
  5. b // 1
  6. var c = [1];
  7. var d = c;
  8. c[0] = 2;
  9. c; // [2]
  10. d; // [2]