前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 **[重学前端-ECMAScript协议上篇]
重点说三种场景:
- 二元 +
- 宽松相等和不相等, ==, !=
- 关系比较 , 比如 <= , >=
两个前缀
这里有两个 前缀说一下
- 前缀 !
用于表明 抽象操作或语法导向操作的调用 是正常的正常的完成记录(normal),而不是其他类型的完成记录,比如break, continue, return, or throw。 - 前缀 ?
用于表明抽象操作或语法导向操作的调用, 如果是一个异常完成,那么应该立即返回这个异常完成,否则继续执行。
为什么不把这几个放在运算符,而是类型转换。 因为运算符的含义本身很好理解,重点是左右操作数的类型转换逻辑。
操作和对应方法
上一节已经罗列各种内容转换的方法,本文简单罗列一些最重要的三个,更多的可以再回顾上文
操作 | 应协议方法 |
---|---|
转为原始值 | ToPrimitive ( input [ , preferredType ] ) |
转为数值(Number + BigInt) | ToNumeric ( value ) |
转为数字(Number) | ToNumber ( argument ) |
转为字符串 | ToString (argument) |
二元 +
对应协议 13.8.1 The Addition Operator ( +
)
在强类型语言中比如Java, C#, 对象相加是通常通过运算符重载来实现的。
在JS世界里面呢,其实应该说思路应该说也是类似的。
不过其最终要么执行字符串拼接 ,要么执行数值(Number或者BigInt)加法。
流程
- 左右两边操作数 转为原始值,
- 如果任何一边的操作数是字符串
- 两边操作数均 转为字符串
- 进行拼接,并返回
- 转为数值(Number或者BigInt)
- 如果左右两边操作数类型不一致,抛出TypeError
- 返回左右两边数值相加的值
在协议中, ** , * , / , % , + , -, << , >> , >>> , & , ^ , or |
这些走的是一套处理逻辑,对应着协议内容
ApplyStringOrNumericBinaryOperator ( lval, opText, rval ),不过二元加法+
有一些特有的步骤。


示例 [ ] + [ ]
javascript
[] + []
// 未定义Symbol.toPrimitive方法,perferredTye不是显式的string,调用顺序 valueOf, toString
// valueOf 返回的是 [] 对象,继续调用 toString 返回是字符串 '',
// 最终结果 返回 ''
'' + ''
// 至少有一边是字符串,均转为字符串
'' + ''
// 字符串拼接
''
示例 [ ] + { }
javascriptjavascript
[] + {}
// [],{} 未定义Symbol.toPrimitive方法,perferredTye不是显式的string,
// []: valueOf 返回[]对象, 继续调用toString, 返回字符串 ''
// {}: valueOf 返回[]对象, 继续调用toString, 返回字符串 '[object Object]'
'' + '[object Object]'
// 至少有一边是字符串,均转为字符串
'' + '[object Object]'
// 字符串拼接
'[object Object]'
示例 { } + [ ]
[] + {}
的结果'[object Object]'
那么反过来 {} + []
结果是不是也是一致的呢,答案不一定是。这里有两种情况
{}
会被认为是代码段,所以 等于+ {}
{}
认为是对象,没有任何变化,依旧是{ } + [ ]
如果你把上面的代码贴入控制台,绝大部分的浏览器都会是第一种情况。
或者 console.log(eval("{} + []"))
javascript
{} + []
// {} 被认为是代码块,等同于 + []
+ []
// []未定义Symbol.toPrimitive方法,perferredTye不是显式的string,调用顺序 valueOf, toString
// valueOf 等于[]对象, toString() 返回 '', 最终返回 ''
+ ''
// 一元+, 底层调用 ToNumber, 空字符串等于 0
0
示例 { } + { }javascript
同{} + []
, {}
绝大部分会被解析为代码块。
javascript
{} + {}
// {} 被认为是代码块,等同于 + {}
+ {}
// {}未定义Symbol.toPrimitive方法,perferredTye不是显式的string,调用顺序 valueOf, toString
// valueOf 等于[]对象, toString() 返回 '[object Object]', 最终返回 '[object Object]'
+ '[object Object]'
// 一元+, 底层调用 ToNumber, 空字符串等于 NaN
NaN
示例 定义Symbol.toPrimitive
javascript
var obj = {
name: "name",
value: 10,
[Symbol.toPrimitive](perferredType){
if(perferredType === "string"){
return this.name
}
return this.value
}
};
obj + obj;
// obj: perferredType不是string, 返回值为this.value,为 10
10 + 10
// 数字相加
20
obj + obj[obj]
// obj[obj]: 此时obj作为属性键,perferredType是string,返回值 this.name, 为 "name"
10 + obj["name"]
// 取值obj["name"]
10 + "name"
// 一遍是字符串,另外一遍转为字符串
"10" + "name"
// 字符串拼接
"10name"
10 + obj
// obj: perferredType不是string, 返回值为this.value,为 10
10 + 10
// 数字相加
20
'10' + obj
// obj: perferredType不是string, 返回值为this.value,为 10
'10' + 10
// 一边是字符串,另一边转为字符串
'10' + '10'
// 字符串拼接
'1010'
'10' + `${obj}`
// perferredType是string,返回值 this.name, 为 "name"
10 + "name"
// 一遍是字符串,另外一遍转为字符串
"10" + "name"
// 字符串拼接
"10name"
示例 异常案例
javascript
var obj = {
name: "name",
value: 10,
toString(){
return this
},
valueOf(){
return this;
}
};
var obj2 = {
name: "object2 name",
value: 20
}
obj + obj; // 因为 toString 和 valueOf 返回值都是对象,
// Uncaught TypeError: Cannot convert object to primitive value
10 + 10n // 都转为原始值后,没有任何一边是字符串,且左右类型不一样
delete Object.prototype.toString
10 + ({}) // 因为删除了原型上的toString,而valueOf返回对象,抛出异常
小结
- 可能产生异常,尤其有一个操作数是对象
-
- 当 Symbol.toPrimitive方法返回值是对象
- toString和valueOf返回值都是对象
- Number和BigInt相加
- 其他情况
- 从流程上来看,优先是字符串拼接,然后是数值相加
非严等运算符: == !=
宽松比较, 对应协议内容 13.11 Equality Operators, 其核心的逻辑是 IsLooselyEqual ( x
, y
)。
对应的有一个严格比较 IsStrictlyEqual, 严格比较比较简单,不语。
流程
假设比较的左边操作数为 x,右边的操作数为 y。语句为: x == y
。
根据协议的逻辑,做了简单的整理,基本的流程如下:
如果类型相同,严格比较
原始值:null 和 undefined
- 如果x 是 null,y 是undefined , 返回 true
- 如果x 是 undefined, y是 null, 返回 true
- 特殊处理有 [[IsHTMLDDA]] 内部属性的对象,比如
document.all
- 如果x是对象,并且有 [[IsHTMLDDA]] 内部属性,y 是null或者 undefined , 返回 true
- 如果y是对象,并且有 [[IsHTMLDDA]] 内部属性,x 是null或者 undefined , 返回 true
- 原始值:字符串和数值比较
- 如果一个操作数是字符串,另外一个是数字,字符串转为数字,再重复整个流程
- 如果一个操作数是字符串,另外一个是BigInt,近似的可以理解为:字符串转为BigInt,再重复整个流程
- 原始值:一边是布尔值
- 如果一个操作数是布尔值,把该操作数转为数字,再重复整个流程
- 有对象
- 如果一个操作数是对象,另外一个是 字符串,数字,BigInt或者Symbol,对象转为原始值,再重复整个流程
原始值:如果一个操作是BigInt, 另外一个是数字
- 如果其中一个是无限值,返回 false
- 如果数学值相等,返回true, 否则返回 false
返回 false
注意红色标出的步骤,也就是会直接出结果的步骤,可以看出一些端倪
- 能直接比较出结果的情况是如下情况
- 同类型
- 至少有一个操作数是 null 和 unfefined
- BigInt 和 Number 比较
- 最后一步的默认返回值 false
- 整个流程类型的转换,整体是向 数值 靠近
- 一边字符串,一边数值, 字符串转数值
- 一边是布尔值,布尔值转数字
- 如果左右操作数不是同类型, 对象肯定是要被转为原始值进行比较的
其实记住这三点,整个比较过程就变得相对容易了。
IsLooselyEqual ( x, y )
==和 != 底层逻辑是一样的,都是IsLooselyEqual ( x, y ),!= 是的 == 返回值的相反值。
IsLooselyEqual ( x, y ) 的整体流程如下,和上面的总结差不多。

第4步的协议内容:
主要就是浏览器环境中的document.all

javascript
document.all == undefined // true
document.all == null // true
!document.all // false
转为原始值注意事项
二元 + 当有一操作数是字符串的时候,操作数是对象转为原值值的时候, perferredType 是字符串。
而宽松相等 ==,!=不存在此类情况。
所以二元+和宽松相等,同样的对象,在转为原始值的时候,perferredType 都是未定义的。
所有如果对象,未定义 Symbol.toPrimitive方法的情况下,转为原始值,方法调用的顺序始终是 valueOf, toString。
在该前提下, []
和 {}
的原始值分别为 ''
和 [object Object]
javascript
[] == {}
// []: valueOf 返回值是 [], 继续调用 toString, 返回值为 '', 不是对象,返回该值
// {}: valueOf 返回值是 [], 继续调用 toString, 返回值为 '[object Object]', 不是对象,返回该值
'' == 'object Object'
// 同类型,显然不等
false
示例 没有对象的比较
字符串和 Number
javascript
'10' == 10
// 字符串转为 Number , '10' => 10
10 == 10
// 同类型
true
字符串和 Number
javascript
'10' == 10n
// 字符串转为 BigInt , '10' => 10n
10n == 10n
// 同类型
true
1111122223333444455556666777788889999n == '1111122223333444455556666777788889999'
// 字符串转为 BigInt , => 1111122223333444455556666777788889999n
1111122223333444455556666777788889999n == 1111122223333444455556666777788889999n
true
一边是布尔值BigInt 和 Number
javascript
false == ''
// 字符串转为数字, '' => 0
fasle == 0
// 布尔值转为数字, false => 0
0 == 0
// 同类型
true
BigInt 和 Number
javascript
10 == 10n
// 数学值相等
true
1111122223333444455556666777788889999n == 1111122223333444455556666777788889999
// 右边精度丢失
false
示例 有对象的比较
javascript
[] == false
// 优先处理布尔值,转为Number, false => 0
[] == 0
// [] 转为原始值, [] => ''
'' == 0
// 字符串转为Number
0 == 0
// 同类型比较
true
javascript
var s0 = Symbol.for(0);
s0 == []
// [] 转为原始值 , [] => ''
s0 == ''
// 字符串转为数字, '' => 0
s0 == 0
// 都已经是原始值, 走协议逻辑的第14步骤,返回 false
false
小结
- 顺序
- 同类型
- null 和 undefined
- 一边是字符串,一遍是数值(Number, BigInt), 字符串转数值
- 一边是布尔值,布尔值转数字
- 一边对象,一边 String, Symbol, Number, BigInt, 对象转原始值
- BigInt 和 Number ,比数学值
- 返回false
关系运算符: < > <= >=
对应协议内容 13.10 Relational Operators, 其底层核心是 IsLessThan ( x, y, LeftFirst )。
经典的案例:
javascript
({}) > ({}) // false
({}) >= ({}) // true
new Date() > 12345678 // true
看似悬疑,其实这个系列的比较就两个核心
- 最终是 字符串 或者 数值的比较。 所以呢,如果出现对象,肯定会出现转原始值。
- 不小于,就是大于等于或者小于等于
流程大逻辑
这四个运算符,其底层是一套逻辑

都是基于 IsLessThan ( x, y, LeftFirst ) 来判断 x
是否小于 y
, 假设这个返回值为 r
。r
的值可能是
true
false
undefined
- 一边是 BigInt, 另外一边是字符串,且字符串转为BigInt失败时
- 如果有x, y 有一个值是NaN
运算符 | 运算逻辑 | 值 r | 最终返回值 (上图蓝色部分) | 说明 |
---|---|---|---|---|
x < y | IsLessThan(x, y, true) | undefined | false | x 小于 y 吗? 小于就返回true, 其他情况返回false. |
true | true | |||
false | false | |||
x > y | IsLessThan(y, x, false) | undefined | false | y 小于 x吗? 小于就返回true, 其他情况返回false。 |
true | true | |||
false | false | |||
x <= y | IsLessThan(y, x, false) | undefined | false | y 小于 x 吗? 不小于就返回 true, 其他情况返回false |
true | false | |||
false | true | |||
x >= y | IsLessThan(x, y, true) | undefined | false | x 小于 y 吗? 不小于就返回true, 其他情况返回false |
true | false | |||
false | true |
对比表格
- IsLessThan 返回 undefined 时, 最终返回值都是 fasle。
- 一种情况是至少有一个操作数是 NaN
- 从字符串转为 BigInt的失败
x <= y
和x >= y
完美阐释 不小于,就大于等于或者小于等于的理念。
IsLessThan ( x, y, LeftFirst )
先简单总结一下顺序
- 左右操作数转为原始值,perferredType 为 number
- 左右都是字符串,按顺序比较码元的值
- 一边是字符串,一边是BigInt, 字符串转 BigInt再比较
- 操作数类型相同
如果是BigInt 或者 Number ,调用对应的方法比较 - 有某个操作数是NaN, 返回 undefined
- 处理无穷
- BigInt 和 Number 比较数学值
协议内部的描述,大致和之前的表格是一样的

重点一起看看 IsLessThan ( x, y, LeftFirst ) ,这里得重点说第三个参数 LeftFirst,从协议内容来看,其控制的是先对哪个操作数 转为原始值。
其最终为了保证是 从左到右执行,先对左边的操作数进行运算。
javascript
var obj1 = {
toString(){ throw new Error("obj1 error")}
};
var obj2 = {
toString(){ throw new Error("obj2 error")}
}
obj1 < obj2 // IsLessThan(x, y, true) Uncaught Error: obj1 error
obj1 > obj2 // IsLessThan(y, x, false) Uncaught Error: obj1 error
obj1 <= obj2 // IsLessThan(y, x, false) Uncaught Error: obj1 error
obj2 >= obj1 // IsLessThan(x, y, true) Uncaught Error: obj1 error


示例 ({ }) < ({ }) ({ }) <= ({ })
javascript
({}) < ({})
// IsLessThan(x, y, true): 转为原始值,都是 '[object Object]'
'[object Object]' < '[object Object]'
// IsLessThan: 字符串比较,比较值为 false
// 依据<协议描述,IsLessThan返回的值是 false, 最终值应该是 false
false
javascript
({}) <= ({})
// IsLessThan(y, x, false): 转为原始值,都是 '[object Object]'
'[object Object]' < '[object Object]'
// IsLessThan(y, x, false): 字符串比较,比较值为 false
// 依据<=协议描述,IsLessThan返回的值是 false, 最终值应该是 true
true
这里理解,可以参照协议的描述内容,也可以参考前面的表格。
示例 一些特别场景
- BigInt和一个不能转为BigInt的字符串比较的时候,返回值都是false。
javascriptjavascript
1n < 'xxx'
// IsLessThan(x, y, true): 一边是BigInt, 一边是字符串,把字符串转为BigInt
// IsLessThan(x, y, true): 字符串转为字符串失败,所以返回值是 undefined
// 依据<协议描述,IsLessThan返回的值是 undefined, 最终值应该是 false
javascript
1n <= 'xxx'
// IsLessThan(y, x, false): 一边是BigInt, 一边是字符串,把字符串转为BigInt
// IsLessThan(y, x, false): 字符串转为字符串失败,所以返回值是 undefined
// 依据<协议描述,IsLessThan返回的值是 undefined, 最终值应该是 false
- 某个操作数为NaN, 始终返回 false
javascript
NaN < 0; // false
NaN <= 0; // false
NaN < NaN; // false
NaN <= NaN; // false
NaN < false; // false
NaN <= false // false
// IsLessThan: 检测到有某个值是 NaN,就直接返回 undefined
// < <= > >= 的统一逻辑都是如果 IsLessThan 返回 undefined, 最终值都是 false
- Number 和 BigInt 是可以比较的,比较的是数学值
javascript
10 < 10n // false
10 <= 10n // true
- Date
javascript
var d = new Date();
d < 100;
// IsLessThan(x, y, true): d转为原始值,perferreType是number, 先调用valueOf,
// IsLessThan(x, y, true): 返回是数字的 1684469949757
1684469949757 < 100;
// IsLessThan(x, y, true): 返回值 false
// 根据 < 协议: IsLessThan 返回false, 最终结果为 false.
总结
二元+ | 非严等运算符 (==,!=) | 关系运算符 (<, <=, >, >=) | |
---|---|---|---|
最终计算/比较类型 | String ,Number, BigInt | 任何数据类型 | String, Number, BigInt , NaN |
转为原始值 | 未传递perferredType | 未传递perferredType | perferredType是number |
流程小结 | 优先字符串然后数值 | 同类型严格比较向数值靠近 | 都是字符串比码元的值向数值靠近 |
- 转为原始值时, perferredType 会影响方法 valueOf , toString的调用顺序
- 类型不一样时,基本是向数值靠近
- 关系运算符 (<, <=, >, >=)的哲学,不小于,就是大于等于或者小于等于