你不知道的JavaScript丛书总结(二)

197 阅读10分钟

该专栏总结自《你不知道的JavaScript》丛书的知识,易错易混点。

类型装换

类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型怨言的运行时。在js中统称为强制类型转换。

抽象值操作

ToString:他负责处理非字符串到字符串的强制类型转换。

  • 如果转化的是极大极小值时,转化后的字符串是指数形式。
    const a = 100000000000000000000000000000000000000000000;
    console.log(a.toString()) //1e+44
  • 对于普通对象来说,除非自定义toString方法,否则都调用Object.prototype.toString(),然后返回"[object Object]"
  • JSON字符串化在对象中遇到undefined, function, symbol时会自动将其忽略,在数组中则会返回null,用来保证数组中元素对应的位置不变。并且对包含循环引用的对象JSON字符串化时报错。
    console.log(JSON.stringify([1, undefined, function() {}, 4])) //[1,null,null,4]
    console.log(JSON.stringify({
      name: "zh",
      age: undefined,
      foo: function() {}
    })) // {"name":"zh"}

如果对象中定义了toJSON方法,JSON字符串化时会首先调用该方法,然后用他的返回值来进行序列化。

ToNumber: 将非非数字值转化为数字值。

  • 注意这两个值的转换。undefined转换为NaN, null转换为0。
    console.log(Number(undefined)) //NaN
    console.log(Number(NaN)) //NaN
    console.log(Number(null)) // 0
    console.log(Number("")) // 0
    console.log(Number([])) // 0
    console.log(Number({})) // NaN
  • 为了将值转换为相应的基本类型值,抽象操作ToPrimitive会首先检查该值是否有valueOf方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toStrin()的返回值来进行强制类型转换。如果valueOf(), toString()均不返回基本类型值,会产生TypeError错误。

ToBoolean: 将非boolean值转换为boolean类型。

  • undefined, null, false, +0, -0, NaN, ""会被强制转化为false, 其他任何值都将被转化为true。

显示强制类型转换

显示强制类型转换是那些显而易见的类型转换。

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

  • String,Number两个内建函数来完成上面的操作。并且遵守ToString,ToNumber规则。
  • 还有很多方法可以实现二者之间的显示转换。
    const a = 42;
    const b = a.toString(); // "42"

    +"3.14" // 3.14
  • 一元运算符+可以将日期对象强制转换为数字。返回结果为Unix时间戳。但是一般通过Date.now()来获取当前时间戳,使用new Date().getTime()来获取指定时间时间戳。
    console.log(+new Date())

字符串转换为数字的解析和转换

  • 解析(parseInt, parseFloat)允许字符串中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。
  • 转换(Number)不允许出现非数字字符,否则会失败并返回NaN。
  • 注意:parseInt,parseFloat传入的是字符串,所以我们将其他类型的数据传入后,他们会先被转换为字符串,然后再按照ToPrimitive规则来进行转化,然后再转换为数字。
    console.log(parseInt([1,2,3])) // 1

上面的方法在es5之前会有一个bug,如果不指定第二个参数,那么它将根据第一个参数第一个字符来确定,将要将字符串数字转化为几进制数字。但是在es5之后,第二个参数默认为10。所以在es5之前环境下,我们需要手动指定第二个参数为10。

  • parseInt解析非字符串类型。很可能会出现意想不到的结果。但是都是先将该类型计算后转换为字符串,然后再根据第二个参数来转化。
    console.log(parseInt(1 / 0, 19)) // parseInt("Infinity", 19) => 18
    console.log(parseInt(0.000008)) // 0 ("0" 来自于"0.000008")
    console.log(parseInt(0.0000008)) // 8 ("8" 来自于"8e-7" )
    console.log(parseInt(false, 16)) // 250 ("fa" 来自于 "false")
    console.log(parseInt("0x10")) // 16
    console.log(parseInt("103", 2)) // 2

显示转换为布尔值

  • 通过调用Boolean,并且遵循ToBoolean规则。
  • 或者通过!!

隐式强制类型转换

隐式强制类型转换指的是那些隐蔽的强制类型转换,副作用也不是很明显。

数字转化为字符串

  • +操作符。 如果操作符两边有一个可以转换为字符串,那么就是字符串拼接操作。如果都是数字,则为数字加法操作。
    const a = [1,2];
    const b = [3,4];
    console.log(a + b) // 1,23,4

上面的结果就是数组通过ToPrimitive后转化为字符串,然后再进行字符串拼接。

  • (3) + ""String(3)的差别。前者是通过ToPrimitive规则转换的,后者通过ToString规则转换的。
    const a = {
      valueOf(){
        return 3
      },
      toString() {
        return 5
      }
    }

    console.log(a +''); // "3"
    console.log(String(a)) // "5"

字符串转换为数字

  • 通过-, *, /等等数字运算符,即可。
  • 由于算术运算符两边都是数字,所以需要先将两边操作数转换为数字。如果是对象类型需要通过ToPrimitive规则转换为字符串,然后再转化为数字。 在编辑器中输出
    console.log({} + []) // [object Object]
    console.log([] + {}) // [object Object]

在浏览器控制台中直接输出

    {} + [] // 0
    [] + {} // [object Object]

为什么上面会产生不同的结果呢? [] + {}: 他先将[]转化为"" => 0, {} 转换为[object Object] {} + []: 他将{}看作是一个空代码块会被忽略。所以最后转换为+ [],然后被转换为0。

隐式强制类型转换为布尔值

下面的情况会发生布尔值隐式强制类型转换:

  • if()语句中的条件判断表达式
  • for()语句中的条件判断表达式(第二个)
  • while()和do...while()循环中的条件判断表达式
  • ? :中的条件判断表达式
  • 逻辑运算符 ||, &&。 转换的时候,遵循ToBoolean抽象操作规则。
  • &&, ||运算符的返回值并不一定是布尔值,而是两个操作数其中一个值。
    const a = 42;
    const b = "abc";
    const c = null;
    console.log(a || b); // 42
    console.log(a && b); // abc
    console.log(c || b); // abc
    console.log(c && b); // null

Symbol的强制类型转换

  • 不能被强制类型转换为数字(显示隐式都报错),但是可以强制类型转换为布尔值(显式和隐式都可以)。可以显式强制类型转换为字符串,但是不能隐式强制类型转换为字符串。
    console.log(Symbol("cool") + "") // TypeError: Cannot convert a Symbol value to a string
    console.log(String(Symbol("cool"))) // Symbol(cool)

宽松相等和严格相等

  • 一般对于二者的理解就是:==检查值是否相等,===检查值和类型是否相等。
  • 但是正确的理解应该是:==允许在相等比较中进行强制类型转换,而===不允许。
  • ==在比较两个不同类型的值时会发生隐式强制类型转换,会将其中之一或二者都转换为相同的类型后在进行比较。
  • 字符串和数字做==比较时,会将字符串做ToNumber规则转换。
"2n" == 2 // false  =>  Number("2n") = NaN
"2" == 2 // true  => Number("2") = 2
  • 布尔值和数字做==比较时,会将boolean值做ToNumber规则转换
false == 0   // true  => Number(false) = 0
false == 2   // false
  • 字符串和boolean做==比较时,会将二者都做ToNumber规则转换。
"n" == true     // false
"n" == false    // false
"2" == true     // false
"2" == false    // false
"1" == false    // false
"1" == true     // true

比较时并没有做ToBoolean操作。所以尽量不要使用== true, == false

  • null和undefined两者永远相等,二者和其他任何值做==比较时都为false。
  • 对象和字符串,数字,布尔值做==表示时,会将对象做ToPrimitive规则转换,然后==操作符两边的操作数转换为数字。
    console.log(42 == [42]) // true  42 == "42" => 42 == 42 => true
  • 基本类型数据通过对应的基本包装类型封装后与原基本类型做==操作时,依旧是相等的。但是null, undefined, NaN则不符合。
    const a = "abc";
    const b = Object(a);
    console.log(a == b) // true

    console.log(null == Object(null)) // false
    console.log(undefined == Object(undefined)) //false
    console.log(NaN == Object(NaN)) // false
  • [null], [undefined], [NaN]通过ToPromitive规则转换后的值。
    console.log([undefined].toString()) // ""
    console.log([null].toString()) // ""
    console.log([NaN].toString()) // NaN
  • 安全运用隐式强制类型转换的原则:
    • 如果两边值其中有true, false,千万不要使用==
    • 如果两边的值有[], "", 0,千万不要使用==
    • 以上请使用===注意:使用==来做比较时,两边的操作数最终都转为数字来判断的。即调用Number()。

语句和表达式

  • 语句想等于句子,表达式相当于短语,运算符则相当于标点符号和连接词。
  • 由单个表达式构成的语句被称为"表达式语句"。
  • 每一条语句都有结果值。我们在代码中是没办法获得这个结果值的。在控制台中输入var a = 1;会返回一个undefined。 代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。我们可以通过eval获取一个代码块的返回值。
    var a, b;
    a = eval("if(true) { b = 4 + 10 }");
    console.log(a) // 14

es7有一个do表达式提案。

    a = do {
      if(true) {
        b = 4 + 10;
      }
    }
    console.log(a) // 14

其目的是将语句当做表达式来处理,从而不需要将语句封装为函数再调用return来返回值。现在测试,会报错,应该是没有被采纳。

  • 语句系列逗号运算符:将多个独立的表达式语句串联成一个语句。表示将最后一个表达式的值,赋值给变量。
    var a = 42, b;
    b = (a++, a);
    a // 43
    b // 43

标签语句

    {
        foo: bar()
    }

上面代码就是一个普通的代码块,但是内部是一个标签语句。我们可以在循环中使用continue 标签, break 标签来实现跳转执行。并且标签也能用于非循环代码块,但是只有break才可以。

    function foo() {
      bar: {
        console.log("Hello");
        break bar;
        console.log("never runs");
      }

      console.log("World")
    }

    foo() // Hello World

运算符的优先级

具体请访问mdn文档

  • ,来连接一系列语句的时候,他的优先级最低,其他操作数的优先级都比他高。
    const a = 42, b;
    b = a++, a;
    a // 43
    b // 42
  • && 运算符的优先级高于||
    console.log(true || false && false) // true
  • ||的优先级高于?:
  • 关联:右关联不是指从右往左执行,而是指从右往左组合。但是执行顺序在任何时候都是从左往右。&&, ?:, =都是右关联。
    var a, b, c;
    a = b = c = 42; // (a = (b = (c = 42)))
    
    var d = a && b || c ? c || b ? a : c && b : a;
    // ((a || b) || c) ? ((c || b) ? a : (c && b)) : a;

函数参数

  • arguments.length计算的是实参的个数。
  • 在非严格模式下,向函数传递参数时,arguments数组中的对应单元会和命名参数建立关联以得到相同的值,相反不传递参数就不会建立关联。
    function foo(a) {
      a = 42;
      console.log(arguments[0]);
    }

    foo(2); // 42
    foo() // undefined

但是在严格模式下,传不传递参数都不会建立关联。

    function foo(a) {
    "use strict"
      a = 42;
      console.log(arguments[0]);
    }

    foo(2); // 2
    foo() // undefined

因此,在开发中不要依赖这种关联机制。实际上,他是js语言引擎底层实现的一个抽象泄漏,并不是语言本身的特性。

这种关联机制与es没有关系,只是语言的一种缺陷,只有在严格模式下,才不会被关联。所以我们在使用只需要遵守一个原则:不要同时访问命名参数和其对应的arguments数组单元。

try...finally

  • finally中的return会覆盖try和catch中return的返回值。
    function foo() {
      try {
        return 42;
      } finally {
        // 无返回值,所以没有覆盖
      }
    }
    function bar() {
      try {
        return 42;
      } finally {
        return ;
      }
    }
    function baz() {
      try {
        return 42;
      } finally {
        return "hello";
      }
    }
    console.log(foo()) // 42
    console.log(bar()) // undefined
    console.log(baz()) // hello

switch

  • 参数 和 case 表达式的匹配算法与===相同。 通常case语句中的switch都是简单值,所以并没有问题。
  • 然而,有时可能会需要通过强制类型转换来进行相等比较(即 == ),这是就需要做一些特殊处理。
    const a = "42";
    switch (true) {
      case a == 10:
        console.log("10 or '10'")
        break;
      case a == 42:
        console.log("42 or '42'")
        break;

      default:
        break;
    }
  • default不一定要写在最后面,可以写在中间位置。效果都一样。