为什么为 const 变量重新赋值不是个静态错误
const 和 let 的唯一区别就是用 const 声明的变量不能被重新赋值(只读变量),比如像下面这样就会报错:
const foo = 1
foo = 2 // TypeError: Assignment to constant variable.
注:本文不会使用“常量”这个术语,因为我觉的这个术语容易有歧义:有些人把数字、字符串等这些不可改变的字面量称为常量,也有人把一些只读属性称为常量,比如 Math.PI,还有人把 ES6 里用 const 声明的变量称为常量。不过一般来说,这点歧义不是个事。
但遗憾的是,这个错误不是个静态错误(static error),而是个运行时错误(runtime error)。静态错误,也被称为解析错误(parsing error),因为是在解析的时候报的错,其实规范里的正统叫法叫提前错误(early error),有时候也能看到对应的的叫法 late error,但其实规范里的正统叫法只有 runtime error。考虑到一般人完全不知道 early error 是什么,所以本文采用静态错误这个术语。
错误当然是越早知道越好,所以静态错误一定比运行时错误好,比如下面这种代码:
const foo = 1
/*
这里有很多行代码
*/
if (this.isInProduction) { // 只在生产环境中执行的代码
/*
这里有很多行代码
*/
foo = 2 // 写这行代码的人忘了 foo 是用 const 声明的了
} alert(foo) // 开发环境弹出 1,线上环境报错,悲剧
假如 foo = 2 是个静态错误,这个代码在开发环境就直接报错了,即便 foo = 2 没有被执行到。
那为什么 ES6 没有把这个错误设计为静态错误呢?其实在 11 年 const 刚刚进入 ES6 草案时,在严格模式下为 const 变量重新赋值就是个静态错误(错误类型为 SyntaxError),同时在严格模式下也是个运行时错误(错误类型为 TypeError),而且 Brendan Eich 同年也在 SpiderMonkey 里实现了这一规定(Firefox 7)。到了 12 年,草案改成了不在严格模式下也要报那个静态错误,SpiderMonkey 的另外一个工程师 Tom Schuster 在 2014 年 11 月 19 号实现了这一改动(Firefox 36)。
有些同学就问了,为什么同一个错误要在两个阶段报?有静态错误的情况下运行时错误应该永远不会触发才对啊?这是因为某些情况下的错误是无法或者说很难静态检测出来的,比如:
const foo = 1
let script = "foo = 2"
eval(script) // 注定是个运行时错误
在 eval 里为 const 变量重新赋值,这个错误无论从规范上还是从实现上还是从逻辑上说,都是不可能静态分析出的,还比如:
function f() {
foo = 2 // 可能是个静态错误吗?
}
const foo = 1
f()
引擎在解析到 foo = 2 的时候,还不知道 foo 在后面会成为个只读变量,引擎很难静态检测出这样的错误。也许引擎可以实现,比如把前面解析到的函数内部的隐式全局变量的信息存下来,如果后面解析到了一个同名的 const 变量,再报错,可否?谁知道呢,反正 Firefox 36 当时是检测不到这样的错误的,把声明和赋值倒过来就可以了:
还有一种情况是,虽然是先声明再重新赋值,但声明和赋值分别处于两个不同的 <script> 标签里,如下:
<script>
const foo = 1
</script>
<script>
foo = 2
</script>
引擎在解析第二个 <script> 里的 foo = 2 时可能不会去管 foo 是不是已经被声明过了(比如你的编辑器在静态解析这个 tab 里的 js 文件的时候,会去考虑另外一个 tab 里的 js 文件吗?),Firefox 36 实现的就是这样的(在 JS 命令行里执行的每一行代码,都相当于是放在网页里一个单独的 <scirpt> 标签里执行的一样):
写在一行就报静态错误(非严格模式下也报静态错误),分成两行就静默失败(没有被静态分析出错误,且运行时错误只在严格模式下才报)。
关于第三种情况,当 Tom Schuster 在 14 年 11 月 7 号提了 bug 准备实现那个改动的时候,我就预料到会产生这样的表现,我当天晚上在 IRC 群里找到了 Tom Schuster(evilpie),询问他有没有觉的这种表现有点怪,他的回答是说这种怪异只会发生在 JS 的命令行里,不会发生在网页里。的确,在正常的网页里,其实很难遇到声明 const 变量和为它重新赋值出现在两个 <script> 里的情况。我当时虽然觉得他说的有点道理,但还是隐约觉的哪里有问题,但连我自己也说不出来问题是什么。
然后大概第二天(记不清了),我就发现,原来早在一个月前(2014 年的 10 月份),SpiderMonkey 的另外一个工程师 Shu-yu Guo 就已经在 esdiscuss 提过一个相关的问题(这个帖子的内容是本文的核心)了,而且问题说的非常简单明了:
1. 关于引擎应该多么努力去检测这个静态错误,规范说的太笼统,可能导致引擎实现有差异。
的确,下面就是当时规范里关于这个 early error 检测的描述,规范只说了一句 can be statically determined,并没有具体说 how,我上面举的一些 Firefox 36 没检测到的情况也证实了这一点。
It is a Syntax Error if LeftHandSideExpression is an IdentifierReference that can be statically determined to always resolve to a declarative environment record binding and the resolved binding is an immutable binding.
2. 静态错误是在任何模式下都报,而运行时错误却是只在严格模式下才报。
我看到这里才恍然大悟,这不就是我前一天觉的有问题的点吗。。。一句代码都能静态的分析出有错了,结果在执行的时候却没错?这说不通啊,没天理了。
ES6 的编辑 Allen Wirfs-Brock 在帖子二楼针对这两点一一做了回复:
1. 关于第一点,这个是已知的问题了,而且已经建了相关的 bug(网站的 https 证书过期了;里面还举了另外两个难以静态检测的例子),规范会尝试去详细阐述 can be statically determined 具体指什么。
2. 关于第二点,这个是规范的 bug,不是故意这样设计的,bug 原因是因为在 ES6 里,为一个 const 变量重新赋值的运行时错误和 ES5 里严格模式下为一个函数表达式的函数名重新赋值的运行时错误是在同一个内部方法(SetMutableBinding)里抛出的:
(function foo() {
"use strict"
foo = 1
})()
因为后者是只在严格模式下报错的,所以前者也继承了这一表现,这是个 bug,这两种错误应该分开。
其实 2 楼的回复已经解决了楼主的疑问,这个帖子原本要讨论的东西已经有结论了,结论就是:不管什么模式都报静态错误(规范会完善具体的静态检测规则);不管什么模式都报运行时错误(所有逃过静态检测的错误都会在这里被捕获)。很完美,不是吗。
然而这时,V8 的工程师 Erik Arvidsson 在三楼跳出来说:带有预解析器的的引擎要实现这个静态检测难度很大,规范要不更强制一点,要不干脆删掉这个要求,模棱两口可能导致各引擎的实现不统一。
然后 V8 的另外一个工程师 Andreas Rossberg 也发帖说了一些看法,我总结一下他说的:
1. 报这个静态错误需要有完整的 AST,而 V8 的预解析器目前做不到这一点
2. 非要让引擎实现这个可能引起很大的性能问题,而且可能很难优化
3. 这种错误不是特别常见,非让引擎处理性价比不高,还是交给 lint 工具去做吧
4. 一个同样很难静态检测出的错误 - 严格模式下为不存在的变量赋值("use strict"; foo = 1),就是个运行时错误,这个错误不应该搞特殊
经过这个帖子的讨论后,在一个月后也就是 2014 年 11 月 18 号的TC39 会议上,TC39 决定把为 const 变量重新赋值的静态错误删去,只留下运行时错误(任何模式)。会议记录里写着删掉的原因是“引擎实现有难度”和“哪些情况下为 const 变量重新赋值应该被静态检测出来没有达成共识”。
然后我又跑到 SpiderMonkey 的 IRC 群里告诉他们:规范改了,你们前两天实现的静态错误应该去掉了,然后招来一群 SpiderMonkey 的人吐槽规范太不稳定了。
关于 let/const,目前网上较为推崇的一种代码风格是全用 const,除非这个变量要被重新赋值,才改成 let。ESLint 有个 prefer-const 规则可以强制你做到这一点,我在此告诫各位,如果你使用这种编码风格,你的编辑器最好开启 ESLint 的 no-const-assign 的规则,否则我不确定这么用 const 能给你带来多大的好处,但我知道有可能让你遭遇文章开头的那种线上 bug。
额外小窍门:如何判断某个错误是静态错误还是运行时错误
绝大多数情况下,SyntaxError 类型的错误就是静态错误,而其他类型的错误就是运行时错误,但也有特例,比如:
1 = 2 // 静态错误,但是个 ReferenceError还有:
/(/ // 在 V8 里是个运行时错误,但是个 SyntaxError,SpiderMonkey 里是个静态错误怎么知道的?我通常是在浏览器开发者工具的控制台里写 alert();然后后面跟上测试代码,比如:
alert();1 = 2 // 不会弹出 alert,证明 1 = 2 是个静态错误和:
alert();/(/ // Chrome 里会弹出 alert,证明 /(/ 是个运行时错误,Firefox 里相反
为什么为 const 变量重新赋值不是个静态错误的更多相关文章
- const变量指针赋值给非const类型的指针运行结果
在c++可以定义一个const变量,然后把变量的值赋给一个非const指针,可以通过指针来改变const变量的值吗?下面的截图给出了答案
- const变量赋值报错分析
const变量赋值报错分析 const变量赋值报错 从变量到常量的赋值是合法C++的语法约定的, 如从char 到const char顺畅: 但从char **到 const char **编译器就会 ...
- [BS-00] const限定常量或者变量(初次赋值后),其值不允许被改变
CONST(C中的CONST) const是一个C语言(ANSI C)的关键字,它限定一个变量不允许被改变,产生静态作用.使用const在一定程度上可以提高程序的安全性和可靠性.另外,在观看别人代码的 ...
- (转)Const,Const函数,Const变量,函数后面的Const
本文转自http://www.cnblogs.com/Fancyboy2004/archive/2008/12/23/1360810.html 看到const 关键字,C++程序员首先想到的可能是co ...
- const变量与define定义常量的区别
一.概念性区别 const 变量就是在普通变量前边加上一个关键字const,它赋值的唯一机会就是“定义时”,此变量不能被程序修改,存储在rodata区. define定义的是常量,不是变量,所以编译器 ...
- c++中的const参数,const变量,const指针,const对象,以及const成员函数
const 是constant 的缩写,“恒定不变”的意思.被const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性.所以很多C++程序设计书籍建议:“Use const whe ...
- ES6 学习笔记(一)let,const和解构赋值
let和const let和const是es6新增的两个变量声明关键字,与var的不同点在于: (1)let和const都是块级作用域,在{}内有效,这点在for循环中非常有用,只在循环体内有效.va ...
- 关于ES6-{块级作用域 let const 解构赋值 数组 字符串 函数的扩展 箭头函数}
关于ES6 块级作用域 任何一对花括号({})中的语句集都属于一个块,在块中声明的变量在代码块外都是不可访问的,称之为块级作用域,ES5以前没有块级作用域 let let 是ES6新增的声明变量的一种 ...
- 关于C++ const 变量
const 的全局变量是储存在一个只读数据段中,虽然你可以定义一个指向它的指针,却会在运行时,在对该地址赋值的时候发生运行错误,而局部的const变量是储存在栈中的,离开作用域后同样会被释放,并且可以 ...
随机推荐
- insertion sort(插入排序)
#include<stdio.h> #include<time.h> int insertion_sort() { ; int a[max],i,j; srand((unsig ...
- 高阶Laplace曲面形变算法(Polyharmonic Deformation)
数学上曲面的连续光滑形变可以通过最小化能量函数来建模得到,其中能量函数用来调节曲面的拉伸或弯曲程度,那么能量函数最小化同时满足所有边界条件的最优解就是待求曲面. 能量函数通常是二次函数形式: 其中S* ...
- BZOJ 3531: [Sdoi2014]旅行 [树链剖分]
3531: [Sdoi2014]旅行 Time Limit: 20 Sec Memory Limit: 512 MBSubmit: 1685 Solved: 751[Submit][Status] ...
- 第5章 Java数组
1.什么是数组 数组可以想象成一个巨大的盒子,这个盒子里面存放的是同一个数据类型的数据 例如:int[] scores = {78,68,94,93}; 2.如何使用Java中的数组 2.1申明数组 ...
- inverse
首先术语inverse 被翻译为反转的意思.inverse 制定了关联关系中的方向. 当set的inverse属性默认情况下,hibernate会按照持久化对象的属性变化来同步更新数据库. 得到两条s ...
- wireshark 分析重传包
如下图所示,经过实验,wireshark把第一次重传包分类为out of order 类型,可以通过tcp.analysis.out_of_order过滤,如果第二次重传,分类为fast retran ...
- EEG preprocess - re-reference EEG预处理 - 重参考
Source: https://blricrex.hypotheses.org/ressources/eeg/pre-processing-for-erps/re-referencing-eeg-da ...
- 如何删除datatable中的一行数据
在C#中,如果要删除DataTable中的某一行,大约有以下几种办法: 1,使用DataTable.Rows.Remove(DataRow),或者DataTable.Rows.RemoveAt(ind ...
- OrchardNoCMS实体关系映射扩展
在OrchardNoCMS中,默认的系统会把实体关系映射保存到mappings.bin文件中. 如果不进行任何修改,默认的可以自动保存关系映射的model是有很大限制的. 条件是model的命名空间必 ...
- 疑难杂症——EF+Automapper引发的查询效率问题解析
前言:前面总结了一些WebApi里面常见问题的解决方案,本来打算来分享下oData+WebApi的使用方式的,奈何被工作所困,只能将此往后推了.今天先来看看EF和AutoMapper联合使用的一个问题 ...