很久之前,看到过这样一种判断
[] == ![]; // true
当时觉得很神奇,翻了些博客,但也似懂非懂。今天翻看博客的时候,偶然又看见了它,感觉跟以前比更清晰了些,所以在此结合 js 类型转换,记录下自己的理解。
[TOC]
一、分解
乍一看,确实比较容易让人迷惑,但是复杂的东西只要能够被分解,我们就能容易地分析、理解它了。
由 JS 运算符优先级可知,“!” 的优先级高于 “==”,也就是说,一定程度上,我们可以将上面的语句改写为:
[] == (![]); // true
或者,更进一步:
var a = [], b = ![];
a == b; // true
二、求值
其实到上一步的时候,你可能已经发现了一个问题,没错,那就是:
var b = ![]; // false
其实这里就涉及到对象类型的真假值的问题。JavaScript 规定,所有的 JavaScript 的引用类型数据都是真值,这里说“引用类型数据”而非对象类型,是因为
typeof null === 'object'; // true
!null === true; // true
笔者并不想纠结于 null 是不是对象类型这样的问题进行讨论。
正如上文所述,任何引用类型的数据都是真值,包括对象、函数等等:
!{} === true; // false
![] === false; // true
!function () {} === false; // true
!window === true; // false
!window.open === false; // true
注意这里说的是任何引用类型,类似 Boolean 此类的包装函数所构造出来的对象当然也在此列:
!new Boolean(0) === false; // true
!new Boolean(true) === true; // false
有时我们想要将某种数据快速转换为一个布尔值,而不想因为使用 Boolean 而导致数据变成对象的时候,可以使用如下形式:
!!0 // false
!!undefined // false
!!1 // true
!!{} // true
!![] // true
!!false // false
!!null // false
!!function () {} // true
顺带一提的是,出了 ! 会导致真假值的判断,直接将一个值作为 if 语句的判断条件也会如此。
if (condition) {
// do something
}
这里只要 condition
是一个真值,if 分支下的 “do something” 处的语句就会被执行。最典型的坑就出在 condition
是一个
- 空数组([])
- 空的 NodeList 实例( 如 document.querySelectorAll('not-exist') )
- 空的 HTMLCollection 实例( 如 document.getElementsByTagName('not-exist') )
- 空的 jQuery 实例( 如 $('not-exist') )...
因为 document.getElementById 没有命中的时候返回 null,也即是一个假值,很多人认为 document.getElementsByTagName、jQuery 等也是如此,或者认为它们返回了一个空的数组,并认为这个空数组“应该”是一个假值。但实际上,无论是空数组、还是空的 NodeList / HTMLCollection / jQuery 的实例,它们本质上都还是引用类型数据,所以它们都是真值。一个比较简单的验证 NodeList / HTMLCollection / jQuery 的实例是否命中的方法是读取它们的 length 属性,如果不为 0 ,则可以认为它们命中了元素。
三、toString / valueOf
经过前两步的分析,我们可以将前面的判断改写为:
[] == false; // true
按照常规思路,引用类型的变量之间的比较,是基于引用的比较,二者如果是相同的引用,则相等,否则不等。如果按照这样的逻辑,引用类型的数据根本不可能和基础类型的数据相等才对,但是这里就真的相等了。
说到这里,就必须提到原生 JS 中 toString / valueOf 这两个处处遍布的方法。
(一) 分类
对于不同类型的对象,js定义了多个版本的 toString 和 valueOf 方法
(1) toString:
- 普通对象,返回 "[object Object]";
- 数组,返回数组元素之间添加逗号合并成的字符串;
- 函数,返回函数的定义式的字符串;
- 日期对象,返回一个可读的日期和时间字符串;
- 正则,返回其字面量表达式构成的字符串;
(2) valueOf:
- 日期对象,返回自1970年1月1日到现在的毫秒数;
- 其它均返回对象本身;
toString / valueOf 两个方法,主要可用于引用类型数据的类型转换,通过调用它们,可以将引用类型数据使用在原本应该使用基本数据类型的地方。
(二)适用场景
原生的 toString / valueOf 分别位于对象的构造函数的 prototype 属性上,如果需要修改,大可直接在实例对象上直接添加 toString / valueOf 方法,这样也不会影响到原型链上的方法。
(1)类型转换
1)对象=>字符串
a. 执行toString,如果返回了一个原始值,则将其转化为字符串
b. 否则执行valueOf方法,如果返回了一个原始值,则将其转化为字符串
c. 否则抛出类型错误
如:
var o = {};
o.toString = function () {
return 'my string';
};
String(o); // my string
2) 对象=>数字
a. 执行valueOf,如果返回了一个原始值,如果需要,则将其转化为数字
b. 否则执行toString,如果返回了一个原始值,则将其转化为数字并返回
c. 否则抛出类型错误
var o = {};
o.valueOf = function () {
return 233;
};
Number(o); // 233
(2)比较和运算
在执行 “>”、“<”、“+”、“-” 等操作的时候,如果涉及到引用类型数据,大部分引用类型数据在运算之前,会先尝试执行其 valueOf 方法,如果该方法返回了一个基本数据类型,则拿该返回值替代对象本身参与运算否则则尝试执行 toString 方法,如果该方法返回了一个基本类型数据,则使用该数据参与操作;如果该方法返回的不是基本类型数据,则尝试执行 valueOf 方法,如果该方法返回了一个基本类型数据,则使用该数据参与操作;否则将提示 TypeError。
var o = {};
o.toString = function () {
return 2;
}
// 此时还没有为 o 添加 valueOf 方法
// 它将先调用继承自 Object.prototype.valueOf 方法
// 返回值是它自身
// 于是则调用这里我们为实例添加的 toString 方法
o == 2; // true
// 这里为实例添加了 valueOf 方法
// 一开始,它就将调用我们为实例添加的 valueOf 方法
// 返回值 1 是基本类型数据
// 则再调用 toString 方法
o.valueOf = function () {
return 1;
}
o == 1; // true
o + 1; // 2
o * 5; // 5
注意前面说的是“大部分引用类型数据”,唯一不遵循此规则的是 Date 类型对象。与其它引用类型数据不同的是,在比较或者计算的时候,它会先尝试调用其 toString 方法,如果没有返回基本数据类型才尝试调用其 valueOf 方法。
var t = new Date();
// t 继承自 Date.prototype 上的 toString / valueOf 都能返回基本类型数据
t.valueOf(); // 返回时间戳,如 1505438878370
t.toString(); // 时间信息字符串,如 "Fri Sep 15 2017 09:27:58 GMT+0800 (CST)"
t + 2344444; // 并不会得到一个时间戳,而是 "Fri Sep 15 2017 09:27:58 GMT+0800 (CST)2344444"
所以当你不清楚它会得到什么值的时候,请自己调用 toString / valueOf 方法,后来 Date.prototype 对象上增加了一个 getTime 方法替代 valueOf 获取时间戳,但是这个方法在 IE 存在兼容性问题,仅 IE9+ 有效。
四、再转换
到这里,其实就很清晰了。
[] == false; // true
其实就是:
([]).toString() == false; // true
也就是:
'' == false; // true
这里就涉及了基本类型数据的隐式转换问题了。基本依照以下规则:
- 两个都是数值,则比较数值
- 两个都是字符串,则比较字符编码值
- 其中一个是数值,则要把另个转化成数值进行比较
- 如果其中一个是对象,则调用 valueOf / toString 方法
- 如果有一个是布尔值,则将其转化成数值
显然这里满足最后一条规则,比较的时候,其实将会尝试将二者转化为数字类型。相当于:
Number('') == Number(false); // true
即:
0 == 0; // true
五、总结
JavaScript 是一门弱类型语言,但是弱类型并不代表没有类型,相反的是,JavaScript 是一门类型丰富的语言,除了常见语言的数字、字符串、布尔、对象、函数、null 等,更是有一个神奇的 undefined 类型。一边是弱类型,一边又是多种类型,这看似矛盾,但由于隐式类型转换的存在,这种矛盾看起来又如此的合理。
P.S. 虽然上面的代码中,我使用了大量的 “==”,而非 “===”,但这仅是学习用的。实际开发的时候,我也推荐使用 “===”。
一方面,如果由于自己的疏忽,没能正确处理好隐式类型转换,往往会造成意料之外的问题,为项目带来潜在的风险,比如我想验证某个变量是否是 undefined,如果采用:
value == undefined;
但实际上,null 也会被匹配进来,可能造成潜在的风险,如果使用 “===” 就不会有这个问题;
另一方面,如果多人协作开发,隐式类型转换往往会为其他人带来困扰,尤其是在成员间开发能力参差不齐的情况下。
例如,我想验证一个值是否是布尔值 true,但是我写了这样的代码:
value == true;
你知道哪些数据会匹配成功么?
网友评论