为何 0.._ 等于 undefined

为何 0.._ 等于 undefinedgit

前言

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

做者提到,用 void 0 替代 undefinded 的缘由其中有一点是前者更短,更省空间。bash

固然最主要的缘由仍是 undefinded 在局部做用域中能够被重写less

下面有人回复 0.._ 长度更短,结果也是 undefinded。 后面解释说是至关于 0['_'],不过没有更深刻的讨论了。ide

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

  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. 严格模式下不会解析成八进制
复制代码

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

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

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

语法分析

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

左值表达式语法

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: 中文版翻译有些地方不够准确,能够先看中文版了解大概,再到原版中详细查看

相关文章
相关标签/搜索