从 [] == ![] 看 js 类型转换

作者: ebfc7d0362e4 | 来源:发表于2017-09-15 10:23 被阅读142次

很久之前,看到过这样一种判断

[] == ![];    // 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;

你知道哪些数据会匹配成功么?

相关文章

  • 从 [] == ![] 看 js 类型转换

    很久之前,看到过这样一种判断 当时觉得很神奇,翻了些博客,但也似懂非懂。今天翻看博客的时候,偶然又看见了它,感觉跟...

  • javaScript中数据类型转换方法

    JS 数据类型转换 方法主要有三种 转换函数、强制类型转换、利用js变量弱类型转换。 1. 转换函数: js提供了...

  • 数据类型转换

    JS 数据类型转换 方法主要有三种 转换函数、强制类型转换、利用js变量弱类型转换。 1. 转换函数: js提供了...

  • JavaScript类型转换

    在js中数据类型转换一般分为两种,即强制类型转换和隐式类型转换(利用js弱变量类型转换)。 强制类型转换 即通过使...

  • 前端开发入门到实战:JavaScript字符串转换数字

    js 字符串转换数字方法主要有三种: 转换函数、强制类型转换、利用js变量弱类型转换。 1. 转换函数: js提供...

  • 前端开发入门到实战:JavaScript字符串转换数字

    js 字符串转换数字方法主要有三种: 转换函数、强制类型转换、利用js变量弱类型转换。 1. 转换函数: js提供...

  • js中的类型转换

    在js中数据转换分为3种:隐式类型转换,强制类型转换,函数转换 1.隐式类型转换 (1):运算符转换 js中的值在...

  • JS类型转换

    方法主要有三种 转换函数、强制类型转换、利用js变量弱类型转换。 1. 转换函数: js提供了parseInt()...

  • js关于字符串和数字的转换

    js字符串转换成数字 js 字符串转换数字方法主要有三种:转换函数、强制类型转换、利用JS变量弱类型特点进行转换 ...

  • JavaScript字符串转换数字

    这里记录js 字符串转换数字的三种主要方法: 转换函数、强制类型转换、利用js变量弱类型转换。 1. 转换函数: ...

网友评论

    本文标题:从 [] == ![] 看 js 类型转换

    本文链接:https://www.haomeiwen.com/subject/xsgjsxtx.html