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

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,233评论 0 4
  • 本章内容 使用对象 创建并操作数组 理解基本的 JavaScript 类型 使用基本类型和基本包装类型 引用类型的...
    闷油瓶小张阅读 681评论 0 0
  • 六月时候,这十分炎热的季节,在其上旬的某天,心中突生想法,要去东辉峡谷走一遭。我选择了单车出行,做此决定,...
    杨旭东_97e5阅读 405评论 0 2
  • 正月十六月儿明, 男女老少烤杂病。 烤烤脚,百病消, 烤烤腚,一年四季不得病。 …… 正月十六家乡有“烤杂病”的习...
    女派阅读 1,492评论 0 0
  • 今天开始学习英语单词
    encome阅读 234评论 0 0