从 ECMAScript 中理解 Symbol 类型转换
Be kind whenever possible. It is always possible. —Dalai Lama
本文为 《lodash 源码阅读》 系列文章,后续内容会在 github 中发布,欢迎 star,gitbook 同步更新。
前言
在阅读 .inernal.createMathOperation 时,发现 lodash 在实现数学运算的时候,均对 Symbol
类型值做了特殊转换处理,下面我们来一探究竟这样做的原因:
特殊性
Symbol
在进行原生类型转换时,存在特殊性。MDN 介绍到使用 symbol
值进行类型转换时需要注意事项:
When trying to convert a symbol to a number, a TypeError will be thrown (e.g. +sym or sym | 0).
Symbol("foo") + "bar" throws a TypeError (can't convert symbol to string). This prevents you from silently creating a new string property name from a symbol, for example.
When using loose equality, Object(sym) == sym returns true.
The "safer" String(sym) conversion works like a call to Symbol.prototype.toString() with symbols, but note that new String(sym) will throw.
我们用代码的形式描述上述情况:
接下来我们将从 ecma-262 标准中理解这些情况发生的原因
隐式转换(implicit conversion)
在 JavaScript 中,当我们进行比较操作或者加减乘除四则运算操作时,常常会触发 JavaScript 的隐式类型转换机制。其中上述的 1、2 两点便是隐式转换带来的影响,我们先来看一下标准中 加法运算 的规则:
Runtime Semantics: Evaluation# AdditiveExpression:AdditiveExpression+MultiplicativeExpression
Let lref be the result of evaluating AdditiveExpression.
Let lval be ?
GetValue
(lref). // 通过内置 GetValue 方法获取参数,Symbol 类型直接返回自身Let rref be the result of evaluating MultiplicativeExpression.
Let rval be ?
GetValue
(rref).Let lprim be ?
ToPrimitive
(lval). // 通过内置 ToPrimitive 方法获取参数原始值,Symbol 类型直接返回自身Let rprim be ?
ToPrimitive
(rval).If
Type
(lprim) is String orType
(rprim) is String, thena. Let lstr be ?
ToString
(lprim).b. Let rstr be ?
ToString
(rprim).c. Return the String that is the result of concatenating lstr and rstr.
Let lnum be ?
ToNumber
(lprim).Let rnum be ?
ToNumber
(rprim).Return the result of applying the addition operation to lnum and rnum. See the Note below 12.8.5.
顺着这份规则顺序,当程序执行字符串加法 symbol + 'bar'
时,程序会对参数执行多次内置方法进行隐式转化,最终我们会进入第 7 点的分叉,这是很关键的一步,程序会对两个参数执行内置 ToString
方法进转换,接下来我们来认识下 ToString
方法。
ToString 转换
ToString 转换规则如下:
参数类型
返回结果
Undefined
返回 "undefined"
Null
返回 "Null"
Boolean
如果参数为真,返回 "true",否则返回 "false"
Number
String
返回参数自身
Symbol
抛出 TypeError
异常
Object
执行下面步骤: 1. 将参数以字符串优先形式执行 ToPrimitive. 2. 将原始值化的值再进行 ToString 运算
我们可以看到,只要是 Symbol
类型值进行 ToString
转换,程序直接抛出 TypeError
异常,ToNumber 方法也是如此,这样就讲得通为什么直接将 Symbol
值进行四则运算时,系统会抛出 TypeError: Cannot convert a Symbol value to a TYPE
的错误了。 为什么规范制定者们要加这条规则呢?
从 MDN 里那句 This prevents you from silently creating a new string property name from a symbol, for example.
可以理解到,这其中一个原因是为了避免开发者在无意中使用 Symbol 类型值隐式的创建了一个字符串类型属性名,可能出现的代码情况如下:
ToPrimitive 转换
特殊性 中描述的第 3 点
When using loose equality, Object(sym) == sym returns true.
当包装对象 Object(sym)
与原始值 sym
进行宽松对比时,返回值为 true
。 ecma262 - Abstract Equality Comparison 中这么规定这种对比方式:
If Type(x) is Object and Type(y) is either String, Number, or Symbol, return the result of the comparison ToPrimitive(x) == y.
当 对象类型 与 基本类型(String/Number/Symbol
)进行宽松对比时,会将 对象类型值 传入 ToPrimitive 方法进行 拆箱转换
成基本类型值,后再进行一次宽松对比,而 Symbol
包装对象 Object(sym)
拆箱后得到的便是 sym
,即:
String() 与 new String() 差异
在 ecma262 - String(value) 是这么定义 String(value)
的:
When String is called with argument value, the following steps are taken:
If no arguments were passed to this function invocation, let s be "".
Else,
a. If
NewTarget
is undefined and Type(value) is Symbol, return SymbolDescriptiveString(value).b. Let s be ? ToString(value).
If
NewTarget
is undefined, return s.Return ? StringCreate(s, ? GetPrototypeFromConstructor(
NewTarget
, "%StringPrototype%")).
其中 NewTarget 是在使用 new
操作符实例化对象后产生的值(即调用了构造器生成对象),即执行 String()
时,程序会进入 2-a
流程,而 new String()
则会进入 2-b
流程
我们可以看出,SymbolDescriptiveString 便是 Symbol.prototype.toString() 最终执行的方法,所以 String(symbol)
最终能成功转化为对应的字符串值,而 new String(symbol)
在执行过程中,调用了 ToString
进行隐式转换,在 ToString 转换 中我们已经了解到此时程序会抛出 TypeError
异常,流程也就不会继续往下走了。
结语
到这里,我们便把 Symbol
类型转换的特殊部分,从语言标准规范中理解了一遍。如果这篇文章有帮助到你,给个 star✨ 呗 🍭~
相关链接
参考
Last updated
Was this helpful?