Skip to content

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

Description

@mqyqingfeng

你好,我是冴羽

你可能觉得 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 干货。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions