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

96
xpwei
2017.10.31 16:38* 字数 6557

值类型转换
将值从一种类型转换为另一种类型通常称为类型转换,这是显示的情况;隐式的情况称为强制类型转换。JavaScript中的强制类型转换总是返回标量基本类型值,如字符串、数字和布尔值,不会返回对象和函数。我们介绍过“封装”,就是为标量基本类型值封装一个相应类型的对象,但这并非严格意义上的强制类型转换。
也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。

var a = 42;
var b = a + ""; // 隐式强制类型转换
var c = String( a ); // 显式强制类型转换

对变量b而言,强制类型转换是隐式的;由于+运算符的其中一个操作数是字符串,所以是字符串拼接操作,结果是数字42被强制类型转换为相应的字符串“42”。

ToString
它负责处理非字符串到字符串的强制类型转换。基本类型值的字符串化规则为:null转换为“null”,undefined转换为“undefined”,true转换为“true”。数字的字符串化则遵循通用规则。不过那些极小和极大的数字使用指数形式:

// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字
a.toString(); // "1.07e21"

对普通对象来说,除非自行定义,否则toString()(Object.prototype.toString())返回内部属性[[Class]]的值,如“[object Object]”。
数组的默认toString()方法经过了重新定义,将所有单元字符串化以后再用“,”连接起来:

var a = [1,2,3];
a.toString(); // "1,2,3"

JSON字符串化
工具函数JSON.stringify(..)在将JSON对象序列化为字符串时也用到了ToString。
对于大多数简单值来说,JSON字符串和toString()的效果基本相同,只不过序列化的结果总是字符串:

JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (含有双引号的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"

所有安全的JSON值都可以使用JSON.stringify(..)字符串化。安全的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(..)会出错。
我们可以向JSON.stringify(..)传递一个可选参数replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除。
如果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]}"

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规范定义了抽象操作ToNumber。
其中true转换为1,false转换为0。undefined转换为NaN,null转换为0。
ToNumber对字符串的处理基本遵循数字常量的相关规则/语法。处理失败时返回NaN(处理数字常量是把你时会产生语法错误)。不同之处是ToNumber对以0开头的十六进制数并不按十六进制处理(而是按十进制)。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作ToPromitive会首先(通过内部操作DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString()的返回值(如果存在)来进行强制类型转换。
如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误。
从ES5开始,使用Object.create(null)创建的对象[[Prototype]]属性为null,并且没有valueIOf()和toString()方法,因此无法进行强制类型转换。

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

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

  • undefined
  • null
  • false
  • +0、-0和NaN
  • “”

假值的布尔强制类型转换结果为false。
从逻辑上说,假值列表以外的都应该是真值。但JavaScript规范对此并没有明确定义,只是给出了一些示例,例如规定所有的对象都是真值,我们可以理解为假值列表意外的值都是真值。

2、假值对象
这个标题似乎有点自相矛盾。前面讲过规范规定所有的对象都是真值,怎么还会有假值对象呢?
浏览器在某些情况下,在常规JavaScript语法基础上自己创建了一些外表值,这些就是“假值对象”。
假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为false。
最常见的例子是document.all,它是一个类数组对象,包含了页面上的所有元素,由DOM(而不是JavaScript引擎)提供给JavaScript程序使用。它以前曾是一个真正意义上的对象,布尔强制类型转换结果为true,不过现在它是一个假值对象。
document.all并不是一个标准用法,早就被废止了。
那为什么它要是假值呢?因为我们经常通过将document.all强制类型转换为布尔值(比如if语句中)来判断浏览器是否是老版本的IE。IE自诞生之日起就始终遵循浏览器标准,较其他浏览器更为有力地推动了Web的发展。
if(document.all){/it's IE/}依然存在于许多程序中,也许会一直存在下去,这对IE的用户体验来说不是一件好事。

3、真值
真值就是假值列表之外的值。

var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );
d;//true
var a = []; // 空数组——是真值还是假值?
var b = {}; // 空对象——是真值还是假值?
var c = function(){}; // 空函数——是真值还是假值?
var d = Boolean( a && b && c );
d;

d依然是true。还是同样的道理,[],{}和function(){}都不在假值列表中,因此它们都是真值。

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

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

1、日期显式转换为数字
一元运算符+的另一个常见用途是将日期(Date)对象强制类型转换为数字,返回结果为Unix时间戳i,以微妙为单位:

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()并不适用。

2、奇特的~运算符

0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0

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,其他情况一律转换为真值。

3、字位截除
一些开发人员使用~~来截除数字值的小树部分,以为这和Math.floor(..)的效果一样,实际上并非如此。

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

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

var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42

解析允许字符串中含有非数字字符,解析按从做到右的顺序,如果遇到非数字字符就停止。而转换不允许出现非数字字符,否则会失败并返回NaN。
ES5自己的parseInt(..)有一个坑导致了很多bug。即如果没有第二个参数来指定转换的基数,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

parseInt(1/0,19)实际上是parseInt("Infinity",19)。第一个字符是“I”,以19为基数时值为18。第二个字符“n”不是一个有效的数字字符,解析到此为止,和“42px”中的“p”一样。
此外还有一些看起来奇怪但实际上解释得通的例子:

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

显式转换为布尔值
与前面的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

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

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(..)和!!来进行显式转换以便让代码更清晰易读。

字符串和数字之间的隐式强制类型转换
通过重载,+运算符即能用于数字加法,也能用于字符串拼接:

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都不是字符串,但是它们都被强制转换为字符串然后进行拼接。
根据ES5规范,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+将进行拼接操作。如果其中一个操作数是对象(包括数组),则首先对其调用ToPrimitive抽象操作,该抽象操作在调用[[DefaultValue]],以数字作为上下文。
你或许注意到这与ToNumber抽象操作处理对象的方式一样。因为数组的valueOf()操作无法得到简单基本类型值,于是它转而调用toString()。因此上例子中的两个数组编程了"1,2"和“3,4”。+将它们拼接后返回“1,23,4”。
简单来说就是,如果+的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),则执行字符串拼接,否则执行数字加法。

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

我们可以将数字和字符串“”相+来将其转换为字符串:

var a = 42;
var b = a + "";
b; // "42"

再来看看从字符串强制类型转换为数字的情况:

var a = "3.14";
var b = a - 0;
b; // 3.14

对象的-操作与+类似:

var a = [3];
var b = [1];
a - b; // 2

为了执行减法运算,a和b都需要被转换为数字,它们首先被转换为字符串(通过toString()),然后再转换为数字。

布尔值到数字的隐式强制类型转换
在将某些复杂的布尔逻辑转换为数字加法的时候,隐式强制类型转换能派上大用场。当然这种情况并不多见,属于特殊情况特殊处理。

function onlyOne(a, b, c) {
    return !!((a && !b && !c) ||
        (!a && b && !c) || (!a && !b && c));
}
var a = true;
var b = false;
onlyOne(a, b, b); // true
onlyOne(b, a, b); // true
onlyOne(a, b, a); // false

以上代码如果有多个参数时(4个、5个,甚至20个),用上面的代码就很难处理了。这是就可以使用从布尔值到数字(0或1)的强制类型转换:

function onlyOne() {
    var sum = 0;
    for (var i = 0; i < arguments.length; i++) {
        // 跳过假值,和处理0一样,但是避免了NaN
        if (arguments[i]) {
            sum += arguments[i];
        }
    }
    return sum == 1;
}
var a = true;
var b = false;
onlyOne(b, a); // true
onlyOne(b, a, b, b, b); // true
onlyOne( b, b ); // false
onlyOne( b, a, b, b, b, a ); // false

同样的功能也可以通过显式强制类型转换来实现:

function onlyOne() {
    var sum = 0;
    for (var i = 0; i < arguments.length; i++) {
        sum += Number(!!arguments[i]);
    }
    return sum === 1;
}

隐式强制类型转换为布尔值
下面的情况会发生布尔值隐式强制类型转换:
(1)if(..)语句中的条件判断表达式。
(2)for(..;..;..)语句中的条件判断表达式(第二个)。
(3)while(..)和do..while(..)循环中的条件判断表达式。
(4)?:中的条件判断表达式。
(5)逻辑运算符||(逻辑或)和&&(逻辑与)左边的操作数(作为条件判断表达式)。

||和&&
&&和||运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。

var a = 42;
var b = "abc";
var c = null;
a || b; // 42
a && b; // "abc"
c || b; // "abc"
c && b; // null

下面是一个十分常见的||的用法:

function foo(a, b) {
    a = a || "hello";
    b = b || "world";
    console.log(a + " " + b);
}
foo(); // "hello world"
foo("yeah", "yeah!"); // "yeah yeah!"

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

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

foo()只有在条件判断a通过时才会被调用。如果条件判断未通过,a&&foo()就会悄然终止(也叫做“短路”),foo()不会被调用。这样的用法对开发人员不太常见,开发人员通常使用if(a){foo();}

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");
}

符号的强制类型转换
ES6允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误:

var s1 = Symbol("cool");
String(s1); // "Symbol(cool)"
var s2 = Symbol("not cool");
s2 + ""; // TypeError

符号不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是true)。

宽松相等和严格相等
==和===都是用来判断两个值是否“相等”,但是它们之间有一个很重要的区别,特别是在判断条件上。
常见的误区是“==检查值是否相等,===检查值和类型是否相等”。听起来蛮有道理,然而还不够准确。
正确的解释是:“==允许在相等比较中进行强制类型转换,而===不允许”。

相等比较操作的性能
有人觉得==会比===慢,实际上虽然强制类型转换确实要多花点时间,但仅仅是微妙级(百万分之一秒)的差别而已。
如果进行比较的两个值类型相同,则==和===使用相同的算法,所以除了JavaScript引擎实际上的细微差别之外,它们之间并没有什么不同。
如果两个值的类型不同,我们就需要考虑有没有强制类型转换的必要,有就用==,没有就用===,不用在乎性能。

字符串和数字之间的相等比较

var a = 42;
var b = "42";
a === b; // false
a == b; // true

具体是怎么转换?是a从42转换为字符串,还是b从“42”转换为数字?
ES5规范这样定义:
(1) 如果Type(x) 是数字,Type(y) 是字符串,则返回x == ToNumber(y) 的结果。
(2) 如果Type(x) 是字符串,Type(y) 是数字,则返回ToNumber(x) == y 的结果。

其他类型和布尔类型之间的相等比较
==最容易出错的一个地方是true和false与其他类型之间的相等比较。

var a = "42";
var b = true;
a == b; // false

我们都知道“42”是一个真值,为什么==的结果不是true呢?
规范是这样说的:
(1) 如果Type(x) 是布尔类型,则返回ToNumber(x) == y 的结果;
(2) 如果Type(y) 是布尔类型,则返回x == ToNumber(y) 的结果。
所以建议,无论什么情况下都不要使用==true和==false。请注意,这里说的只是==,===true和===false不允许强制类型转换,所以并不涉及ToNumber。

var a = "42";
// 不要这样用,条件判断不成立:
if (a == true) {
    // ..
}
// 也不要这样用,条件判断不成立:
if (a === true) {
    // ..
}
// 这样的显式用法没问题:
if (a) {
    // ..
}
// 这样的显式用法更好:
if (!!a) {
    // ..
}
// 这样的显式用法也很好:
if (Boolean(a)) {
    // ..
}

null和undefined之间的相等比较
null和undefined之间的==也涉及隐式强制类型转换:
(1) 如果x 为null,y 为undefined,则结果为true。
(2) 如果x 为undefined,y 为null,则结果为true。
在==中null和undefined相等(它们也与其自身相等),除此之外其他值都不存在这种情况。
也就是说在==中null和undefined是一回事,可以相互进行隐式强制类型转换:

var a = null;
var b;
a == b; // true
a == null; // true
b == null; // true
a == false; // false
b == false; // false
a == ""; // false
b == ""; // false
a == 0; // false
b == 0; // false

下面是显式的做法,其中不涉及强制类型转换,个人觉得更繁琐一些(大概执行效率也会更低):

var a = doSomething();
if (a === undefined || a === null) {
    // ..
}

对象和非对象之间的相等比较
ES5规定:
(1) 如果Type(x) 是字符串或数字,Type(y) 是对象,则返回x == ToPrimitive(y) 的结果;
(2) 如果Type(x) 是对象,Type(y) 是字符串或数字,则返回ToPrimitive(x) == y 的结果。

var a = 42;
var b = [ 42 ];
a == b; // true

[42]首先调用ToPrimitive抽象操作,返回“42”,变成“42”==42,然后又变成42==42,最后二者相等。

之前介绍过的ToPrimitive抽象操作的所有特性(如toString()、valueOf())在这里都适用。

之前我们介绍过“拆封”,即“打开”封装对象,返回其中的基本数据类型值。==中的ToPromitive强制类型转换也会发生这样的情况:

var a = "abc";
var b = Object( a ); // 和new String( a )一样
a === b; // false
a == b; // true

但有一些值不这样,原因是==算法中其他优先级更高的规则:

var a = null;
var b = Object( a ); // 和Object()一样
a == b; // false
var c = undefined;
var d = Object( c ); // 和Object()一样
c == d; // false
var e = NaN;
var f = Object( e ); // 和new Number( e )一样
e == f; // false

因为没有对应的封装对象,所以null和undefined不能够被封装,Object(null)和Object()均返回一个常规对象。
NaN能够被封装为数字封装对象,但拆封之后NaN==NaN返回false,因为NaN不等于NaN。

比较少见的情况
首先来看看更改内置原生原型会导致哪些奇怪的结果:
1、返回其他数字:

Number.prototype.valueOf = function () {
    return 3;
};
new Number(2) == 3; // true

2==3不会有这个问题,因为2和3都是数字基本类型值,不会调用Number.prototype.valueOf()方法。而Number(2)涉及ToPrimitive强制类型转换,因此会调用valueOf()。

还有更奇怪的情况:

if (a == 2 && a == 3) {
    // ..
}

你也许觉得这不可能,因为a不会同时等于2和3,但“同时”一词并不准确,因为a==2在a==3之前执行。
如果让a.valueOf()每次调用都产生副作用,比如第一次返回2,第二次返回3,就会出现这样的情况。这实现起来很简单:

var i = 2;
Number.prototype.valueOf = function () {
    return i++;
};
var a = new Number(42);
if (a == 2 && a == 3) {
    console.log("Yep, this happened.");
}

2、假值的相等比较

"0" == null; // false
"0" == undefined; // false
"0" == false; // true -- 晕!
"0" == NaN; // false
"0" == 0; // true
"0" == ""; // false
false == null; // false
false == undefined; // false
false == NaN; // false
false == 0; // true -- 晕!
false == ""; // true -- 晕!
false == []; // true -- 晕!
false == {}; // false
"" == null; // false
"" == undefined; // false
"" == NaN; // false
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
"" == {}; // false
0 == null; // false
0 == undefined; // false
0 == NaN; // false
0 == []; // true -- 晕!
0 == {}; // false

3、极端情况

[] == ![] // true

让我们看看!运算符都做了些什么?根据ToBoolean规则,它会进行布尔值的显式强制类型转换(同时反转奇偶校验位)。所以[]==![]变成了[]==false。前面我们讲过false==[],最后的结果就顺理成章了。

2 == [2]; // true
"" == [null]; // true
0 == "\n"; // true
42 == "43"; // false
"foo" == 42; // false
"true" == true; // false
42 == "42"; // true
"foo" == [ "foo" ]; // true

4、完整性检查

"0" == false; // true -- 晕!
false == 0; // true -- 晕!
false == ""; // true -- 晕!
false == []; // true -- 晕!
"" == 0; // true -- 晕!
"" == []; // true -- 晕!
0 == []; // true -- 晕!

其中有4中情况涉及==false,之前我们说过应该避免,应该不难掌握。现在剩下后面3种。
正常情况下我们应该不会这样来写代码,我们应该不太可能会用==[]来做条件判断,而是用==""或者==0,如:

function doSomething(a) {
    if (a == "") {
        // ..
    }
}

如果不小心碰到doSomething(0)和doSomething([])这样的情况,结果会让你大吃一惊。
又如:

function doSomething(a,b) {
    if (a == b) {
        // ..
    }
}

doSomething("",0) 和doSomething([],"") 也会如此。

5、安全运用隐式强制类型转换
我们要对==两边的值认真推敲,以下两个原则可以让我们有效地避免出错:

  • 如果两边的值中有true或者false,千万不要使用==。
  • 如果两边的值中有[]、“”或者0,尽量不要使用==。

这时最好用===来避免不经意的强制类型转换。这两个原则可以让我们避开几乎所有强制类型转换的坑。

有一种情况下强制类型转换是绝对安全的,那就是typeof操作。typeof总是返回七个字符串之一,其中没有空字符串。所以在类型检查过程中不会发生隐式强制类型转换。typeof x=="function"是100%安全的,和typeof x==="function"一样。

抽象关系比较
a<b中涉及的隐式强制类型转换不太引人注意,不过还是很有必要深入了解一下。
比较双方首先调用ToPrimitive,如果结果出现非字符串,就根据ToNumber规则将双方强制类型转换为数字来进行比较。

var a = [ 42 ];
var b = [ "43" ];
a < b; // true
b < a; // false

如果比较双方都是字符串,则按字母顺序来进行比较:

var a = [ "42" ];
var b = [ "043" ];
a < b; // false

a和b并没有被转换为数字,因为ToPrimitive返回的是字符串,所以ToPrimitive返回的是字符串,所以这里比较的是“42”和“043”两个字符串,它们分别以“4”和“0”开头。因为“0”在字母顺序上小于“4”,所以最后结果为false。
同理:

var a = [ 4, 2 ];
var b = [ 0, 4, 3 ];
a < b; // false

a转换为“4,2”,b转换为“0,4,3”,同样是按字母顺序进行比较。
再比如:

var a = { b: 42 };
var b = { b: 43 };
a < b; // ??

结果还是false,因为a是[object Object],b也是[object Object],所以按照字母顺序a<b并不成立。
下面的例子就有些奇怪了:

var a = { b: 42 };
var b = { b: 43 };
a < b; // false
a == b; // false
a > b; // false
a <= b; // true
a >= b; // true

根据规范a<=b被处理为b<a,然后将结果反转。因为b<a的结果是false,所以a<=b的结果是true。
这可能与我们设想的大相径庭,即<=应该是“小于或者等于”。实际上JavaScript中<=是“不大于”的意思(即!(a>b),处理为!(b<a))。同理a>=b处理为b<=a。
相等比较有严格相等,关系比较却没有“严格关系比较”。也就是说如果要避免a>b中发生隐式强制类型转换,我们只能确保a和b为相同的类型,除此之外别无他法。

Web note ad 1