美文网首页
中卷(1.4)

中卷(1.4)

作者: 风声233 | 来源:发表于2018-08-14 00:01 被阅读11次

    强制类型转换

    值类型转换

    将值从一种类型转换为另一种类型通常称为类型转换(type casting),这是显示的情况;隐式的情况称为强制类型转换(coercion)。
    也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。
    然而在 js 中通常将他们统称为强制类型转换,我个人则倾向于用“隐式强制类型转换”和“显示强制类型转换”来区分。
    二者的区别显而易见:我们能够从代码中看出那些地方是显示强制类型转换,而隐式强制类型转换则不那么明显,通常式某些操作产生的副作用。
    例如:

    var a = 42;
    var b = a + ""; // 隐式强制类型转换
    var c = String( a ); // 显示强制类型转换
    

    抽象值操作

    1.ToString

    该抽象操作负责处理非字符串到字符串的强制类型转换。
    基本类型值的字符串化规则为:null 转换为 "null",undefined 转换为 "undefined",true 转换为 "true"。数字的字符串化则遵循通用规则,不过之前讲过的 那些极小和极大的数字使用指数形式。
    对普通对象来说,除非自己定义,否则 toString() (Object.prototype.toString()) 返回内部属性 [[Class]] 的值,如 "[object Object]"。
    然而前面我们介绍过,如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
    数组的默认 toString() 方法经过了重新定义:

    var a = [1,2,3];
    a.toString(); // "1,2,3"
    

    JSON 字符串化
    工具函数 JSON.stringify(..) 在将 JSON 对象序列化时也用到了 ToString。
    请注意,JSON 字符串化并非严格意义上的强制类型转换,因为其中也设计了 ToString 的相关规则。
    对于大多数简单值来说, JSON 字符串化和 toString() 的效果基本相同,只不过序列化的结果总是字符串:

    JSON.stringify( 42 ); // "42"
    JSON.stringify( "42" ); // ""42""(含有双引号的字符串)
    JSON.stringify( null ); // "null"
    JSON.stringify( true ); // "true"
    

    所有安全的 JSON 值都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值始值能够呈现为有效 JSON 格式的值。
    为了简单起见,我们来看看什么是不安全的 JSON 值。undefined、function、symbol 和包含循环引用的对象都不符合 JSON 的结构标准,其他支持 JSON 的语言都无法处理它们。
    JSON.stringify(..) 在对象中遇到 undefined、function 和 symbol 时会自动将其忽略,在数组中则会返回 null,以保证单元位置不变。
    例如:

    JSON.stringify( undefined ); // undefined
    JSON.stringify( function(){} ); // undefined
    
    JSON.stringify(
      [1, undefined, function(){},4]
    ); // "[1,null,null,4]"
    JSON.stringify(
      { a:2, b:function(){} }
    ); // "{"a":2}"
    

    对包含循环引用的对象执行 JSON.stringify(..) 会出错。

    如果对象定义了 toJSON() 方法,JSON 字符串化时会首先调用该方法,然后用它的返回值来进行序列化。
    如果要对含有非法的 JSON 值得对象做字符串化,或者对象中的某些值无法被序列化时,就需要定义 toJSON() 方法来返回一个安全的 JSON 值。
    例如:

    var o = {};
    
    var a = {
      b: 42,
      c: o,
      d: function(){}
    };
    
    // 在a中创建一个循环引用
    o.e = a;
    
    // 循环引用在这里会产生错误
    JSON.stringify( a );
    
    // 自定义的 JSON 序列化
    a.toJSON = function(){
      return { b: this.b };
    };
    
    JSON.stringify( a ); // "{"b":42}"
    

    很多人误以为 toJSON 返回的是 JSON 字符串化后的值,其实不然,除非我们确实想要对字符串进行字符串化(通常不会!)。toJSON() 返回的值应该是一个适当的值,可以是任何类型,然后再由 JSON.stringify(..) 对其进行字符串化。
    也就是说,toJSON() 应该“返回一个能够被字符串化的安全的 JSON 值”,而不是“返回一个 JSON 字符串”。
    例如:

    var a = {
      val: [1,2,3],
      
      // 可能是我们想要的结果!
      toJSON: function(){
        return this.val.slice( 1 );
      }
    };
    
    var b = {
      val: [1,2,3],
      
      // 可能不是我们想要的结果!
      toJSON: function(){
        return "[" +  
          this.val.slice( 1 ).join() +
        "]";
      }
    };
    
    JSON.stringify( a ); // "[2,3]"
    JSON.stringify( b ); // ""[2,3]""
    

    现在介绍几个不太为人所知但却非常有用的功能。
    我们可以向 JSON.stringify(..) 传递一个可选参数 replacer,它可以是数组或者函数,用来指定对象序列化过程中哪些属性应该被处理,哪些应该被排除,和 toJSON() 很像。
    如果 replacer 是一个数组,那么它必须是一个字符串数组,其中包含序列化要处理的对象的属性名称,除此以外的其他属性则被忽略。
    如果 replacer 是一个函数,它会对对象本身调用一次,然后对对象中的每个属性各调用一次,每次传递两个参数,键和值。如果要忽略某个键就返回 undefined,否则返回指定的值。

    var a = {
      b: 42,
      c: "42",
      d: [1,2,3]
    }
    
    JSON.stringify( a, ["b","c"]); // "{"b":42,"c":"42"}"
    
    JSON.stringify( a, function(k,v){
      if (k !== "c") return v;
    })
    // "{"b":42,"d":[1,2,3]}"
    

    JSON.stringify 还有一个可选参数 space,用来指定输出的缩进格式。

    var a = {
      b: 42,
      c: "42",
      d: [1,2,3]
    };
    
    JSON.stringify( a, null, 3 );
    //"{
    //   "b": 42,   
    //   "c": "42",
    //   "d": [
    //      1,
    //      2,
    //      3
    //   ]
    //}"
    
    JSON.stringify( a, null, "-----");
    //"{
    //-----"b": 42,
    //-----"c": "42",
    //-----"d": [
    //----------1,
    //----------2,
    //----------3
    //-----]
    //}"
    

    请记住,JSON.stringify(..) 并不是强制类型转换。在这里介绍是因为它涉及 ToString 强制类型转换,具体表现在以下两点。
    1.字符串、数字、布尔值和 null 的 JSON.stringify(..) 规则与 ToString 基本相同。
    2.如果传递给 JSON.stringify(..) 的对象中定义了 toJSON() 方法,那么该方法会在字符串化前调用,以便将对象转换为安全的值

    2.ToNumber

    其中 true 转换为 1, false 转换为 0。undefined 转换为 NaN,null 转换为 0。
    ToNumber 对字符串的处理基本遵循数字常量的相关规则 / 语法。处理失败时返回 NaN。
    对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
    为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先检查该值是否有 valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值来进行强制类型转换。
    如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
    从 ES5 开始,使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null,并且没有 valueOf() 和 toString() 方法,因此无法进行强制类型转换。

    我们稍后会详细介绍数字的强制类型转换,下面的示例代码中我们假定 Number(..) 已经实现了此功能。

    var a = {
      valueOf: function(){
        return "42";
      }
    };
    
    var b = {
      toString: function(){
        return "42";
      }
    };
    
    var c = [4,2]
    c.toString = function(){
      return this.join(""); // "42"
    }
    
    Number( a ); // 42
    Number( b ); // 42
    Number( c ); // 42
    Number( "" ); // 0
    Number( [] ); // 0
    Number( ["abc"] ); //NaN 
    
    3.ToBoolean

    首先也是最重要的一点是,js 中有两个关键词 true 和 false,分别代表布尔类型中的真和假。我们常误以为值 1 和 0 分别等同于 true 和 false。在有些语言中可能是这样,但在 js 中布尔值和数字是不一样的。虽然我们可以将 1 强制类型转换为 true,将 0 强制类型转换为 false,反之亦然,但它们并不是一回事。

    • 假值(falsy value)
      js 中的值可以分为以下两类:
      1)可以被强制类型转换为 false 的值
      2)其他(被强制类型转换为 true 的值)
      js 规范具体定义了一小撮可以被强制类型转换为 false 的值
      以下是这些假值:
      undefined、null、false、+0、-0、NaN 和 ""
      假值的布尔强制类型转换结果为 false。
      从逻辑上说,价值列表以外的都应该是真值(truthy)。但 js 规范对次并没有明确定义,只是给出了一些示例,例如规定所有的对象都是真值。
    • 假值对象(falsy object)
      这个标题似乎有点自相矛盾。前面讲过规范规定所有的对象都是真值,怎么还有价值对象呢?
      有些人可能会以为假值对象就是包装了假值的封装对象,其实不然!
      例如:
    var a = new Boolean( false );
    var b = new Number( 0 );
    var c = new String( "" ); 
    
    var d = Boolean( a && b && c);
    d; // true
    

    d 为 true,说明 a、b、c 都为 true。
    请注意,这里 Boolean(..) 对 a && b && c 进行了封装,有人可能问为什么。我们暂且记下,稍后会做说明。你可以试试不用 Boolean(..) 的话 d = a && b && c 会产生什么结果。
    如果价值对象并非封装了假值的对象,那它究竟是什么?
    值得注意的是,虽然 js 代码会出现假值对象,但它实际上并不属于 js 语言的范畴。
    浏览器在某些特定情况下,在常规 js 语法基础上自己创建了一些外来值,这些就是“假值对象”。
    假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false。
    最常见的例子是 document.all,它是一个类数组对象,包含了页面上的所有元素,由 DOM 提供给 js 程序使用。
    那为什么它是假值呢?因为我们经常通过将 document.all 强制类型转换为布尔值来··判断浏览器是否是老版本的 IE。

    if(document.all) { /* it's old version IE */ }
    
    • 真值(truthy value)
      真值就是假值列表之外的值。例如:
    var a = "false";
    var b = "0";
    var c = "''";
    
    var d = Boolean(a && b && c);
    d; // true
    

    再如:

    var a = [];
    var b = {};
    var c = function(){};
    
    var d = Boolean( a && b && c );
    d; // true
    

    显示强制类型转换

    显示强制类型转换是那些显而易见的类型转换,很多类型转换都属于此列。
    对显示强制类型转换几乎不存在非议,它类似于静态语言中的类型转换,已被广泛接受,不会有什么坑。我们后面再讨论这个话题。

    字符串和数字之间的显示转换

    我们从最常见的字符串和数字之间的强制类型转换开始
    字符串和数字之间的转换是通过 String(..) 和 Number(..) 两个内建函数(原生构造函数)来实现的,请注意它们前面没有 new 关键字,并不创建封装对象。
    下面是两者之间的显示强制类型转换:

    var a = 42;
    var b = String( a );
    
    var c = "3.14";
    var d = Number( c );
    
    b; // "42"
    d; // 3.14
    

    除了 String(..) 和 Number(..) 以外,还有其他方法可以实现字符串和数字之间的显示转换:

    var a = 42;
    var b = a.toString();
    
    var c = "3.14";
    var d = +c;
    
    b; // "42"
    d; // 3.14 
    

    一元运算符 +c 可以将 c 转换为数字,而非数字的加法运算。
    不过有时也容易产生误会。例如:

    var c = "3.14";
    var d = 5+ +c;
    
    d; // 8.14
    

    一元运算符 - 和 + 一样,并且它还会反转数字的符号位。由于 -- 会被当作递减运算符来处理,所以我们不能使用 -- 来撤销反转,而应该像 - -"3.14" 这样,在中间加一个空格,才能得到正确结果 3.14。

    1.日期显示转换为数字
    一元运算符 + 的另一个常见的用途是将日期对象强制类型转换为数字,返回的结果为 Unix 时间戳,以毫秒为单位:

    var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
    +d; // 1408369986000
    

    我们常用下面的方法获得当前的时间戳:

    var timestamp = +new Date();
    

    js 有一处奇特的语法,即构造函数没有参数时可以不用带()。
    于是我们可能会碰到 var timestamp = +new Date; 这样的写法。

    不过最好还是使用 ES5 中新加入的静态方法 Date.now()。我们不建议对日期类型使用强制类型转换。

    2.奇特的 ~ 运算符
    一个常被人忽视的地方是 ~ 运算符(即字位操作“非”)相关的强制类型转换。
    字位操作符只适用于32位整数,运算符会强制操作数使用32位格式。这是通过抽象操作 ToInt32 来实现的。
    ToInt32 首先执行 ToNumber 强制类型转换,比如 "123" 会先转换为123,然后再执行 ToInt32。
    虽然严格说来并非强制类型转换(因为返回值类型并没有发生变化),但字位运算符(如 | 和 ~)和某些特殊数字一起使用时会产生类似强制类型转换的效果,返回另外一个数字。
    例如 | 运算符(字位操作“或”)的空操作 0 | x,它仅执行 ToInt32 转换:

    0 | -0; //0
    0 | NaN; // 0
    0 | Infinity; // 0
    0 | -Infinity; // 0
    

    以上这些特殊数字无法以32位呈现(因为它们来自 64 位 IEEE 754 标准),因此返回 0。
    再回到 ~。它首先将值强制类型转换为32位数字,然后执行字位操作“非”(对每一个字位进行反转)。
    对 ~ 还可以有另外一种诠释,源自早期的计算机科学和离散数学:~ 返回 2 的补码。这样一来问题就清楚多了!
    ~x 大致等同于 -(x+1)。
    很奇怪,但相对更容易说明问题。

    ~42; // -(42+1) ==> -43
    

    另外,在 -(x+1) 中唯一能都得到 0(或者严格说是 -0)的 x 值是 -1。也就是说如果 x 为-1时, ~ 和一些数字值在一起会返回假值0,其他情况则返回真值。
    这个特点很有用处,因为 -1 是一个“哨位值”,即被赋予了特殊含义的值,在 C 语言中我们用 -1 来表示函数执行失败,用大于等于 0 的值表示函数执行成功。
    js 中字符串的 indexOf(..) 方法也遵循这一惯例,该方法在字符串中搜索指定的子字符串,如果找到就返回子字符串的位置(从0开始),否则返回 -1。
    indexOf(..) 不仅能够得到子字符串的位置,还可以用来检查字符串中是否包含指定的子字符串,相当于一个条件判断。
    例如:

    var a = "Hello World";
    
    if (a.indexOf( "lo" ) >= 0){ // true
      // 找到匹配!
    }
    if (a.indexOf("lo") != -1){ // true
      // 找到匹配!
    }
    if (a.indexOf("lo") < 0){ // true
      // 没有找到匹配!
    }
    if (a.indexOf("lo") == -1){ // true
      // 没有找到匹配!
    }
    

    大于等于 0 和 ==-1 这样的写法不是很好,成为“抽象渗漏”,意思是在代码中暴露了底层的实现细节,这里是指用 -1 作为失败时的返回值,这些细节应该被屏蔽掉。

    现在我们终于明白 ~ 的用处了!~ 和 indexOf() 一起可以将结果强制类型转换为 真 / 假值:

    var a = "Hello World";
    
    ~a.indexOf( "lo" ); // -4 <--真值!
    
    if(~a.indexOf( "lo" )){ // true
      // 找到匹配!
    }
    
    ~a.indexOf( "ol" ); // 0 <--假值!
    !~a.indexOf( "ol" ); // true
    if(!~a.indexOf( "ol" )){ // true
      // 没有找到匹配!
    }
    

    由 -(x+1) 推断 ~-1 的结果应该是 -0,然而实际上结果是0,因为它是字位操作而非数字运算。

    3.字位截除
    一些开发人员使用 ~~ 来截除数字值得小数部分,以为这和 Math.floor(..) 的效果一样,实际上并非如此。
    ~~ 中的第一个 ~ 执行 ToInt32 并反转字位,然后第二个 ~ 再进行一次字位反转,即将所有的字位反转回原值,最后得到的仍然是 ToInt32 的结果。
    ~~ 和 !! 很相似
    对 ~~ 我们要多加注意。首先它只适用于 32 位数字,更重要的是它对负数的处理与 Math.floor(..) 不同。

    Math.floor( -49.6 ); // -50
    ~~-49.6; // -49
    

    ~~x 能将值截除为一个32位整数,x | 0 也可以,而且看起来还更简洁。
    出于对运算优先级的考虑,我们更倾向于使用 ~~x:

    ~~1E20 / 10; // 166199296
    
    1E20 | 0 / 10; // 1661992960
    (1E20 | 0 )/ 10; // 166199296
    

    显示解析数字字符串

    例如:

    var a = "42";
    var b = "42px";
    
    Number( a ); // 42
    parseInt( a ); // 42
    
    Number( b ); // NaN
    parseInt( b ); // 42
    

    解析允许字符串中含有非数字字符,解析从左到右进行,如果遇到非数字字符就停止解析。而转换不允许出现数字字符,否则会失败并返回 NaN。
    解析字符串中的浮点数可以使用 parseFloat(..) 函数。
    不要忘了 parseInt(..) 针对的是字符串值。向 parseInt(..) 传递数字和其他类型的参数是没有用的,比如 true、function(){...} 和 [1,2,3]。
    非字符串参数会首先被强制类型转换位字符串,依赖这样的隐式强制类型转换并非上策,应该避免向 parseInt(..) 传递非字符串参数。
    从 ES5 开始 parseInt(..) 默认转换位十进制数,除非另外指定。如果你的代码需要在 ES5 之前的环境运行,请记得将第二个参数设置为 10。

    parseInt 解析非字符串

    例如:

    parseInt( 1/0, 19 ); // 18
    

    parseInt( 1/0, 19 ) 实际上是 parseInt("Infinity", 19)。第一个字符是 "I",以 19 为基数时值为 18。第二个字符 "n" 不是一个有效的数字字符,解析到此为止。

    此外还有一些看起来很奇怪但实际上能解释得通的例子:

    parseInt( 0.000008 ); // 0 ("0" 来自于 "0.000008")
    parseInt( 0.0000008 ); // 8 ("8" 来自于 "8e-7")
    parseInt( false, 16 ); // 250 ("fa" 来自于 "false")
    parseInt( parseInt, 16); // 15 ("f" 来自于 "function..")
    
    parseInt( "0x10" ); // 16
    parseInt( "103", 2 ); // 2
    

    其实 parseInt(..) 函数是十分靠谱的,只要使用得当就不会有问题。因为使用不等而导致一些莫名奇妙的结果,并不能归咎与 js 本身。

    显示转换位布尔值

    与前面的 String(..) 和 Number(..) 一样, Boolean(..)(不带 new)是显示的 ToBoolean 强制类型转换:

    var a = "0";
    var b = [];
    var c = {};
    
    var d = "";
    var e = 0;
    var f = null;
    var g;
    
    Boolean( a ); // true
    Boolean( b ); // true
    Boolean( c ); // true
    
    Boolean( d ); // false
    Boolean( e ); // false
    Boolean( f ); // false
    Boolean( g ); // false
    

    和前面讲的 + 类似,一元运算符 ! 显示地将值强制类型转换为布尔值。但是它同时还将真值反转为假值(或者将假值反转为真值)。所以显示强制类型转换为布尔值最常用的方法是 !!,因为第二个 ! 会将结果反转为原值:

    var a = "0";
    var b = [];
    var c = {};
    
    var d = "";
    var e = 0;
    var f = null;
    var g;
    
    !!a; // true
    !!b; // true
    !!c; // true
    
    !!d; // false
    !!e; // false
    !!f; // false
    !!g; // false
    

    显示 ToBoolean 的另外一个用处,是在 JSON 序列化过程中将其值强制类型转换为 true 或 false:

    var a = [
      1,
      function(){ /*..*/ },
      2,
      function(){ /*..*/ }
    ];
    
    JSON.stringify( a ); // "[1,null,2,null]"
    
    JSON.stringify( a, function(key,val){
      if (typeof val == "function"){
        // 函数的 ToBoolean 强制类型转换
        return !!val;
      }
      else{
        return val;
      }
    } );
    // "[1,true,2,true]"
    

    隐式强制类型转换

    隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。
    显式强制类型转换旨在让代码更加清晰可读,而隐式强制类型转换看起来就像是它的对立面,会让代码变得晦涩难懂。
    对强制类型转换的诟病大多是针对隐式强制类型转换。
    但是隐式强制类型转换的作用是减少冗余,让代码更简洁。

    字符串和数字之间的隐式强制类型转换

    通过重载,+ 运算符即能用于数字加法,也能用于字符串拼接。js 怎样来判断我们要的是哪个操作?例如:

    相关文章

      网友评论

          本文标题:中卷(1.4)

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