为什么 0.._ 等于 undefined
# 前言
今天看文章 为什么用「void 0」代替「undefined」 (opens new window) 的时候,
作者提到,用 void 0
替代 undefined
的原因其中有一点是前者更短,更省空间。
当然最主要的原因还是 undefined 在局部作用域中可以被重写
下面有人回复 0.._
长度更短,结果也是 undefined
。 后面解释说是相当于 0['_']
,不过没有更深入的讨论了。
当时心中产生了几个问题:
0.._
是如何隐式转换成undefined
的- 为何(几乎)没有人采用
0.._
的写法代替void 0
# 0.._
的隐式转换
# 词法分析
对于10进制数字来说,后面接 .
操作符,js 引擎并不知道该 .
是小数点还是访问属性的 .
,因此有如下规定:
前面的数字为10进制,已带小数点的,则该 .
是访问属性,否则即为小数点;
若不是10进制,则 .
是访问属性
0.0._ // 输出 undefined 相当于 (0.0)._
0.._ // 相当于 (0.)._
00._ // 前面为 8进制
true._ // 输出 undefined
0._ // 语法错误 .后面应该接数字
'use strict';
00._ // Uncaught SyntaxError: Octal literals are not allowed in strict mode. 严格模式下不会解析成八进制
2
3
4
5
6
7
8
注:以上是测试得出的结论,规范中没找到。
不过按编译原理的知识,引擎会先根据 词法解析-数值字面量 (opens new window) 找到 0.
这个数值字面量词法,接着才进行语法分析
同时 附加语法-数值字面量 (opens new window) 中提到非 strict 模式下 NumericLiteral 才允许 OctalIntegerLiteral 八进制的词法
# 语法分析
接下来就是 为何数值字面量能够进行属性访问 的问题了。这是一个左值表达式。
左值表达式 (opens new window) 语法,这里列举部分
LeftHandSideExpression :
NewExpression
CallExpression
CallExpression :
MemberExpression Arguments
CallExpression Arguments
CallExpression [ Expression ]
CallExpression . IdentifierName
MemberExpression :
PrimaryExpression
FunctionExpression
MemberExpression [ Expression ]
MemberExpression . IdentifierName
new MemberExpression Arguments
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
左值表达式-属性访问 (opens new window) 有两者方式
- MemberExpression . IdentifierName
- MemberExpression [ Expression ]
前者等同于 MemberExpression [ <identifier-name-string> ]
<identifier-name-string>
是一个字符串字面量,它与 Unicode 编码后的 IdentifierName 包含相同的字符序列。
对于 MemberExpression [ Expression ]
表达式,其执行顺序如下:
- 令 baseReference 为解释执行 MemberExpression 的结果 .
- 令 baseValue 为 GetValue (opens new window)(baseReference).
- 令 propertyNameReference 为解释执行 Expression 的结果 .
- 令 propertyNameValue 为 GetValue(propertyNameReference).
- 调用 CheckObjectCoercible(baseValue) (opens new window).
- 令 propertyNameString 为 ToString(propertyNameValue).
- 如果正在执行中的语法产生式包含在严格模式代码当中,令 strict 为 true, 否则令 strict 为 false.
- 返回一个 引用类型 (opens new window) 的值。该引用类型,其基 (base) 值为 baseValue, 其引用名称(referenced name)为 propertyNameString, 严格模式标记为 strict.
以 0.._
为例,其等同于 0['_']
,即 MemberExpression = 0,Expression = '_'
,按以下步骤进行
- baseReference = 0
- baseValue = GetValue(baseReference) = 0
- propertyNameReference = '_'
- propertyNameValue = GetValue(propertyNameReference) = '_'
- baseValue = ToObject(0) = new Number(0) // 生成一个临时包装对象
即
Number { __proto__: Number, [[PrimitiveValue]]: 0}
- propertyNameString = ToString(propertyNameValue) = '_'
- strict 设置
- 生成引用,其基值为
Number { __proto__: Number, [[PrimitiveValue]]: 0}
,引用名称为_
。在该基值(及原型链)中进行_
属性的寻找。最后没有找到,返回undefined
其实关键的就是执行 CheckObjectCoercible(0)
的时候调用 ToObject
返回了一个临时包装对象
这点规范说的有点模糊,只说了 CheckObjectCoercible 在其参数无法用 ToObject 转换成对象的情况下抛出一个异常,但是没有说 baseValue 会进行 ToObject 转换。 在 JS的基本数据类型的临时包装类型对象的触发条件和生命周期是多久? - 貘吃馍香的回答 - 知乎 (opens new window) 中有人进行了回答。
# 为何不用 0.._
代替 void 0
我们从 可读性、性能、正确性 三个方面分析
# 可读性
与 void 0
相比,0.._
仅减少了一个字符,但是该写法大大减低了可读性。
对于压缩工具来说,不在乎可读性,那么我们从性能角度分析。
# 性能
var COUNT = 100000000
var tmp
console.time("test1")
for(let i=0;i<COUNT;i++){
if(tmp === void 0){
}
}
console.timeEnd("test1")
// test1: 61.760986328125ms
console.time("test2")
for(let i=0;i<COUNT;i++){
if(tmp === 0.._){
}
}
console.timeEnd("test2")
// test2: 74.657958984375ms
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void 0
更快一点,但这个影响不大,单次指令之间的执行差异在微秒之内。
最后就看两者的值是不是正确的,即结果永远为 undefined
# 正确性
对于 void 0
,void 是关键字,不会被外部改变,因此返回值永远返回 undefined ,见 void 运算符 (opens new window)
对于 0.._
,我们上面分析到,在基值
中进行引用名称
的查找时,会往原型链中查找,因此改变 Number、Object 等的原型属性,0.._
值就不一样了
console.log(0.._) // undefined
Object.prototype._ = 0
console.log(0.._) // 0
Number.prototype._ = 1
console.log(0.._) // 1
2
3
4
5
可以看到, 0.._
结果不是固定的,因此不能用于替换 void 0
# 参考
ps: 中文版翻译有些地方不够准确,可以先看中文版了解大概,再到原版中详细查看