为什么 0.._ 等于 undefined

2,491 阅读4分钟

为什么 0.._ 等于 undefined

前言

今天看文章 为什么用「void 0」代替「undefined」 的时候,

作者提到,用 void 0 替代 undefined 的原因其中有一点是前者更短,更省空间。

当然最主要的原因还是 undefined 在局部作用域中可以被重写

下面有人回复 0.._ 长度更短,结果也是 undefined。 后面解释说是相当于 0['_'],不过没有更深入的讨论了。

当时心中产生了几个问题:

  1. 0.._ 是如何隐式转换成 undefined
  2. 为何(几乎)没有人采用 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. 严格模式下不会解析成八进制

:以上是测试得出的结论,规范中没找到。

不过按编译原理的知识,引擎会先根据 词法解析-数值字面量 找到 0. 这个数值字面量词法,接着才进行语法分析

同时 附加语法-数值字面量 中提到非 strict 模式下 NumericLiteral 才允许 OctalIntegerLiteral 八进制的词法

语法分析

接下来就是 为何数值字面量能够进行属性访问 的问题了。这是一个左值表达式。

左值表达式 语法,这里列举部分

LeftHandSideExpression :
NewExpression
CallExpression

CallExpression :
MemberExpression Arguments
CallExpression Arguments
CallExpression [ Expression ]
CallExpression . IdentifierName

MemberExpression :
PrimaryExpression
FunctionExpression
MemberExpression [ Expression ]
MemberExpression . IdentifierName
new MemberExpression Arguments

左值表达式-属性访问 有两者方式

  • MemberExpression . IdentifierName
  • MemberExpression [ Expression ]

前者等同于 MemberExpression [ <identifier-name-string> ]

<identifier-name-string> 是一个字符串字面量,它与 Unicode 编码后的 IdentifierName 包含相同的字符序列。

对于 MemberExpression [ Expression ] 表达式,其执行顺序如下:

  1. 令 baseReference 为解释执行 MemberExpression 的结果 .
  2. 令 baseValue 为 GetValue(baseReference).
  3. 令 propertyNameReference 为解释执行 Expression 的结果 .
  4. 令 propertyNameValue 为 GetValue(propertyNameReference).
  5. 调用 CheckObjectCoercible(baseValue).
  6. 令 propertyNameString 为 ToString(propertyNameValue).
  7. 如果正在执行中的语法产生式包含在严格模式代码当中,令 strict 为 true, 否则令 strict 为 false.
  8. 返回一个 引用类型 的值。该引用类型,其基 (base) 值为 baseValue, 其引用名称(referenced name)为 propertyNameString, 严格模式标记为 strict.

0.._ 为例,其等同于 0['_'],即 MemberExpression = 0,Expression = '_',按以下步骤进行

  1. baseReference = 0
  2. baseValue = GetValue(baseReference) = 0
  3. propertyNameReference = '_'
  4. propertyNameValue = GetValue(propertyNameReference) = '_'
  5. baseValue = ToObject(0) = new Number(0) // 生成一个临时包装对象

Number { __proto__: Number, [[PrimitiveValue]]: 0}

  1. propertyNameString = ToString(propertyNameValue) = '_'
  2. strict 设置
  3. 生成引用,其基值为 Number { __proto__: Number, [[PrimitiveValue]]: 0},引用名称为 _。在该基值(及原型链)中进行_属性的寻找。最后没有找到,返回 undefined

其实关键的就是执行 CheckObjectCoercible(0) 的时候调用 ToObject 返回了一个临时包装对象

这点规范说的有点模糊,只说了 CheckObjectCoercible 在其参数无法用 ToObject 转换成对象的情况下抛出一个异常,但是没有说 baseValue 会进行 ToObject 转换。 在 JS的基本数据类型的临时包装类型对象的触发条件和生命周期是多久? - 貘吃馍香的回答 - 知乎 中有人进行了回答。

为何不用 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

void 0 更快一点,但这个影响不大,单次指令之间的执行差异在微秒之内。

最后就看两者的值是不是正确的,即结果永远为 undefined

正确性

对于 void 0 ,void 是关键字,不会被外部改变,因此返回值永远返回 undefined ,见 void 运算符

对于 0.._,我们上面分析到,在基值中进行引用名称的查找时,会往原型链中查找,因此改变 Number、Object 等的原型属性,0.._ 值就不一样了

console.log(0.._) // undefined
Object.prototype._ = 0
console.log(0.._) // 0
Number.prototype._ = 1
console.log(0.._) // 1

可以看到, 0.._ 结果不是固定的,因此不能用于替换 void 0

参考

  1. es5 规范_中文版
  2. es5 规范

ps: 中文版翻译有些地方不够准确,可以先看中文版了解大概,再到原版中详细查看