JavaScript 虽然不是我的启蒙语言,但绝对是对我影响最大的编程语言之一。半年以前,我的首选语言就是它。如今如果要做正经的前端开发工作,就不可能离得开 JS 或 TS.在 TIOBE 排行榜,JS 稳居第六,只落后于人尽皆知的 Python 和 C 系语言,还有万恶的 Java.由于太过流行,这门脚本语言又被搬到后端,甚至可以用来写命令行应用。要是像 Rust 一样进入 Linux 内核,JS 就无所不能了。
不过,我相信所有熟悉 JavaScript 的程序都会同意:这是一门混乱不堪的语言。不仅是语言本身的设计,它背后庞大的生态也是一团乱麻,给用户和开发者带来的体验都不能说得上舒适,有时候甚至非常糟糕。
本文要讨论的是 JavaScript 语言本身、人们如何使用它、NPM 生态如何塑造了今天的 Web 开发体验。作为用户,我还打算说服你默认禁用 JavaScript.
不过我不会写太多有关 TypeScript 的内容,因为我其实没有用过这门语言。
1 + “1” == “11”
我们先来谈谈 JavaScript 的优点:它无处不在,非常易用。比如,如果你是在浏览器里读这篇文章的话,你现在就可以按下 ?+Shift+I(或者 Ctrl + Shift + I)打开审查元素界面,找到 Console(控制台),在里面输入 JavaScript 代码执行。浏览器内置了 JavaScript 解释器。
不妨做个实验,在刚才我说的控制台里输入这样一段代码:
a = [1, 1001, 2, 3, 42, 4]
放心,这不是恶意程序,这只是定义了一个名字为 a 的数组。接下来,我们尝试对这个数组进行排序,排序的结果自然应该是 [1, 2, 3, 4, 42, 1001] 对吧?JavaScript 的另一个优点是许多操作数据的方法都被封装好了,你可以直接使用,不必自己编写。
现在,在控制台里输入 a.sort(),排序数组。
a.sort()
// => (6) [1, 1001, 2, 3, 4, 42]
哦不,为什么排序出来的数组是这个样子?
因为 JavaScript 是动态类型语言?数组里什么类型的数据都可以塞,可以把字符串、数字和对象放在一起,所以很难考虑到是要按照字符顺序进行排序,还是按照数字大小顺序进行排序,对吧?
我们不妨把相同的代码输入 Python 解释器试试,Python 也是一门动态类型语言。只不过这门语言里不限数据类型的集合数据类型叫作元组(Tuple),使用圆括号表示。
>>> a = (1, 1001, 2, 3, 42, 4)
>>> sorted(a)
[1, 2, 3, 4, 42, 1001]
看起来 Python 用同样简洁的代码完成了任务。我们再来看看另一们动态类型语言,这次上场的是一门 Lisp 方言——Clojure.
user=> (def a '(1 1001 2 3 4 42))
#'user/a
user=> (sort a)
(1 2 3 4 42 1001)
Clojure 也没有任何问题。看来 JavaScript 的问题并非源自「它是动态类型语言」这个事实,相反,设计得优秀的动态类型语言不但不容易出现类型问题,用起来还非常讨喜。
给数组排序的反直觉设计或许不算大问题,给 sort() 方法传入一个谓词函数即可,用 a.sort((x, y) => x - y) 就可以得到预期的结果了。
那就让我们继续实验,你知道 1+1 = 2 对吧?可是 JavaScript 却不知道:
1 + "1"
// => '11'
NaN 的意思是 Not a Number(不是数字)对吧?可是 JavaScript 却表示反对:NaN 的数据类型是数字。
typeof NaN
// => 'number'
此外,JavaScript 还证伪了等式的传递性。
0 == "0"
// => true
0 == []
// => true
"0" == []
// => false
我知道,我知道,这些问题都有解释。JavaScript 会把字符串转换为数字,把数字转换为字符串,所以有字符串出现在 + 的某一端就会被当作拼接操作而不是相加;NaN 是把其他类型强制转换为数字失败时显示的错误值,由于是转换为数字的结果,所以本质上还是数字;而第三个问题是因为 [] 和 0 都是零值(Zero Value),所以相等,而 "0" 虽然会被转换为数字 0,但并非零值——所以 JavaScript 引入了 ===(严格等于)。
让我们再来看看那些设计合理的动态类型语言是怎么处理这些问题的吧。
首先,试图对字符串进行 + 操作应该报错,因为数字相加和字符串拼接本来就是两个语义,不应该由同一个符号表示。
user=> (+ 1 "1")
;; Execution error (ClassCastException) at user/eval3 (REPL:1).
;; class java.lang.String cannot be cast to class java.lang.Number (java.lang.String and java.lang.Number are in module java.base of loader 'bootstrap')
user=> (str 1 "1")
"11"
也正是因为对非数字进行算术操作会扔出错误,NaN 这个错误值就不会出现,如果真的需要表示数据无法解析为数组,那么返回空值就好,比如 nil 或者 null.
user=> (parse-long "12")
12
user=> (parse-long "avc")
nil
;; nil 的类型当然是 nil
user=> (type nil)
nil
同样,如果不随意转换字符串和数字,零值的问题从一开始就不会发生。
user=> (= "0" 0)
false
再来做一道选择题吧,以下哪个 JavaScript 表达式的值是 false?

A. typeof null === 'object'; B. Math.min() > Math.max();
C. NaN === NaN; D. 0 == ''
答案是 C,NaN 不是自等的,判断一个值是否为 NaN 要用 .isNaN() 方法。是的,一切都是 object,最小数比最大数大,以及老生常谈的,0 等于空字符串。
还有一个设计缺陷非常好笑,简直是 JavaScript 所有类型问题的集大成者。
{} + {}
// => NaN
({} + {})
// => '[object Object][object Object]'
{} + []
// => 0
这门语言还有变量作用域等各方面的问题,至此,我相信可以得出结论说:JavaScript 是一门为了做到方便,而被设计得极其不符合直觉的语言。方便和符合直觉可以共存,但 JavaScript 给出的是错误答案。
不过,随着规范化的不断推进,不少设计缺陷已经有规避方案了,在意类型安全可以使用 TypeScript,变量作用域问题也可以严格限制全局变量,多用 let 和 const 解决。尽管不至于经常因设计缺陷而感到困扰,但还是很难不在意,这些缺陷一开始为什么会存在?
十天诞生的编程语言
2000 前最流行的浏览器叫作 Netscape Navigator(前身是 Mosaic),1995 年 Netscape 决定要给浏览器添加创建交互式网页的能力,他们原本打算把 Java 或 Scheme 嵌入浏览器中(真是两条…… 截然相反的道路),为此雇佣了 Brenden Eich 完成 Scheme 的嵌入工作,后来他们决定造一门新语言,于是让这个新雇佣的程序员在两周内完成并发布了 LiveScript.由于当时 Java 很火,所以后来更名为 JavaScript,两门语言并无直接关联。
联想到仓促的开发周期,就不难理解 JavaScript 为什么有这么多的缺陷。再加上 JavaScript 不是社区产物,而是一家企业为自家浏览器产品开发的差异化功能,长时间都没有统一的规范,就和 Markdown 一样混乱。
比如 JavaScript 没有原生的模块系统,主流的支持模块的 JavaScript 标准有两个:EMCAScript 和 CommonJS.前者使用 import 和 export,后者使用 require() 导入模块。目前,EMCAScript 受到广泛支持,已经成为事实意义上的 JS 标准。
给这门仓促开发的编程语言打的补丁还有很多,比如 TypeScript 就硬生生把它变成了一门强类型语言(前面我已经讨论过 JS 的问题并非源自它是弱类型语言这个事实)。
打补丁能解决的问题是有限的,为了保证兼容性(更何况是一门所有浏览器甚至服务器后端都在使用的编程语言),很多设计缺陷保留至今,加上各种补丁反而让 JavaScript 看起来像个臃肿丑陋的怪物。比如,JavaScript 自带的时间类型非常难用,最近推出的 Temporal 解决了这个问题,解决方法是增加了新的时间类型,同时兼容旧的、有问题的时间类型。这个 API 截至目前的兼容性是「Limited Availability」,Safari 浏览器还不兼容。很难相信,现代化的用于处理时间和日期的基础数据类型,竟然在 2026 年还没有得到广泛支持。
无处不在的过度抽象
除了这门语言本身的问题,它的使用者也经常受到嘲笑。
首先,如今人们开发一个最简单的网页都会用 React 或者 Vue.js 等现代的前端框架,还要为这些框架导入样式库(比如 TailwindCSS 和 shadcn),哪怕他们只是想写个甚至不需要 Reactivity、甚至没有复杂交互功能的简单网页。

“一个正态分布曲线,最左侧是一个看起来脑袋很小的人,他说"Pure HTML CSS and JS is the best option",曲线顶端是一个戴眼镜的急哭了的人,他说“Noo, we need a framework for JS, library for styling and 146 other dependencies.”,最右侧是一位穿着斗篷的智者,他说"Pure HTML CSS and JS is the best option"
“纯 HTML、CSS 和 JS 最好” “不!我们需要框架和样式库,还有 146 个其他的依赖项。”
React 里有个概念叫作虚拟 DOM,这就是对真实 DOM 树的额外抽象,如果 DOM 本身足够复杂,操作虚拟 DOM 就会容易管理不少。只不过,在大部分网页开发中,额外的抽象层都是过度抽象,做一个简单的网页也要用脚手架搭一个完整的项目结构也完全是过度工程。更何况这不会让开发效率有质的提升,反而会显着拖慢网页的加载速度。
JavaScript 开发者还经常被嘲笑热衷于造新框架,先让我不查资料用手指数一数我知道的 JavaScript 框架名:
- React(严格来说是一个库)
- Vue.js
- Next.js
- Nuxt.js
- Svelte
- Solid
- Angular
- Preact
- Hono
- Gatsby
- Ember.js
- Modern.js
- Backbone.js
- Express.js
- Astro
看来一双手还数不过来,一定还有我不知道的框架,如果有人能做更全面的统计就好了。此外,不少开发者还喜欢造一些小框架,如果总数加起来超过 100,我不会惊讶。
就算手写 HTML 需要编写很多重复代码,但 Neocities 社区就在没有框架的情况下创造了很多有趣的个人网站,这无疑说明很多网页需求并不需要进行了一层甚至很多层抽象的框架来解决。如果一个网站只有几个页面,或者根本不是网页应用,只展示文档内容,那根本不需要框架。况且,使用 JavaScript 加载内容对 SEO 非常不友好(Vue.js 是用来做单页应用的,依赖前端路由,所有网页路径都会被转发到同一个入口文件,实际上整个网站都只有一个页面),而且没有加载 JS 的自动程序无法识别网页内容,像我这种默认禁用 JS 的用户也根本看不到网页。
JavaScript 是用来设计交互式网页的,必须加载 JavaScript 才能看到文本内容就是很蠢的设计。
诚然,手写 HTML 很低效,应该把一部分复杂度合理地外包给计算机程序,建立适当的抽象,所以我们有静态网站生成器(SSG,Static Site Generator)这种东西。就算是 WordPress 和 Typecho 这类需要后端才能运行的动态博客程序,本质上也是在服务端把 HTML 网页拼接好再发送 HTML 和必要的静态资源给访客,不需要加载非必要的 JavaScript,也没有建立过度的抽象。
写到这里我想我需要解释一下「抽象」这个词,抽象层级一般是用来屏蔽软件架构的复杂度的。比如,审计银行流水的业务规则不应该了解如何向数据库发送请求,如果相关的代码里既有各种校验逻辑,又有 SQL 语句,那这个软件维护起来就很困难,应该把后者分离到数据持久层。同理,如果你要编写的网络应用的 DOM 结构非常复杂,比如 Cloudflare 的仪表盘,包含侧边、顶栏、数据展示等网页元素,还要管理从后端 API 获取的用户数据,有些数据的请求还是异步的,更新后又需要修改元素,那完全不做抽象,手写 HTML 就是很蠢的,这时候就应该抽象出组件的概念,以及专门的状态管理和路由模块等等。
但如果,你要编写的仅仅是简单的计数器,用户点击之后将数据库里的某个字段增加 1,总共需要编写的代码也就一百行,业务逻辑足够简单,那么把业务逻辑和 SQL 语句写在一起就没有问题,并不会让软件架构变得难以维护。同理,如果要编写简单的网页,手写 HTML 或者使用静态网站生成器就足够了,使用高度抽象、专门用来解决复杂问题的框架就是用大炮打蚊子,不仅有病,还容易伤及无辜。
依赖地狱和供应链投毒
由于 JavaScript 开发者非常喜欢使用框架,还非常喜欢引入各种依赖,用于解决鸡毛蒜皮的小事。如果某天出现了专门输出 Hello World 的 JavaScript 库,我不会感到意外。
我不止一次听到经验丰富的前端开发者抱怨 JavaScript 的开发体验,许多工作几乎都是在更新、维护、升级依赖,以及升级依赖之后由于 API 变动,需要做的额外的适配工作。如果不做这些工作,项目可能在几个月之内就会过时,变得不可用。尽管锁定依赖版本号之后也能够继续使用旧版本,但一旦选择更新某一个依赖,就需要更新无数个其他依赖,因为一个 JavaScript 库大概率也依赖了十几个、几十个甚至上百个其他库,而这些库又使用了更多的库……
由于依赖之间的层级过多,很容易造成依赖地狱(dependency hell)问题。举一个简单的例子,假设你的项目使用了软件包 A 和软件包 B,而它们同时依赖软件包 C.某一天,软件包 C 发布了新的版本,A 及时跟进了,也发布了新版本,但 B 的开发者还没有反应过来。这时,如果你更新 A,就不得不更新 C,然而 B 依赖的是旧版本的 C,不兼容新版本——A 和 B 的依赖不能被同时满足。这还只是比较简单的例子,现实中的依赖关系可能更加复杂。
其他语言当然也会出现依赖地狱,但远不如 JavaScript 那样复杂,毕竟其他和 JavaScript 有着相似地位的语言大概率有完善的标准库,而 JavaScript 一开始只作为嵌入在浏览器里的脚本语言仓促设计,在留下许多历史包袱的同时也没有考虑到如今会承载的工程化开发,许多简单且常用的操作都需要引入外部库才能实现。你敢信,要在这门专门为浏览器设计的语言里好好地发一个 HTTP 请求,最常用的做法之一竟然是引入 axios 库1.顺带一提,这个库最近还被投毒了。
1.不过值得一提的是,有 Web API 这样的规范一直在解决 JavaScript 标准库不完善的问题,比如 axios 其实就可以用 Fetch API 替代了。

如果你在 GitHub 上发布过一些 JavaScript 项目,那么你大概会频繁地受到 GitHub Dependabot 的骚扰。还在用 Giscus 的那段时间,我常常在 GitHub 新消息图标出现时感到兴奋,以为收到了读者留言,结果新消息的内容是:
Security vulnerability in … affects at least one of your repositories.
在 JavaScript 的世界里,安全漏洞非常常见。从我自己的经验来看,Next.js 以及 React 相关技术栈非常容易出现这种问题,然而我使用 Next.js 开发的并不是对安全性非常敏感的复杂应用(这又回到了前文提到的「过度抽象」问题上),每隔一两周就收到这样一封邮件(是的,GitHub 还会给我发邮件)提醒安全漏洞问题,真的构成骚扰了。
可是安全性却不可忽视,除了软件本身的安全漏洞,NPM(Node.js 的包管理器,也是最大的 JavaScript 软件包分发平台)自身的抗风险能力也很低,隔三岔五就被投毒。让我继续报菜名,列举一下最近发生的 NPM 遭到投毒的事件报道。
2026/3/31 Axios NPM 供应链攻击
2025/11 Shai-Hulud 蠕虫污染 NPM 生态
2025/9 NPM 邮件钓鱼供应链攻击
2025/3 PhantomRaven NPM 供应链攻击
2025/2 Operation Marstech Mayhem 通过 NPM 偷窃加密货币和开发者数据
2024/12 Solana SDK 后门偷窃密钥
剩下的就不一一列举了,因为实在是太多了。其实也不能全怪 NPM,隔壁 Python 的包索引 PyPi 也在最近遭到投毒,详情可以阅读刘家财写的《 全球软件供应链安全演进:从 LiteLLM 投毒事件审视 Rust 包管理及分布式架构转型 》,文中还对比了 Zig、Go、Rust、Python 和 JavaScript(NPM)包管理的风险指数,其中 Zig 是风险最低也最去中心化的,而 NPM 则是另一个极端。
供应链投毒对 JavaScript 开发者的影响很大,执行 npm install 或者其他日常使用的命令之后就可能会有恶意代码在电脑上不知不觉地执行,偷走自己的加密钱包、SSH 密钥等重要信息,开发者的数字身份都有可能受到很大威胁,不仅仅是电脑中毒那么简单。
开发者很不好过,用户也没有好果子吃。
网页交互的越界
我在前文多次提到,我在使用浏览器时默认禁用 JavaScript.这是为什么?NPM 投毒有风险是因为恶意代码会直接在开发者的电脑上执行,难不成恶意的 JavaScript 代码还能跃出浏览器,直接访问设备上的数据吗?就算可以,难道现代浏览器不是把系统权限都管理的很好吗?
的确,除非手动允许网站访问文件、摄像头和外部设备等系统资源,网站并没有办法直接获取储存在本地的密钥等关键信息,但 JavaScript 的确可以在用户没有任何感知的情况下做某些事情,比如获取 Cookies、浏览器版本、安装的浏览器插件等,还能占用你的 CPU 资源进行计算。前者可以用来生成浏览器指纹跟踪用户(一般是用来投放定向广告,但也有隐私风险),如果遇到跨站脚本攻击(XSS),储存在 Cookies 里的登录凭证就可能被偷取;后者被利用的方式就很简单粗暴了,攻击者可以用你的 CPU 在后台悄悄挖矿,这叫做 Cryptojacking.
还有一些问题并不是恶意攻击造成的,而是某些不尊重用户的开发者(有一些甚至不是开发者,只能说是站长吧)的流氓行为。是的,我觉得以下都是流氓行为:在用户暂时离开网页时修改浏览器标签页的图标和标题,禁用系统的右键菜单,禁用审查元素(或者说控制台)和在用户不知情的情况下自动播放音频。
无论是恶意攻击还是流氓行为,都是在用户不知情的情况下加载 JavaScript 脚本导致的。这些脚本是不道德的,比如跟踪器。既然现有的浏览器还没有办法给某些可被利用的 API 设置较高的权限要求,像是否允许网站弹出通知那样让用户自行选择,那么用户就必须另寻他路了,方法就是主动屏蔽和拦截不想要的脚本。
最基础的做法是使用 uBlock Origin 这类脚本,屏蔽跟踪器。说起跟踪器,就不得不提到 Google Analytics 等分析工具都会利用浏览器指纹等手段跟踪用户,这对科技公司来说兴许是必要的商业举措(尽管依然不道德),但许多个人站长也使用此类分析工具。我建议使用 GoatCounter 等不跟踪用户的 Web 统计工具。
当然,uBlock Origin 只会屏蔽那些明显的恶意脚本,往往基于社区维护的屏蔽规则,肯定会有漏网之鱼。如果遇到突然出现的 XSS 攻击,只用浏览器插件就难以阻止了(不要觉得如今 XXS 已经不常见了,之前 Discord 还 被一张 SVG 图片黑了 )。更何况,有些脚本不是恶意,只是恶心,一般的屏蔽器没理由屏蔽它。
如果你已经在使用 uBlock Origin 了,那么你可以直接在设置里选择默认禁用 JavaScript.当你需要在某个网站上加载脚本时,可以为这个站点单独启用 JavaScript.推荐的做法是,不要在你不信任的网站上加载 JavaScript.诚然,这有点麻烦,因为你需要在每次访问一个新网站时确认它是否值得信任,然后再手动开启 JavaScript,不过有不少网站非常尊重用户,不需要 JavaScript 也能正常使用,或者单独提供了无需 JavaScript 的版本,比如 Kagi 和 DuckDuckGo 就证明了搜索引擎不需要 JS 也能正常工作。
禁用 JavaScript 也会导致基于 React 和 Vue 等前端框架的网站完全不可用。我对此的态度比较激进,请谨慎对待:如果一个网站只提供图文内容,竟然还使用这种框架开发而不是提供静态网页,那就没有必要继续访问,高质量的内容在其他地方也能找到;如果你访问的是网页应用,那么没有 JavaScript 就无法使用是完全合理的,确保这个应用是自由且开源的或者你信任的,再手动开启 JavaScript.
对于匿名用户来说,默认禁用 JavaScript 几乎能够完全杜绝被跟踪的可能性,总之百利而无一害。唯一会带来不便的点是使用 Paypal 或 Stripe 支付,或者使用网页版淘宝、京东购物时,因为需要在多个网域之间跳转、进行安全验证和支付操作,需要在多个网站上手动启用 JavaScript,比较麻烦,这时暂时禁用 uBlock Origin 就好。
更多有关禁用 JavaScript 的观点和教程可以参考 disable-javascript.org 。说来有趣,我是在 マリウス 的网站上发现这个项目的,他在网站上添加了一段「流氓」脚本,在用户暂时离开网页时将浏览器图标和标题替换为「地球是平的协会」「亚马逊:老婆抱枕」和「大脚怪裸照」等文本,让其他人以为用户访问的是奇奇怪怪的网站。当回到这个页面时,就会读到マリウス留下的建议:你应该只在自己信任的网站上加载 JavaScript.
最后
我说了不少 JavaScript 的坏话,但毋庸置疑,的确有不少人正在持续推进规范化和相关标准的修订,让这门语言变得更易用。为网页添加交互能力本身是极具开创性的点子,也构成了如今万维网不可或缺的一部分,但令人唏嘘的是,这个极具开创性的点子在一开始就没能得到足够的重视,在极短的开发周期内急急忙忙地上线,留下了许多历史包袱;又由于标准库的不完善,和中心化且抗风险能力低的包管理,这门语言给开发者带来了不少依赖和安全性相关的风险,并且漏洞频发;再加上科技公司对技术的滥用,用户的使用体验也得不到保障,而用户体验就是 JavaScript 一开始被创造的原因,它现在已经成为了在万维网甚至服务器后端行走的怪物。
我不讨厌这门语言,我也希望它变得更好,但就目前而言,开发者和用户都应该谨慎对待这项多少有些失控的技术。

评论(0)