Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

重学js —— 类型转换 #54

Open
lizhongzhen11 opened this issue Nov 13, 2019 · 0 comments
Open

重学js —— 类型转换 #54

lizhongzhen11 opened this issue Nov 13, 2019 · 0 comments
Labels
js基础 Good for newcomers 重学js 重学js系列 规范+MDN

Comments

@lizhongzhen11
Copy link
Owner

lizhongzhen11 commented Nov 13, 2019

抽象运算 —— 类型转换

阅读这章之前,先思考下面代码:

+true // 1
+false // 0

+null // 0
+undefined // NaN

+'-0' // -0
+'Infinity' // Infinity
+'-Infinity' // -Infinity

''+(-0) // "0"
''+(-Infinity) // "-Infinity"

Boolean(NaN) // false
Boolean(Infinity) // true
Boolean(-Infinity) // true

Boolean(Symbol()) // true
+Symbol() // 报错
''+Symbol() // 报错
Symbol().toString() // "Symbol()"

// 对象转换
+{} // NaN
+[] // 0
+[1] // 1
+[Infinity] // Infinity
+[-0] // 0
+ [1, 2] // NaN
+[{}] // NaN
''+[] // ""
''+[1] // "1"
''+[1,2,3] // "1,2,3"
''+[Infinity, Infinity] // "Infinity,Infinity"
''+{} // "[object Object]"
''+function test (){} // "function test (){}"
[1, 2] + [2, 1] // '1,22,1'

老实说,我不能完全正确说出结果,也不能保证说出 为什么?

正因为我的js基础如此之垃圾,所以阅读规范中关于类型转换一章并翻译,同时配合MDN进行学习,希望有底气的回答上面代码的表现行为及原因。

正文

抽象运算不是ECMAScript语言的一部分;规范中定义它们仅是为了帮助指定ECMAScript语言的语义。

当需要时,ECMAScript会自动隐式执行类型转换。类型转换抽象运算只能接收 ECMAScript语言类型,不能接收 规范类型。

BigInt类型没有隐式类型转换;程序必须显式的调用BigInt进行类型转换。

ToPrimitive ( input [ , PreferredType ] )

将输入的input转换为非Object类型值。如果对象能够转换为多个原始类型,则可以使用可选提示PreferredType来确定需要转换成哪个类型。

  • 如果inputObject类型
    • 如果 PreferredType 不存在,定义过程变量 hint 赋值为 "default"
    • 如果 PreferredTypeString,定义过程变量 hint 赋值为 "string"
    • 否则,
      • 断定 PreferredTypeNumber
      • 定义过程变量 hint 赋值为 "number"
    • 定义过程变量exoticToPrim,执行GetMethod(input, @@toPrimitive)并赋值给exoticToPrim
    • 如果exoticToPrim不是undefined
      • 定义过程变量 result,调用 Call(exoticToPrim, input, « hint ») 并赋值给 result
      • 如果 result 不是 Object 类型则返回 result
      • 否则报类型错误
    • 如果 hint"default",将 hint 赋值为 "number"
    • 返回 调用OrdinaryToPrimitive(input, hint)的返回值
  • 如果input一开始就不是Object类型,直接原路返回

注意:如果没传 PreferredType,那么默认就是 number。 但是可以通过定义一个 @@toPrimitive 来覆盖默认行为。DateSymbol对象会覆盖默认的ToPrimitive行为。当没有PreferredType时,Date默认转为string

OrdinaryToPrimitive ( O, hint )

主要是为了将 Object 转为 原始数据类型。

  • O 必须是对象
  • hint 必须是 String 类型且值为 "string""number"
  • 如果 hint"string"
    • 定义过程变量 methodNames,值为List « "toString", "valueOf" »
  • 如果 hint"number"
    • 定义过程变量 methodNames,值为List « "valueOf", "toString" »
  • 顺序遍历 methodNames 这个List,定义过程变量 name 来缓存List中的每个值
    • 定义过程变量 method,执行 Get(O, name)赋值给 method
    • 执行 IsCallable(method),如果为 true
      • 定义过程变量 result,执行 Call(method, O)赋值给 result这里本质上就是在执行 valueOf() 或 toString(),先后顺序看methodNames这个List
      • 如果 result 不是 Object,则返回 result
  • 如果以上都不满足,报类型错误

ToBoolean ( argument )

直接看表就好:

参数类型 结果
Undefined false
Null false
Boolean argument
Number +0, -0, NaN 都返回 false,其余都是 true
String 空字符串即长度为0的返回false,否则都是true
Symbol true
BigInt 0n 返回 false,其余都是true
Object true

ToNumeric ( value )

顾名思义,转为数值的,只不过有NumberBigInt两种可能。

ToNumber ( argument )

这个是转换为Number类型的了。见下表:

参数类型 结果
Undefined NaN
Null 0
Boolean true => 1, false => +0
Number argument
String string转number
Symbol 类型报错
BigInt 规范目前写的有问题,见tc39/ecma262#1766
Object 先转为原始值;然后将原始值转为Number

注意:

2019.11.14号,ECMAScript262规范目前规定传入BigInt参数会报错,从测试来看,使用 + 操作应该默认的是ToNumber,符合要求;但是使用Number()却能正常转换,而该算法内部依然使用ToNumber,却不报错?

+0n // Uncaught TypeError: Cannot convert a BigInt value to a number
Number(+0n) // Uncaught TypeError: Cannot convert a BigInt value to a number
Number(0n) // 0

我去知乎提过这个问题,有大佬指出这是规范编写时的bug,还没有修复,见知乎

根据链接我去查看了issue里面的其它链接,ToNumber ( argument )这里依然是抛类型错误,但是下面的 注意 告诉我:

该规范的主要设计决策是禁止隐式转换,并迫使程序员自己使用显式转换。

此外,最新的BigInt 文档补丁里的Number(value)方法内部把ToNumber(value) 改成 ToNumeric(value)了

String转Number

针对String转Number,如果语法无法将String解释为StringNumericLiteral的扩展,则ToNumber的结果为 NaN

该语法的结尾符号全部由Unicode基本多语言平面(BMP)中的字符组成。因此,如果字符串包含任何成对或不成对的前导代理尾随代理代码单元,则ToNumber的结果将为NaN。

注意:

  • StringNumericLiteral可以包括 前导 和 尾随空白 以及 行终止符
  • 十进制的StringNumericLiteral可以具有任意数量的前导 0 数字
  • 十进制的StringNumericLiteral可以包含 +- 表示其符号
  • 空 或 仅包含空格 的StringNumericLiteral会转换为 +0
  • Infinity-InfinityStringNumericLiteral 认可,但它们不是 NumericLiteral
  • StringNumericLiteral 不能包含 BigIntLiteralSuffix

String类型的数字转换为Number类型的数字见:https://tc39.es/ecma262/#sec-runtime-semantics-mv-s

经过测试,小数点如果超过15位,那么会看第16位的值,然后不断进行四舍五入。本质还是和Number类型一样的。

测试:

+'9.999999999999999999999' // 10
+'9.1234567891234567891234' // 9.123456789123457
+'9.123456789999999' // 9.123456789999999
+'9.1234567899999991234567' // 9.123456789999999
+'9.1234567899999999234567' // 9.12345679
+'9.123456789123499' // 9.123456789123498
+'99999999999999999999' // 100000000000000000000

ToInteger( argument )

  • 先执行ToNumber(argument)并将值赋给过程变量number
  • 如果numberNaN,则返回 +0
  • 如果number+0, -0, +Infinity 或 -Infinity,则直接返回number(这里用Infinity代替规范中的符号)
  • 以上都不符合,则返回与number相同符号的 Number类型值,其大小是 floor(abs(number))。先取正,再四舍五入。

ToInt32 ( argument )

argument 转成32位(4字节)有符号的整数,取值范围在 Math.pow(2, -31) ~ Math.pow(2, 31) - 1 之间。

  • 先执行 ToNumber(argument) 并将值赋给过程变量 number
  • 如果 numberNaN, +0, -0, +Infinity-Infinity,则返回 +0
  • 定义过程变量 int,其值是 与 number 相同符号的 Number类型值且其大小是 floor(abs(number))
  • 定义过程变量 int32bit,其值为对 int 执行 2的32次幂 按模运算
  • 如果 int32bit >= 2的31次幂,返回 int32bit - 2的32次幂;否则返回 int32bit

ToUint32 ( argument )

argument 转成32位无符号的整数,取值范围在 0 ~ Math.pow(2, 32) - 1 之间。

  • 前 4 步与 ToInt32 一致
  • 最后直接返回 int32bit

ToInt16 ( argument )

转为16位(2字节)有符号整数,取值范围在 -32768 ~ 32767

  • 前三步与 ToInt32 一致
  • 定义过程变量 int16bit,其值为对 int 执行 2的16次幂 按模运算
  • 如果 int32bit >= 2的15次幂,返回 int16bit - 2的16次幂;否则返回 int16bit

ToUint16 ( argument )

转为16位(2字节)无符号整数,取值范围在 0 ~ Math.pow(2, 16) - 1

  • 前四步与 ToInt16 一致
  • 最后直接返回 int16bit

ToInt8 ( argument )

转为 8 位有符号整数,取值范围在 -128 ~ 127,即 Math.pow(2, -7) ~ Math.pow(2, 7) - 1

  • 前三步与 ToInt16 一致
  • 定义过程变量 int8bit,其值为对 int 执行 2的8次幂 按模运算
  • 如果 int8bit >= 2的7次幂,返回 int8bit - 2的8次幂;否则返回 int8bit

ToUint8 ( argument )

转为 8 位无符号整数,取值范围在 0 ~ 255,即 0 ~ Math.pow(2, 8) - 1

  • 前四步与 ToInt8 一致
  • 最后直接返回 int8bit

ToUint8Clamp ( argument )

目的和 ToUint8 一样,但是过程不一样

  • 定义过程变量 number,将 ToNumber(argument) 结果赋值给 number
  • 如果 numberNaN,返回 +0
  • 如果 number <= 0, 返回 +0
  • 如果 number >= 255, 返回 255
  • 如果不符合以上条件,定义过程变量 f,对 number 四舍五入并赋值给 f
  • 如果 f + 0.5 < number,返回 f + 1
  • 如果 f + 0.5 > number, 返回 f
  • 如果 f 是奇书,返回 f + 1
  • 都不满足,返回 f

ToUint8Clamp 和 Math.round 不同,ToUint8Clamp 舍入到一半,但是 Math.round 是四舍五入。

ToBigInt ( argument )

  • 定义过程变量 prim,执行 ToPrimitive(argument, hint Number) 将结果赋值给 prim
  • prim 值对照下表返回
参数类型 结果
Undefined 返回类型错误
Null 返回类型错误
Boolean true => 1n,false => 0n
BigInt 返回prim
Number 返回类型错误
String 执行StringToBigInt(prim),如果结果是NaN,返回类型错误,否则返回结果
Symbol 返回类型错误

其实这里对 Number 类型的参数执行结果和上面的 ToNumber 一样令人困惑,但这里的本意是指 禁止我们开发中将Number隐式转换为BigInt,要想转换,必须显式调用

根据 重学js —— js数据类型:BigInt 对象 一章,BigInt(Number 类型值) 会做特殊处理,不会走 ToBigInt 方法

StringToBigInt ( argument )

调用 https://tc39.es/ecma262/#sec-tonumber-applied-to-the-string-type 处的算法,略微有点不同:

  • DecimalDigits替换StrUnsignedDecimalLiteral,不允许无穷大,小数点或指数。
  • 如果数学值是 NaN,返回 NaN;否则,返回完全对应于数学值的BigInt,而不是四舍五入为数字。

ToBigInt64 ( argument )

转换为64位有符号整数值,取值范围 Math.pow(2, -63) ~ Math.pow(2, 63) - 1

  • 定义过程变量 n,调用 ToBigInt(argument) 将结果赋值给 n
  • 定义过程变量 int64bit,对 n 进行2的64次幂 取模运算,将结果赋值给 int64bit
  • 如果 int64bit >= Math.pow(2, 63),返回 int64bit - Math.pow(2, 64);否则返回 int64bit

ToBigUint64 ( argument )

转换为64位无符号整数值,取值范围 0 ~ Math.pow(2, 64) - 1

  • 前两步与 ToBigInt64 一致
  • 返回 int64bit

ToString ( argument )

见表:

参数类型 结果
Undefined "undefined"
Null "null"
Boolean true => "true",false => "false"
Number 返回! Number::toString(argument)
String 返回 argument
Symbol 类型错误
BigInt 返回! BigInt::toString(argument)
Object 执行ToPrimitive(argument, hint String) 将值赋给 primValue ,再返回 ToString(primValue)

这里 Symbol 转为 String,调用 toString() 或者 String() 来转换是OK的,但是直接使用 "" + Symbol() 会报错,本意应该还是希望我们进行显式转换。

ToObject ( argument )

见下表:

参数类型 结果
Undefined 类型错误
Null 类型错误
Boolean 返回一个Boolean对象,其[[BooleanData]]内置插槽值为argument,详见Boolean Objects
Number 返回一个Number对象,其[[NumberData]]内置插槽值为argument,详见Number Objects
String 返回一个String对象,其[[StringData]]内置插槽值为argument,详见String Objects
Symbol 返回一个Symbol对象,其[[SymbolData]]内置插槽值为argument,详见Symbol Objects
BigInt 返回一个BigInt对象,其[[BigIntData]]内置插槽值为argument,详见BigInt Objects
Object 返回argument

ToPropertyKey ( argument )

听名字就知道,转换成对象能用的属性类型:String 和 Symbol

ToLength ( argument )

给类数组对象用的,确定其长度

  • 先执行 ToInteger(argument) 并赋值给 len
  • 如果 len <= +0,返回 +0
  • 否则返回 len 与 Math.pow(2, 53) 两者中较小的数

CanonicalNumericIndexString ( argument )

如果是ToString会生成的Number的字符串表示形式,则返回转换为数值的参数,或者是字符串 "-0"。否则返回 undefined

有点绕人,就是看argument能不能转换成 Number,不能返回 undefined,能得话区分 -0

ToIndex ( value )

如果是有效的整数索引值,则将其值参数转换为非负整数。

回归代码

回到开篇给出的那些代码,结合规范,其中一些代码能直接给出原因,例如:

+true // 1
+ false // 0
+null // 0
+undefined // NaN
Boolean(NaN) // false
Boolean(Infinity) // true
Boolean(-Infinity) // true
Boolean(Symbol()) // true
+Symbol() // 报错
''+Symbol() // 报错
+'-0' // -0 这里看上文的String转Number
+'Infinity' // Infinity 这里看上文的String转Number
+'-Infinity' // -Infinity 这里看上文的String转Number

但是还有很多代码,仍需要深究为什么?

例如以下代码,需要看Number::toString(argument)

''+(-0) // "0"
''+(-Infinity) // "-Infinity"

Number::toString(argument)中规定无论 +0 还是 -0,都返回 "0"小于0的argument会返回带-的字符串"-argument"。

还有与对象相关的转换为 NumberString

+{} // NaN
+[] // 0
+[1] // 1
+[Infinity] // Infinity
+[-0] // 0
+ [1, 2] // NaN
+[{}] // NaN
''+[] // ""
''+[1] // "1"
''+[1,2,3] // "1,2,3"
''+[Infinity, Infinity] // "Infinity,Infinity"
''+{} // "[object Object]"
''+function test (){} // "function test (){}"

上面不论是转为 Number 还是 String,其本质都需要调用 ToPrimitive 算法,问题的关键点就在该算法内部调用的OrdinaryToPrimitive算法的规则。其实本质上就是调用对象的 valurOf()toString()

实现个ToPrimitive

const ToPrimitive = (input, PreferredType) => {
  if (typeof input !== 'object' && typeof input !== "symbol") {
    return input;
  }
  let hint;
  if (!PreferredType) {
    hint = "defaullt";
  } else {
    hint = typeof PreferredType === 'String' ? "string" : "number";
  }
 if (typeof input === "symbol") {
    let exoticToPrim = Symbol.prototype[Symbol.toPrimitive];
    if (exoticToPrim !== undefined) {
      let result = exoticToPrim.call(input);
      if (typeof result !== 'object') {
        return result;
      } else {
        throw "TypeError";
      }
    }
  }
  if (hint === "defaullt") {
    hint = "number";
  }
  if (hint === "number") {
    if (typeof input.valueOf() !== 'object') {
      return input.valueOf();
    }
    if (typeof input.toString() !== 'object') {
      return input.toString();
    }
  } else {
    if (typeof input.toString() !== 'object') {
      return input.toString();
    }
    if (typeof input.valueOf() !== 'object') {
      return input.valueOf();
    }
  }
  throw "TypeError";
}

参考

  1. Js中的对象转换为数值类型时是不是返回结果都是NaN?
  2. JS那些巧妙的数据类型转换--实用版

2020-07-29 补充

数组类型转换时犯浑了!

[1, 2] + [2, 1] // 1,22,1

我误以为数组和对象的 toString 返回一致,其实我记错了,原题啊都能记错!!!

({}).toString() // "[object Object]"

我TM看到 [1, 2] 这种心想怎么转为数值啊?是不是转为 "[object Array]" 啊???
其实数组转为原始值,存在多个元素的话会通过类似 join(',') 的操作转为字符串的!!!

@lizhongzhen11 lizhongzhen11 added the js基础 Good for newcomers label Nov 13, 2019
@lizhongzhen11 lizhongzhen11 added the 重学js 重学js系列 规范+MDN label Jan 6, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
js基础 Good for newcomers 重学js 重学js系列 规范+MDN
Projects
None yet
Development

No branches or pull requests

1 participant