0
点赞
收藏
分享

微信扫一扫

Type Conversion详解

在 JavaScript 的世界里,数据类型之间的转换无处不在。即使你没有主动显式地去转换,但 JavaScript 在私底下“偷偷地”帮我们做了很多类型转换的工作。那么,它们究竟是按照什么规则去转换的呢?我们试图在本文中找到答案,来解开谜底。

ECMAScript 是负责制定标准的,而 JavaScript 则是前者的一种实现。

在 ECMAScript 标准中定义一组转换抽象操作,常见的抽象操作(Abstract Operation)有 ToPrimitiveToBooleanToNumberToStringToObject 等等。

一、ToPrimitive

1. ToPrimitive

在 ECMAScript 标准中,使用 ToPrimitive 抽象操作将引用类型转换为原始类型。(详情看 #sec-7.1.1

参数 input 是文章开头提到的 8 种数据类型的值(Undefined、Null、Boolean、String、Symbol、Number、BigInt、Object)。参数 PreferredType 是可选的,表示要转换到的原始值的预期类型,取值只能是字符串 "default"(默认)、"string""number" 之一。

ToPrimitive 操作,可概括如下:

  1. input 是 ECMAScript 语言类型的值。
  2. 如果 input 是引用类型,那么
    1. 如果没有传递 PreferredType 参数,使 hint 等于 "default"
    2. 如果参数 PreferredType 提示 String 类型,使 hint 等于 "string"
    3. 否则,参数 PreferredType 提示 Number 类型,使 hint 等于 "number"
    4. 使 exoticToPrim 等于 GetMethod(input, @@toPrimitive) 的结果(大致意思是,获取 input 对象的 @@toPrimitive 属性值,并将其赋给 exoticToPrim)。
    5. 如果 exoticToPrim 不等于 undefined(即 input 对象含 @@toPrimitive 属性),那么
      1. 使 result 等于 Call(exoticToPrim, input, « hint ») 的结果(大致意思是,执行 @@toPrimitive 方法,即 exoticToPrim(hint))。
      2. 如果 result 不是引用类型,则返回 result
      3. 否则抛出 TypeError 类型错误。
    6. 如果 hint"default",则将 hint 设为 "number"
    7. 返回 OrdinaryToPrimitive(input, hint)
  3. 返回 input(即原始类型的值直接返回)。

用口水话再总结一下,如下(哈哈):

  1. 如果 input 是原始类型,直接返回 input(不做转换操作)。
  2. 如果参数 PreferredType 是 String(Number)类型,那么使得 hint 等于 "string""number"),否则 hint 等于默认的 "default"
  3. 如果 input 中存在 @@toPrimitive 属性(方法),若 @@toPrimitive 方法的返回值为原始类型,则 ToPrimitive 的操作结果就是该返回值,否则抛出 TypeError 类型错误。
  4. 如果经过以上步骤之后 hint"default",则使 hint 等于 "number"
  5. 返回 OrdinaryToPrimitive(input, hint) 操作的结果。

那么 OrdinaryToPrimitive 的操作是怎样的呢?我们接着往下看...

2. OrdinaryToPrimitive

详情看:#sec-7.1.11

参数 O 为引用类型。参数 hint 为 String 类型,其值只能是字符串 "string""number" 之一。

(官话)OrdinaryToPrimitive 操作,可概括如下:

  1. O 是引用类型。
  2. hint 是 String 类型,且 hint 的值只能是 "string""number" 之一。
  3. 如果 hint"string",使 methodNames 等于 « "toString", "valueOf" »(其中 «» 表示规范中的 List,类似于数组)。
  4. 如果 hint"number",使 methodNames 等于 « "valueOf", "toString" »
  5. 遍历 methodNames,使 name 等于每个迭代值,并执行:
    1. 使 method 等于 Get(O, name)(即获取对象 Oname 属性,相当于获取对象的 toStringvalueOf 属性,具体执行顺序视 hint 而定)。
    2. 如果 IsCallable(method) 结果为 true,那么:
      1. 使 result 等于 Call(method, O) 结果(即调用 method() 方法)。
      2. 如果 result 为原始类型,则返回 result
  6. 抛出 TypeError 类型错误。

(口水话)再总结一下:

  1. 当经过 ToPrimitive 操作,然后执行 OrdinaryToPrimitive(input, hint) 操作,那么步骤如下:
  2. 如果 hint"string",它会先调用 input.toString() 方法,
    1. toString() 结果为原始类型,则直接返回该结果。
    2. 否则,继续调用 input.valueOf() 方法,若结果为原始类型,则返回该结果,否则抛出 TypeError 类型错误。
  3. hint"number",它先调用 input.valueOf() 方法,
    1. valueOf() 结果为原始类型,则直接返回该结果。
    2. 否则,继续调用 input.toString() 方法,若结果为原始类型,则返回该结果,否则抛出 TypeError 类型错误。
3. 一些示例
  • -*/% 这四种操作符都会把符号两边的操作数先转换为数字再进行运算。
  • + 的作用可以是数值求和,也可以是字符串拼接。
    • 若符号两边操作数都是数字,则进行数字运算。
    • 若符号一边是字符串,则会把另一端转换为字符串进行拼接操作。
// 运算符: x + y

// Number + Number -> 数字相加
1 + 2 // 3

// Boolean + Number -> 数字相加
true + 1 // 2

// Boolean + Boolean -> 数字相加
false + false // 0

// Number + String -> 字符串连接
5 + 'foo' // "5foo"

// String + Boolean -> 字符串连接
'foo' + false // "foofalse"

// String + String -> 字符串连接
'foo' + 'bar' // "foobar"
const obj = {
  [Symbol.toPrimitive]: hint => {
    if (hint === 'number') {
      return 1
    } else if (hint === 'string') {
      return 'string'
    } else {
      return 'default'
    }
  }
}

+obj          // 1              hint is "number"
`${obj}`      // "string"       hint is "string"
obj + ''      // "default"      hint is "default"
obj + 1       // "default1"     hint is "default"
Number(obj)   // 1              hint is "number"
String(obj)   // "string"       hint is "string"

二、ToBoolean

将一个操作数转换为布尔值,这应该是最简单的了。(详情看 #sec-7.1.2

所以总结下来就是:

操作数 结果
undefinednullfalse+0-0NaN''0n false
除以上这些(falsy)值之外 true

在 JavaScript 中,如果一个操作数 argument 通过 ToBoolean(argument) 操作后被转换为 true,那么这些操作数称为真值(truthy),否则为虚值(falsy)。

// 转换为 Boolean 值的两种方式
!!x
Boolean(x)

三、ToNumber

将一个操作数转换为数字值。(详情看 #sec-7.1.4

参数类型 结果
Undefined NaN
Null +0
Boolean true 转换为 1false 转换为 +0
Number 直接返回,不做类型转换。
String 1. 纯数字的字符串转换为相应的数字;
2. 空字符串 '' 转为 +0
3. 否则为 NaN

其中 0x 开头的字符串被当成 16 进制。
Symbol 无法转换,抛出 TypeError 错误。
BigInt 无法转换,抛出 TypeError 错误。
Object 两个步骤:

1. 将引用类型转化为原始值 ToPrimitive(argument, 'number')
2. 转化为原始值后,进行 ToNumber(primValue) 操作,即按上面的类型转换。
Number(undefined) // NaN
Number(null) // 0

'\n  123  \t' == 123 // true

+'string' // NaN
+true // 1
+[] // 0
+{} // NaN

四、ToString

将一个操作数转换为字符串类型的值。(详情看 #sec-7.1.17

参数类型 结果
Undefined undefined
Null null
Boolean true 转换为 "true"false 转换为 "false"
Number 1. NaN 转换为 "NaN"
2. +0-0 转换为 "0"
3. 其中 Infinity-Infinity 分别转换为 "Infinity""-Infinity"
4. 若 x 是小于 0 的负数,则返回 "-x";若 x 是大于 0 的正数,则返回 "x"
5. 其他不常用的数值,请看 #sec-6.1.6.1.20
String 直接返回,不做类型转换。
Symbol 无法转换,抛出 TypeError 错误。
BigInt 10n 转换为 "10"
Object 两个步骤:

1. 将引用类型转化为原始值 ToPrimitive(argument, 'string')
2. 转化为原始值后,进行 ToString(primValue) 操作,即按上面的类型转换。
// 下面会导致 Symbol('foo') 进行隐式转换,即 ToString(Symbol),按以上规则,是会抛出异常的
console.log(Symbol('foo') + 'bar' ) // TypeError: Cannot convert a Symbol value to a string

// Symbol('foo') 结果是 Symbol 的原始值,再调用其包装对象的属性时,会自动转化为包装对象再调用其 toString() 方法
console.log(Symbol('foo').toString() + 'bar' ) // "Symbol(foo)bar"

抛一个有趣的问题:

// 运行出错
var name = Symbol() // TypeError: Cannot convert a Symbol value to a string

// 正常运行,不会抛出错误
let name = Symbol()

// 为什么呢 ❓❓❓

五、ToObject

将一个操作数转换为引用类型的值。(详情看 #sec-7.1.18

参数类型 结果
Undefined 无法转换,抛出 TypeError 错误。
Null 无法转换,抛出 TypeError 错误。
Boolean 返回 new Boolean(argument)
Number 返回 new Number(argument)
String 返回 new String(argument)
Symbol 返回 Object(Symbol(argument))
BigInt 返回 Object(BigInt(argument))
Object 直接返回,不做类型转换。
// 错误示例
const sym = new Symbol() // TypeError: Symbol is not a constructor

// 正确示例
const sym = Symbol()
console.log(typeof sym) // "symbol"
const symObj = Object(sym)
console.log(typeof symObj) // "object"

// BigInt 同理

六、参考

举报

相关推荐

0 条评论