JavaScript 9 个先有库再有 API 的故事

0 阅读2分钟

你好,我是冴羽

你可能觉得 querySelectorclassListPromise 这些是浏览器原生的能力,但实际上,它们都是从第三方库“抄”来的。

这就是经典的 Web 平台的演进逻辑:先有社区方案,再有标准化

本篇带你盘点浏览器 API 中被 JS 库“启发”的 9 个案例。

如果你写 JavaScript 有些年头了,这篇文章会让你回忆起很多往事。

如果你是新人,也欢迎了解一下你每天都在用的这些 API 的前世今生。

1. querySelector 和 querySelectorAll

用 CSS 选择器从 DOM 中选元素,现在看起来理所当然。

但这不是一开始就有的。

在这之前,你得用 getElementByIdgetElementsByClassNamegetElementsByTagName,还要遍历 .children 进行过滤。

Dojo 的 dojo.query 率先实现了这个想法。

然后 jQuery 的 $()把它变成了整整一代 Web 开发者的默认思维模型。

浏览器最终发布了自己的版本:

const button = document.querySelector(".buy-now");
const allButtons = document.querySelectorAll(".buy-now");

console.log(button.textContent);

allButtons.forEach((btn) => {
  console.log(btn.textContent);
});

2. 声明式 UI:popovertarget 和 command

10 年前,Modal 得这样写:

const button = document.querySelector(".open-modal");
const modal = document.querySelector("#my-modal");

button.addEventListener("click", () => {
  modal.classList.add("is-open");
});

Bootstrap 看到这个模式到处重复,就用属性替代了代码:

<button data-toggle="modal" data-target="#my-modal">Open</button>

这是因为 Bootstrap 的 jQuery 插件会读取 data-* 属性,帮你完成显示和隐藏。

这样你就不用写任何 JS 代码了。

平台吸收了这个理念:

<button popovertarget="my-popover">Open</button>

<div id="my-popover" popover>
  <p>Hello from a popover.</p>
  <button popovertarget="my-popover" popovertargetaction="hide">Close</button>
</div>

command 通过明确命名动作,把同样的模式扩展到其他内置元素(比如 <dialog>):

<button commandfor="my-dialog" command="show-modal">Open dialog</button>

<dialog id="my-dialog">
  <p>Hello from a dialog.</p>
  <button commandfor="my-dialog" command="close">Close</button>
</dialog>

3. classList

classList 之前,操作 className 属性是一件非常复杂的事情。

添加一个 class 你得分割、检查、合并。删除一个 class 你得进行过滤。可烦死我了。

后来 jQuery 的 .addClass().removeClass().toggleClass().hasClass() 解决了这些问题。

Web 也发布了自己的标准:

const button = document.querySelector(".buy-now");

button.classList.add("is-loading");
button.classList.remove("is-disabled");
button.classList.toggle("is-active");
button.classList.contains("is-loading"); // boolean
button.classList.replace("is-loading", "is-success"); // jQuery 中没有

4. 字符串和数组的工具方法

一大批工具方法其实都来自工具库:Underscore、Lodash、MooTools、Prototype.js 这些。

一个不完全的列表:

  • Array.prototype.mapfilterreduceforEach

  • Array.prototype.findfindIndexsomeevery

  • Array.prototype.includes

  • Array.prototype.flatflatMap

  • String.prototype.trimtrimStarttrimEnd

  • String.prototype.includesstartsWithendsWith

  • String.prototype.repeat

  • String.prototype.padStartpadEnd

  • Object.keysvaluesentries

  • Object.assign

这些方法每一个过去都需要装个库来实现。

5. structuredClone

克隆可谓是 JS 经典的面试题了。

后来大家开始使用 Lodash 的 _.cloneDeep,或者使用一个经典的 JSON.parse(JSON.stringify(obj)) 技巧。

但都有些小问题。

20 年后,终于出了 structuredClone

const original = {
  name: "Jad",
  createdAt: new Date("2026-01-01"),
  tags: new Map([["role", "instructor"]]),
  nested: { items: [1, 2, 3] },
};

const copy = structuredClone(original);

copy.nested.items.push(4);

console.log(original.nested.items); // [1, 2, 3] (未改变)
console.log(copy.nested.items); // [1, 2, 3, 4]
console.log(copy.createdAt instanceof Date); // true
console.log(copy.tags instanceof Map); // true

6. Promises

当年关于 Promise 的库可多了…… Dojo Deferred、jQuery Deferred、Q、Bluebird 等等。

最终出了统一的 Promises/A+ 规范。

再后来,浏览器实现了 Promise。

如今 Promises 和 async/await 是语言的默认异步模型了:

async function loadUser(id) {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) {
    throw new Error("Failed to load user");
  }
  return response.json();
}

const user = await loadUser(42);
console.log(user.name);

7. ES Modules

JavaScript 刚出现时竟然没有模块系统。

于是 Node.js 使用 CommonJS 规范,浏览器通过 RequireJS 使用 AMD 规范。

最终浏览器发布了自己的:

// math.js
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;
// app.js
import { add, PI } from "./math.js";

console.log(add(2, 3)); // 5
console.log(PI); // 3.14159

8. Temporal

JavaScript 的 Date 对象有问题了 30 年,终于出了 Temporal 彻底替代:

const birthday = Temporal.PlainDate.from("2026-06-27");
const reminder = birthday.subtract({ days: 7 });

console.log(reminder.toString()); // 2026-06-20
console.log(birthday.toString()); // 2026-06-27 (未改变)

9. Element.closest()

过去,向上遍历 DOM 找到最近的匹配祖先过去意味着手写 while 循环,或者用 jQuery 的 .closest()

现在它是内置的了。

一时间你可能想不到它的用户,一个典型用例是事件委托:

document.addEventListener("click", (event) => {
  const button = event.target.closest("button.action");
  if (!button) return; // 点击不在(或不在内部)匹配的按钮上

  const action = button.dataset.action;
  console.log("action:", action);
});

使用 closest(),你就可以找出实际点击了哪个按钮。

最后

当然了,不是每个浏览器特性都始于库,很多也直接来自浏览器工程师和标准机构。

但这条特定的路径 —— 先有社区方案,再有标准化是最健康的演进方式之一。

因为这是一个反馈循环,而不是竞争。

这些库在生产环境中被成千上万的开发者测试,收集 bug 反馈,不断迭代。

那些活下来的模式,最终被标准化进了平台本身。

但不用伤心,平台吸收库不是库“输了”。而是它们成功到变得不必要了。

谨此怀念一下当年写的那么多代码吧,都已经成了历史的尘埃。

我是冴羽,10 年笔耕不辍,专注前端领域,更新了 10+ 系列、300+ 篇原创技术文章,翻译过 Svelte、Solid.js、TypeScript 文档,著有小册《Next.js 开发指南》、《Svelte 开发指南》、《Astro 实战指南》。

欢迎围观我的“网页版朋友圈”,关注我的公众号:冴羽(或搜索 yayujs),每天分享前端知识、AI 干货。