从一道面试题开始,重温类型转换
一道题目
最近准备回成都,开始了各种面试,这次面试官出了一道函数坷里化的题,题目如下:
实现如下函数
1
2
3 sum(1) // 1
sum(1)(2)(3) // 6
sum(1)(2)(3)(4) // 10
面试的时候这道题愣了一下,没有答出来,主要纠结了一下在不定参的时候,如果能够链式调用,那一定需要返回的是一个函数,按照题目要求,值需要既是一个函数,又是一个数字。
面试结束后仔细想了下,正好想到之前看《你不知道的JavaScript》中的对于对象类型的toPrimitive操作,面试后很快写了个Demo出来。
1 | function sum(...args){ |
这里实际上关于使用toString
以及Symbol.toPrimitive
可以达成效果的,但具体原理有一些差别。
这里就借助这个机会回顾一下Js整个类型转换逻辑。
隐式类型转换
总所周知,js作为一种弱类型语言,在需要的场景下,会进行隐式的类型转换,为此 ECMA262 Type Conversion 中规定了一系列类型转换的抽象方法,以及在各种运算符操作及函数中规定了如何去调用这些类型转换方法。
抽象方法
ECMA262 Type Conversion 中规定了许多抽象方法,例如ToString
、ToNumber
、toPrimitive
、toBoolean
,其他的抽象方法也有很多,这里只简单介绍一下toPrimitive
方法,步骤很清楚明白。
ToPrimitive(input[,preferredType])
toPrimitive
方法主要用于将对象转换为原始值,它接受两个参数,第一个参数为任意的合法值,第二个参数为倾向转换的类型(string
或number
)。
其执行逻辑如下
- 如果input为对象,就:
- 获取对象上定义的
@@Primitive
方法,也就是Symbol.Primitive
- 如果
@@Primitive
方法不为undefined
,就- 如果
preferredType
未指定,就将hint
默认设置为"default"
- 否则,如果
preferredType
为string
,就把hint
设置为"string"
- 否则,
preferredType
为number
,就把hint
设置为"number"
- 将
result
设置为执行@@Primitive(input, hint)
方法的结果 - 如果
result
的类型不为object
,将resule
作为结果返回 - 抛一个
TypeError
错误
- 如果
- 如果
preferredType
未指定,就默认设置为number
- 返回 调用
OrdinaryToPrimitive(input,preferredType)
方法的结果
- 获取对象上定义的
- 否则将input作为返回值
OrdinaryToPrimitive(O,hint)
如果
hint
为"string"
,就- 将
mthodNames
设置为<<"toString","valueOf">>
- 将
如果
hint
为"number"
,就- 将
mthodNames
设置为<<"valueOf","toString">>
- 将
按顺序取出
mthodNames
中的name
,对于,每一项- 将
method
设置为对应对象上name
的值 - 如果
method
可以被调用,就- 将
result
设置为执行method()
方法的结果 - 如果
result
的类型不为object
,将resule
作为结果返回
- 将
- 将
抛一个
TypeError
错误
这里就是ToPrimitive
的逻辑了,整个逻辑就是优先调用Symbol.Primitive
方法,如果不存在,再根据偏好顺序,执行valueOf
及toString
并作为结果返回。
运算符
在上面我们大致讲了类型转换中,将对象转为原始值的ToPrimitive
方法,它被调用的一个典型案例就是使用+
运算符,也就是本文的例子。在ECMA262 ApplyStringOrNumericBinaryOperator 中,规定了+
运算符的行为。
ApplyStringOrNumericBinaryOperator( lval, opText, rval )
对于字符串或数字的运算都被定义在这个方法下,这里简便起见,我们仅看类型转换的部分
- 如果
opText
为+
,就- 将
lprim
设为ToPrimitive(lval)
执行的结果 - 将
rprim
设为ToPrimitive(rval)
执行的结果 - 如果
lprim
或rprim
的类型为string
,就- 将
lstr
设为ToString(rprim)
执行的结果 - 将
rstr
设为ToString(rprim)
执行的结果 - 将
lstr
与rstr
连接后返回的新字符串作为结果返回
- 将
- 将
lval
设为lprim
- 将
rval
设为rprim
- 将
- 将
lnum
设为ToNumeric(lval)
执行的结果 - 将
rnum
设为ToNumeric(rval)
执行的结果 - 如果
lnum
与rnum
的值不匹配,返回一个TypeError
错误 - 执行定义的数学运算,相关逻辑省略
运行结果解析
有了上面定义的运算符逻辑及对象转换逻辑,我们就能比较清晰的明白上面的例子在执行时,valueOf
及toString
,还有Symbol.toPrimitive
的调用逻辑是什么,有什么区别。
valueOf()
为了方便对比,我们写一个最小化的Demo
1 | const demo = { |
这里按照上述的流程,会执行以下的步骤
demo + 1
, 执行toPrimitive(demo)
toPrimitive(demo)
检查没有@@Primitive
方法,并且没有指定preferredType
,默认偏好类型设置为value。- 按照偏好类型,首先获取
valueOf
方法,并执行 - 执行后,返回
3
,类型不为object
,返回后续结果。 - 执行后续流程。
toString()
实际上,重写toString()
方法也能拿到一样的结果。
1 | const demo = { |
在这种场景下,toString()
和valueOf
是近乎等同的。
Symbol.toPrimitive(hint)
Symbol.toPrimitive
方法是在ES6中出现的,让js有了覆写对象转换为原始值这一行为的能力,入参hint
会根据转换的偏向类型,传入'string'
、'number'
、'default'
,上面的例子也可以使用Symbol.toPrimitive
达到一样的效果。
Symbol.toPrimitive
方法除会接受入参以外,如果返回值未返回原始值类型,就直接抛出TypeError
1 | const demo = { |
而对于toString
和valueOf
方法,由于有默认的Object.prototype.toString
方法,总是会输出对应的对象描述。
如果没有toString
和valueOf
方法,也会抛出这个错误。
1 | Object.create(null) + 1 // Uncaught TypeError: Cannot convert object to primitive value |
小结
这里实际上是第一次阅读规范文档去解决自己的疑惑,规范实际写的非常简洁明了,很多时候要是不明白js的一些行为,阅读规范文档是一个好的办法。
参考文档
MDN Symbol.toPrimitive
ECMA262 Type Conversion
ECMA262 ApplyStringOrNumericBinaryOperator
《你不知道的JavaScript 中卷》强制类型转换