神秘情况:
console.log(a==1 && a==2) // true
??? 为何一个变量可以同时等于多个值呢?我们来看看前置知识
阅读本文您将收获
- 对象类型运算时的隐式转换规则
- ECMAScript 规范角度理解 对象类型 不同情况的转换规则
- ECMAScript 规范中 ToPrimitive、OrdinaryToPrimitive 、@@toPrimitive 函数的工作原理和场景
- 对象的 toString、valueOf、Symbol.toPrimitive 方法作用和原理
ToPrimitive
在前面的 《让人头疼的隐式转换?必然也有迹可循》 中已经介绍过 ToPrimitive 方法了。 它用于将一个复杂数据类型转化为基本数据类型。
我们来详细看一下 ECMAScript 对该方法的定义
ToPrimitive ( input [ , PreferredType ] )
The abstract operation ToPrimitive takes an input argument and an optional argument PreferredType. The abstract operation ToPrimitive converts its input argument to a non-Object type. If an object is capable of converting to more than one primitive type, it may use the optional hint PreferredType to favour that type. Conversion occurs according to the following algorithm:
抽象操作 ToPrimitive 接受一个 input 参数和一个可选参数 PreferredType。抽象操作 ToPrimitive 将其 input 参数转换为非对象类型。如果一个对象能够转换为多个基元类型,它可以使用可选的提示PreferredType 来支持该类型。根据以下算法进行转换:
Assert: input is an ECMAScript language value.
If Type (input) is Object, then
If PreferredType is not present, let hint be "default".
Else if PreferredType is hint String, let hint be "string".
Else,
Assert: PreferredType is hint Number.
Let hint be "number".
Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
If exoticToPrim is not undefined, then
Let result be ? Call(exoticToPrim, input, « hint »).
If Type(result) is not Object, return result.
Throw a TypeError exception.
If hint is "default", set hint to "number".
Return ? OrdinaryToPrimitive(input, hint).
Return input.
该方法的定义比较简单, 接收一个需要被转换的对象 input 参数和一个用于描述转换目标类型的可选参数 PreferredType 。 其中 PreferredType 是一个类型, 可选值有 String 和 Number
原始值: 原始类型的值,原始类型包括 null、undefined、number、string、boolean、BigInt 和 Symbol
-
如果 input 不是原始值
-
如果不传递 PreferredType, 则 hint 为 "default"
-
否则,如果 PreferredType 为 String, 则 hint 为 "string".
-
否则, hint 为 "number".
-
如果定义了 input[@@toPrimitive] 方法, 则
- 调用 input[@@toPrimitive](hint) ;如果返回一个原始值,则 JavaScript 将其返回。1. 否则,JavaScript 抛出一个 TypeError 异常。
-
hint 是 "default", 将 hint 设置为 "number".
-
返回 OrdinaryToPrimitive(input, hint)
-
-
否则,直接返回 input, 不做任何转换。
后面我们会介绍 @@toPrimitive 和 OrdinaryToPrimitive 方法。 如果其返回值是一个原始值则将其返回,否则抛出一个 TypeError 异常
OrdinaryToPrimitive
OrdinaryToPrimitive ( O, hint )
When the abstract operation OrdinaryToPrimitive is called with arguments O and hint, the following steps are taken:
Assert: Type(O) is Object.
Assert: Type(hint) is String and its value is either "string" or "number".
If hint is "string", then
a、Let methodNames be « "toString", "valueOf" ».
Else,
a、Let methodNames be « "valueOf", "toString" ».
For each name in methodNames in List order, do
a、Let method be ? Get(O, name).
b、If IsCallable(method) is true, then i、Let result be ? Call(method, O). ii、If Type(result) is not Object, return result.
Throw a TypeError exception.
OrdinaryToPrimitive 方法的作用也特别简单, 接收 被转化的对象 O 和 期望转化后数据类型 hint ,hint 是一个字符串, 可选值为 "number" 或者 "string"
-
如果 hint 为 "string"
- 调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,JavaScript 抛出一个 TypeError 异常。
-
否则
- 调用 toString 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
- 否则,JavaScript 抛出一个 TypeError 异常。
@@toPrimitive(hint) 和 Symbol.toPrimitive(hint)
Symbol.prototype [ @@toPrimitive ] ( hint )
This function is called by ECMAScript language operators to convert a Symbol object to a primitive value. The allowed values for hint are "default", "number", and "string".
When the
@@toPrimitivemethod is called with argument hint, the following steps are taken:Return ? thisSymbolValue(this value).
The value of the "name" property of this function is "[Symbol.toPrimitive]".
This property has the attributes { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }.
@@toPrimitive 方法在上面 toPrimitive 方法的介绍中已经提到过
当 toPrimitive 方法中 input[@@toPrimitive] 方法被定义时, 就会调用 input[@@toPrimitive](hint) 作为返回值。默认的 @@toPrimitive 实现效果与 OrdinaryToPrimitive(hint) 一致
只是前者 hint 参数值可能为 "default" | "string" | "number",后者 hint 参数值可能为 "string" | "number"
所以对其默认实现效果类似下面的代码
Object.prototype['@@toPrimitive'] = function(hint){
if(hint === 'default') hint = 'number'
return OrdinaryToPrimitive(this, hint)
}
但是 @@toPrimitive 方法的实际实现方法名为 Symbol.toPrimitive。所以上面的代码就变成了这样
Object.prototype[Symbol.toPrimitive] = function(hint){
if(hint === 'default') hint = 'number'
return OrdinaryToPrimitive(this, hint)
}
你可以尝试修改一个对象的 Symbol.toPrimitive 方法来覆盖其默认实现
let obj = {
[Symbol.toPrimitive](hint){
console.log(hint)
return 1
}
}
console.log(Number(obj));
// number 当Number(obj) obj参数为一个对象时, 返回 obj[Symbol.toPrimitive]('number')
// 1
Object 类型的转换 规则
在前面的 《让人头疼的隐式转换?必然也有迹可循》 文章中已经介绍过 Object 类型的转换规则了。
一元运算符+
+obj 的效果相当于执行 Number(obj), 其结果和执行原理前面已经解释过了
二元运算符+
当计算 obj1 + obj2 时:
- lprim = ToPrimitive(obj1)
- rprim = ToPrimitive(obj2)
- 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
- 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果
比较运算符==
我们在 《让人头疼的隐式转换?必然也有迹可循》 文章中已经介绍过 == 运算符的类型转换规则
当执行x == y 时:
- 如果x与y是同一类型:
- x是Undefined,返回true
- x是Null,返回true
- x是数字:
- x是NaN,返回false
- y是NaN,返回false
- x与y相等,返回true
- x是+0,y是-0,返回true
- x是-0,y是+0,返回true
- 返回false
- x是字符串,完全相等返回true,否则返回false
- x是布尔值,x和y都是true或者false,返回true,否则返回false
- x和y指向同一个对象,返回true,否则返回false
- x是null并且y是undefined,返回true
- x是undefined并且y是null,返回true
- x是数字,y是字符串,判断x == ToNumber(y)
- x是字符串,y是数字,判断ToNumber(x) == y
- x是布尔值,判断ToNumber(x) == y
- y是布尔值,判断x ==ToNumber(y)
- x是字符串或者数字,y是对象,判断x == ToPrimitive(y)
- x是对象,y是字符串或者数字,判断ToPrimitive(x) == y
- 返回false
toString 和 valueOf
不推荐在 toString 或者 valueOf 中返回非原始值, 因为我们通过 OrdinaryToPrimitive 的实现得知这样做可能会导致 抛出 TypeError
推荐 toString 中 返回 Stirng 类型的值, valueOf 中 返回 Number 类型的值,这样更符合其语义。
在上面 OrdinaryToPrimitive 方法中提到过 toString 和 valueOf 方法,当一个对象的 OrdinaryToPrimitive 方法被调用时必然会 执行其中一个或者多个方法。
当 ToPrimitive(obj) 被执行时, 会根据 PreferredType 参数的不同执行不同的逻辑, 但是最终都会执行 OrdinaryToPrimitive 方法作为返回结果。
结论: 当一个 对象执行 ToPrimitive(obj) 时, 如果不覆盖其 @@toPrimitive 方法, 必然会执行其toString 或者 valueOf 方法中的一个或者多个。否则 必然执行其 @@toPrimitive 方法。
进入正题
有了上面的前置知识和结论,我们来看看之前的代码
console.log(a == 1 && a == 0)// true
你是否能创造出a呢?
当然能的,上面根据上面的知识点知道, 只要将 a 定义为一个对象,修改其 Symbol.toPrimitive、toString、valueOf中的任意一个都能实现
我们来实现一下各个版本
let obj = { i: 1}
let objNumber = function(){ return this.i++ }
// 方法1:修改其valueOf方法
obj.valueOf = objNumber
// 方法2:修改toString
// 不过上面我们说过这样不符合语义,所以我们应该避免在 toString 中返回非 String 类型的值
obj.toString = objNumber
// 方法3:修改 a[Symbol.toPrimitive]
// 但是需要注意,添加 a[Symbol.toPrimitive] 方法后,ToPrimitive 将不会自动调用 toString 和 valueOf
obj[Symbol.toPrimitive] = objNumber
// 当然你可以利用 a[Symbol.toPrimitive] 方法传入的参数来做不同的事情,像下面这样
obj[Symbol.toPrimitive] = function(hint){
if(hint === 'string') return `value is ${this.i}`
return this.i++
}
console.log(String(a)) // "value is 2"
console.log(Number(a)) // 2
总结
对象类型与字符串或者数字进行 == 运算时会调用 ToPrimitive(obj),而 ToPrimitive 方法中 会尝试调用 obj[Symbol.toPrimitive] 方法, 然后再尝试调用 OrdinaryToPrimitive 方法
OrdinaryToPrimitive 方法中会尝试调用 toString 和 valueOf,其调用顺序取决于 OrdinaryToPrimitive 的参数。
我们可以通过修改对象的 Symbol.toPrimitive、toString、valueOf方法来控制其类型转换的结果