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

+ 运算符 #7

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

+ 运算符 #7

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

+ 运算符

JavaScript 让人疑惑的地方之一可能就是隐式转换。

我们可能都见过下面这些代码:

{} + {}; // "[object Object][object Object]"
[] + {}; // "[object Object]"
+[];     // 0

不同于其他语言,这样的结果让人很困惑。

下面就以 + 运算符(包括:一元 + 运算符和加法运算符(+))介绍 JavaScript 的隐式转换。

加法运算符 +

无论隐式转换有怎么样的怪异行为,都需要遵循 ECMAScript 的规定。

加法运算符(+)

加法运算符执行字符串相接或是数值相加。

产生式 AdditiveExpression : AdditiveExpression + MultiplicativeExpression 按照下面的过程执行:

  1. 令 lref 为解释执行 AdditiveExpression 的结果。
  2. 令 lval 为 GetValue(lref)。
  3. 令 rref 为解释执行 MultiplicativeExpression 的结果。
  4. 令 rval 为 GetValue(rref)。
  5. 令 lprim 为 ToPrimitive(lval)。
  6. 令 rprim 为 ToPrimitive(rval)。
  7. 如果 Type(lprim) 为 String 或者 Type(rprim) 为 String,则:
    1. 返回由 ToString(lprim) 和 ToString(rprim) 连接而成的字符串。
  8. 返回将加法运算作用于 ToNumber(lprim) 和 ToNumber(rprim) 的结果。参见 11.6.3 后的注解。

注:在 第5步第6步ToPrimitive 的调用没有提供暗示类型。所有除了 Date 对象的 ECMAScript 原生对象会在没有提示的时候假设提示是 NumberDate 对象会假设提示是 String。宿主对象可以假设提示是任何东西。

注:第7步 与关系比较算法的 第3步 不同(11.8.5),使用逻辑 “或” 而不是 “与”。

加法运算符目的就是执行字符串相接或者数值相加。至于开始举例代码,是加法运算符把对象数组转为字符串或者数字之后相加的结果。

前六点,是转为字符串或数值的过程。需要关注内部方法:ToPrimitive

ToPrimitive

ToPrimitive 抽象操作接受一个值,和一个可选的期望类型作参数。ToPrimitive 运算符把其值参数转换为非对象类型。如果对象有能力被转换为不止一种原语类型,可以使用可选的期望类型来暗示那个类型。根据下表完成转换:

输入类型 结果
未定义   结果等于输入的参数(不转换)。
空值 结果等于输入的参数(不转换)。
布尔值 结果等于输入的参数(不转换)。
数值 结果等于输入的参数(不转换)。
字符串 结果等于输入的参数(不转换)。
对象 返回该对象的默认值。调用该对象的内部方法 [[DefaultValue]] 来恢复这个默认值,调用时传递暗示期望类型(所有 ECAMScript 本地对象的 [[DefaultValue]] 一樣)。

ToPrimitive 把值转为非对象类型。如果参数是非对象,则不转换,否则根据对象内部方法 [[DefaultValue]] 返回默认值。

[[DefaultValue]]

[[DefaultValue]] (hint)

当 暗示 String 时调用 O 的 [[DefaultValue]] 内部方法,采用以下步骤:

  1. 令 toString 为用参数 "toString" 调用对象 O 的 [[Get]] 内部方法的结果。
  2. 如果 IsCallable(toString) 是 true,则
    1. 令 str 为用 O 作为 this 值,空参数列表调用 toString 的 [[Call]] 内部方法的结果。
    2. 如果 str 是原始值,返回 str。
  3. 令 valueOf 为用参数 "valueOf" 调用对象 O 的 [[Get]] 内部方法的结果。
  4. 如果 IsCallable(valueOf) 是 true,则
    1. 令 val 为用 O 作为 this 值,空参数列表调用 valueOf 的 [[Call]] 内部方法的结果。
    2. 如果 val 是原始值,返回 val。
  5. 抛出一个 TypeError 异常。

当 暗示 Number 时调用 O 的 [[DefaultValue]] 内部方法,采用以下步骤:

  1. 令 valueOf 为用参数 "valueOf" 调用对象 O 的 [[Get]] 内部方法的结果。
  2. 如果 IsCallable(valueOf) 是 true,则
    1. 令 val 为用 O 作为 this 值,空参数列表调用 valueOf 的 [[Call]] 内部方法的结果。
    2. 如果 val 是原始值,返回 val。
  3. 令 toString 为用参数 "toString" 调用对象 O 的 [[Get]] 内部方法的结果。
  4. 如果 IsCallable(toString) 是 true,则
    1. 令 str 为用 O 作为 this 值,空参数列表调用 toString 的 [[Call]] 内部方法的结果。
    2. 如果 str 是原始值,返回 str。
  5. 抛出一个 TypeError 异常。

当没有指定 暗示 时调用 O 的 [[DefaultValue]] 内部方法,除了 Date 对象的行为用 暗示 String 来处理以外,其它情况的行为都作为 暗示 Number 来处理。

上面说明的 [[DefaultValue]] 在原生对象中只能返回原始值。如果一个宿主对象实现了它自身的 [[DefaultValue]] 内部方法,那么必须确保其 [[DefaultValue]] 内部方法只能返回原始值

hint 参数只有 String 和 Number 两种类型。而且只有 Date 对象使用 String。hint 参数如果是 String 就先调用 toString 函数,后调用 valueOf 函数,否则顺序相反。

这里以 hint 参数为 Number 为例:先调用 valueOf ,在调用 toString。

注意最后一段:

上面说明的 [[DefaultValue]] 在原生对象中只能返回原始值。如果一个宿主对象实现了它自身的 [[DefaultValue]] 内部方法,那么必须确保其 [[DefaultValue]] 内部方法只能返回原始值

[[DefaultValue]] 只能返回原始值。即使宿主对象实现自身的 [[DefaultValue]] 内部方法也要保证返回原始值。

我们不能修改 [[DefaultValue]] 内部方法,但是可以修改 valueOf 或 toString 方法。

let obj = {
	valueOf: function () {
		return 1;
	}
};

console.log(1 + obj); // 1

这里采用了一元运算符 + ,也会使用到 [[DefaultValue]] 内部方法,这个后面介绍。如果 valueOf 返回的不是原始值

let obj = {
	valueOf: function () {
		return {};
	}
};

console.log(+obj); // NaN

结果是 NaN,因为在 [[DefaultValue]] 内部方法中,会抛出 TypeError 异常。一元运算符 + 对于未定义的值都会返回 NaN。

回到加法运算符规则中。

第七点,如果经过 ToPrimitive 之后,存在至少一个字符串,那么结果就是 ToString(lprim) 和 ToString(rprim) 连接而成的字符串。

第八点,如果都没有字符串,那么返回将加法运算作用于 ToString(lprim) 和 ToString(rprim) 的结果。至于这个结果也是有规定的:

加法作用于数字

+ 运算符作用于两个数字类型的操作数时表示加法,产生两个操作数之和。- 运算符表示减法,产生两个数字之差。

加法是满足交换律的运算,但是不总满足结合律。

加法遵循 IEEE 754 二进制双精度幅度浮点算法规则:

  • 若两个操作数之一为 NaN,结果为 NaN
  • 两个正负号相反的无穷之和为 NaN
  • 两个正负号相同的无穷大之和是具有相同正负的无穷大。
  • 无穷大和有穷值之和等于操作数中的无穷大。
  • 两个负零之和为 -0
  • 两个正零,或者两个正负号相反的零之和为 +0
  • 零与非零有穷值之和等于非零的那个操作数。
  • 两个大小相等,符号相反的非零有穷值之和为 +0
  • 其它情况下,既没有无穷大也没有 NaN 或者零参与运算,并且操作数要么大小不等,要么符号相同,结果计算出来后会按照 IEEE 754 round-to-nearest 取到最接近的能表示的数。如果值过大不能表示,则结果为相应的正负无穷大。如果值过小不能表示,则结果为相应的正负零。ECMAScript 要求支持 IEEE 754 规定的渐进下溢。

- 运算符作用于两个数字类型时表示减法,产生两个操作数之差。左边操作数是被减数右边是减数。给定操作数 a 和 b,总是有 a–b 产生与 a + (-b) 产生相同结果。

总的来说这个结果就是平时我们的数学加法算法,但是考虑了无穷大、无穷小、+0、-0 之类的情况。

ToString 和 ToNumber

在最后两点中出现了 ToString 和 ToNumber。

ToString

ToString 运算符根据下表将其参数转换为字符串类型的值:

输入类型 结果
未定义   "undefined"
空值 "null"
布尔值 如果参数是 true,那么结果为 "true"。如果参数是 false,那么结果为 "false"
数值 9.8.1
字符串 返回输入的参数(不转换)。
对象 应用下列步骤:令 primValue 为 ToPrimitive(输入参数, 暗示字符串类型)。返回 ToString(primValue)。

需要注意的是数值的情况,需要根据以下规则转换:

ToString 抽象操作将数字 m 转换为字符串格式的给出如下所示:

  1. 如果 m 是 NaN,返回字符串 "NaN"
  2. 如果 m 是 +0-0,返回字符串 "0"
  3. 如果 m 小于零,返回连接 "-"ToString(-m) 的字符串。
  4. 如果 m 无限大,返回字符串 "Infinity"
  5. 否则,令 n、k 和 s 皆为整数,且满足 k ≥ 110k-1 ≤ s < 10k 、 s×10n-k 的数字值为 m 且 k 足够小。注意 k 是 s 的十进制位数。s 不能被 10 整除,且 s 的至少要求的有效数字位数不一定要被这些标准唯一确定。 Note.png
  6. 如果 k ≤ n ≤ 21,返回由 k 个 s 在十进制表示中的数字组成的字符串(有序的,开头没有零),后面连接 n-k 个 "0" 字符。
  7. 如果 0 < n ≤ 21,返回由 s 在十进制表示中的、最多 n 个有效数字组成的字符串,后面跟随一个小数点 ".",再后面是余下的 k-n 个 s 在十进制表示中的数字。
  8. 如果 -6 < n ≤ 0,返回由字符 "0" 组成的字符串,后面跟随一个小数点 ".",再后面是 -n 个 "0" 字符,最后是 k 个 s 在十进制表示中的数字。
  9. 否则,如果 k = 1,返回由单个数字 s 组成的字符串,后面跟随小写字母 "e",根据 n-1 是正或负,再后面是一个加号 "+" 或减号 "-" ,再往后是整数 abs(n-1) 的十进制表示(没有前置的零)。
  10. 返回由 s 在十进制表示中的、最多的有效数字组成的字符串,后面跟随一个小数点 ".",再后面是余下的是 k-1 个 s 在十进制表示中的数字,再往后是小写字母 "e",根据 n-1 是正或负,再后面是一个加号 "+" 或减号 "-" ,再往后是整数 abs(n-1) 的十进制表示(没有前置的零)。

注:下面的评语可能对指导实现有用,但不是本标准的常规要求。

  • 如果 x 是除 -0 以外的任一数字值,那么 ToNumber(ToString(x)) 与 x 是完全相同的数字值。
  • s 至少要求的有效数字位数并非总是由 第5步 中所列的要求唯一确定。

注:对于那些提供了比上面的规则所要求的更精确的转换的实现,我们推荐下面这个步骤 5 的可选版本,作为指导:

否则,令 n, k, 和 s 是整数,使得 k ≥ 1, 10k-1 ≤ s < 10ks×10****n-k 的数字值是 m,且 k 足够小。如果有数倍于 s 的可能性,选择 s × 10****n-k 最接近于 m 的值作为 s 的值。如果 s 有两个这样可能的值,选择是偶数的那个。要注意的是,ks 在十进制表示中的数字的个数,且 s 不被 10 整除。

注:ECMAScript 的实现者们可能会发现,David M 所写的关于浮点数进行二进制到十进制转换方面的文章和代码很有用:

这个规则非常复杂,记住前四点就可以了

如果对象转字符串时,需要先 ToPrimitive(obj, String) 转为原始值(在加法运算符时,到达这一步时,已经经过了 ToPrimitive 转换了,不存在对象的情况)。

如果是 symbol 转字符串,会抛类型错误 TypeError (ES6 中规定)。

ToNumber

ToNumber 抽象操作根据下表将其参数转换为数值类型的值:

输入类型 结果
未定义   NaN
空值 +0
布尔值 如果参数是 true,结果为 1。如果参数是 false,此结果为 +0
数字 结果等于输入的参数(不转换)。
字符串 参见下文的文法和注释。
对象 应用下列步骤:令primValue为 ToPrimitive(输入参数, 暗示数值类型)。返回 ToNumber(primValue)。

需要注意的是字符串转数字,直接看这里,也是非常复杂。

对象转数字,也需要先 ToPrimitive(obj, Number) 转为原始值,然后才转为数字。

如果是 symbol 转数字,会抛类型错误 TypeError (ES6 中规定)。

具体分析

{} + {} // "[object Object][object Object]"

首先分析 {} 经过 ToPrimitive 会得到什么值。

因为 {} 是对象,会调用对象的 [[DefaultValue]] 内部方法。{} 不是 Date 类型,所以 hint 是 Number,会先调用对象的 valueOf 方法,返回 {} 自身,不是原始值,继续调用对象 toString 方法,返回 “[object Object]” 字符串。

因为 ToPrimitive({}) 结果是字符串,所以把两个结果连接起来就得到 “[object Object][object Object]”。

[] + {} // "[object Object]"

[] 也是会调用对象的 [[DefaultValue]] 内部方法。因为数组没有 valueOf 方法,所以调用 toString 方法,返回的值为空 “”。ToPrimitive({}) 的值我们知道是 “[object Object]” ,所以结果就是 “[object Object]” 。

{} + [] // "[object Object]"

{} + [] 在最新的浏览器中测试结果是 "[object Object]",已经不存在网上说的结果为 0 的情况。

一元运算符 +

一元运算符 +

产生式 UnaryExpression : + UnaryExpression 按照下面的过程执行 :

  1. 令 expr 为解释执行 UnaryExpression 的结果。
  2. 返回 ToNumber(GetValue(expr))。

一元运算符 + 规则比较简单,直接返回 ToNumber(GetValue(expr)) 结果。ToNumber 在上面已经介绍过了。

let obj = {};

console.log(+obj); // NaN
let obj = {
	valueOf: function () {
		return 1;
	}
};

console.log(+obj); // 1

参考资料

ECMAScript 英文文档

ECMAScript 维基百科 中文文档

js隐式装箱-ToPrimitive

深入浅出弱类型JS的隐式转换

怪异的JavaScript系列(三)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant