2021-07-25
对 JavaScript / TypeScript 的批评
写几年 js/ts 了,在这里喷一下。

# 各方面的一致性

我们先从如何构造字符串开始谈谈,在 JavaScript 里通常用单引号或者双引号来表示字符串,也可以用模版字符串来表达字符串,比如:
00const str1: string = 'hello, world 1';
01const str2: string = "hello, world 2";
02const str3: string = `hello, world 3`;
03console.log(str1, str2, str3);
该用哪种方式做字符串构造呢?这显然是一个主观问题,有的人认为单引号好,也有人觉得双引号好,更有人觉得模版字符串是最好的 …
然而,如果严格遵守语言设计所谓的一致性原则,只会剩下一种构造法了,比如说只留下单引号,它能支持模版字符串的所有特性:
00const a = 123;
01// 另外一个平行世界里的 js 支持单引号模版
02const str = 'the a is ${a}';
然后便不会有人纠结用哪种构造方法了,eslint 也不必开发出三种构造方式 … 这里锅主要是为了兼容历史代码,不是 tc39 的问题,因为社区在模版字符串之前就有类似的 logger 组件了
00logger.info('asd ${aa}', { aa: 123 });

JS 一致性差还可以在诸多设计细节中窥见
  • 分号可写可不写 … 导致引擎实现方不得不实现了 ASI 自动分号插入 (eslint / babel 开发者也一样得去实现) 这些都造成了无谓的浪费 —— 一开始设计就强制分号或者没有分号不就好了?
  • 语言有两个空值 undefinednull 两者又有各种微妙的不同
  • typeof 的问题,比如 typeof nulltypeof []
  • 模块标准不一致 —— 社区里存在事实上的 require 模块标准 和 ES Module
  • toString, valueOf 这些方法没有像 Symbol.iterator 那样的 symbol 常量 (简单来说就是没搞全)
  • 'use strict' 指定严格模式, 已无语。
    这个应该由 <script use-strict> 设置属性或在 node 指定 --use-strict 来开启比较合适
  • 一些内置 API 的设计
    • Date getMonths 从 0 开始,但其他几个 getDate 之类是从 1 开始 (虽然是 Java 的锅但多少沾点)
    • 语言提供了浅拷贝的 Object.assign 但没提供深拷贝的相关方法
  • 基类构造器行为不一致
    • new Number('1')Number('1') 两者结果不一致 有微妙的不同
    • new Array('1')Array('1') 两者结果一致
    • new Date()Date() 不一样

# js 没有静态类型

没有静态类型的语言约等于汇编,纯 js 是浏览器的汇编,报错只会发生在运行时,下面列几个因为没有静态类型而鸡肋的 js 特性
  • 没有静态类型系统却搞了两种等值判断 =====
    如果有静态类型系统的话 == 和 === 反而是一种 feature, 实现 toString 方法即可达成判断,甚至能像某些语言那样做到重载运算符
  • Set Map Array 等事实上带泛型的对象,纯 js 下你只能靠上下文人肉推断了
  • null 和 undefined,如果 js 带了静态类型在开发阶段大概率会被这两个定义恶心到,从而改成只有一个空值

# 没有编译打包

这导致了 webpack / rollup 等打包工具的爆火,但是,当你想在浏览器上跑原生 esm 的时候就犯难了,太多东西是这些构建工具帮你铺平了,语言自身没有提供任何工具方法帮你 all-in-one 一份 js 文件。

# TypeScript 缺少静态绑定

TypeScript 作为 js 的超集,本身是可以做很多 js 做不到的事,比如 struct 定义接口:
00// 定义声明了一种静态结构
01struct User {
02  name: string;
03  age: number;
04  setAge(fn: (x: number) => number): void {
05    // 这里 this 是静态绑定的
06    this.age = fn(this.age);
07  }
08}
09
10// 即便是在结构后也能调用到 setAge (类型静态绑定)
11const eczn: User = { name: 'eczn', age: 17 };
12({ ...eczn }).setAge(x => x + 1);
13
14import useSWR, { mutate } from 'swr';
15function UserCard(props: { user: User }) {
16  return (
17    <div onClick={() => {
18      mutate(
19        `/api/user/${props.user.name}`,
20        props.user.setAge(x => x + 1)
21      );
22    }}>
23      Current Age: { props.user.age }
24    </div>
25  )
26}
这种方式可以解决层层透传 props 的过程中 props 本身自带的方法的问题。
上述代码,一个可行的解释是:
00interface User {
01  name: string;
02  age: number;
03}
04function User$setAge(obj: User) {
05  obj.age = fn(obj.age);
06}
07const eczn: User = { name: 'eczn', age: 17 };
08User$setAge({ ...eczn }, x => x + 1);
09
10import useSWR, { mutate } from 'swr';
11function UserCard(props: { user: User }) {
12  return (
13    <div onClick={() => {
14      mutate(
15        `/api/user/${props.user.name}`,
16        User$setAge(props.user, x => x + 1)
17      );
18    }}>
19      Current Age: { props.user.age }
20    </div>
21  )
22}
当然,这一切的最大前提是得有一个强大的静态类型系统。
ts 目前并不想带超过标准定义的东西,更不想带 runtime,所以暂时没有计划支持这类特性,其他语言的优秀实践跟 js/ts 无缘了




回到顶部