ECMAScript 类型转换 下

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 **[重学前端-ECMAScript协议上篇]

重点说三种场景:

  • 二元 +
  • 宽松相等和不相等, ==, !=
  • 关系比较 , 比如 <= , >=

两个前缀

tc39.es/ecma262/#se...

这里有两个 前缀说一下

  • 前缀 !
    用于表明 抽象操作或语法导向操作的调用 是正常的正常的完成记录(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)加法

流程

  1. 左右两边操作数 转为原始值
  2. 如果任何一边的操作数是字符串
    1. 两边操作数均 转为字符串
    2. 进行拼接,并返回
  3. 转为数值(Number或者BigInt)
  4. 如果左右两边操作数类型不一致,抛出TypeError
  5. 返回左右两边数值相加的值

在协议中, ** , * , / , % , + , -, << , >> , >>> , & , ^ , 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]'

那么反过来 {} + []结果是不是也是一致的呢,答案不一定是。这里有两种情况

  1. {}会被认为是代码段,所以 等于 + {}
  2. {} 认为是对象,没有任何变化,依旧是 { } + [ ]

如果你把上面的代码贴入控制台,绝大部分的浏览器都会是第一种情况。

或者 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返回对象,抛出异常

小结

  1. 可能产生异常,尤其有一个操作数是对象
    1. 当 Symbol.toPrimitive方法返回值是对象
    2. toString和valueOf返回值都是对象
    3. Number和BigInt相加
    4. 其他情况
  1. 从流程上来看,优先是字符串拼接,然后是数值相加

非严等运算符: == !=

宽松比较, 对应协议内容 13.11 Equality Operators, 其核心的逻辑是 IsLooselyEqual ( x, y )

对应的有一个严格比较 IsStrictlyEqual, 严格比较比较简单,不语。

流程

假设比较的左边操作数为 x,右边的操作数为 y。语句为: x == y

根据协议的逻辑,做了简单的整理,基本的流程如下:

  1. 如果类型相同,严格比较
  2. 原始值:null 和 undefined
    1. 如果x 是 null,y 是undefined , 返回 true
    2. 如果x 是 undefined, y是 null, 返回 true
  3. 特殊处理有 [[IsHTMLDDA]] 内部属性的对象,比如 document.all
    1. 如果x是对象,并且有 [[IsHTMLDDA]] 内部属性,y 是null或者 undefined , 返回 true
    2. 如果y是对象,并且有 [[IsHTMLDDA]] 内部属性,x 是null或者 undefined , 返回 true
  4. 原始值:字符串和数值比较
    1. 如果一个操作数是字符串,另外一个是数字,字符串转为数字,再重复整个流程
    2. 如果一个操作数是字符串,另外一个是BigInt,近似的可以理解为:字符串转为BigInt,再重复整个流程
  5. 原始值:一边是布尔值
    1. 如果一个操作数是布尔值,把该操作数转为数字,再重复整个流程
  6. 有对象
    1. 如果一个操作数是对象,另外一个是 字符串,数字,BigInt或者Symbol,对象转为原始值,再重复整个流程
  7. 原始值:如果一个操作是BigInt, 另外一个是数字
    1. 如果其中一个是无限值,返回 false
    2. 如果数学值相等,返回true, 否则返回 false
  8. 返回 false

注意红色标出的步骤,也就是会直接出结果的步骤,可以看出一些端倪

  1. 能直接比较出结果的情况是如下情况
    1. 同类型
    2. 至少有一个操作数是 null 和 unfefined
    3. BigInt 和 Number 比较
    4. 最后一步的默认返回值 false
  2. 整个流程类型的转换,整体是向 数值 靠近
    1. 一边字符串,一边数值, 字符串转数值
    2. 一边是布尔值,布尔值转数字
  3. 如果左右操作数不是同类型, 对象肯定是要被转为原始值进行比较的

其实记住这三点,整个比较过程就变得相对容易了。

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

小结

  1. 顺序
    1. 同类型
    2. null 和 undefined
    3. 一边是字符串,一遍是数值(Number, BigInt), 字符串转数值
    4. 一边是布尔值,布尔值转数字
    5. 一边对象,一边 String, Symbol, Number, BigInt, 对象转原始值
    6. BigInt 和 Number ,比数学值
    7. 返回false

关系运算符: < > <= >=

对应协议内容 13.10 Relational Operators, 其底层核心是 IsLessThan ( x, y, LeftFirst )

经典的案例:

javascript 复制代码
({}) > ({})         		// false
({}) >= ({})        		// true

new Date() > 12345678   // true

看似悬疑,其实这个系列的比较就两个核心

  1. 最终是 字符串 或者 数值的比较。 所以呢,如果出现对象,肯定会出现转原始值。
  2. 不小于,就是大于等于或者小于等于

流程大逻辑

这四个运算符,其底层是一套逻辑

都是基于 IsLessThan ( x, y, LeftFirst ) 来判断 x是否小于 y, 假设这个返回值为 rr的值可能是

  • 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

对比表格

  1. IsLessThan 返回 undefined 时, 最终返回值都是 fasle。
    1. 一种情况是至少有一个操作数是 NaN
    2. 从字符串转为 BigInt的失败
  2. x <= yx >= y完美阐释 不小于,就大于等于或者小于等于的理念。

IsLessThan ( x, y, LeftFirst )

先简单总结一下顺序

  1. 左右操作数转为原始值,perferredType 为 number
  2. 左右都是字符串,按顺序比较码元的值
  3. 一边是字符串,一边是BigInt, 字符串转 BigInt再比较
  4. 操作数类型相同
    如果是BigInt 或者 Number ,调用对应的方法比较
  5. 有某个操作数是NaN, 返回 undefined
  6. 处理无穷
  7. 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
流程小结 优先字符串然后数值 同类型严格比较向数值靠近 都是字符串比码元的值向数值靠近
  1. 转为原始值时, perferredType 会影响方法 valueOf , toString的调用顺序
  2. 类型不一样时,基本是向数值靠近
  3. 关系运算符 (<, <=, >, >=)的哲学,不小于,就是大于等于或者小于等于
相关推荐
毕小宝几秒前
编写一个网页版的音频播放器,AI 加持,So easy!
前端·javascript
万水千山走遍TML几秒前
JavaScript性能优化
开发语言·前端·javascript·性能优化·js·js性能
Aphasia3111 分钟前
react必备JS知识点(一)——判断this指向👆🏻
前端·javascript·react.js
会飞的鱼先生16 分钟前
vue3中slot(插槽)的详细使用
前端·javascript·vue.js
小小小小宇31 分钟前
一文搞定CSS Grid布局
前端
0xHashlet37 分钟前
Dapp实战案例002:从零部署链上计数器合约并实现前端交互
前端
知心宝贝38 分钟前
🔍 从简单到复杂:JavaScript 事件处理的全方位解读
前端·javascript·面试
安余生大大39 分钟前
关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
前端
前端涂涂40 分钟前
express查看文件上传报文,处理文件上传,以及formidable包的使用
前端·后端
凌叁儿40 分钟前
从零开始搭建Django博客③--前端界面实现
前端·python·django