当前位置 博文首页 > 前端开发博客:关于JS隐式类型转换的完整总结

    前端开发博客:关于JS隐式类型转换的完整总结

    作者:[db:作者] 时间:2021-06-15 09:20

    点击上方“前端开发博客”,选择“设为星标”

    回复“2”加入前端群

    不管是在技术聊天群还是论坛里还是在面试官的卷子里,总能碰到?[] + {} == ??之类的问题,如果你不了解其中的原理,那么就插不上话,只能眼睁睁地等大佬解答了。

    类型 Type

    说到底还是JS类型转换的问题,首先我们先温习一下JS的8种内置类型:

    • Number

    • String

    • Boolean

    • Null

    • Undefined

    • Object

    • Symbol (ES2015)

    • BigInt (ESNext stage 4)

    是不是感觉还有Function,毕竟能用typeof获取到?不,函数、数组都是Object的子类型。

    类型分为基本类型复合类型两种,除了对象,其它都是基本类型。

    To Primitive

    发音:[?pr?m?t?v]
    结构:toPrimitive(input:?any, preferedType?:?'string' |'number')
    作用:内部方法,将任意值转换成原始值

    转换规则:

    1. 如果是基本类型,则不处理。

    2. 调用valueOf(),并确保返回值是基本类型。

    3. 如果没有valueOf这个方法或者valueOf返回的类型不是基本类型,那么对象会继续调用toString()方法。

    4. 如果同时没有valueOf和toString方法,或者返回的都不是基本类型,那么直接抛出TypeError异常。

    注意:如果 preferedType=string,那么2、3顺序调换。

    接着,我们看下各个对象的转换实现

    对象valueOf()toString()默认 preferedType
    Object原值"[object Object]"Number
    Function原值"function xyz() {...}"Number
    Array原值"x,y,z"Number
    Date数字"Sat May 22 2021..."String
    1. 数组的toString()可以等效为join(","),遇到null, undefined都被忽略,遇到symbol直接报错,遇到无法ToPrimitive的对象也报错。

    2. 使用模板字符串或者使用String(...)包装时,preferedType=string,即优先调用 .toString()。

    [1, null, undefined, 2].toString() === '1,,,2';
    
    // Uncaught TypeError: Cannot convert a Symbol value to a string
    [1, Symbol('x')].toString()
    
    // Uncaught TypeError: Cannot convert object to primitive value
    [1, Object.create(null)].toString()

    To Number

    一些特殊值转为数字的例子,等下要用到

    Number("0") === 0;
    Number("") === 0;
    Number("   ") === 0;
    Number("\n") === 0;
    Number("\t") === 0;
    Number(null) === 0;
    Number(false) === 0;
    
    Number(true) === 1;
    
    Number(undefined); // NaN
    Number("x"); // NaN

    加减法 +-

    加减法运算中遵循了一些隐式转换规则:

    遇到对象先执行ToPrimitive转换为基本类型,然后按照基本类型的规则处理

    // {}.toString() === "[object Object]"
    1 + {} === "1[object Object]"
    
    // [2, 3].toString() === "2,3"
    1 + [2, 3] === "12,3"
    [1] + [2, 3] === "1,2,3"
    
    function test() {}
    // test.toString() === "function test() {}"
    10 + test === "10function test() {}"

    加法过程中,遇到字符串,则会被处理为字符串拼接

    上面的对象最后也都转成了字符串,遵循本条规则。接着来几个纯字符串的例子

    1 + "1" === "11"
    1 + 1 === 2
    1 + 1 + "1" === "21"
    "1" + 1 === "11"
    "1" + "1" === "11"
    1 + "1" + 1 === "111"

    减法操作时,一律需要把类型转换为Number,进行数学运算

    3 - 1 === 2
    3 - '1' === 2
    '3' - 1 === 2
    '3' - '1' - '2' === 0
    
    // [].toString() => "" => Number(...) => 0
    3 - [] === 3
    
    // {}.toString() => "[object Object]" => Number(...) => NaN
    3 - {} // NaN

    加法操作时,遇到非字符串的基本类型,都会转Number

    1 + true === 2
    1 + false === 1
    1 + null === 1
    1 + null + false + 1 === 2
    1 + undefined // NaN
    1 + undefined + false // NaN
    1 + undefined + [1] === "NaN1"
    1 + undefined + "1" === "NaN1"
    
    // 1 + false
    1 + ![] === 1
    1 + !{} === 1
    !{} + !{} === 0

    + x 和 一元运算 +x 是等效的(以及- x),都会强制转换成Number

    + 0 === 0
    - 0 === -0
    
    // 1 + (+"1")
    1 + + "1" === 2
    
    // 1 + (+(+(+["1"])))
    1 + + + + ["1"] === 2
    
    // 1 + (-(+(-[1])))
    1 + - + - [1] === 2
    
    // 1 - (+(-(+1)))
    1 - + - + 1 === 2
    
    1 - + - + - 1 === 0
    
    // 1 + [""]
    1 + + [""] === 1
    
    // ["1", "2"].toString() => "1,2" => Number(...) => NaN
    1 + + ["1", "2"] // NaN
    
    // 吃根香蕉????
    // "ba" + (+undefined) + "a" => "ba" + NaN + "a"
    ("ba" + + undefined + "a").toLowerCase() === "banana"

    回到一开始抛出的问题[] + {},这样太简单了吧?

    [].toString() === "";
    {}.toString() === "[object Object]";
    
    [] + {} === "[object Object]";

    {} 在最前面时可能不再是对象

    不是对象是什么?是你的八块腹肌?别急,看看经典的例子

    {} + [] === 0;
    { a: 2 } + [] === 0;

    这啥玩意?说好的"[object Object]"呢?

    好吧,这是{}其实代表的是代码块,最后就变成了+ [],根据前面的原则,数组先被转换成字符串"",接着因为+x的运算,字符串被转成数字0

    那 { a: 2 } 总该是对象了吧?其实这时候a不是代表对象属性,而是被当成了标签(label),标签这东西IE6就已经有了。所以如果我们写成对象是会报错的,逗号要改成分号才能通过编译。

    // Uncaught SyntaxError: Unexpected token ':'
    { a: 2, b: 3 } + []
    
    // 分号OK
    { a: 2; b: 3 } + [] === 0;

    ??注意:在 Node?>= 13.10.0?的版本,{}被优先解释为空对象,仅在非对象结构的情况才会被认为是代码块。

    // nodeJs >= 13.10.0 的运行结果
    
    {} + [] === "[object Object]";
    { a: 2 } + [] === "[object Object]";
    { a: 2, b: 3 } + [] === "[object Object]";
    
    // 注意是分号,当成代码块
    { a: 2; b: 3 } + [] === 0;
    // 有JS语句或者表达式,当成代码块
    { var a = 1; } + [] === 0;
    { ; } + [] === 0;
    { 123 } + [] === 0;
    { 1 + 2 } + [] === 0;

    定论还是下的太早了,我们还是有办法让引擎优先处理成代码块

    // 所有node版本
    
    ;{} + [] === 0;
    ;{ a: 2 } + [] === 0;
    
    // Uncaught SyntaxError: Unexpected token ':'
    ;{ a: 2, b: 3 } + [];

    加个分号?有趣。


    在使用这个刺激的规则前,你需要记住的是,代码块 {} 必须在最前面,否则它就是对象,但也有特例(分号)。

    console.log({} + []); // "[object Object]"
    alert({} + []); // "[object Object]"
    
    function test(x) { console.log(x); }
    test({} + []); // "[object Object]"
    
    var result = {} + [];
    console.log(result); // "[object Object]"
    
    ({}) + [] === "[object Object]"
    
    !{} // !对象 => false
    
    {} // 对象
    { a: 2 } // 对象
    { a: 2, b: 3 } // 对象
    
    { a: 2; b: 3 } // 代码块
    ;{ a: 2 } // 代码块
    { a: 2 }; // 代码块

    symbol不能加减

    如果在表达式中有symbol类型,那么就会直接报错。比如1 + Symbol("x")报错如下:

    Uncaught TypeError: Cannot?convert?a?Symbol?value?to?a?number

    宽松相等 ==

    相等于全等都需要对类型进行判断,当类型不一致时,宽松相等会触发隐式转换。下面介绍规则:

    对象与对象类型一致,不做转换

    {} != {}
    [] != {}
    [] != []

    对象与基本类型,对象先执行ToPrimitive转换为基本类型

    // 小心代码块
    "[object Object]" == {}
    [] == ""
    [1] == "1"
    [1,2] == "1,2"

    数字与字符串类型对比时,字符串总是转换成数字

    "2" == 2
    [] == 0
    [1] == 1
    // [1,2].toString() => "1,2" => Number(...) => NaN
    [1,2] != 1

    布尔值先转换成数字,再按数字规则操作

    // [] => "" => Number(...) => 0
    // false => 0
    [] == false
    
    // [1] => "1" => 1
    // true => 1
    [1] == true
    
    // [1,2] => "1,2" => NaN
    // true => 1
    [1,2] != true
    
    "0" == false
    "" == false

    null、undefined、symbol

    null、undefined与任何非自身的值对比结果都是false,但是null == undefined?是一个特例。

    null == null
    undefined == undefined
    null == undefined
    
    null != 0
    null != false
    
    undefined != 0
    undefined != false
    
    Symbol('x') != Symbol('x')

    对比 < >

    对比不像相等,可以严格相等(===)防止类型转换,对比一定会存在隐式类型转换。

    对象总是先执行ToPrimitive为基本类型

    [] < [] // false
    [] <= {} // true
    
    {} < {} // false
    {} <= {} // true

    任何一边出现非字符串的值,则一律转换成数字做对比

    // ["06"] => "06" => 6
    ["06"] < 2   // false 
    
    ["06"] < "2" // true
    ["06"] > 2   // true
    
    5 > null     // true
    -1 < null    // true
    0 <= null    // true
    
    0 <= false   // true
    0 < false    // false
    
    // undefined => Number(...) => NaN
    5 > undefined // false

    To Boolean

    既然是总结,那么可能还要讲一下布尔值的隐式转换。这个还是比较常见的,我们来看下有哪些地方会使用到:

    • if(...)

    • for(;...;)

    • while(...)

    • do while(...)

    • ... ? :

    • ||

    • &&

    既然知道会转换,那么什么值是真值,什么值是假值呢?换个思路,假值以外都是真值。看看哪些是假值:

    • undefined

    • null

    • false

    • +0

    • -0

    • NaN

    • ""

    还有吗?


    欢迎纠正错误,欢迎学习交流收藏。我是原罪,一个极客。

    关于本文

    作者:原罪

    https://segmentfault.com/a/1190000040048164

    推荐阅读

    Javascript 中数据类型那些可能会中招的细节

    END

    关注下方「前端开发博客」,回复?“电子书”

    领取27本精选电子书

    ?? 看完两件事

    如果你觉得这篇内容对你挺有启发,我想邀请你帮我两个小忙:

    1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

    2. 关注公众号「前端开发博客」,每周重点攻克一个前端面试重难点

    如果觉得这篇文章还不错,来个【分享、点赞、在看】三连吧,让更多的人也看到~

    公众号也开始通过互动率推送了,互动少了可能就很晚或者收不到文章了。

    大家点个在看,星标我的公众号,就可以及时获得推文。

    点个在看少个Bug