字符串和数字之间的显式转换

字符串和数字之间的转换是通过 String(..)Number(..) 这两个内建函数(原生构造函数)来实现的,注意它们前面没有 new 关键字,并不创建封装对象。

  1. var a = 42;
  2. var b = String( a );
  3. var c = "3.14";
  4. var d = Number( c );
  5. b; // "42"
  6. d; // 3.14

除了 String(..)Number(..) 以外,还有其他方法可以实现字符串和数字之间的显式转换:

  1. var a = 42;
  2. var b = a.toString();
  3. var c = "3.14";
  4. var d = +c;
  5. b; // "42"
  6. d; // 3.14

a.toString() 是显式的(“toString”意为“to a string”),不过其中涉及隐式转换。因为 toString()42 这样的基本类型值不适用,所以 js 引擎会自动为 42 创建一个封装对象(参见第 3 章),然后对该对象调用 toString()。这里显式转换中含有隐式转换。
上例中 +c+ 运算符的一元(unary)形式(即只有一个操作数)。+ 运算符显式地将 c 转 换为数字,而非数字加法运算(也不是字符串拼接,见下)。
+c 是显式还是隐式,取决于你自己的理解和经验。如果你已然知道一元运算符 + 会将操作数显式强制类型转换为数字,那它就是显式的。如果不知道的话,它就是隐式强制类型转换。

在 JavaScript 开源社区中,一元运算 + 被普遍认为是显式强制类型转换。

看如下例子(一般不这样写)

  1. var c = "3.14";
  2. var d = 5+ +c;
  3. d; // 8.14

一元运算符 -+ 一样,并且它还会反转数字的符号位。由于 -- 会被当作递减运算符来处 理,所以我们不能使用--来撤销反转,而应该像- -"3.14"这样,在中间加一个空格,才能得到正确结果 3.14

日期显式转换为数字

一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为 Unix 时间戳,以微秒为单位(从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间):

  1. var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
  2. +d; // 1408369986000
  3. // 获取当前时间戳
  4. var timestamp = +new Date();

构造函数没有参数时可以不用带 ()。于是可能会碰到var timestamp = +new Date;这样的写法。对一般的函数调用fn()并不适用。

其他的转换方法:

  1. var timestamp = new Date().getTime();
  2. // var timestamp = (new Date()).getTime();
  3. // var timestamp = (new Date).getTime();

不过最好还是使用 ES5 中新加入的静态方法 Date.now():

  1. var timestamp = Date.now(); // 1624861074034

老版本浏览器提供 Date.now() 的 polyfill 也很简单:

  1. if (!Date.now) {
  2. Date.now = function() {
  3. return +new Date();
  4. };
  5. }

不建议对日期类型使用强制类型转换,应该使用 Date.now() 来获得当前的时间戳,使用 new Date(..).getTime() 来获得指定时间的时间戳。

奇特的 ~ 运算符

字位运算符只适用于 32 位整数,运算符会强制操作数使用 32 位格式。这是通过抽象操作 ToInt32 来实现的(ES5 规范 9.5 节)。
ToInt32 首先执行 ToNumber 强制类型转换,比如 “123” 会先被转换为 123,然后再执行 ToInt32。
虽然严格说来并非强制类型转换(因为返回值类型并没有发生变化),但字位运算符(如 | 和 ~)和某些特殊数字一起使用时会产生类似强制类型转换的效果,返回另外一个数字。
例如|运算符(字位操作“或”)的空操作(no-op)0 | x,它仅执行ToInt32转换:

  1. 0 | -0; // 0
  2. 0 | NaN; // 0
  3. 0 | Infinity; // 0
  4. 0 | -Infinity; // 0

以上这些特殊数字无法以 32 位格式呈现(因为它们来自 64 位 IEEE 754 标准),因此 ToInt32 返回 0。
~操作符它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字位进行反转)。这与 ! 很相像,不仅将值强制类型转换为布尔值 <,还对其做字位反转
字位反转是个很晦涩的主题,一般不很少需要关系到字位级别。
~ 还可以有另外一种诠释,源自早期的计算机科学和离散数学:~ 返回 2 的补码。这样一来问题就清楚多了!
~x 大致等同于 -(x+1)。很奇怪,但相对更容易说明问题:

  1. ~42; // -(42+1) ==> -43

-(x+1) 中唯一能够得到 0(或者严格说是 -0)的 x 值是 -1。也就是说如果 x-1 时,~和一些数字值在一起会返回假值 0,其他情况则返回真值。
-1 是一个“哨位值”,哨位值是那些在各个类型中(这里是数字)被赋予了特殊含义的值。 在 C 语言中用 -1 来代表函数执行失败,用大于等于 0 的值来代表函数执行成功。
js 中字符串的 indexOf(..) 方法也遵循这一惯例,该方法在字符串中搜索指定的子字符串,如果找到就返回子字符串所在的位置(从 0 开始),否则返回 -1
indexOf(..) 不仅能够得到子字符串的位置,还可以用来检查字符串中是否包含指定的子字符串,相当于一个条件判断。例如:

  1. var a = "Hello World";
  2. if (a.indexOf( "lo" ) >= 0) { // true
  3. // 找到匹配!
  4. }
  5. if (a.indexOf( "lo" ) != -1) { // true
  6. // 找到匹配!
  7. }
  8. if (a.indexOf( "ol" ) < 0) { // true
  9. // 没有找到匹配!
  10. }
  11. if (a.indexOf( "ol" ) == -1) { // true
  12. // 没有找到匹配!
  13. }

>= 0== -1这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里是指用 -1 作为失败时的返回值,这些细节应该被屏蔽掉。
~indexOf() 一起可以将结果强制类型转换(实际上仅仅是转换)为真 / 假值:

  1. var a = "Hello World";
  2. ~a.indexOf( "lo" ); // -4 <-- 真值!
  3. if (~a.indexOf( "lo" )) { // true
  4. // 找到匹配!
  5. }
  6. ~a.indexOf( "ol" ); // 0 <-- 假值!
  7. !~a.indexOf( "ol" ); // true
  8. if (!~a.indexOf( "ol" )) { // true
  9. // 没有找到匹配!
  10. }

如果 indexOf(..) 返回 -1~ 将其转换为假值 0,其他情况一律转换为真值。

由 -(x+1) 推断 ~-1 的结果应该是 -0,然而实际上结果是 0,因为它是字位操作而非数学运算。

字位截除

一些开发人员使用 ~~ 来截除数字值的小数部分,以为这和 Math.floor(..) 的效果一样, 实际上并非如此。
~~ 中的第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是 ToInt32 的结果。
注意:~~ 只适用于 32 位数字,更重要的是它对负数的处理与 Math. floor(..) 不同。

  1. Math.floor( -49.6 ); // -50
  2. ~~-49.6; // -49

~~x 能将值截除为一个 32 位整数,x | 0 也可以,而且看起来还更简洁。
出于对运算符优先级的考虑,可能更倾向于使用 ~~x:

  1. ~~1E20 / 10; // 166199296
  2. 1E20 | 0 / 10; // 1661992960
  3. (1E20 | 0) / 10; // 166199296

在使用 ~~~ 进行此类转换时需要确保其他人也能够看得懂。

显式解析数字字符串

解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字。但解析和转换两者之间还是有明显的差别。

  1. var a = "42";
  2. var b = "42px";
  3. Number( a ); // 42
  4. parseInt( a ); // 42
  5. Number( b ); // NaN
  6. parseInt( b ); // 42

解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。
解析和转换之间不是相互替代的关系。它们虽然类似,但各有各的用途。如果字符串右边的非数字字符不影响结果,就可以使用解析。而转换要求字符串中所有的字符都是数字, 像 “42px” 这样的字符串就不行。
解析字符串中的浮点数可以使用 parseFloat(..) 函数。
parseInt(..) 针对的是字符串值。向 parseInt(..) 传递数字和其他类型的参数是没有用的,比如 truefunction(){...}[1,2,3]
非字符串参数会首先被强制类型转换为字符串,依赖这样的隐式强制类型 转换并非上策,应该避免向 parseInt(..) 传递非字符串参数。

  1. parseInt(true) // NaN
  2. parseInt(function(){}) // NaN
  3. parseInt([1,2,3]) // 1
  4. parseInt(['123']) // 123
  5. parseInt(['abc']) // NaN

ES5 之前的 parseInt(..) 有一个坑导致了很多 bug。即如果没有第二个参数来指定转换的基数(又称为 radix),parseInt(..) 会根据字符串的第一个字符来自行决定基数。
如果第一个字符是 x 或 X,则转换为十六进制数字。如果是 0,则转换为八进制数字。 以 x 和 X 开头的十六进制相对来说还不太容易搞错,而八进制则不然。例如:

  1. var hour = parseInt( selectedHour.value );
  2. var minute = parseInt( selectedMinute.value );
  3. console.log(
  4. "The time you selected was: " + hour + ":" + minute
  5. );

上面的代码看似没有问题,但是当小时为 08、分钟为 09 时,结果是 0:0,因为 8 和 9 都不是有效的八进制数。
将第二个参数设置为 10,即可避免这个问题:

  1. var hour = parseInt( selectedHour.value, 10 );
  2. var minute = parseInt( selectedMiniute.value, 10 );

从 ES5 开始 parseInt(..) 默认转换为十进制数,除非另外指定。如果代码需要在 ES5 之前的环境运行,请记得将第二个参数设置为 10

解析非字符串

  1. parseInt( 1/0, 19 ); // 18

很多人想当然地以为(实际上大错特错)“如果第一个参数值为 Infinity,解析结果也应该是 Infinity”,返回 18 也太无厘头了。
parseInt(1/0, 19) 实际上是 parseInt("Infinity", 19)。第一个字符是 “I”,以 19 为基数时值为 18。第二个字符 “n” 不是一个有效的数字字符,解析到此为止,和 “42px” 中的 “p” 一样。
最后的结果是 18,而非 Infinity 或者报错。

  1. parseInt( new String( "42") ); // 42

因为它的参数也是一个非字符串。如果你认为此时应该将 String 封装对象拆封(unbox) 为 “42”,那么将 42 先转换为 “42” 再解析回 42 不也合情合理吗?
这种半显式、半隐式的强制类型转换很多时候非常有用。例如:

  1. var a = {
  2. num: 21,
  3. toString: function() { return String( this.num * 2 ); }
  4. };
  5. parseInt( a ); // 42

parseInt(..) 先将参数强制类型转换为字符串再进行解析。
还有一些其他特殊的例子:

  1. parseInt( 0.000008 ); // 0 ("0" 来自于 "0.000008")
  2. parseInt( 0.0000008 ); // 8 ("8" 来自于 "8e-7")
  3. parseInt( false, 16 ); // 250 ("fa" 来自于 "false")
  4. parseInt( parseInt, 16 ); // 15 ("f" 来自于 "function..")
  5. parseInt( "0x10" ); // 16
  6. parseInt( "103", 2 ); // 2

显式转换为布尔值

与前面的 String(..)Number(..) 一样,Boolean(..)(不带 new)是显式的 ToBoolean 强制类型转换:

  1. var a = "0";
  2. var b = [];
  3. var c = {};
  4. var d = "";
  5. var e = 0;
  6. var f = null;
  7. var g;
  8. Boolean( a ); // true
  9. Boolean( b ); // true
  10. Boolean( c ); // true
  11. Boolean( d ); // false
  12. Boolean( e ); // false
  13. Boolean( f ); // false
  14. Boolean( g ); // false

虽然 Boolean(..) 是显式的,但并不常用。
和前面讲过的 + 类似,一元运算符 ! 显式地将值强制类型转换为布尔值。但是它同时还将真值反转为假值(或者将假值反转为真值)。所以显式强制类型转换为布尔值最常用的方法是 !!,因为第二个 ! 会将结果反转回原值:

  1. var a = "0";
  2. var b = [];
  3. var c = {};
  4. var d = "";
  5. var e = 0;
  6. var f = null;
  7. var g;
  8. !!a; // true
  9. !!b; // true
  10. !!c; // true
  11. !!d; // false
  12. !!e; // false
  13. !!f; // false
  14. !!g; // false

if(..).. 这样的布尔值上下文中,如果没有使用 Boolean(..)!!,就会自动隐式地进行 ToBoolean 转换。建议使用 Boolean(..)!! 来进行显式转换以便让代码更清晰易读。
显式 ToBoolean 的另外一个用处,是在 JSON 序列化过程中将值强制类型转换为 truefalse:

  1. var a = [
  2. 1,
  3. function(){ /*..*/ },
  4. 2,
  5. function(){ /*..*/ }
  6. ];
  7. JSON.stringify( a ); // "[1,null,2,null]"
  8. JSON.stringify( a, function(key,val){
  9. if (typeof val == "function") {
  10. // 函数的ToBoolean强制类型转换
  11. return !!val;
  12. }
  13. else {
  14. return val;
  15. }
  16. } );
  17. // "[1,true,2,true]"
  1. var a = 42;
  2. var b = a ? true : false;

三元运算符 ? : 判断 a 是否为真,如果是则将变量 b赋值为 true,否则赋值为 false。 表面上这是一个显式的 ToBoolean 强制类型转换,因为返回结果是 true 或者 false
然而这里涉及隐式强制类型转换,因为 a 要首先被强制类型转换为布尔值才能进行条件判断。这种情况称为“显式的隐式”,有百害而无一益,我们应彻底杜绝。建议使用 Boolean(a)!!a 来进行显式强制类型转换。