一, 导读
由于各种历史原因 JavaScript 的类型转换真的令人吐血. 本文是老弟翻阅各种材料自己总结出的 JavaScript 强制类型转换规则, 整理了 3 张表和 1 个分析方法, 便于记忆, 小伙伴们可以先看结论, 继续往下看分析有助于理解和记忆.
前言: 首先要明白类型转换话题的范畴有多大? 我们要讨论的是 Boolean,String,Number,Array,Function,Date,Object 这么多类型之间互相转来转去吗? 当然不是, 我们的目标只有 2 个: 基本类型转成基本类型, 复合类型转成基本类型. new String('hello'),new Number(1024)这种用装箱操作来把基本类型转成复合类型就不属于讨论的范畴(装箱甚至不能算是类型转换), 再比如你让数字转成函数, 数组转成函数本身就没意义. 所以我们的目标就是基本类型转成基本类型, 复合类型转成基本类型.
二, 结论
一法三表, 指的是记住三张表格里的特殊转换, 其他情况全都可以用一法分析出来.
一法: 基本法.
基本法是我按照 ES5 规范的 ToPrimitive 方案抽离出来的叫法. 步骤是: 任何类型 (基本类型和复合类型) 做转换时先检查自己是否有 valueOf()方法, 如果有并且返回基本类型值, 就使用该值做强制类型转换, 如果 valueOf()返回的还是复合类型, 那就放弃转去调用 toString()方法, 把 toString()返回的值做强制类型转换. 如果 valueOf()和 toString()都不返回基本类型 (或不存在) 就做不了类型转换并且报 TypeError 错.
三表之一: 各类型 toString()和 valueOf()方法返回值对照表:
表一
三表之二: 各类型转成基本类型对照表:
表二
三表之三: 所有隐式类型转换情形发生时的分析方法:
表三
三, 分析
1, 表一的分析. valueOf()和 toString()的作用很重要
基本法就是完全依赖这 2 个方法的.
(1)JS 在 Boolean.prototype,String.prototype,Number.prototype,Symbol.prototype,Function.prototype,Date.prototype,Array.prototype,Object.prototype 上都分别定义了各自的 valueOf()和 toString()方法.
(2)拿上面 let num = 12 举例, 我们知道它的原型链是下图这样的, 也就是所有基本类型都能用 Object.prototype 的 toString()和 valueOf()两个方法, 拿上面 let num = 12 举例, 如果 JS 没在 Number.prototype 上覆盖定义 toString()和 valueOf()的话, 那么 num.toString()的结果就是 "[object Number]" 了. 正是因为全部的基本类型都覆盖了这 2 个方法, 才有了表一五花八门的结果.
Number 类型变量的原型链
(3)可以看到, 除了 Date 对象的 valueOf 返回了当前时间到 1970-1-1 日的毫秒数 (跟 date.getTime() 的效果一样)以外, 其他 8 个全部和 Object 的效果一致 -- 返回对象本身, 其中 Srting,Number,Boolean,Symbol 这 4 个本身就是基本类型. 注意这里的 valueOf 全都是对象各自覆盖后了的, 而不是通过原型链找到的 Object.valueOf(), 尽管返回的结果是一样的.
(4)相反, 所有的对象不仅覆盖了 toString 方法, 还彻底改变了返回值, Array 对象就是所有元素调用自己的 toString 再拼接起来; Date 对象就是调用 toDateString()和 toTimeString()再拼接起来; 其他都是直接加个引号.
(5)let num = 12 和 let num = new Number(12), 虽然两个不相等, 但是他们的 toString 和 valueOf 返回值是一样的. 因为 new Number(12)起到的作用是装箱, 它的核心还是 12 这个数字.
(6)let num = 12,num.toString()和 Object.prototype.toString().call( num )肯定是不一样的.
(7)因为 valueOf()返回的都是对象本身, 所以开发者直接取对象变量就好了, 基本不会去调用 valueOf(), 基本都是自动被引擎调用.
2, 表二的分析
(1)首先必须清楚的是表二里 "转 Number","转 String" 和 "转 Boolean" 指的是采用 Number(),String(),Boolean()这三个全局函数显式转换的结果.
(2)先看转 String 那一列, 会发现除了 null,undefined 两个, 其他类型的显式转换全部可以按照基本法转换得到 (调用自己 toString() 得出的结果), 这说明了显式类型转换和隐式转换的结果保持了一致, 这个肯定得一致, 要不然程序员得疯掉. null 和 undefined 有点特殊, 用 String(null)会得到 "null" 字符串.
(3)再看转 Number 那一列, 会发现所有的复合类型转成 Number 也可以按照基本法转换得到, 所以复合类型要转成数字, 都是一律先转成字符串, 字符串再转成数字. 比如 [1] 要转成数字, 按照基本法首先看 valueOf()返回数组对象本身, 数组不是基本类型, 所以调用 toString()得到字符串 "1","1" 再转成数字是 1. 所以剩下的只要记住这一列 null,undefined,Boolean 和 String 类型这 4 种转成 Number 的结果就很轻松了.
(4)再看转 Boolean 那一列, 转 Boolean 很好记忆, 记住只有 false 和 undefined,null,+0,-0,NaN,"", 这 7 个值会转成 false, 其他一律转成 true.
3, 表三的分析
表一表二列举出的是每种类型转成基本类型的结果. 表三就是为了说明将要发生类型转换时 JS 时怎么转的,"1" == 1 类型不一致肯定要转成一样的再比较, 那么会把 "1" 转成数字呢? 还是把 1 转成字符串呢? 还是谁在 == 左边就转成对方的类型呢?
(1)单元 + 法, 减乘除, 取余, 自增, 自减这些只适用于数学计算, 所以一律会转成数字. 比如 1-{age:30},{age:30}用基本法转成 "[object Object]" 再转成数字得到 NaN,NaN+1 还是 NaN.
(2)==,!= 这两个宽松相等判断.== 恐怕是面试官最爱问的了. 看完这个分析再恶心的都是手到擒来. 首先结论 ES 规范说明 == 除非中途两边类型一致了, 否则最终都要转成数字. 具体步骤是:
第 1 步: 两边全部用基本法转成基本类型;
第 2 步: 如果两边依然类型不一样, 则统一转成数字再比较.
注意: 用基本法转换后, 不要管结果是什么, 直接转成数字再比较.
比如 经典的 [] == ![],! 的优先级高于 ==, 所以先算![], 对照表二知道结果是 false, 所以变成判断 [] == false, 用基本法转换[] 转换成了 ""(空字符串), 变成判断"" == false,ok 要转数字, 对照表二,"" 会转成 0,false 也转成数字 0, 所以最后 0==0 成立.
(3)双元 + 的情形要拎出来做典型, 因为它可以用于数学加法和字符串拼接, 比如 1+2,"1"+"2". 所以双元 + 要看 + 号两边元素的类型. 具体步骤是:
第 1 步:+ 号两边类型不一致, 先用基本法转成基本类型;
第 2 步: 如果 + 号两边的基本类型有一个是字符串, 就把另一个转成字符串再拼接;
第 3 步: 如果 + 号两边的基本类型都不是字符串, 就都转成数字再相加.
比如 1+{age:30},{age:30}用基本法转成 "[object Object]" 是字符串, 所以把 1 转成 "1" 再拼接起来等于 "1[object Object]"; 再比如 true+false, 用基本法转一下, 都得到自己,+ 号两边都是 Boolean 不是字符串, 所以两边都转成数字, true 转成 1,false 转成 0, 相加得到 1.
(4)转 Boolean 类型的就对照表二就可以了.
4, 特殊情况分析
(1)Date 类型遇到双元 + 时. 例如: let date = new Date(); let res = date + 1; 实际的结果是输出 "Fri Mar 20 2020 20:14:10 GMT+0800 (中国标准时间)1". 按道理这里 date 应该用基本法调用先调用 valueOf()转换成基本类型 1584670735713 再做加法, 但是没有, 而是调用了 toString()转换成了字符串再做拼接.
(2)Object.create(null)创建的对象做转换时. 例如: let res = Object.create(null) + "hello" 会报错 Uncaught TypeError: Cannot convert object to primitive value. 是因为用 Object.create(null)创建的对象的原型是 null, 所以没有 valueOf()和 toString()方法, 没法做类型转换.
(3)稀疏数组, 即数组有空值时. 看下面这个例子
var arr = [0]; arr[1] = undefined; arr[2] = null; arr[4]=4; 创建的数组是这样的[0,undefined,null, 空值, 3].
- arr.toString(); // "0,,,,4"
- String(arr); // "0,,,,4"
- String(arr[1]); // "undefined"
- String(arr[2]); // "null"
- String(arr[3]); // "undefined"
注意到, 如果是直接调用数组的 toString()或者用全局 String()显式转换时, JS 会把 undefined,null, 空值转成 ""空字符串参与拼接; 而如果单独抠出来用 String()做显式转换的话却会变成"undefined","null"和"undefined". 这还是值得注意的.
四, 习题(做完不再怕面试)
小伙伴们可以按照本文一法三表的方法完成下面的习题, 再去浏览器验证下.
- ,[] + {}
- ,{} + new Date()
- ,[] - new Date()
- ,{} + true
- ,{} - false
- ,[] + ""7,[] + true +"2"+ 3 -"99"
- ,null - undefined
- ,null == false
- ,null == undefined
- ,undefined == false
- ,true == "45"
- ,false == 45
- , "" == 0
- , "" == false
- ,0 == []
- ,0 == {}
- ,"0" == 0
- ,"0" == false
- ,"0" == []
- ,[] == ![]
- ,"" == [null]
- ,2 == [2]
- ,2 == ["2"]
- ,1 == [true]
来源: http://www.jianshu.com/p/ef1846973e64