• JavaScript: 自动类型转换-续


    上一篇文章中,我们详细讲解了JavaScript中的自动类型转换,由于篇幅限制,没能覆盖到所有的转换规则,这次准备详细讲解一下。

    上次我们提到了对象类型参与运算时转换规则:

    1). 在逻辑环境中执行时,会被转换为true

    2). 在字符串环境和数字环境中,它的valueOf()方法和toString()方法会依次被调用,然后根据返回值进行再次转换。首先,valueOf()方法会被调用,如果其返回值是基础类型,则将这个返回值转为目标类型,如果返回值不是基础类型,则再试图调用toString()方法,然后将返回值转型。如果最终的返回值不是基础类型,则转型会抛出一个异常,如果是基础类型,则会相应的转为字符串或数字。

    接着上次的讲,当加号“+”作为一元操作符应用在对象类型上面时,valueOf()和toString()方法,将会有机会被调用,最终返回值会被转为数字类型,我们因而会得到一个数字或NaN。先来看看valueOf()和toString()的调用顺序:

    var o = {
        valueOf: function() {
            return '3';
        },
        toString: function() {
            return '5';
        }
    };
    
    var foo = +o;
    
    console.log(foo);    // 3

    可以看到,valueOf()方法被调用,返回了字符串类型的'3',然后被转为数字类型的3,而toString()方法并没有被调用,我们再次移除valueOf()方法:

    var o = {
        toString: function() {
            return '5';
        }
    };
    
    var foo = +o;
    
    console.log(foo);    // 5

    这时候toString()方法就被调用了,根据其返回值'5',对象被成功转为了数字5。

    估计很多初学者都会觉得,如果定义了valueOf()方法,就去调用valueOf()方法,如果没定义,就去调用toString()方法,其实是不准确的。

    实际上,valueOf()方法总会在第一时间被调用,至于toString()方法的调用与否,那得看valueOf()方法的返回值了,我们上面也提到了,如果其返回值是基础类型,那么toString()方法根本没有机会被调用,而如果其返回值是引用类型,则会再试图调用toString()方法得到最终值。

    通常,对象原型中的valueOf()方法返回其自身引用,拿上面的例子来讲:

    var o = {
        toString: function() {
            return '5';
        }
    };
    
    console.log(o.valueOf() === o);  // true

    我们用了全等(===)操作符来比较其valueOf()返回值和其自身,发现是完全相同的,证明对象原型中的valueOf()的返回值的确是其自身,上面结果等同于下面这段代码:

    // 重写实例中的valueOf()方法,其返回值是对象自身
    
    var o = {
        valueOf: function() {
            return this;
        },
        toString: function() {
            return '5';
        }
    };
    
    console.log(o.valueOf() === o);  // true

    现在我们稍加修改,就可以看出在类型转换过程中,到底发生了什么:

    var o = {};
    
    Object.prototype.valueOf = function() {
        
        console.log('valueOf() called');
    
        return [];
    };
    
    Object.prototype.toString = function() {
        
        console.log('toString() called')
            
        return '5';
    }
    
    var a = +o;
    
    // output: valueOf() called
    // output: toString() called
    
    console.log(a);        // 5
    
    var b = o + '';
    
    // output: valueOf() called
    // output: toString() called
    
    console.log(b);        // '5'

    上面的代码中,我们改为修改原型方法valueOf()和toString(),分别在方法内部添加了控制台输出语句,另外,在valueOf()内部我们返回了一个数组对象。在对象参与运算时可以看到,两个方法依次被调用,不管是数字环境还是字符串环境,都先调用了valueOf()方法,由于返回值不是基础类型,所以还需再调用toString()方法,得到一个最终的返回值,然后将其转为目标类型。如果我们将valueOf()中的数组返回值替换为一个基础类型,toString()方法将不会有机会执行,大家可以亲自试一下。

    上面也提到,对象原型的valueOf()方法默认是返回对象自身的,实际上,常见对象类型的valueOf()方法都会返回其自身:

    var o = {};
    
    var fn = function(){};
    
    var ary = [];
    
    var regex = /./;
    
    
    o.valueOf() === o;                // true
    
    fn.valueOf() === fn;              // true
    
    ary.valueOf() === ary;            // true
    
    regex.valueOf() === regex;        // true

    不过有个特殊的例外,Date类型的valueOf()会返回一个毫秒数:

    var date = new Date(2017, 1, 1);
    
    var time = date.valueOf();
    
    console.log(time);                      // 1485878400000
    
    console.log(time === date.getTime());    // true

    所以我们就会很容易明白,在Date实例上应用一元加号操作符,是如何返回一个数字的:

    var date = new Date(2017, 1, 1);
    
    var time = +date;
    
    console.log(time);    // 1485878400000

    不过Date真是个神奇的物种,如果我们直接跟拿它和一个时间毫秒数相加,并不会得到期望的结果:

    var date = new Date(2017, 1, 1);
    
    var time = date + 10000;
    
    console.log(time);    // 'Wed Feb 01 2017 00:00:00 GMT+0800 (CST)10000'

    它竟然转为了字符串,然后与数字进行了字符串连接操作!为什么会是这样的呢?原因在于,对于一元加号操作符运算,目的很明确,就是求正操作,因此引擎调用了其valueOf()方法,返回时间戳数字,而对于后者的二元加号加号操作运算,其存在加法和连接符这样的二义性,所以引擎可以有选择地将操作数转为不同的目标类型,与其他对象不同的是,Date类型更倾向于转为字符串类型,所以toString()会被先行调用。下面这段话是ECMA规范中关于Date类型转为基础类型的描述

    大概的意思就是,对象在转为基础类型时,通常都会调用toPrimitive(hint)这样的方法,传入一个提示参数,指定其目标类型,如果不指定,其他对象的默认值都是number,而Date类型与众不同,它的默认值是string。

    我们上面也提到了,一元加号操作符是求正运算,所以引擎能够识别并为其指定number目标类型,而二元加号操作符存在二义性,引擎使用了default作为提示参数,Date类型将默认值认为是string,所以我们也理解了上面的例子,即使是Date对象和数字相加,它也不会先调用valueOf()方法得到数字,而是先调用toString()得到一个字符串。

    上面讲解了这么多,相信大家对于对象类型的转型规则都熟悉了,那么对于常见的对象,究竟是如何转为基础类型的呢?举个例子:

    var foo = +[];            // 0 ( [] -> '' -> 0 )
    
    var foo = +[3];           // 3 ( [3] -> '3' -> 3 )
    
    var foo = +[3, 5];        // NaN ( [3, 5] -> '3,5' -> NaN )

    从上面的代码可以看出,对于数组对象来说,要转为数字,就要遵循对象类型的转型规则,因为数组原型的valueOf()方法会返回其自身引用,所以最终会再试图调用其toString()方法,而它的toString()会返回一个字符串,这个字符串是由逗号分隔的数组元素集,那很容易理解了,对于空数组,必然返回一个空字符串,然后这个空字符串转型为数字之后就会变为0,而对于非空数组,如果只有一个元素并且元素可以转为数字,则结果第一个元素对应的数字,如果又多个元素,因为toString()返回的结果中存在逗号,所以无法转型成功,会返回一个NaN。

    但如果我们尝试数组和一个数字相加,则还是会得到一个字符串的结果:

    var foo = [] + 3;          // '3'
    
    var foo = [3] + 3;       // '33'
    
    var foo = [3, 5] + 3;     // '3,53'

    你也许会说,这不是很像上面的Date类型吗?是的,结果看上去很相似,但其内部的执行过程还是有差异的,它的valueOf()会先执行,出现上面的结果,是由于valueOf()返回了this,然后再次调用toString()返回了字符串,加号操作符在这里成了字符串连接符了。

    类似的还有字面量对象,看下面例子:

    var foo = {} + 0;        // '[object Object]0'
    
    var foo = {} + [];       // '[object Object]'
    
    var foo = {} + {};       // '[object Object][object Object]'

    不过如果是在命令行直接输入下面表达式,结果会有所出入:

    {} + 0;        // 0
    
    {} + [];       // 0
    
    {} + {};       // NaN

    其原因是,前面的字面量对象被解释成了代码块,没有参与运算,只有后面的一部分会返回最终的结果,后面的转换过程可以参照以上我们讲解的内容。

    对象的类型转换规则,就先讲到这里,下面来讲一下比较操作符中的类型转换

    比较操作符有以下几种:>, >=, <, <=, ==, ===。除了最后的全等操作符以外,其他几个在比较不同类型的数据时,均存在值的类型转换。

    对于前四种来说,都遵循着以下规则:

    1). 当两个操作数都为字符串类型时,不进行数据类型转换,直接比较每个字符

    2). 当两个操作数不同时为字符串时,将操作数转为数字类型,然后进行比较

    3). 如果操作数中存在对象类型,先将对象转为基础类型,然后再根据上面两条进行值的比较。

    而对于“==”操作符,则是多了一条特殊的规则:null和undefined在比较时不进行数据转换,null和自身比较、null和undefined比较都会返回true,和其他值比较都会返回false;undefined和自身比较、undefined和null比较都会返回true,和其他值比较都会返回false。

    以下比较操作不存在数据类型转换:

    '3a' < '3b';          // true
    
    '' == '0';            // false
    
    null == undefined;    // true
    
    null == 0;            // false
    
    undefined == false;   // false

    需要注意的是最后两个个表达式,由于我们在上一篇文章中讲到,null值在数字环境下会转型为0,很多人觉得这个表达式结果为true,但是不要忽略了上面关于null和undefined的规则,这里是不会有类型转换发生的,同样的,undefined在比较操作符中也只会识别其自身和null值,并且不会发生数据类型转换。

    在下面几个表达式中,操作数不全为字符串,所以要将操作数转为数字后再进行比较:

    3 == '3';             // true
    
    3 < '5';             // true
    
    0 == '0';        // true
    
    0 == '';        // true
    
    0 == false;        // true
    
    1 <= true;            // true
    
    null >= 0;            // true

    注意,最后一个表达式中的null,在遇到>、>=、<、<=这几个操作符时会被转为数字0的,这与上面的规则有所不同。

    最后,对象在参与逻辑运算时,同样会遵循前面的转型规则:

    var o = {
        toString: function() {
            return '3';
        },
        valueOf: function() {
            return '5';
        }
    }
    
    o > 4;        // true

    特别注意的是,前面我们介绍到,对象在条件语句中是视为true的,但要避免下面这样的比较:

    if ([]) {
        // todo
    }
    
    if ([] == true) {
        // todo
    }

    第二个条件语句块是不会执行的,原因在于空的数组对象被转为数字0了,而true被转为数字1,比较结果为false,所以里面的代码永远无法得到执行,开发时要警惕这样的写法。

    写了这么多关于自动类型转换的内容,大家也可以体会到JS有多么的灵活,想要驾驭好这门语言,不是件容易的事,还需细细体会,好好研究才行。

    本文完。

    参考资料:

    http://es5.github.io/#x11.8.5

    http://es5.github.io/#x11.9.3

    http://www.2ality.com/2012/01/object-plus-object.html

    http://www.2ality.com/2013/04/quirk-implicit-conversion.html

    https://www.united-coders.com/matthias-reuter/all-about-types-part-2/

    http://www.adequatelygood.com/Object-to-Primitive-Conversions-in-JavaScript.html

  • 相关阅读:
    CAGradientLayer
    AndroidStudio -- AndroidStuido中找不到cache.properties文件
    logcat -- 基本用法
    UiAutomator -- UiObject2 API
    Android UiAutomator UiDevice API
    Ubuntu 连接手机 不识别设备 -- 解决办法
    Ubuntu Cannot run program "../SDK/build-tools/xxx/aapt": erro = 2 No such file or directory
    Junit4
    Android Studio下运行UiAutomator
    Gradle sync failed: failed to find Build Tools revision 21.1.2
  • 原文地址:https://www.cnblogs.com/liuhe688/p/5966066.html
Copyright © 2020-2023  润新知