Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript 类型转换不完全记录 #13

Open
afishhhhh opened this issue Jan 15, 2021 · 0 comments
Open

JavaScript 类型转换不完全记录 #13

afishhhhh opened this issue Jan 15, 2021 · 0 comments
Labels

Comments

@afishhhhh
Copy link
Owner

这篇文章主要记录从对象转换为 Primitive Value 的过程,至于基本类型之间的转换,可以看看其他的文章,或者直接去看规范。
首先,我们需要知道,下文中会出现的一些函数,比如 ToObjectToStringToNumber 以及 ToPrimitive 等等,都是在规范中实现的函数,我们没有办法直接通过 JavaScript 访问到。

valueOf

valueOf() 方法返回指定对象的原始值。

来自MDN

一般情况下,所有的对象都可访问到 valueOf 方法,比如 Array,虽然自身没有 valueOf 方法,但是可以根据原型链从 Object.prototype 中找到。

Object 原型上的 valueOf 方法会调用规范中的 ToObject(argument) 函数。顾名思义,这个函数会返回一个对象。如果 argument 本身就是对象,就返回自身;如果是 nullundefined 则抛错;像 booleanstringnumber 以及 symbol 这样的基本类型,就会返回对应的包装对象。相反,如果这些包装类型调用 valueOf 方法,则会返回对应的基本类型值,并不是返回自身,因为这些包装类型都实现了自己的 valueOf 方法。

我们可以将 ToObject() 大致类比为 Object(),只不过后者在处理 nullundefined 的时候会返回 {}

toString

toString() 方法返回一个表示该对象的字符串。

来自MDN

提一下 Number.prototype.toString 以及 Array.prototype.toString 吧。

  • Number.prototype.toString 接受一个参数 radix,可以是二进制,八进制等等,当然默认是十进制。

  • Array.prototype.toString 内部调用的是 join 方法,join 的实现在这里

ToPrimitive(input[, preferredType])

The abstract operation ToPrimitive converts its input argument to a non-Object type.

来自规范

简单讲 ToPrimitive 会将一个值转换为 Primitive Value。在梳理 ToPrimitive 的执行过程前,我们先了解一个内置的 Symbol

  • Symbol.toPrimitive

    这个值可以作为对象的属性名,指向一个方法,用于控制对象如何转换为 Primitive Value。这个方法会在 ToPrimitive 的执行过程中使用到。并不是所有内置对象都有这个属性的,只有 Date.prototype 以及 Symbol.prototype 存在这个属性。虽然普通对象上没有这个属性,但我们是可以手动添加上这个属性,比如:

    var o = {
      [Symbol.toPrimitive]: function(hint) {}
    }

下面就梳理下 ToPrimitive 的执行过程:

  1. 如果 input 的类型是 object

    a. 声明一个变量 hint,如果 preferredType 不存在,将 hint 赋值为 default

    b. 如果 preferredType 为 stringnumber,将 hint 赋值为 stringnumber

    c. 判断 input 是否存在 Symbol.toPrimitive 属性,如果存在则调用该属性指向的方法

    • 如果返回值是 Primitive Value,则返回该值;否则抛错

    d. 不存在该方法,当 hintdefault 是,重新赋值为 number

    e. 如果 hintstring,则按顺序调用 input 的 toString 以及 valueOf 方法,直到返回 Primitive Value

    f. 如果 hintnumber,则按顺序调用 input 的 valueOf 以及 toString 方法,直到返回 Primitive Value

    g. 如果最终没有返回 Primitive Value,则抛错

  2. input 本身就是一个 Primitive Value,直接返回 input

其中步骤 e-f 对应的是规范中的 OrdinaryToPrimitive 函数

ToNumber(argument)

当尝试把一个对象转换为数字时,会有以下两个步骤:

  1. 调用 ToPrimitive(input, Number),返回值为 primValue

  2. 调用 ToNumber(primValue)

比如我们会用到的 Number(value),就会使用到 ToNumber 这个内部函数(前提是你传了一个参数,不然的话就直接返回 0 了)。另外,一元 + 运算也相当于 Number(value)

举个例子:Number({}) // NaN

  1. {} 作为 argument,调用 ToNumber(argument)

  2. {} 作为 input,调用 ToPrimitive(input, Number)hint 被赋值为 number

  3. 因为 {} 不存在 Symbol.toPrimitive 属性,所以按顺序调用 valueOf 以及 toString

  4. 调用 valueOf 返回自身,不是 Primitive Value

  5. 调用 toString 返回 [object Object]

  6. [object Object] 返回,作为 primValue

  7. 调用 ToNumber(primValue) 返回 NaN

简单讲:一般情况下,尝试将对象转换为数字时,会调用 valueOf 以及 toString,直到返回 Primitive Value

ToString(argument)

ToNumber 类似,当尝试把对象转换为字符串时,也会有两个步骤:

  1. 调用 ToPrimitive(input, String),返回值为 primValue

  2. 调用 ToString(primValue)

举个例子:String({}) // [object Object]

  1. {} 作为 argument,调用 ToString(argument)

  2. {} 作为 input,调用 ToPrimitive(input, String)hint 被赋值为 string

  3. 因为 {} 不存在 Symbol.toPrimitive 属性,所以按顺序调用 toString 以及 valueOf

  4. 调用 toString 返回 [object Object],是一个 Primitive Value

  5. [object Object] 返回,作为 primValue

  6. 调用 ToString(primValue) 返回 [object Object]

简单讲:一般情况下,尝试将对象转换为字符串时,会调用 toString 以及 valueOf,直至返回 Primitive Value

二元 + 运算

我们知道 + 不仅能进行数学加法,又可以连接字符串。不仅 1 + '1' 可以执行,甚至像 null + 1[] + {} 等等运算都可以执行。我们可以根据规范梳理下二元 + 的运算过程,在这个过程中也用到了 ToPrimitive 函数:

  1. 对两个操作数调用 ToPrimitive(input),此时没有指定 preferredType,hint 会被赋值为 default

  2. 判断两个返回值的类型,如果其中有一个为 string

    a. 对两个返回值执行 ToString

    b. 进行字符串连接

  3. 对两个返回值执行 ToNumber,执行数学加法

[] + {} 为例,分析一下:

  1. 执行 ToPrimitive([]),根据规则,会调用 [].toString(),返回值为 ''

  2. 执行 ToPrimitive({}),同样会调用 ({}).toString(),返回值为 [object Object]

  3. 进行字符串连接得到 [object Object]

那么,如果手动改变了 valueOf 或者 toString 的行为呢,比如:

var o1 = {
  valueOf: function () {
    return 1
  }
}
var o2 = {
  toString: function () {
    return 2
  }
}

当执行 o1 + o2 时,最终就是执行数学加法,结果为 3。

== 运算

根据规范,x == y 运算的执行过程如下:

  1. 如果 x 与 y 的类型相同,执行 x === y=== 执行步骤

  2. 如果 x 与 y 中,其中一个为 undefined 另一个为 null,则返回 true

  3. 如果 x 与 y 中,其中一个为 string 另一个为 number,则返回 ToNumber(one) == another 的结果

  4. 如果 x 与 y 中,存在一个 boolean,则返回 ToNumber(one) == another 的结果

  5. 如果 x 与 y 中,其中一个为 object,另一个为任一 stringnumber 或者 symbol,则返回 ToPrimitive(one) == another 的结果

  6. 以上情况之外,返回 false

我们以 [] == ![] 为例来分析这个过程:

  1. ![] 会调用 ToBoolean([]) 并取反,得到结果 false

  2. 比较 [] == false,根据上文步骤 4,得到 [] == 0

  3. 根据上文步骤 5,得到 '' == 0

  4. 根据上文步骤 3,得到 0 == 0

  5. 返回 true

再看 Symbol.toPrimitive

上文提到,在一般情况下,当对象转字符串或者数字时,会调用 valueOf 以及 toString。那么,除一般情况以外会是怎么样呢?

回到 Symbol.toPrimitive 属性,我们现在手动为普通对象添加这个属性(上文说过,除了 Date.prototypeSymbol.prototype,其他对象都没有这个属性)。

var o = {
  [Symbol.toPrimitive]: function (hint) {
    switch (hint) {
      case 'number':
        return 1
      case 'string':
        return 'str'
      case 'default':
        return 'default'
      default:
        throw new Error()
    }
  }
}
Number(o) // 1
String(o) // 'str'
o + 1     // 'default1'

如果理解了上文的 ToPrimitive 函数,就可以知道:如果一个对象存在 Symbol.toPrimitive 属性,那么 valueOf 以及 toString 方法都不会被调用了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant