来看一道有关于 js 隐式转换的题:
// 请问如下的输出结果是什么?
+{}
乍一看,题目很短好像很简单,这是一个隐式转换的题。但仔细想想,却道不出它的转换过程。如果你和我一样对隐式转换有些模糊不清,那我们就一起来研究研究 js 隐式转换的细节吧。
隐式转换是什么?
js 是一个弱类型语言。在一些场景下,它会将当前操作数的类型转化成其它的基本数据类型,以满足当前场景下的使用,这就是 js 的隐式转换。这个机制一方面使得 js 更加灵活,另一方面却可能使代码难以理解。所以要想熟练使用js,理解 js 的隐式转换必不可少。
隐式转换的四种方式
js 中有4种常见的隐式转换方式,分别是 ToPrimitive、ToBoolean、ToNumber 和 ToString。
ToPrimitive
将一个复杂数据类型转为基本数据类型
ToPrimitive 方法通过调用传入值的 valueOf 方法 或 toString 方法,并以方法返回的基本数据类型值作为转化后的值。转化过程两个方法并非一定都被调用,只有当调用的第一个方法返回的值不是基本数据类型时,才会继续调用下一个方法。如果两个方法的返回值都不是基本数据类型,那么就会报错。而具体先调用 valueOf 还是 toString,则由传入的第二参数决定。
下面用 javascript 简单实现一下 ToPrimitive:
const primitiveTypes = ['Undefined', 'Null', 'Number', 'String', 'Boolean']
const util = {
getType (val) {
return Object.prototype.toString.call(val).slice(8, -1)
},
isprimitive (val) {
return primitiveTypes.includes(this.getType(val))
}
}
function toPrimitive (input, preferredType) {
if (util.isPrimitive(input)) return input // 本身就是基础数据类型,直接返回
// 没有传 preferredType 时,如果 input 是日期类型,则默认是 String,否则默认 Number
if (preferredType === undefined) {
preferredType = util.getType(input) === 'Date' ? 'String' || 'Number'
}
// Number 时,调用顺序为 valueOf -> toString
if (preferredType === 'Number') {
const value = input.valueOf()
if (util.isPrimitive(value)) return value
const str = input.toString()
if (util.isPrimitive(str)) return str
}
// String 时,调用顺序为 toString -> valueOf
if (preferredType === 'String') {
const str = input.toString()
if (util.isPrimitive(str)) return str
const value = input.valueOf()
if (util.isPrimitive(value)) return value
}
throw new Error('valueOf 和 toString 方法返回的都不是基础数据类型')
}
注意到,toPrimitive 如果不传第二个参数,大多数情况下默认为 Number,只有在传入 Date 对象时,才默认为 String。至于为什么 Date 转基本数据类型时要采用 toString,那是因为早期 js 是运行于浏览器的,直接面向的是用户,所以隐式转换为字符串形式会比时间戳的形式更贴近用户。
以上是复杂数据类型转基本数据类型的实现过程,在这个过程中有两个关键的方法:valueOf 和 toString。在我们没有改写对象的这两个方法之前,js 默认已经为它们初始化了这些方法,现在我们就一起来看一看初始方法的行为。
valueOf
大多数构造函数都继承了 Object.prototype 的 valueOf 方法,除了以下4种构造函数重写了 valueOf 方法:Number、String、Boolean 和 Date。
// === 重写之后的 valueOf 的表现: ===
let num = new Number(12)
console.log(num.valueOf === Object.prototype.valueOf) // false
console.log(num.valueOf()) // 12
let str = new String(12)
console.log(str.valueOf === Object.prototype.valueOf) // false
console.log(str.valueOf()) // '12'
let boo = new Boolean(12)
console.log(boo.valueOf === Object.prototype.valueOf) // false
console.log(boo.valueOf()) // true
let date = new Date()
console.log(date.valueOf === Object.prototype.valueOf) // false
console.log(date.valueOf()) // 1584452005327
// === 未重写的 valueOf 的表现:返回值都是对象自身 ===
let arr = [1]
console.log(arr.valueOf === Object.prototype.valueOf) // true
console.log(arr.valueOf() === arr) // true
let fn = function () {}
console.log(fn.valueOf === Object.prototype.valueOf) // true
console.log(fn.valueOf() === fn) // true
let err = new Error ('')
console.log(err.valueOf === Object.prototype.valueOf) // true
console.log(err.valueOf() === err) // true
let reg = new RegExp('^a')
console.log(reg.valueOf === Object.prototype.valueOf) // true
console.log(reg.valueOf() === reg) // true
toString
除了对象自身之外,其它的构造函数均重写 toString 方法
let num = new Number(12)
console.log(num.toString === Object.prototype.toString) // false
console.log(num.toString()) // "12"
let str = new String(12)
console.log(str.toString === Object.prototype.toString) // false
console.log(str.toString()) // "12"
let boo = new Boolean(12)
console.log(boo.toString === Object.prototype.toString) // false
console.log(boo.toString()) // "true"
let obj = {a: 1}
console.log(obj.toString === Object.prototype.toString) // true
console.log(obj.toString()) // "[object Object]"
let arr = [1, 2, 3]
console.log(arr.toString === Object.prototype.toString) // false
console.log(arr.toString()) // "1,2,3"
let fn = function () {return 1}
console.log(fn.toString === Object.prototype.toString) // false
console.log(fn.toString()) // "function () {return 1}"
let date = new Date()
console.log(date.toString === Object.prototype.toString) // false
console.log(date.toString()) // "Tue Mar 17 2020 21:48:24 GMT+0800 (中国标准时间)"
let err = new Error ('123')
console.log(err.toString === Object.prototype.toString) // false
console.log(err.toString()) // "Error: 123"
let reg = new RegExp('^a')
console.log(reg.toString === Object.prototype.toString) // false
console.log(reg.toString()) // "/^a/"
ToBoolean
将值转为布尔值,经测试与 Boolean 函数的表现行为一致
参数 | 结果 |
---|---|
undefined | false |
null | false |
boolean | 无需转化 |
string | ” -> false;其它 true |
number | 0/NaN -> false;其它 true |
object | true |
ToNumber
将值转为数值,经测试与 Number 函数的表现行为一致
参数 | 结果 |
---|---|
undefined | NaN |
null | 0 |
boolean(true/false) | 1/0 |
string | ” -> 0; ‘123’ -> 123; ‘123a’ -> NaN; |
number | 无需转化 |
object | 先 ToPrimitive(input, Number),再转数值 |
ToString
将值转为字符串,经测试与 String 函数的表现行为一致
参数 | 结果 |
---|---|
undefined | ‘undefined’ |
null | ‘null’ |
boolean(true/false) | ‘true’ / ‘false’ |
number | 123 -> ‘123’ |
string | 无需转化 |
object | 先 ToPrimitive(input, String),再转字符串 |
什么情况下会进行隐式转换?
if 条件 | 与或运算 | “!” 操作符
这几个操作符都会将操作数进行 toBoolean 转换。
+ 操作符
该操作符的目的,是为了得出一个数值,所以会对操作数进行 ToNumber 隐式转换。
+undefined // NaN
+null // 0
+true(+false) // 1(0)
+'12' // 12
+'12a' // NaN
+{} // NaN
看到了上面栗子中的 +{} 了吗,这就是我们文章开头所说的那道题,其实它的实际转化流程是这样的:
1、{} 传入 ToPrimitive,首先调用了 valueOf,返回了它自身
2、由于 valueOf 的返回值不是基本数据类型,那么继续执行 toString 方法,返回 ‘[object Object]’
3、'[object Object]’ 通过以字符串的方式去转数值,被转成了 NaN
+ 运算符
现在讨论一下 “+” 运算符。它与前面的 “+”操作符不同,它的目的在于得出一个相加后的数值或者字符串,而到底是相加为数值还是字符串由操作数的类型决定。如果有1个操作数是字符串,那么就拼接成字符串,否则数值相加。所以一开始就会将操作数转为基本数据类型。
这个过程的伪代码:
// left + right
pLeft = ToPrimitive(left)
pRight = ToPrimitive(right)
if(pLeft is string || pRight is string)
return concat(ToString(pLeft), ToString(pRight))
else
return add(ToNumber(pLeft), ToNumber(pRight))
-*/ 运算符
这3个运算符的目的都是为了得出一个数值,所有运算符两边的操作数都通过 toNumber 转化,再计算。如果有一个有一个转为 NaN ,则最后的返回值为 NaN。
== 和 != 运算符
“==” 和 “!=” 运算符是为了比较它们的操作数是否相等,而操作数需要在同一类型下才能比较,所以如果不是同一类型,需要将操作数往类型一致的方向进行隐式转换。
先上一波 “==” 的隐式转换规则:
比较运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比较按如下方式进行:
1、若 Type(x) 与 Type(y) 相同, 则
1* 若 Type(x) 为 Undefined, 返回 true。
2* 若 Type(x) 为 Null, 返回 true。
3* 若 Type(x) 为 Number, 则
(1)、若 x 为 NaN, 返回 false。
(2)、若 y 为 NaN, 返回 false。
(3)、若 x 与 y 为相等数值, 返回 true。
(4)、若 x 为 +0 且 y 为 −0, 返回 true。
(5)、若 x 为 −0 且 y 为 +0, 返回 true。
(6)、返回 false。
4* 若 Type(x) 为 String, 则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。 否则, 返回 false。
5* 若 Type(x) 为 Boolean, 当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。
6* 当 x 和 y 为引用同一对象时返回 true。否则,返回 false。
2、若 x 为 null 且 y 为 undefined, 返回 true。
3、若 x 为 undefined 且 y 为 null, 返回 true。
4、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。
5、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。
6、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。
7、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。
8、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。
9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。
10、返回 false。
看到上面这一堆规则,是不是感觉头要大了。不急,我这里整理总结了一下:
- 类型相等直接比较(注意 NaN 与其它数值不等)
- 类型不等时
-
-
null == undefined 返回 true;如果只有它们两者中的一个,返回 false
-
boolean 操作数先 toNumber。之后,复杂类型操作数 toPrimitive。转化到最后,如果两边的类型依旧不同,那两边肯定分别是 Number 和 String。所以将 String 类型的操作符 toNumber 之后进行比较。
-
比较运算符
“<” “>” “<=” “>=” 这几个属于比较比较运算符。比较运算符的目的是为了对比两边的数值或者字符串是否相等。当两边都是字符串时,按字符串进行对比,否则按数值进行对比。所以需要在一开始就将操作数转为基本数据类型。
这个过程的伪代码:
// left < right
pLeft = ToPrimitive(left)
pRight = ToPrimitive(right)
if(pLeft is string && pRight is string)
return compareStr(pLeft, pRight)// 字符串对比
else
return compareNum(ToNumber(pLeft), ToNumber(pRight)) // 数值对比
基于隐式转换的机制,有什么应用场景?
场景一:
用户通过表单输入值之后,我们拿到的值通常会是一个全是数字的字符串,如果我们需要进行 –/,那么我们只需要直接将字符串进行 –/ 即可,因为操作数会隐式转化为数值(请先忽略数值运算中的精度损耗)。如果我们需要进行 + 运算,则需要对每一个操作数通过 +操作符进行转化。
'12' - '3' // 9
'12' * '3' // 36
'12' / '3' // 4
+'12' - (+'3') // 9
场景…(欢迎留言补充)
原文链接:https://juejin.cn/post/7228990409908666425 作者:白哥学前端