Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

    • 编程语言

      • CSS

      • HTML

      • JavaScript

        • ECMAScript6入门
        • JS 手写题

        • JS技巧
        • JS 学习笔记
        • Promise then 原理分析
        • async 函数编译原理
        • 你不知道的JavaScript(上)
        • 再谈闭包
        • 浏览器剪切板协议
        • 前端实现相对路径转绝对路径的几种方法
        • 为什么 0.._ 等于 undefined
          • 前言
          • 0.._ 的隐式转换
            • 词法分析
            • 语法分析
          • 为何不用 0.._ 代替 void 0
            • 可读性
            • 性能
            • 正确性
          • 参考
        • 前端项目中常用的位操作技巧
        • 如何利用前端剪切板实现文件上传
        • 快速理解 JS 装饰器
        • 趣味js-只用特殊字符生成任意字符串
        • 重学 JS 原型链
        • 面试官问:怎么避免函数调用栈溢出
      • Rust

      • TypeScript

      • WebAssembly

    • 开发工具

    • 前端调试

    • 浏览器原理

    • 浏览器生态

  • 应用框架

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 前端基础
  • 编程语言
  • JavaScript
gahing
2019/10/10
目录

为什么 0.._ 等于 undefined

# 前言

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

作者提到,用 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. 严格模式下不会解析成八进制
1
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

1
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 ] 表达式,其执行顺序如下:

  1. 令 baseReference 为解释执行 MemberExpression 的结果 .
  2. 令 baseValue 为 GetValue (opens new window)(baseReference).
  3. 令 propertyNameReference 为解释执行 Expression 的结果 .
  4. 令 propertyNameValue 为 GetValue(propertyNameReference).
  5. 调用 CheckObjectCoercible(baseValue) (opens new window).
  6. 令 propertyNameString 为 ToString(propertyNameValue).
  7. 如果正在执行中的语法产生式包含在严格模式代码当中,令 strict 为 true, 否则令 strict 为 false.
  8. 返回一个 引用类型 (opens new window) 的值。该引用类型,其基 (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的基本数据类型的临时包装类型对象的触发条件和生命周期是多久? - 貘吃馍香的回答 - 知乎 (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
1
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
1
2
3
4
5

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

# 参考

  1. es5 规范_中文版 (opens new window)
  2. es5 规范 (opens new window)

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

编辑 (opens new window)
#ECMAScript
上次更新: 2024/09/01, 23:56:56
前端实现相对路径转绝对路径的几种方法
前端项目中常用的位操作技巧

← 前端实现相对路径转绝对路径的几种方法 前端项目中常用的位操作技巧→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式