写几年 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 开发者也一样得去实现) 这些都造成了无谓的浪费 —— 一开始设计就强制分号或者没有分号不就好了?
- 语言有两个空值
undefined
和null
两者又有各种微妙的不同 typeof
的问题,比如typeof null
和typeof []
- 模块标准不一致 —— 社区里存在事实上的 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 无缘了