js-design-error
— title: "JS 的设计失误" —
[JavaScript 的设计失误 - Offline](https://haoqun.blog/zh/2016/javascript-design-regrets-cf9619ba)
- typeof null
=
'object' - typeof NaN
=
'number' https://github.com/lewisjellis/nantalk - NaN, isNaN(), Number.isNaN()
- 分号自动插入(Automatic Semicolon insertion,ASI)机制
- Restricted Productions
- 漏加分号的情况
- semicolon-less 风格
=, ==
与 Object.is():隐式类型转换,对比 https://dorey.github.io/JavaScript-Equality-Table/- Falsy values:JavaScript 中至少有六种假值(在条件表达式中与 false 等价):0, null, undefined, false, '' 以及 NaN。
- +、- 操作符相关的隐式类型转换:大致可以这样记:作为二元操作符的 + 会尽可能地把两边的值转为字符串,而 - 和作为一元操作符的 + 则会尽可能地把值转为数字。
- null、undefined 以及数组的 "holes"
不过数组里的 "holes" 就非常难以理解了。
产生 holes 的方法有两种:一是定义数组字面量时写两个连续的逗号:`var a = [1, , 2]`;二是使用 `Array` 对象的构造器,`new Array(3)`。
数组的各种方法对于 holes 的处理非常非常非常不一致,有的会跳过(`forEach`),有的不处理但是保留(`map`),有的会消除掉 holes(`filter`),还有的会当成 undefined 来处理(`join`)。这可以说是 JavaScript 中最大的坑之一,不看文档很难自己理清楚。
具体可以参考这两篇文章:
[Array iteration and holes in JavaScript](http://www.2ality.com/2013/07/array-iteration-holes.html)
[ECMAScript 6: holes in Arrays](http://www.2ality.com/2015/09/holes-arrays-es6.html)
- Array-like objects
JavaScript 中,类数组但不是数组的对象不少,这类对象往往有 length 属性、可以被遍历,但缺乏一些数组原型上的方法,用起来非常不便。
在 ES2015 中,arguments 对象不再被建议使用,我们可以用 rest parameter (function f(…args) {})代替,这样拿到的对象就直接是数组了。
不过在语言标准之外,DOM 标准中也定义了不少 Array-like 的对象,比如 NodeList 和 HTMLCollection。 对于这些对象,在 ES2015 中我们可以用 spread operator 处理。
- arguments
在非严格模式(sloppy mode)下,对 arguments 赋值会改变对应的 形参。
- 函数级作用域 与 变量提升(Variable hoisting)
函数级作用域本身没有问题,但是如果如果只能使用函数级作用域的话,在很多代码中它会显得非常 反直觉,比如上面这个循环的例子,对程序员来说,根据花括号的位置确定变量作用域远比找到外层函数容易得多。
JavaScript 引擎在执行代码的时候,会先处理作用域内所有的变量声明,给变量分配空间(在标准里叫 binding),然后再执行代码。
这本来没什么问题,但是 var 声明在被分配空间的同时也会被初始化成 undefined(ES5 中的 CreateMutableBinding),这就相当于把 var 声明的变量提升到了函数作用域的开头,也就是所谓的 "hoisting"。
ES2015 中引入的 let / const 则实现了 temporal dead zone,虽然进入作用域时用 let 和 const 声明的变量也会被分配空间,但不会被初始化。在初始化语句之前,如果出现对变量的引用,会报 ReferenceError。
在标准层面,这是通过把 CreateMutableBing 内部方法分拆成 CreateMutableBinding 和 InitializeBinding 两步实现的,只有 VarDeclaredNames 才会执行 InitializeBinding 方法。
- let / const
然而,let 和 const 的引入也带来了一个坑。主要是这两个关键词的命名不够精确合理。
const 关键词所定义的是一个 immutable binding(类似于 Java 中的 final 关键词),而非真正的常量( constant ),这一点对于很多人来说也是反直觉的。
ES2015 规范的主笔 Allen Wirfs-Brock 在 ESDiscuss 的一个帖子里 表示,如果可以从头再来的话,他会更倾向于选择 let var / let 或者 mut / let 替代现在的这两个关键词,可惜这只能是一个美好的空想了。
- for…in
for…in 的问题在于它会遍历到原型链上的属性,这个大家应该都知道的,使用时需要加上 obj.hasOwnProperty(key) 判断才安全。
在 ES2015+ 中,使用 for (const key of Object.keys(obj)) 或者 for (const [key, value] of Object.entries()) 可以绕开这个问题。
- with
依赖运行时语义,影响优化。
- eval
eval 的问题不在于可以动态执行代码,这种能力无论如何也不能算是语言的缺陷。
- 作用域
它的第一个坑在于传给 eval 作为参数的代码段能够接触到当前语句所在的闭包。
- Direct Call vs Indirect Call
首先,eval 是全局对象上的一个成员函数;
但是,window.eval() 这样的调用 不算是 直接调用,因为这个调用的 base 是全局对象而不是一个 "environment record"。
间接调用 eval 最大的用处(可能也是唯一的实际用处)是在任意地方获取到全局对象(然而 Function('return this')() 也能做到这一点),如果 Jordan Harband 的 [`System.global` 提案](https://github.com/tc39/proposal-global)(发布于 ES2020)能进入到标准的话,这最后一点用处也用不到了……
- 非严格模式下,赋值给未声明的变量会导致产生一个新的全局变量
- Value Properties of the Global Object
我们平时用到的 `NaN`, `Infinity`, `undefined` 并不是作为 primitive value 被使用(而 `null` 是 primitive value),[而是定义在全局对象上的属性名](https://es5.github.io/#x15.1.1)。
在 ES5 之前,这几个属性甚至可以被覆盖,直到 ES5 之后它们才被改成 non-configurable、non-writable。
然而,因为这几个属性名都不是 JavaScript 的保留字,所以可以被用来当做变量名使用。即使全局变量上的这几个属性不可被更改,我们仍然可以在自己的作用域里面对这几个名字进行覆盖。
- Stateful RegExps
JavaScript 中,正则对象上的函数是有状态的,这使得这些方法难以调试、无法做到线程安全。
- weird syntax of import
- Array constructor inconsistency
- Primitive type wrappers
- Date Object
- prototype
作为对象属性的 `prototype`,其实根本就不是我们讨论原型继承机制时说的「原型」概念。 [`fallbackOfObjectsCreatedWithNew` would be a better name.](https://johnkpaul.github.io/presentations/empirejs/javascript-bad-parts/#/11)
而对象真正意义上的原型,在 ES5 引入 Object.getPrototypeOf() 方法之前,我们并没有常规的方法可以获取。
不过很多浏览器都实现了非标准的 _proto__(IE 除外),在 ES2015 中,这一扩展属性也得以标准化了。
- Object destructuring syntax
解构赋值时给变量起别名的语法有点让人费解,虽然这并不能算作是设计失误(毕竟很多其他语言也这么做),但毕竟不算直观。