42

为什么 0.._ 等于 undefined

 4 years ago
source link: https://www.tuicool.com/articles/EviyueU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

为什么 0.._ 等于 undefined

前言

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

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

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

下面有人回复 0.._ 长度更短,结果也是 undefinded 。 后面解释说是相当于 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 八进制的词法

语法分析

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

左值表达式语法

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}

  6. propertyNameString = ToString(propertyNameValue) = '_'
  7. strict 设置
  8. 生成引用,其基值为 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK