# JS Design Error

--- title: "JS 的设计失误" ---

[JavaScript 的设计失误 - Offline](https://haoqun.blog/zh/2016/javascript-design-regrets-cf9619ba)

1. typeof null === 'object'
2. typeof NaN === 'number' <https://github.com/lewisjellis/nantalk>
3. NaN, isNaN(), Number.isNaN()
4. 分号自动插入（Automatic Semicolon insertion，ASI）机制

- Restricted Productions
- 漏加分号的情况
- semicolon-less 风格

1. ==, === 与 Object.is()：隐式类型转换，对比 <https://dorey.github.io/JavaScript-Equality-Table/>
2. Falsy values：JavaScript 中至少有六种假值（在条件表达式中与 false 等价）：0, null, undefined, false, '' 以及 NaN。
3. +、- 操作符相关的隐式类型转换：大致可以这样记：作为二元操作符的 + 会尽可能地把两边的值转为字符串，而 - 和作为一元操作符的 + 则会尽可能地把值转为数字。
4. 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)
1. Array-like objects

JavaScript 中，类数组但不是数组的对象不少，这类对象往往有 length 属性、可以被遍历，但缺乏一些数组原型上的方法，用起来非常不便。

在 ES2015 中，arguments 对象不再被建议使用，我们可以用 rest parameter （function f(...args) {}）代替，这样拿到的对象就直接是数组了。

不过在语言标准之外，DOM 标准中也定义了不少 Array-like 的对象，比如 NodeList 和 HTMLCollection。 对于这些对象，在 ES2015 中我们可以用 spread operator 处理。

1. arguments

在非严格模式（sloppy mode）下，对 arguments 赋值会改变对应的 形参。

1. 函数级作用域 与 变量提升（Variable hoisting）

函数级作用域本身没有问题，但是如果如果只能使用函数级作用域的话，在很多代码中它会显得非常 反直觉，比如上面这个循环的例子，对程序员来说，根据花括号的位置确定变量作用域远比找到外层函数容易得多。

JavaScript 引擎在执行代码的时候，会先处理作用域内所有的变量声明，给变量分配空间（在标准里叫 binding），然后再执行代码。

这本来没什么问题，但是 var 声明在被分配空间的同时也会被初始化成 undefined（ES5 中的 CreateMutableBinding），这就相当于把 var 声明的变量提升到了函数作用域的开头，也就是所谓的 "hoisting"。

ES2015 中引入的 let / const 则实现了 temporal dead zone，虽然进入作用域时用 let 和 const 声明的变量也会被分配空间，但不会被初始化。在初始化语句之前，如果出现对变量的引用，会报 ReferenceError。

在标准层面，这是通过把 CreateMutableBing 内部方法分拆成 CreateMutableBinding 和 InitializeBinding 两步实现的，只有 VarDeclaredNames 才会执行 InitializeBinding 方法。

1. let / const

然而，let 和 const 的引入也带来了一个坑。主要是这两个关键词的命名不够精确合理。

const 关键词所定义的是一个 immutable binding（类似于 Java 中的 final 关键词），而非真正的常量（ constant ），这一点对于很多人来说也是反直觉的。

ES2015 规范的主笔 Allen Wirfs-Brock 在 ESDiscuss 的一个帖子里 表示，如果可以从头再来的话，他会更倾向于选择 let var / let 或者 mut / let 替代现在的这两个关键词，可惜这只能是一个美好的空想了。

1. for...in

for...in 的问题在于它会遍历到原型链上的属性，这个大家应该都知道的，使用时需要加上 obj.hasOwnProperty(key) 判断才安全。

在 ES2015+ 中，使用 for (const key of Object.keys(obj)) 或者 for (const [key, value] of Object.entries()) 可以绕开这个问题。

1. with

依赖运行时语义，影响优化。

1. 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）能进入到标准的话，这最后一点用处也用不到了......

1. 非严格模式下，赋值给未声明的变量会导致产生一个新的全局变量
2. 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 的保留字，所以可以被用来当做变量名使用。即使全局变量上的这几个属性不可被更改，我们仍然可以在自己的作用域里面对这几个名字进行覆盖。

1. Stateful RegExps

JavaScript 中，正则对象上的函数是有状态的，这使得这些方法难以调试、无法做到线程安全。

1. weird syntax of import
2. Array constructor inconsistency
3. Primitive type wrappers
4. Date Object
5. prototype

作为对象属性的 `prototype`，其实根本就不是我们讨论原型继承机制时说的「原型」概念。 [`fallbackOfObjectsCreatedWithNew` would be a better name.](https://johnkpaul.github.io/presentations/empirejs/javascript-bad-parts/#/11)

而对象真正意义上的原型，在 ES5 引入 Object.getPrototypeOf() 方法之前，我们并没有常规的方法可以获取。

不过很多浏览器都实现了非标准的 _<sub>proto</sub>__（IE 除外），在 ES2015 中，这一扩展属性也得以标准化了。

1. Object destructuring syntax

解构赋值时给变量起别名的语法有点让人费解，虽然这并不能算作是设计失误（毕竟很多其他语言也这么做），但毕竟不算直观。


相关：[[js-basics|js-basics]]
