关于前端:深入-JavaScript-类型转换

1次阅读

共计 8113 个字符,预计需要花费 21 分钟才能阅读完成。

本文首发于集体博客 clloz.com,欢送拜访。

前言

JavaScript 中的类型转换是一个十分让人头大的内容,其实咱们平时的编码个别会尽量避免让本人陷入不确定的类型转换中。然而很多时候面试会考查这方面的常识,并且搞清楚类型转换的机制可能让咱们在遇到一些奇葩问题的时候晓得产生了什么。咱们不肯定要记住所有的类型转换的可能性,只有记住一些罕用的,以及如何进行查问即可。

装箱拆箱

在探讨具体的类型转换场景之前,咱们先来说一下装箱拆箱操作。在这之前你应该温习一下 JavaScript 中对于数据类型的常识,你能够看我的这一篇文章:JS 数据类型和判断办法。

装箱 wrapper

JavaScript 中目前共有八种数据类型 Undefined, Null, Number, String, Boolean, BigInt, Symbol, Object。除了 Object 其余都是根本数据类型(primitive values,也称原始值,原始类型)。所谓根本数据类型就是它们是一种即非对象也没有属性和办法的数据,根本类型间接代表了最底层的语言实现。

所有根本类型的值都是不可扭转的。但须要留神的是,根本类型自身和一个赋值为根本类型的变量的区别。变量会被赋予一个新值,而原值不能像数组、对象以及函数那样被扭转。即根本类型值能够被替换,但不能被扭转。比方,JavaScript 中对字符串的操作肯定返回了一个新字符串,原始字符串并没有被扭转。

既然如此,为什么咱们还能在 Number 或者 String 上应用办法呢?这就引出了 JavaScript 中的根本包装类型(primitive wrapper types,也成为原始包装类型),因为咱们有在根本类型上频繁操作的需要(比方 String 的截取,Number 的格局转换等),所以 JavaScript 也为根本类型内置了一系列的 API。然而只有对象能力应用办法,所以 JavaScript 就用根本包装类型来让根本类型可能领有属性和办法。除了 nullundefined 之外,所有根本类型都有其对应的包装对象:

  • String 为字符串根本类型。
  • Number 为数值根本类型。
  • BigInt 为大整数根本类型。
  • Boolean 为布尔根本类型。
  • Symbol 为字面量根本类型。

其中最重要的就是 StringNumberBoolean 三种原始包装类型,也是咱们本文重点探讨的内容。

这些类型与其余援用类型类似,但同时也具备与各自的根本类型相应的非凡行为。实际上,每当读取一个根本类型值的时候,后盾就会创立一个对应的根本包装类型的对象,从而让咱们 可能调用一些办法来操作这些数据。看上面的例子:

let str1 = "clloz";
let str2 = str1.substring(2);

下面的代码中咱们创立了一个根本类型的字符串 str1,而后咱们调用了 str1substring 办法,从逻辑上来讲根本类型不应该有办法的。实际上 JavaScript 在背地为咱们创立了一个根本包装类型,大抵过程如下:

let temp = new String("clloz");
let str2 = temp.substring(2);
temp = null;

援用类型与根本包装类型的次要区别就是对象的生存期。应用 new 操作符创立的援用类型的实例,在执行流来到以后作用域之前都始终保留在内存中。而主动创立的根本包装类型的对象,则只存在于一 行代码的执行霎时,而后立刻被销毁。这意味着咱们不能在运行时为根本类型值增加属性和办法。

let str1 = "clloz";
str1.color = "red";
console.log(str1.color);  // undefined

个别状况下,咱们不须要手动进行装箱操作,因为装箱后的根本类型就变成了一个对象,typeof 将返回 object,在转换为 Boolean 的时候也会转换成 true,比方 Boolean(new Boolean(false)) 将返回 true。咱们只须要依据本人的需要来创立根本类型即可,将是否须要装箱的判断交给引擎,一般来说咱们能在代码中优化的内容,引擎肯定会帮咱们进行优化。

最初说一说进行装箱的几种办法,这些办法对除了 nullundefined 的根本类型都无效(nullundefined 没有原生构造函数,因为它们并不需要 API):

  1. new 操作符调用对应类型的构造函数。
  2. 应用 Object 函数,带不带 new 都能够。Object() 构造函数将会依据参数的不同做以下操作:

    • 如果给定值是 nullundefined,将会创立并返回一个空对象
    • 如果传进去的是一个根本类型的值,则会结构其包装类型的对象
    • 如果传进去的是援用类型的值,依然会返回这个值,经他们复制的变量保有和源对象雷同的援用地址
    • 当以非构造函数模式被调用时,Object 的行为等同于 new Object()
  3. 利用 call
let a = 2;
console.log(typeof a) //number
let t = (function(){return this;}).call(a)
console.log(typeof t) //object

拆箱 toPrimitive

装箱的操作是为了让咱们可能应用一些为根本类型内置的 API。但有时咱们也须要对对象进行拆箱操作,比方当咱们进行四则运算,进行比拟等逻辑运算,等等。

JavaScript 规范中,规定了 ToPrimitive 函数,它是对象类型到根本类型的转换(即,拆箱转换)。拆箱转换会尝试调用 valueOftoString 来取得拆箱后的根本类型。如果 valueOftoString 都不存在,或者没有返回根本类型,则会产生类型谬误 TypeErrorString 的拆箱转换会优先调用 toString。在 ES6 之后,还容许对象通过显式指定 @@toPrimitive Symbol 来笼罩原有的行为。

这里为了让大家彻底明确拆箱的机制,咱们间接把 ECMAScript2021 规范中的定义拿过去解读一下:

我次要讲一下 2 中的步骤:

  • a:获取 input@@toPrimitive 办法,input 是一个对象。
  • b:如果 @@toPrimitive 不是 undefined,而后

    • i:如果 @@toPrimitive 办法中没有指定第二个参数,那么 hint 设为 default
    • ii:如果第二个参数是 string,那么 hint 设为 string
    • iii:如果第二个参数是 number,那么 hint 设为 number
    • iv:以 inputhint 为参数调用 @@toPrimitive 办法。
    • v:如果执行后果不是一个对象,那么返回后果。
    • vi:如果执行后果是一个对象,抛出 TypeError
  • c:如果没有定义 @@toPrimitive 办法,并且没有指定 preferredType,那么 preferredType 设为 number
  • d:返回 OrdinaryToPrimitive(input, preferredType)

所以当咱们没有指定 @@toPrimitive 办法的时候,就是执行 OrdinaryToPrimitive(input, preferredType),该函数定义如下:

它承受两个参数 Ohint,也就是咱们下面 d 步骤中的 inputpreferredType。次要步骤如下:

  • hint 必须是 string 或者 number 的一种。
  • 如果 hintstring,就按顺序调用对象的 toStringvalueOf 办法,如果调用后后果不是对象则返回。
  • 如果 hintnumber,就按顺序调用对象的 valueOftoString 办法,如果调用后后果不是对象则返回。

其实逻辑还是比拟清晰,并没有很简单,最初在说一说 toPrimitive 中的 b 状况。@@toPrimitive 办法就是让咱们自定义拆箱的规定,而不是依据规范的规定进行,咱们能够依据本人的需要定制拆箱的规定。@@ 结尾的名字是规范中的 Well-Known Symbols,他们是内置的 Symbol,作为属性的 key。在 ES2016 引入 Symbol 后咱们曾经能够拜访这些 Symbol,比方 @@match@@matchAll 等等,咱们在编码中能够间接应用 String.prototype.matchString.prototype.matchAll 来调用,他们在引擎外部即调用的 Symbol 对应的办法。@@ 是在规范文档中的名字,咱们在 JavaScript 编码中应用的名字是将 @@ 替换为 Symbol.,所以咱们给对象增加 @@toPrimitive 属性就是增加一个 Symbol.toPrimitive 属性。当引擎调用 @@toPrimitive 的时候就会找到咱们定义的办法。

// 一个没有提供 Symbol.toPrimitive 属性的对象,参加运算时的输入后果
var obj1 = {};
console.log(+obj1);     // NaN
console.log(`${obj1}`); // "[object Object]"
console.log(obj1 + ""); //"[object Object]"

// 接上面申明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输入后果
var obj2 = {[Symbol.toPrimitive](hint) {if (hint == "number") {return 10;}
        if (hint == "string") {return "hello";}
        return true;
    }
};
console.log(+obj2);     // 10      -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); //"true"-- hint 参数值是"default"

内置 Symbol 参考 Symbol – MDN

类型转换

当初咱们曾经晓得装箱和拆箱的规定,也就是把握了类型转换的工具,剩下的只有搞清楚哪个场景用哪个工具进行转换即可。

显式强制类型转换

在探讨隐式强制类型转换之前,咱们先讨论一下显式强制类型转换。

所谓 显式强制类型转换 指的就是咱们间接调用 Number()String()Boolean() 构造函数(不带 new)对一个值进行类型转换。咱们还是来解读规范文档。

留神一点,规范文档中的蓝色的办法后面的 ! 不是取反的意思,你能够忽视掉,就当做执行前面的办法就能够。

Number

下面的截图就是对 Number 构造函数的定义,内容很简略:

  • 如果不是用 new 调用的,则返回 ToNumeric(value) 的值,value 是咱们传入的值,如果没有传入 value,那么就返回 +0
  • 如果是用 new 调用,则生成根本包装类型对象。

ToNumeric 的定义如下:

表格非常清晰,我就不解读了。表格中没有说的是 StringStringNumber 在规范中定义了十分长的内容,我集体了解就是不合乎 JavaScript 格局的 string 返回 NaN,其余返回对应的数字。所谓的合乎格局就包含 0o0 结尾的八进制,0x 结尾的十六进制,0b 结尾的二进制,迷信计数法等。

String

还是从规范解读:

  • 如果不是 new 调用 String 构造函数,返回 ToString(value)
  • 如果是以 new 调用 String 构造函数,返回根本包装类型对象。

ToStrong 定义如下:

这当中 Number::String 在规范中定义比较复杂,应该是进行了严格的数学定义,咱们按咱们失常的了解就能够了。-0,-0 都是 0NaN 返回 "NaN"

Boolean

  • 如果不是 new 调用 Boolean 构造函数,返回 ToBoolean(value)
  • 如果是以 new 调用 Boolean 构造函数,返回根本包装类型对象。

隐式强制类型转换

隐式强制类型转换可能是更让人头疼的一部分,其实只有搞清楚规范,隐式的转换也是用的咱们下面看到的那些办法进行转换的,咱们也不用记分明每一个规定,只有晓得到哪里去查,还有编码中防止一些会出问题的转换。我这里就找出一些咱们比拟常见的隐式转换的场景对规范进行解读。

算数运算符

在规范中所有的算数运算符最初都是由上面这个办法执行的 lval 即操作符右边的值,opText 即操作符,rval 即操作符左边的值:

  • 如果操作符是 +

    • 计算 ToPrimitive(lval) 赋值给 lprim
    • 计算 ToPrimitive(rval) 赋值给 rprim
    • 如果 lprimrprim 中有一个类型是 String

      • 计算 ToString(lprim) 赋值给 lstr
      • 计算 ToString(rprim) 赋值给 rstr
      • 拼接 lstrrstr 并返回
    • lprim 赋值给 lval
    • rprim 赋值给 rval
  • 计算 ToNumeric(lval),赋值给 lnum
  • 计算 ToNumeric(rval),赋值给 rnum
  • 如果 Type(lnum)Type(rnum) 不同,抛出一个 TypeError
  • 进行算数运算

咱们能够看到这段定义中的办法都是咱们在下面显示转换中介绍过的办法。在算数操作符中 ToPrimitive() 是并没有传入 hint 的,所以就用默认 number,所以在算术运算的类型转换中,总是先调用 valueOf,后调用 toString()

一元操作符

一元操作符的定义都非常简单,这里就不贴图了,间接给一个总结,如果你想看相干定义点击 ECMAScript 2021 – Unary Operators

  • ++ -> ToNumeric
  • -- -> ToNumeric
  • + -> ToNumeric
  • - -> ToNumeric
  • ~ -> ToNumeric
  • ! -> ToBoolean

这里再给大家举个例子 'a' + + 'a'(留神两个加号不能相连)失去的后果是 aNaN,因为第二个 + 作为一元操作符,调用 ToNumber() 最初的后果是 NaN。而后执行 'a' + NaN,就是回到算术运算符的定义,有一个是 String 两个都转成 String 而后返回拼接的字符串,所以最初的后果是 aNaN。你也能够找一些例子进行验证。

关系运算符

所有的关系运算符(<, >, <=, >=)的后果都是依据 Abstract Relational Comparison 的返回值计算,所以咱们先着重剖析这个办法,看下图。因为在规范中对立用小于号,所以用 leftFirst 示意是大于操作符还是小于操作符,true 则为小于关系符,false 则为大于关系符。

咱们能够看到第一部就是进行拆箱操作,hintnumber,也就是先调用 valueOf,在调用 toString

当两个操作数 operand 都是字符串的时候,会调用一个办法 IsStringPrefix(a, b) 来计算结果。这个办法的意思就是:比方判断 a<b 的后果,就是判断 a 是不是 b 的一个前缀,就是 a 加上另一个字符串能形成 b,如果能,则返回 true;如果 ba 的前缀,则返回 false,所以 'cl' < 'clloz' 会返回 true。如果不存在前缀关系,则进行 code unit 比拟,在 JavaScript 中是 UTF-16 编码,从最低位开始进行码点比拟雷同则进入下一位,如果能找到一位是 a 的码点小于 b 则返回 true 否则返回 false。个别的字符串咱们只有依据扩大 ASCII 进行比拟即可。

console.log('cllob' < 'clloc') //true
console.log('cllob' < 'clloa') //false
console.log('clloba' < 'cllob') /false

bigInt 咱们就跳过,因为使用不是很多。咱们间接进入上面的 ToNumeric,将两个操作数都进行 ToNumeric,如果失去的后果类型雷同,则调用对应类型的 T::lessThan. ToNumeric 的后果要么是 Number 要么是 BigInt,要么抛错,所以咱们只有看 Number::lessThan(x, y) 的定义即可:

  • 如果 xNaN,返回 undefined
  • 如果 yNaN,返回 undefined
  • 如果 xy 是雷同的数值,返回 false
  • 如果 xy 一个是 +0 一个是 -0 返回 false
  • 如果 xy 任意一个为 $\pm \infty$,返回 false
  • 其余状况进行数值比拟(非零并且不是无穷),x < y 返回 true,否则返回 false

留神,失去的 Abstract Relational Comparison 的返回值不是最终的后果 。对于 <, > 来说,如果 Abstract Relational Comparison 返回值是 undefined,则则返回 false,否则间接返回 Abstract Relational Comparison 的返回值。对于 <=, >=,如果 Abstract Relational Comparison 的返回值是 trueundefined,则返回 false,否则返回 true

这里可能有同学纳闷 <=>= 的逻辑是不是错了,Abstract Relational Comparison 的返回值是 true 应该返回 true,这里规范外面是将 <=, >=leftFirst 绝对于 <, > 去了一个相同,这样能放弃 lessThan 中的逻辑最简略,即 <, >false 的时候 <=, >=true<, >true 的时候 <=, >=false。否则因为有第三条相等规定在,逻辑会比较复杂。具体的定义看 Relation-Operators -ECMAScript

上面来几个例子:

console.log(null < -0) //false null 被转为 +0,和 -0 进行 lessThan 返回 false,所以最终后果为 false

console.log(NaN < 10) //false 只有有 NaN,lessThan 的后果就是 undefined,对于 < 和 > 来说 undefined 最初返回 false

console.log(NaN <= 10) //false 对于 <= 和 >= 来说,undefined 就是 false
相等操作符

相等操作符有四个 ==, !=, ===, !==,定义在 Equality-Operators – ECMAScript,其中最要害的就是两个办法:Abstract Equality ComparisonStrict Equality Comparison,前者是双等号的办法,后者是全等号的办法。定义间下图:

咱们能够看到两个办法的定义长度齐全不同 。双等号能够算作是 JavaScript 中的一个设计失误,十分不倡议应用。这里我就说一说全等好的定义。

  • 如果两个操作数的类型不同,返回 false
  • 如果两个操作数的类型都是 Number,调用 Number::equal(x, y) 办法,返回办法的返回值(BigInt 不探讨)。该办法定义在 Number::equal – ECMAScript

    • 两个操作数有一个是 NaN,返回 false
    • 两个操作数是同一个数值,返回 true
    • 两个操作数别离是 +0-0,返回 true
    • 下面的条件都不满足,返回 false
  • 如果类型不是 NumberBigInt,则返回 SameValueNonNumeric(x, y) 的返回值。

    • 断言:两者不是 NumberBigInt
    • 断言:两者类型雷同
    • 如果 x 类型是 Undefined,返回 true
    • 如果 x 类型是 Null,返回 true
    • 如果 x 类型是 String,则必须 xy 的所有码点序列完全一致才返回 true,否则返回 false
    • 如果 x 类型是 Boolean,则必须 xy 同为 truefalse 才返回 true,否则返回 false
    • 如果 x 类型是 Symbol,则必须 xy 是同一个 Symbol 才返回 true,否则返回 false
    • 如果 xy 是同一个对象,返回 true,否则返回 false

相等操作符到这里就讲完了,双等号我没有认真看,因为我从来不用,也不倡议大家应用。如果你有趣味能够仔细阅读一下图片中的规范定义。

总结

这篇文章应该将双等号以外的绝大多数类型转换的状况都说分明了,而且是依据规范来讲的,还是比拟清晰的。其实整个思路理下来也不是十分的简单,所以有时候就是 Just Do It! 。心愿这篇文章给你带来帮忙,如果有谬误的中央,欢送斧正。

正文完
 0