JavaScript的强制类型转换(你不知道的JavaScript)

JavaScript 中的 || 和 &&

    逻辑运算符 ||(或)和 &&(与)应该并不陌生,也许正因为如此有人觉得它们在JavaScript 中的表现也和在其他语言中一样。这里面有一些非常重要但却不太为人所知的细微差别。我其实不太赞同将它们称为“逻辑运算符”,因为这不太准确。
    称它们为“选择器运算符”(selector operators)或者“操作数选择器运算符”(operand selector operators)更恰当些。为什么?因为和其他语言不同,在 JavaScript 中它们返回的并不是布尔值。它们的返回值是两个操作数中的一个(且仅一个)。即选择两个操作数中的一个,然后返回它的值。

引述 ES5 规范 11.11 节:
    && 和 || 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
例如:
    var a = 42;
    var b = "abc";
    var c = null;
    a || b; // 42
    a && b; // "abc"
    c || b; // "abc"
    c && b; // null 
    
在 C 和 PHP 中,上例的结果是 true 或 false,在 JavaScript(以及 Python 和 Ruby)中却是某个操作数的值。

|| 和 && 首先会对第一个操作数(a 和 c)执行条件判断,如果其不是布尔值(如上例)就
先进行 ToBoolean 强制类型转换,然后再执行条件判断。

对于 || 来说,如果条件判断结果为 true 就返回第一个操作数(a 和 c)的值,如果为
false 就返回第二个操作数(b)的值。

&& 则相反,如果条件判断结果为 true 就返回第二个操作数(b)的值,如果为 false 就返
回第一个操作数(a 和 c)的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果(其中可能涉及强制类型转换)。
c && b 中 c 为 null,是一个假值,因此 && 表达式的结果是 null(即 c 的值),而非条件判断的结果 false。

换一个角度来理解:
    a || b;
    // 大致相当于(roughly equivalent to):
    a ? a : b;
    a && b;
    // 大致相当于(roughly equivalent to):
    a ? b : a;
    
之所以说大致相当,是因为它们返回结果虽然相同但是却有一个细微的差别。在 a ? a : b 中,如果 a 是一个复杂一些的表达式(比如有副作用的函数调用等),它有可能被执行两次(如果第一次结果为真)。而在 a || b 中 a只执行一次,其结果用于条件判断和返回结果(如果适用话)。a  b 和 a ? b : a 也是如此。

下面是一个十分常见的 || 的用法,也许你已经用过但并未完全理解:
    function foo(a,b) {
        a = a || "hello";
        b = b || "world";
        console.log( a + " " + b );
    }
    foo(); // "hello world"
    foo( "yeah", "yeah!" ); // "yeah yeah!"
    a = a || "hello"(又称为 C# 的“空值合并运算符”的 JavaScript 版本)检查变量 a,如果还未赋值(或者为假值),就赋予它一个默认值("hello")。这里需要注意!
    
    foo( "That’s it!", "" ); // "That’s it! world" <-- 晕!
第二个参数 "" 是一个假值(falsy value,参见 4.2.3 节),因此 b = b || "world" 条件不成立,返回默认值 "world"。

===============================================================================
    
再来看看 &&。

有一种用法对开发人员不常见,然而 JavaScript 代码压缩工具常用。就是如果第一个操作数为真值,则 && 运算符“选择”第二个操作数作为返回值,这也叫作“守护运算符”,即前面的表达式为后面的表达式“把关”:

    function foo() {
      console.log( a );
    }
    var a = 42;
    a && foo(); // 42

foo() 只有在条件判断 a 通过时才会被调用。如果条件判断未通过,a && foo() 就会悄然
终止(也叫作“短路”,short circuiting),foo() 不会被调用。

这样的用法对开发人员不太常见,开发人员通常使用 if (a) { foo(); }。但 JavaScript
代码压缩工具用的是 a && foo(),因为更简洁。以后再碰到这样的代码你就知道是怎么
回事了。

|| 和 && 各自有它们的用武之地,前提是我们理解并且愿意在代码中运用隐式强制类型
转换。

a = b || "something" 和 a && b() 用到了“短路”机制。

此时会有疑问:既然返回的不是 true 和 false,为什么 a && (b || c) 这样的表达式在
if 和 for 中没出过问题?

答:这或许并不是代码的问题,问题在于你可能不知道这些条件判断表达式最后还会执行布尔
值的隐式强制类型转换。

例如:
    var a = 42;
    var b = null;
    var c = "foo";
    if (a && (b || c)) {
      console.log( "yep" );
    }
这里 a && (b || c) 的结果实际上是 "foo" 而非 true,然后再由 if 将 foo 强制类型转换为布尔值,所以最后结果为 true。现在明白了吧,这里发生了隐式强制类型转换。如果要避免隐式强制类型转换就得这样:
    if (!!a && (!!b || !!c)) {
      console.log( "yep" );
    }

JSON字符串化

工具函数 JSON.stringify(..) 在将 JSON 对象序列化为字符串时也用到了 ToString。
请注意,JSON 字符串化并非严格意义上的强制类型转换,因为其中也涉及 ToString 的相
关规则,所以这里顺带介绍一下。

对大多数简单值来说,JSON 字符串化和 toString() 的效果基本相同,只不过序列化的结
果总是字符串:
    JSON.stringify( 42 ); // "42"
    JSON.stringify( "42" ); // ""42"" (含有双引号的字符串)
    JSON.stringify( null ); // "null"
    JSON.stringify( true ); // "true"
所有安全的 JSON 值(JSON-safe)都可以使用 JSON.stringify(..) 字符串化。安全的
JSON 值是指能够呈现为有效 JSON 格式的值。

为了简单起见,我们来看看什么是不安全的 JSON 值。undefined、function、symbol
(ES6+)和包含循环引用(对象之间相互引用,形成一个无限循环)的对象都不符合 JSON
结构标准,支持 JSON 的语言无法处理它们。JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null(以保证单元位置不变)。

例如:
    JSON.stringify( undefined ); // undefined
    JSON.stringify( function(){} ); // undefined
    JSON.stringify(
      [1,undefined,function(){},4]
    ); // "[1,null,null,4]"
    JSON.stringify(
      { a:2, b:function(){} }
    ); // "{"a":2}"
    
对包含循环引用的对象执行 JSON.stringify(..) 会出错。如果对象中定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化。如果要对含有非法 JSON 值的对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义 toJSON() 方法来返回一个安全的 JSON 值。

例如:
    var o = { };
    var a = {
     b: 42,
     c: o,
     d: function(){}
    };
    // 在a中创建一个循环引用
    o.e = a;
    // 循环引用在这里会产生错误
    // JSON.stringify( a );
    // 自定义的JSON序列化
    a.toJSON = function() {
     // 序列化仅包含b
     return { b: this.b };
    };
    JSON.stringify( a ); // "{"b":42}"
    
    很多人误以为 toJSON() 返回的是 JSON 字符串化后的值,其实不然,除非我们确实想要对
字符串进行字符串化(通常不会!)。toJSON() 返回的应该是一个适当的值,可以是任何
类型,然后再由 JSON.stringify(..) 对其进行字符串化。
也就是说,toJSON() 应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回
一个 JSON 字符串”。

例如:
    var a = {
     val: [1,2,3],
     // 可能是我们想要的结果!
     toJSON: function(){
     return this.val.slice( 1 );
     }
    };
    var b = {
     val: [1,2,3],
     // 可能不是我们想要的结果!
     toJSON: function(){
     return "[" +
     this.val.slice( 1 ).join() +
     "]";
     }
    };
    JSON.stringify( a ); // "[2,3]"
    JSON.stringify( b ); // ""[2,3]""
这里第二个函数是对 toJSON 返回的字符串做字符串化,而非数组本身。
现在介绍几个不太为人所知但却非常有用的功能。

我们可以向 JSON.stringify(..) 传递一个可选参数 replacer,它可以是数组或者函数,用
来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和 toJSON() 很像。如果 replacer 是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象
的属性名称,除此之外其他的属性则被忽略。

如果 replacer 是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用
一次,每次传递两个参数,键和值。如果要忽略某个键就返回 undefined,否则返回指定
的值。
    var a = {
     b: 42,
     c: "42",
     d: [1,2,3]
    };
    JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
    JSON.stringify( a, function(k,v){
     if (k !== "c") return v;
    } );
    // "{"b":42,"d":[1,2,3]}"
    
    tips: 如果 replacer 是函数,它的参数 k 在第一次调用时为 undefined(就是对对象
本身调用的那次)。if 语句将属性 "c" 排除掉。由于字符串化是递归的,因此数组 [1,2,3] 中的每个元素都会通过参数 v 传递给 replacer,即 1、2 和 3,参数 k 是它们的索引值,即 0、1 和 2。

JSON.string 还有一个可选参数 space,用来指定输出的缩进格式。space 为正整数时是指定
每一级缩进的字符数,它还可以是字符串,此时最前面的十个字符被用于每一级的缩进:
    var a = {
     b: 42,
     c: "42",
     d: [1,2,3]
    };
    JSON.stringify( a, null, 3 );
    // "{
        // "b": 42,
        // "c": "42",
        // "d": [
            // 1,
            // 2,
            // 3
        // ]
    // }"
 
    JSON.stringify( a, null, "-----" );
    // "{
    // -----"b": 42,
    // -----"c": "42",
    // -----"d": [
    // ----------1,
    // ----------2,
    // ----------3
    // -----]
    // }"
请记住,JSON.stringify(..) 并不是强制类型转换。在这里介绍是因为它涉及 ToString 强
制类型转换,具体表现在以下两点。
(1) 字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。
(2) 如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方法,那么该方法会在字符
串化前调用,以便将对象转换为安全的 JSON 值。

ToNumber

有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了
抽象操作 ToNumber。
其中 true 转换为 1,false 转换为 0。undefined 转换为 NaN,null 转换为 0。

tips: ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法(参见第 3 章)。处理失败时返回 NaN(处理数字常量失败时会产生语法错误)。不同之处是 ToNumber 对以 0 开头的十六进制数并不按十六进制处理(而是按十进制,参见第 2 章)。

对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString()的返回值(如果存在)来进行强制类型转换。

如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。

从 ES5 开始,使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null,并且没
有 valueOf() 和 toString() 方法,因此无法进行强制类型转换。

例如:
    var a = {
     valueOf: function(){
        return "42";
     }
    };
    var b = {
     toString: function(){
        return "42";
     }
    };
    var c = [4,2];
    c.toString = function(){
        return this.join( "" ); // "42"
    };
    Number( a ); // 42
    Number( b ); // 42
    Number( c ); // 42
    Number( "" ); // 0
    Number( [] ); // 0
    Number( [ "abc" ] ); // NaN

ToBoolean

首先也是最重要的一点是,JavaScript 中有两个关键词 true 和 false,分别代表布尔类型
中的真和假。我们常误以为数值 1 和 0 分别等同于 true 和 false。在有些语言中可能是这
样,但在 JavaScript 中布尔值和数字是不一样的。虽然我们可以将 1 强制类型转换为 true,
将 0 强制类型转换为 false,反之亦然,但它们并不是一回事。

1. 假值(falsy value)
我们再来看看其他值是如何被强制类型转换为布尔值的。

JavaScript 中的值可以分为以下两类:
(1) 可以被强制类型转换为 false 的值
(2) 其他(被强制类型转换为 true 的值)

JavaScript 规范具体定义了一小撮可以被强制类型转换为 false 的值。
ES5 规范 9.2 节中定义了抽象操作 ToBoolean,列举了布尔强制类型转换所有可能出现的
结果。
以下这些是假值:
    • undefined
    • null
    • false
    • +0、-0 和 NaN
    • ""
假值的布尔强制类型转换结果为 false。

从逻辑上说,假值列表以外的都应该是真值(truthy)。但 JavaScript 规范对此并没有明确定义,只是给出了一些示例,例如规定所有的对象都是真值,我们可以理解为假值列表以外的值都是真值。

2. 假值对象(falsy object)
这个标题似乎有点自相矛盾。前面讲过规范规定所有的对象都是真值,怎么还会有假值对
象呢?
有人可能会以为假值对象就是包装了假值的封装对象(如 ""、0 和 false),实际不然。

例如:
    var a = new Boolean( false );
    var b = new Number( 0 );
    var c = new String( "" );
它们都是封装了假值的对象(参见第 3 章)。那它们究竟是 true 还是 false 呢?答案很简单:
    var d = Boolean( a && b && c );
    d; // true
    d 为 true,说明 a、b、c 都为 true。
    
    tips: 请注意,这里 Boolean(..) 对 a && b && c 进行了封装,有人可能会问为什
么。我们暂且记下,稍后会作说明。你可以试试不用 Boolean(..) 的话 d = a
&& b && c 会产生什么结果。

如果假值对象并非封装了假值的对象,那它究竟是什么?
值得注意的是,虽然 JavaScript 代码中会出现假值对象,但它实际上并不属于 JavaScript 语
言的范畴。

浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来(exotic)值,这些就是“假值对象”。

假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false。

最常见的例子是 document.all,它是一个类数组对象,包含了页面上的所有元素,由DOM(而不是 JavaScript 引擎)提供给 JavaScript 程序使用。它以前曾是一个真正意义上的对象,布尔强制类型转换结果为 true,不过现在它是一个假值对象。

document.all 并不是一个标准用法,早就被废止了。

3. 真值(truthy value)
真值就是假值列表之外的值。
例如:
    var a = "false";
    var b = "0";
    var c = "''";
    var d = Boolean( a && b && c );
    d;
这里 d 应该是 true 还是 false 呢?
答案是 true。上例的字符串看似假值,但所有字符串都是真值。不过 "" 除外,因为它是
假值列表中唯一的字符串。

再如:
    var a = []; // 空数组——是真值还是假值?
    var b = {}; // 空对象——是真值还是假值?
    var c = function(){}; // 空函数——是真值还是假值?
    var d = Boolean( a && b && c );
    d;
d 依然是 true。还是同样的道理,[]、{} 和 function(){} 都不在假值列表中,因此它们都
是真值。

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

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

下面是两者之间的显式强制类型转换:
    var a = 42;
    var b = String( a );
    var c = "3.14";
    var d = Number( c );
    b; // "42"
    d; // 3.14
    
除了 String(..) 和 Number(..) 以外,还有其他方法可以实现字符串和数字之间的显式转换:
    var a = 42;
    var b = a.toString();
    var c = "3.14";
    var d = +c;
    b; // "42"
    d; // 3.14
a.toString() 是显式的(“toString”意为“to a string”),不过其中涉及隐式转换。因为
toString() 对 42 这样的基本类型值不适用,所以 JavaScript 引擎会自动为 42 创建一个封
装对象(参见第 3 章),然后对该对象调用 toString()。这里显式转换中含有隐式转换。

上例中 +c 是 + 运算符的一元(unary)形式(即只有一个操作数)。+ 运算符显式地将 c 转
换为数字,而非数字加法运算(也不是字符串拼接,见下)。

+c 是显式还是隐式,取决于你自己的理解和经验。如果你已然知道一元运算符 + 会将操作
数显式强制类型转换为数字,那它就是显式的。如果不明就里的话,它就是隐式强制类型
转换,让你摸不着头脑。

不过这样有时候也容易产生误会。例如:
    var c = "3.14";
    var d = 5+ +c;
    d; // 8.14
一元运算符 - 和 + 一样,并且它还会反转数字的符号位。由于 -- 会被当作递减运算符来处理,所以我们不能使用 -- 来撤销反转,而应该像 - -"3.14" 这样,在中间加一个空格,才能得到正确结果 3.14。运算符的一元和二元形式的组合你也许能够想到很多种情况,下面是一个疯狂的例子:
    1 + - + + + - + 1; // 2
尽量不要把一元运算符 +(还有 -)和其他运算符放在一起使用。上面的代码可以运行,但非常糟糕。此外 d = +c(还有 d =+ c)也容易和 d += c 搞混,两者天壤之别。

1. 日期显式转换为数字
一元运算符 + 的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为
Unix 时间戳,以微秒为单位(从 1970 年 1 月 1 日 00:00:00 UTC 到当前时间):
    var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
    +d; // 1408369986000
    我们常用下面的方法来获得当前的时间戳,例如:
    var timestamp = +new Date();
    
    JavaScript 有一处奇特的语法,即构造函数没有参数时可以不用带 ()。于是
我们可能会碰到 var timestamp = +new Date; 这样的写法。这样能否提高代
码可读性还存在争议,因为这仅用于 new fn(),对一般的函数调用 fn() 并
不适用。

将日期对象转换为时间戳并非只有强制类型转换这一种方法,或许使用更显式的方法会更好一些:
    var timestamp = new Date().getTime();
    // var timestamp = (new Date()).getTime();
    // var timestamp = (new Date).getTime();
    不过最好还是使用 ES5 中新加入的静态方法 Date.now():
    var timestamp = Date.now();
    为老版本浏览器提供 Date.now() 的 polyfill 也很简单:
    if (!Date.now) {
      Date.now = function() {
        return +new Date();
      };
    }
我们不建议对日期类型使用强制类型转换,应该使用 Date.now() 来获得当前的时间戳,使用 new Date(..).getTime() 来获得指定时间的时间戳。

2. 奇特的 ~ 运算符
    一个常被人忽视的地方是 ~ 运算符(即字位操作“非”)相关的强制类型转换,它很让人
费解,以至于了解它的开发人员也常常对其敬而远之。秉承本书的一贯宗旨,我们在此深
入探讨一下 ~ 有哪些用处。

字位运算符只适用于 32 位整数,运算符会强制操作数使用 32 位格式。这是通过抽象操作 ToInt32 来实现的。

ToInt32 首先执行 ToNumber 强制类型转换,比如 "123" 会先被转换为 123,然后再执行
ToInt32。

虽然严格说来并非强制类型转换(因为返回值类型并没有发生变化),但字位运算符(如 | 和 ~)和某些特殊数字一起使用时会产生类似强制类型转换的效果,返回另外一个数字。

例如 | 运算符(字位操作“或”)的空操作(no-op)0 | x,它仅执行 ToInt32 转换:
    0 | -0; // 0
    0 | NaN; // 0
    0 | Infinity; // 0
    0 | -Infinity; // 0
以上这些特殊数字无法以 32 位格式呈现(因为它们来自 64 位 IEEE 754 标准),因此ToInt32 返回 0。

关于 0 | ___ 是显式还是隐式仍存在争议。从规范的角度来说它无疑是显式的,但如果对字位运算符没有这样深入的理解,它可能就是隐式的。为了前后保持一致,我们这里将其视为显式。再回到 ~。它首先将值强制类型转换为 32 位数字,然后执行字位操作“非”(对每一个字位进行转)。

tips:这与 ! 很相像,不仅将值强制类型转换为布尔值 <,还对其做字位反转

对 ~ 还可以有另外一种诠释,源自早期的计算机科学和离散数学:~ 返回 2 的补码。这样
一来问题就清楚多了!~x 大致等同于 -(x+1)。很奇怪,但相对更容易说明问题:~42; // -(42+1) ==> -43也许你还是没有完全弄明白 ~ 到底是什么玩意?为什么把它放在强制类型转换一章中介绍?稍安勿躁。

 -(x+1) 中唯一能够得到 0(或者严格说是 -0)的 x 值是 -1。也就是说如果 x 为 -1 时,~
和一些数字值在一起会返回假值 0,其他情况则返回真值。然而这与我们讨论的内容有什么关系呢?-1 是一个“哨位值”,哨位值是那些在各个类型中(这里是数字)被赋予了特殊含义的值。在 C 语言中我们用 -1 来代表函数执行失败,用大于等于 0 的值来代表函数执行成功。

JavaScript 中字符串的 indexOf(..) 方法也遵循这一惯例,该方法在字符串中搜索指定的子
字符串,如果找到就返回子字符串所在的位置(从 0 开始),否则返回 -1。

indexOf(..) 不仅能够得到子字符串的位置,还可以用来检查字符串中是否包含指定的子
字符串,相当于一个条件判断。例如:
    var a = "Hello World";
    if (a.indexOf( "lo" ) >= 0) { // true
     // 找到匹配!
    }
    if (a.indexOf( "lo" ) != -1) { // true
     // 找到匹配!
    }
    if (a.indexOf( "ol" ) < 0) { // true
     // 没有找到匹配!
    }
    if (a.indexOf( "ol" ) == -1) { // true
     // 没有找到匹配!
    }
>= 0 和 == -1 这样的写法不是很好,称为“抽象渗漏”,意思是在代码中暴露了底层的实
现细节,这里是指用 -1 作为失败时的返回值,这些细节应该被屏蔽掉。

现在我们终于明白 ~ 有什么用处了! ~ 和 indexOf() 一起可以将结果强制类型转换(实际
上仅仅是转换)为真 / 假值:
    var a = "Hello World";
    ~a.indexOf( "lo" ); // -4 <-- 真值!
    if (~a.indexOf( "lo" )) { // true
     // 找到匹配!
    }
    ~a.indexOf( "ol" ); // 0 <-- 假值!
    !~a.indexOf( "ol" ); // true
    if (!~a.indexOf( "ol" )) { // true
     // 没有找到匹配!
    }
如果 indexOf(..) 返回 -1,~ 将其转换为假值 0,其他情况一律转换为真值。

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

从技术角度来说,if (~a.indexOf(..)) 仍然是对 indexOf(..) 的返回结果进行隐式强制类
型转换,0 转换为 false,其他情况转换为 true。但我觉得 ~ 更像显式强制类型转换,前
提是我对它有充分的理解。个人认为 ~ 比 >= 0 和 == -1 更简洁。

3. 字位截除


一些开发人员使用 ~~ 来截除数字值的小数部分,以为这和 Math.floor(..) 的效果一样,实际上并非如此。~~ 中的第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有字位反转回原值,最后得到的仍然是ToInt32 的结果。

对 ~~ 我们要多加注意。首先它只适用于 32 位数字,更重要的是它对负数的处理与 Math.
    floor(..) 不同。
    Math.floor( -49.6 ); // -50
    ~~-49.6; // -49
    ~~x 能将值截除为一个 32 位整数,x | 0 也可以,而且看起来还更简洁。
    出于对运算符优先级(详见第 5 章)的考虑,我们可能更倾向于使用 ~~x:
    ~~1E20 / 10; // 166199296
    1E20 | 0 / 10; // 1661992960
    (1E20 | 0) / 10; // 166199296
我们在使用 ~ 和 ~~ 进行此类转换时需要确保其他人也能够看得懂。

显式解析数字字符显式转换为布尔值串

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

例如:
    var a = "42";
    var b = "42px";
    Number( a ); // 42
    parseInt( a ); // 42
    Number( b ); // NaN
    parseInt( b ); // 42
    
    解析允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回 NaN。解析和转换之间不是相互替代的关系。它们虽然类似,但各有各的用途。如果字符串右边的非数字字符不影响结果,就可以使用解析。而转换要求字符串中所有的字符都是数字,像 "42px" 这样的字符串就不行。
    
    不要忘了 parseInt(..) 针对的是字符串值。向 parseInt(..) 传递数字和其他类型的参数是没有用的,比如 true、function(){...} 和 [1,2,3]。
    
    非字符串参数会首先被强制类型转换为字符串(参见 4.2.1 节),依赖这样的隐式强制类型转换并非上策,应该避免向 parseInt(..) 传递非字符串参数。
    
    ES5 之前的 parseInt(..) 有一个坑导致了很多 bug。即如果没有第二个参数来指定转换的基数(又称为 radix),parseInt(..) 会根据字符串的第一个字符来自行决定基数。如果第一个字符是 x 或 X,则转换为十六进制数字。如果是 0,则转换为八进制数字。以 x 和 X 开头的十六进制相对来说还不太容易搞错,而八进制则不然。例如:
    var hour = parseInt( selectedHour.value );
    var minute = parseInt( selectedMinute.value );
    console.log(
     "The time you selected was: " + hour + ":" + minute
    );
    上面的代码看似没有问题,但是当小时为 08、分钟为 09 时,结果是 0:0,因为 8 和 9 都不
    是有效的八进制数。
    将第二个参数设置为 10,即可避免这个问题:
    var hour = parseInt( selectedHour.value, 10 );
    var minute = parseInt( selectedMiniute.value, 10 );
   
   从 ES5 开始 parseInt(..) 默认转换为十进制数,除非另外指定。如果你的代码需要在 ES5之前的环境运行,请记得将第二个参数设置为 10。
   
====================================================================
   
   解析非字符串
   
曾经有人发帖吐槽过 parseInt(..) 的一个坑:
    parseInt( 1/0, 19 ); // 18
    很多人想当然地以为(实际上大错特错)“如果第一个参数值为 Infinity,解析结果也应该是 Infinity”,返回 18 也太无厘头了。
    
    其中第一个错误是向 parseInt(..) 传递非字符串,这完全是在自找麻烦。此时 JavaScript会将参数强制类型转换为它能够处理的字符串。
有人可能会觉得这不合理,parseInt(..) 应该拒绝接受非字符串参数。但如果这样的话,它是否应该抛出一个错误?这是 Java 的做法。一想到 JavaScript 代码中到处是抛出的错误,要在每个地方加上 try..catch,我整个人都不好了。那是不是应该返回 NaN ?也许吧,但是下面的情况是否应该运行失败?
    parseInt( new String( "42") );
因为它的参数也是一个非字符串。如果你认为此时应该将 String 封装对象拆封为 "42",那么将 42 先转换为 "42" 再解析回 42 不也合情合理吗?

这种半显式、半隐式的强制类型转换很多时候非常有用。例如:
    var a = {
     num: 21,
     toString: function() { return String( this.num * 2 ); }
    };
    parseInt( a ); // 42
    parseInt(..) 
    先将参数强制类型转换为字符串再进行解析,这样做没有任何问题。因为传递错误的参数而得到错误的结果,并不能归咎于函数本身。
    
    怎么来处理 Infinity(1/0 的结果)最合理呢?有两个选择:"Infinity" 和 "∞",JavaScript选择的是 "Infinity"。JavaScript 中所有的值都有一个默认的字符串形式,这很不错,能够方便我们调试。再回到基数 19,这显然是个玩笑话,在实际的 JavaScript 代码中不会用到基数 19。它的有效数字字符范围是 0-9 和 a-i(区分大小写)。
    
    parseInt(1/0, 19) 实际上是 parseInt("Infinity", 19)。第一个字符是 "I",以 19 为基数时值为 18。第二个字符 "n" 不是一个有效的数字字符,解析到此为止,和 "42px" 中的 "p"一样。
    
    最后的结果是 18,而非 Infinity 或者报错。所以理解其中的工作原理对于我们学习JavaScript 是非常重要的。

此外还有一些看起来奇怪但实际上解释得通的例子:
    parseInt( 0.000008 ); // 0 ("0" 来自于 "0.000008")
    parseInt( 0.0000008 ); // 8 ("8" 来自于 "8e-7")
    parseInt( false, 16 ); // 250 ("fa" 来自于 "false")
    parseInt( parseInt, 16 ); // 15 ("f" 来自于 "function..")
    parseInt( "0x10" ); // 16
    parseInt( "103", 2 ); // 2
    其实 parseInt(..) 函数是十分靠谱的,只要使用得当就不会有问题。因为使用不当而导致
一些莫名其妙的结果,并不能归咎于 JavaScript 本身。

显式转换为布尔值

现在我们来看看从非布尔值强制类型转换为布尔值的情况。

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

    var a = "0";
    var b = [];
    var c = {};
    var d = "";
    var e = 0;
    var f = null;
    var g;
    Boolean( a ); // true
    Boolean( b ); // true
    Boolean( c ); // true
    Boolean( d ); // false
    Boolean( e ); // false
    Boolean( f ); // false
    Boolean( g ); // false
    
    虽然 Boolean(..) 是显式的,但并不常用。
    
    和前面讲过的 + 类似,一元运算符 ! 显式地将值强制类型转换为布尔值。但是它同时还将真值反转为假值(或者将假值反转为真值)。所以显式强制类型转换为布尔值最常用的方法是 !!,因为第二个 ! 会将结果反转回原值:

    var a = "0";
    var b = [];
    var c = {};
    var d = "";
    var e = 0;
    var f = null;
    var g;
    !!a; // true
    !!b; // true
    !!c; // true
    !!d; // false
    !!e; // false
    !!f; // false
    !!g; // false
    
    在 if(..).. 这样的布尔值上下文中,如果没有使用 Boolean(..) 和 !!,就会自动隐式地进行 ToBoolean 转换。建议使用 Boolean(..) 和 !! 来进行显式转换以便让代码更清晰易读。显式 ToBoolean 的另外一个用处,是在 JSON 序列化过程中将值强制类型转换为 true 或false:
    
    var a = [
         1,
         function(){ /*..*/ },
         2,
         function(){ /*..*/ }
    ];
    JSON.stringify( a ); // "[1,null,2,null]"
    JSON.stringify( a, function(key,val){
         if (typeof val == "function") {
            // 函数的ToBoolean强制类型转换
            return !!val;
         }
         else {
            return val;
         }
    } );
    // "[1,true,2,true]"
    
    下面的语法对于熟悉 Java 的人并不陌生:
    var a = 42;
    var b = a ? true : false;
    三元运算符 ? : 判断 a 是否为真,如果是则将变量 b 赋值为 true,否则赋值为 false。表面上这是一个显式的 ToBoolean 强制类型转换,因为返回结果是 true 或者 false。然而这里涉及隐式强制类型转换,因为 a 要首先被强制类型转换为布尔值才能进行条件判断。这种情况称为“显式的隐式”,有百害而无一益,我们应彻底杜绝。建议使用 Boolean(a) 和 !!a 来进行显式强制类型转换。

字符串和数字之间的隐式强制类型转换

通过重载,+ 运算符即能用于数字加法,也能用于字符串拼接。JavaScript 怎样来判断我们
要执行的是哪个操作?例如:
    var a = "42";
    var b = "0";
    var c = 42;
    var d = 0;
    a + b; // "420"
    c + d; // 42
    
    这里为什么会得到 "420" 和 42 两个不同的结果呢?通常的理解是,因为某一个或者两个操作数都是字符串,所以 + 执行的是字符串拼接操作。这样解释只对了一半,实际情况要复杂得多。
    例如:
    var a = [1,2];
    var b = [3,4];
    a + b; // "1,23,4"
    a 和 b 都不是字符串,但是它们都被强制转换为字符串然后进行拼接。原因何在,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive 抽象操作(规范 9.1 节),该抽象操作再调用 [[DefaultValue]](规范 8.12.8
节),以数字作为上下文。

你或许注意到这与 ToNumber 抽象操作处理对象的方式一样(参见 4.2.2 节)。因为数组的valueOf() 操作无法得到简单基本类型值,于是它转而调用toString()。因此上例中的两个数组变成了 "1,2" 和 "3,4"。+ 将它们拼接后返回 "1,23,4"。

简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),则执行字符串拼接;否则执行数字加法。

tips:有一个坑常常被提到,即 [] + {} 和 {} + [],它们返回不同的结果,分别是"[object Object]" 和 0。

对隐式强制类型转换来说,这意味着什么?
我们可以将数字和空字符串 "" 相 + 来将其转换为字符串:
    var a = 42;
    var b = a + "";
    b; // "42"
    
    tips:+ 作为数字加法操作是可互换的,即 2 + 3 等同于 3 + 2。作为字符串拼接操作则不行,但对空字符串 "" 来说,a + "" 和 "" + a 结果一样。
    
    a + ""(隐式)和前面的 String(a)(显式)之间有一个细微的差别需要注意。根据ToPrimitive 抽象操作规则,a + "" 会对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串。而 String(a) 则是直接调用 ToString()。它们最后返回的都是字符串,但如果 a 是对象而非数字结果可能会不一样!
    
    例如:
        var a = {
         valueOf: function() { return 42; },
         toString: function() { return 4; }
        };
        a + ""; // "42"
        String( a ); // "4"
你一般不太可能会遇到这个问题,除非你的代码中真的有这些匪夷所思的数据结构和操作。在定制 valueOf() 和 toString() 方法时需要特别小心,因为这会影响强制类型转换的结果。

再来看看从字符串强制类型转换为数字的情况。
    var a = "3.14";
    var b = a - 0;
    b; // 3.14
    - 是数字减法运算符,因此 a - 0 会将 a 强制类型转换为数字。也可以使用 a * 1 和 a /
1,因为这两个运算符也只适用于数字,只不过这样的用法不太常见。

对象的 - 操作与 + 类似:
    var a = [3];
    var b = [1];
    a - b; // 2
为了执行减法运算,a 和 b 都需要被转换为数字,它们首先被转换为字符串(通过toString()),然后再转换为数字。字符串和数字之间的隐式强制类型转换真如人们所说的那样糟糕吗?我个人不这么看。b = String(a)(显式)和 b = a + ""(隐式)各有优点,b = a + "" 更常见一些。虽然饱受诟病,但隐式强制类型转换仍然有它的用处。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容