从 [] == ![] 看 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 类型转换

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