JS 在执行过程中会发生类型隐式转换,这里抄了 ECMA 规范的『+』和『==』运算符的解释
ECMA 规范
加法运算符(+)
加法运算符启动字符串相接或是数值相加。
产生式 AdditiveExpression : AdditiveExpression + MultiplicativeExpression 按照下面的过程执行:
- 令
lref
为解释执行 AdditiveExpression 的结果。 - 令
lval
为 GetValue(lref)。 - 令
rref
为解释执行 MultiplicativeExpression 的结果。 - 令
rval
为 GetValue(rref)。 - 令
lprim
为 ToPrimitive(lval)。 - 令
rprim
为 ToPrimitive(rval)。 - 如果 Type(lprim) 为 String 或者 Type(rprim) 为 String,则:
- 返回将加法运算作用于 ToNumber(lprim) 和 ToNumber(rprim) 的结果。参见 11.6.3 后的注解。
注:在 第5步 和 第6步 中 ToPrimitive 的调用没有提供暗示类型。所有除了 Date 对象的 ECMAScript 原生对象会在没有提示的时候假设提示是 Number;Date 对象会假设提示是 String。宿主对象可以假设提示是任何东西。
抽象相等比较算法
以 x 和 y 为值进行 x == y 比较会产生的结果可为 true 或 false。比较的执行步骤如下:
- 若 Type(x) 与 Type(y) 相同, 则
- 若 Type(x) 为 Undefined, 返回 true。
- 若 Type(x)为 Null, 返回 true。
- 若 Type(x)为 Number,则
- 若 x 为 NaN,返回 false。
- 若 y 为 NaN,返回 false。
- 若 x 与 y 为相等数值,返回 true。
- 若 x 为 +0 且 y 为 −0,返回 true。
- 若 x 为 −0 且 y 为 +0,返回 true。
- 返回 false。
- 若 Type(x) 为 String,则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。否则,返回 false。
- 若 Type(x) 为 Boolean,当 x 和 y 为同为 true 或者同为 false 时返回 true。否则,返回 false。
- 当 x 和 y 为引用同一对象时返回 true。否则,返回 false。
- 若 x 为 null 且 y 为 undefined,返回 true。
- 若 x 为 undefined 且 y 为 null,返回 true。
- 若 Type(x) 为 Number 且 Type(y) 为 String,返回 x == ToNumber(y) 的结果。
- 若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
- 若 Type(x) 为 Boolean,返回比较 ToNumber(x) == y 的结果。
- 若 Type(y) 为 Boolean,返回比较 x == ToNumber(y) 的结果。
- 若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
- 若 Type(x) 为 Object 且 Type(y) 为 String 或 Number,返回比较 ToPrimitive(x) == y 的结果。
- 返回 false。
注:根据上述等于的定义:
- 字符串比较可以以:"" + a == "" + b 硬性触发。
- 数值比较可以以:+a == +b 硬性触发。
- 布尔比较可以以:!a == !b 硬性触发。
注:等于运算符有以下的不变量:
- A != B 与 !(A == B) 相等。
- 除了 A 与 B 的执行顺序以外,A == B 与 B == A 相等。
注:等于运算符不总是可传递。举例来说,两个代表相同 String 值但是不同的 String 对象会分别与 String 值 ==,但是两个对象间不相等。
注:String 的比较使用代码单元序列的简单等号比较。这里不打算使用更复杂的、语义化的字符或字符串序列,和 Unicode 规范的整理序列进行比较。因此,在 Unicode 标准中相等的 String 值可能在本算法中不相等。也就是,这个算法假定了所有字符串已经正规化。
理解学习
字符串/数字的转换规则
在将目标 O 转换为 String 或 Number 类型时,有以下操作:
如果期望类型是 String
,优先尝试调用 O 的 toString
方法,如果调用结果是基本类型,则将其转换为 String 并返回结果;如果 toString
方法不存在,或者返回结果不是基本类型,则继续尝试调用 O 的 valueOf
方法,如果结果是基本类型,则将其转换为 String 类型并返回结果,如果结果不是基本类型,则抛出错误。
转换为 Number 类型的过程与转换为 String 的过程基本一致,区别在于转换为 Number 是优先调用 O 的 valueOf
方法。
这里 O 是一个对象,需要注意的是基本类型的情况,基本类型没有方法属性,但是 JS 存在自动包装机制,会自动转换成对应的类型对象实例,如:
(1).toString()
相当于new Number(1).toString()
,所以你没有办法重写一个基本类型变量的toString
、valueOf
方法,只能重写它的原型对象的方法,例a=1; a.toString();
内部逻辑是(new Number(a)).toString()
,每次都会生成一个新对象
// 对象默认继承了Object,其拥有默认的`valueOf`及`toString`方法
var obj1 = {};
String(obj1); // -> [object Object]
Number(obj1); // -> NaN (相当于Number('[object Object]')),因为valueOf返回的结果不是基本类型,所以使用 toString 方法的返回值
// 覆盖了默认的 valueOf、toString 方法
var obj2 = {valueOf: () => 1, toString: () => 'a'};
String(obj2); // -> a
Number(obj2); // -> 1
[Symbol.toPrimitive] 属性
ES6 添加了新的基本类型: Symbol,Symbol 的 toPrimitive 静态属性可以用来给对象部署类型转换的方法,其调用优先级高于 valueOf
和 toString
,且如果存在 [Symbol.toPrimitive]
属性,不论其执行结果如何,不会再调用 valueOf 及 toString 方法(即覆盖)。其执行逻辑与 valueOf 及 toString 类似,都是返回基本类型再转换成目标类型,否则报错。
Symbol.toPrimitive
属性方法有一个参数 type
表示期望类型,取值有 default
,number
,string
var obj = {
[Symbol.toPrimitive](type){
console.log(type);
return type === 'number' ? 1 : 'toPrimitive';
},
valueOf(){return 2;},
toString(){return 'toString';}
}
Number(obj); // -> 1
String(obj); // -> toPrimitive
type 有一个特殊的取值: default
,在执行 加法运算符(+) 操作的时候,会进行这个类型的转换,就是上面 加法运算符(+) 规范的第 5、6 条所涉及的 ToPrimitive
操作,且在下面的注释里提到了:『没有提供暗示类型』,没有即取 default
,
所以上面的 obj
在执行 obj+1
的时候打印的信息是:
obj+1
// default
// toPrimitive1
扩展理解
在『加法运算符』规范的注释里还解释道:
所有除了 Date 对象的 ECMAScript 原生对象会在没有提示的时候假设提示是 Number;Date 对象会假设提示是 String。宿主对象可以假设提示是任何东西。
也就是说 ECMAScript 原生对象在执行 +
操作的时候,Date 会转换为String基本类型,而其他原生对象会转换为Number基本类型,而根据之前总结的『字符串/数字的转换规则』,换句话说,如果没有部署[Symbol.toPrimitive]
方法,Date 就是先执行的 toString 方法, 其它原生对象先执行的 valueOf 方法。
d = new Date;
console.log(d + 1);
// 输出: 'Mon Feb 01 2021 19:22:55 GMT+0800 (中国标准时间)1'
// 重写 toString 方法
d.valueOf= () => 1;
d.toString = () => 2;
console.log(d + 1);
// 输出 3
// ------------
// 重写数组的valueOf 与 toString 方法
arr = [];
arr.valueOf = () => 1;
arr.toString = () => 'a';
console.log(a+1);
// 输出 2
确实是这样。再来创建个对象试一下
obj = {
toString(){return 'a'},
valueOf(){return 1}
}
console.log(obj+1);
// 输出 2
console.log(obj + '1');
// 输出 "11"
也遵循这一规则。
所以简单总结的个人理解就是:在『+』、『==』运算中对象隐式转换为基本类型的规则为 除了原生 Date 对象是优先调用 toString,其它对象优先调用 valueOf,前一个返回了一个基本类型的值,就会停止调用下一个
注. 这里没有把 toPrmitive 方法放进来一起讨论,如果有 toPrimitive,都是执行对象的
Stmbol.toPrimitive('default')
方法