• home > webfront > ECMAS > emphasis >

    JavaScript类型转换规则说明:加法 ==类型转换说明

    Author:zhoulujun@live.cn Date:

    ECMAScript运行环境会在需要时执行自动类型转换。定义一套关于转换的抽象操作有助于阐明某些结构的语义。ToString - 将非字符串转为字符串。ToNumber - 将非数转为数字。ToBoolean - 将非布尔值转为布尔值。ToPrimitive - 将引用类

    Gary Bernhardt在一个简短的演讲视频“Wat”中指出了一个有趣的JavaScript怪癖:在把对象和数组混合相加时,会得到一些你意想不到的结果

    []+{}   结果是 "[object Object]"

    {}+[]   结果是 0

    ({}+[])   结果是 "[object Object]"

    +[]     结果是 0   ,此处+为正号

    +{}     结果是 NaN   ,此处+为正号

    [].toString()    结果是 ""

    ({}).toString() 结果是 "[object Object]"    

    {}.toString() 会出错,Uncaught SyntaxError: Unexpected token .

    0+[]    结果是 "0"

    0+{}    结果是 "0[object Object]"

    之前在写《深度克隆从C#/C/Java漫谈到JavaScript真复制》,JSON.parse 方法会丢失数据结构 , 是因为

    JSON.parse(JSON.stringify(target))数据及结构丢失

    JSON.stringify() 将值转换为相应的JSON格式:

    • 转换值如果有 toJSON() 方法,该方法定义什么值将被序列化。Date 日期调用了 toJSON() 将其转换为了 string 字符串(同Date.toISOString()),因此会被当做字符串处理。

    • 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。

    • 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中

    • undefined和null、任意的函数、正则表达式、symbol 值、NaN 和 Infinity 等,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null(出现在数组中时)。函数、undefined 被单独转换时,会返回 undefined,如JSON.stringify(function(){}) or JSON.stringify(undefined)。NaN 和 Infinity 格式的数值及 null 都会被当做 null。正则转换为空对象。

    • 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误

    • 所有以 symbol 为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。

    • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性参考文章:

    种字符串化与强制转换并不完全是同一种东西。toJSON() 将对象变为一个适用于字符串化的 JSON 安全的值

    JSON.stringify(..) 的第二个参数值是可选的,它称为 替换器(replacer)。这个参数值既可以是一个 array 也可以是一个 function。与 toJSON() 为序列化准备一个值的方式类似,它提供一种过滤机制,指出一个 object 的哪一个属性应该或不应该被包含在序列化形式中,来自定义这个 object 的递归序列化行为。

    但其实也与下面的类型转换规则 toString()想关联。

    而上面的这些运算操作 会进行隐式类型转换,规则是调用其 valueOf() 或 toString() 以取得一个非对象的值(primitive value)。如果两个值中的任何一个是字符串,则进行字符串串接,否则进行数字加法

    类型转换和强制类型转换

    类型转换发生在静态类型语言的编译阶段,而强制类型转换发生在动态类型语言的运行时(runtime),因此在 Java 中只有强制类型转换。强制类型转换一般还可分为 隐式强制类型转换(implicit coercion)和 显式强制类型转换(explicit coercion)。

    ECMAScript运行环境会在需要时执行自动类型转换。定义一套关于转换的抽象操作有助于阐明某些结构的语义。这些抽象操作不是语言本身的一部分;它们被定义在这里是为了协助语言的语义规范。这些关于转换的抽象操作是多态的,它们可以接受任何ECMAScript语言类型,但不能接受规范类型。

    原文:  https://www.w3.org/html/ig/zh/wiki/ES5/类型转换与测试。其实我们不需要记住那么多,字符串、数字、布尔值之间类型转换的基本规则 ES5定义了一些操作,诸如ToString、ToNumber、ToBoolean、ToPrimitive抽象操作。下面精炼下:

    字符串、数字和布尔值之间的转换规则(抽象操作)

    通过ToPrimitive()将值转换为原始值

    想要将对象转换成原始值,必然会调用toPrimitive()内部函数

    通过ToPrimitive()将值转换为原始值

    JavaScript引擎内部的抽象操作ToPrimitive()有着这样的签名:

    ToPrimitive(input, PreferredType?)

    可选参数PreferredType可以是Number或者String,它只代表了一个转换的偏好,转换结果不一定必须是这个参数所指的类型,但转换结果一定是一个原始值.如果PreferredType被标志为Number,则会进行下面的操作来转换输入的值 (§9.1):

    1. 如果输入的值已经是个基本类型,则直接返回它.

    2. 否则,如果输入的值是一个对象.会检查该值是否有valueOf()方法,如果则调用该对象的valueOf()方法,如果valueOf()方法的返回值是基本类型,则返回这个原始值.

    3. 否则,调用这个对象的toString()方法.如果toString()方法的返回值基本类型,则返回这个原始值.

    4. 否则,抛出TypeError异常.


    如果PreferredType被标志为String,则转换操作的第二步和第三步的顺序会调换.

    如果没有PreferredType这个参数,则PreferredType的值会按照这样的规则来自动设置:Date类型的对象会被设置为String,其它类型的值会被设置为Number.


    如果输入的值是一个对象,则会首先会调用ToPrimitive(obj, String)将该对象转换为原始值,然后再调用ToString()将这个原始值转换为字符串.

    var obj = {
        valueOf: function () {
            console.log("valueOf");
            return {}; // 没有返回原始值
        },
        toString: function () {
            console.log("toString");
            return {}; // 没有返回原始值
        }
    }

    这个试一试

    通过ToNumber()将值转换为数字

    下面的表格解释了ToNumber()是如何将原始值转换成数字的

    参数结果
    undefinedNaN
    null+0
    布尔值true被转换为1,false转换为+0
    数字无需转换
    字符串由字符串解析为数字.例如,"324"被转换为324

    如果输入的值是一个对象,则会首先会调用ToPrimitive(obj, Number)将该对象转换为原始值,然后在调用ToNumber()将这个原始值转换为数字.

    • undefined => "undefined"

    • null => "null"

    • number => "number"  极大数和极小数用指数表示

    • boolean => "boolean"

    • Symbol => "Symbol"

    • Object => instance.toString || Object.prototype.toString //规则由抽象操作ToPrimitive抽象操作里完成  

    var a = {
        valueOf: function(){
            return "42";
        }
    };
    var b = {
        toString: function(){
            return "42";
        }
    };
    Number( a );// 42
    Number( b );// 42


    通过ToString()将值转换为字符串

    下面的表格解释了ToString()是如何将原始值转换成字符串的

    参数结果
    undefined"undefined"
    null"null"
    布尔值"true"  或者 "false"
    数字数字作为字符串,比如. "1.765"
    字符串无需转换

    通过ToBoolean转换为布尔值

    以下列表会转换为假值

    • 0,+0,-0,NaN

    • “”

    • undefined

    • null

    • false

    除以上假值列表以外的值,都将被转化为真值


    ==中的隐式类型转换规则

    • ===:称为等同符,当两边值的类型相同时,直接比较值,若类型不相同,比较是否是同一个引用地址。

      • 值都是NaN,可以用isNaN()或Object.is()

    • ==:称为等值符,当等号两边的类型相同时,直接比较值是否相等,若不相同,则先转化为类型相同的值,再进行比较;

    • Object.is(),使用Same-value equality(同值相等)算法,比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

      • +0不等于-0。

      • NaN等于自身。

    根据上面的转换规则,我们来看下==比较的 

    字符串和数字之间的比较

    ES5规范11.9.3.4-5规定如下 x == y

    • 如果Type(x) 是数字,Type(y) 是字符串,则返回x == ToNumber(y) 的结果。

    • 如果Type(x) 是字符串,Type(y) 是数字,则返回ToNumber(x) == y 的结果。

    总结 数字与字符串相比,比较的是将字符串转为数字

    其他类型和布尔类型的比较

    ES5规范11.9.3.4-5规定如下 x == y

    • 如果Type(x) 是布尔类型,则返回ToNumber(x) == y 的结果;

    • 如果Type(y) 是布尔类型,则返回x == ToNumber(y) 的结果。

    总结 其他类型与布尔类型比,现将布尔类型转为数字,然后执行数字与数字之间的比较或数字与字符串之间的比较

    undefined和null之间的转换

    • 如果x 为null,y 为undefined,则结果为true。

    • 如果x 为undefined,y 为null,则结果为true。

    总结 在== 中null 和undefined 是一回事,可以相互进行隐式强制类型转换。其他值都不存在这种情况

    两个比较特殊的==判断

    NaN == NaN // =>false

     [] == ![]  // => true


    加法转换规则

    加号操作符会将preferedType看成Number,调用ES内部的toPrimitive(input,Number)方法,得到空字符串

    两个空数组相加时,结果是我们所预料的:

        [] + []''

    []会被转换成一个原始值,首先尝试valueOf()方法,返回数组本身(this):

        var arr = [];> arr.valueOf() === arrtrue

    这样的结果不是原始值,所以再调用toString()方法,返回一个空字符串(是一个原始值).因此,[] + []的结果实际上是两个空字符串的连接.

    将一个空数组和一个空对象相加,结果也符合我们的预期:

        [] + {} '[object Object]'

    意想不到的结果

    如果加号前面的第一个操作数是个空对象字面量,则结果会出乎我们的意料(下面的代码在Firefox控制台中运行):

    {} + {}

    NaN

    这是怎么一回事?原因就是JavaScript引擎将第一个{}解释成了一个空的代码块并忽略了它.NaN其实是后面的表达式+{}计算的结果 (加号以及后面的{}).这里的加号并不是代表加法的二元运算符,而是一个一元运算符,作用是将它后面的操作数转换成数字,和Number()函数完全一样.例如:

    > +"3.65"
    3.65

    转换的步骤是这样的:

    +{}
    Number({})
    Number({}.toString())  // 因为{}.valueOf()不是原始值Number("[object Object]")
    NaN

    为什么第一个{}会被解析成代码块呢?原因是,整个输入被解析成了一个语句,如果一个语句是以左大括号开始的,则这对大括号会被解析成一个代码块.所以,你也可以通过强制把输入解析成一个表达式来修复这样的计算结果:

    > ({} + {})'[object Object][object Object]'

    另外,一个函数或方法的参数也会被解析成一个表达式:

    > console.log({} + {})
    [object Object][object Object]

    经过前面的这一番讲解,对于下面这样的计算结果,你也应该不会感到吃惊了:

    > {} + []0

    在解释一次,上面的输入被解析成了一个代码块后跟一个表达式+[].转换的步骤是这样的:

    +[]
    Number([])
    Number([].toString())  // 因为[].valueOf()不是原始值Number("")0

    有趣的是,Node.js的REPL在解析类似的输入时,与Firefox和Chrome(和Node.js一样使用V8引擎)的解析结果不同.下面的输入会被解析成一个表达式,结果更符合我们的预料:

    > {} + {}'[object Object][object Object]'
    > {} + []'[object Object]'


      减法运算转换规则

      • 如果两个操作符都是数值, 则执行常规的算术减法操作,并返回结果。

      • 如果有一个操作数是NAN, 则结果也是NaN。

      • 如果有一个操作数是字符串、布尔值、null、undefined则先在后台调用Number()方法将其转换为数值, 然后在根据根据前面的规则进行减法计算,如果转换的结果是NaN, 则减法的结果就是NaN。

      • 如果有一个操作数是对象,则调用对象的 valueof() 方法以取得该方法返回后的值,如果得到的值是NaN,则减法的结果就是NaN, 如果对象没有valueOf()方法,则调用其toString()方法并将得到的字符串转为数值。

      其他数学操作运算也是一样的




      参考文章:

      Javascript中{}+[]===0为true,而[]+{}===0为false,因为啥 https://blog.csdn.net/carcarrot/article/details/97786733

      JavaScript values: not everything is an object

      JS原始值转换算法---toPrimitive() https://blog.csdn.net/suxuelengyin/article/details/82759437

      你不懂JS:类型与文法(You Dont Know JS)(第一版) https://www.bookstack.cn/read/You-Dont-Know-JS-types-grammar/ch4.2.md


      转载本站文章《JavaScript类型转换规则说明:加法 ==类型转换说明》,
      请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2016_0317_7714.html