在维护年代较久的 Web 项目时,经常会遇到同时引入 jQuery 与 Prototype.js 的情况。jQuery 把 $ 当作核心入口,Prototype.js 也把 $ 当作最常用的元素选择函数。两者一旦在同一页面加载,后加载的库会直接覆盖先加载库的全局 $ 变量,导致一部分代码仍然按旧逻辑调用 $ 时出现类型错误。更隐蔽的是 Prototype.js 对 Array、Object 等内置原型做了扩展,这种原型链污染会改变 for in 遍历结果,并干扰依赖对象纯净性的组件。

理解这两个问题不能只停留在表面报错。$ 符号冲突本质是全局作用域下的引用覆盖,而原型链污染则是库设计哲学带来的兼容成本。下面从变量覆盖机制、noConflict 释放、原型扩展影响以及工程化隔离四个层面展开。
一、全局 $ 符号为什么会互相覆盖
浏览器中通过 <script> 标签引入的顶层函数和变量,默认都会挂载到 window 对象上。jQuery 载入后会创建 window.jQuery 和 window.$ 两个引用,且二者指向同一个函数。Prototype.js 同样会创建 window.$,并且还提供 window.Prototype、window.$$ 等入口。由于普通脚本是按顺序同步执行,当页面先加载 jQuery 再加载 Prototype.js,Prototype.js 内部的赋值操作会把 window.$ 指向自己的选择器函数,jQuery 原先占用在 $ 上的引用就被彻底替换。反过来先加载 Prototype.js 再加载 jQuery,也会出现同样问题,只是被覆盖的对象互换。
这种覆盖并不是两个库在运行时相互检测,而是 JavaScript 对对象属性赋值的最基本行为。全局变量就是 window 的属性,重复赋值只会更新属性值,不会保留历史值。因此当业务代码中一部分插件使用 $ 发起 Ajax,另一部分使用 $ 获取 DOM 元素时,只要加载顺序改变,两者的行为都会瞬间发生错位。更麻烦的是报错信息可能是 $ is not a function,也可能是某个方法不存在,定位起来并不直观。
二、用 jQuery.noConflict 释放 $ 引用
解决 $ 覆盖最直接的办法是使用 jQuery.noConflict()。调用该方法后,jQuery 会把 window.$ 恢复为加载 jQuery 之前的值,同时返回 jQuery 函数本身,开发者可以用一个局部变量保存这个返回值。代码示例如下:
<script src="prototype.js"></script>
<script src="jquery.js"></script>
<script>
var $j = jQuery.noConflict();
// 这里 $ 属于 Prototype.js
var item = $('item');
// 这里 $j 属于 jQuery
$j('#content').addClass('active');
</script>
上面代码先把 Prototype.js 放在前面,jQuery 后加载时会暂时占用 $,但 noConflict 执行后,$ 会还给 Prototype.js,jQuery 则通过 $j 继续使用。这种写法适合旧代码以 Prototype.js 为主的项目。
如果反过来希望 jQuery 继续使用 $,而 Prototype.js 换成其他变量,可以在加载 Prototype.js 前先保存它的 $,或者在 Prototype.js 加载完成后执行对应释放逻辑。但由于 Prototype.js 没有像 jQuery.noConflict 这样统一且成熟的释放接口,实际维护时一般建议让 Prototype.js 保留 $,jQuery 使用 jQuery 或自定义别名,这样对旧代码侵入最小。也可以把所有 jQuery 调用放在立即执行函数里,通过参数传入局部 $,避免污染外部作用域。代码如下:
(function($) {
$(document).ready(function() {
$('a.external').on('click', function() {
// 使用局部 $ 调用 jQuery
});
});
})(jQuery.noConflict());
这种模式的好处是函数内部仍可沿用 $ 写法,外部则不受影响。需要特别注意的是,noConflict 只能恢复一次 $,如果页面中还存在其他库也使用 $,恢复结果取决于加载顺序,因此建议在所有库加载完成后再统一调用 noConflict。
三、原型链污染的具体表现与检测方式
Prototype.js 的设计思路是通过扩展内置原型来提供更丰富的 API。例如它会给 Object.prototype 添加 keys、values、extend 等方法,给 Array.prototype 添加 each、collect 等方法,给 Function.prototype 添加 bind 等。这种全局扩展让代码书写变得更简洁,但副作用是任何普通对象都会从原型链上继承这些方法。最典型的问题是使用 for in 循环时,这些原型方法会作为可枚举属性被遍历出来。即使加一层 if 判断,也会给团队带来额外约束。
原型链污染并不只影响 for in。某些 JSON 序列化逻辑、对象合并函数、浅比较工具在遍历对象时如果没有正确过滤继承属性,就可能把 Prototype.js 注入的方法当成业务字段处理,导致提交给接口的数据多出 keys、extend 等字段。对象是否为空判断也会受到影响,很多开发者习惯用 for in 后设置标记,却忽视了继承属性会进入循环。可用 Object.hasOwnProperty 检测属性是实例自身还是继承来自原型。例如:
var data = { id: 1 };
for (var key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
console.log('自身属性:', key);
}
}
在现代 JavaScript 中更推荐使用 Object.keys 获取自身可枚举属性,避免直接依赖 for in。若需要完全不受原型链影响,可以创建无原型对象,例如使用 Object.create(null)。这种对象不继承 Object.prototype 的任何方法,适合用作字典或缓存结构。不过它也会失去 toString、hasOwnProperty 等基础方法,使用时要根据场景权衡。
需要说明的是,jQuery 一般不会直接修改 Object.prototype,所以标题中提到的原型链污染主要来自 Prototype.js。但如果项目里的插件或业务代码自身也习惯扩展内置原型,那风险会叠加。工程上应尽量禁止扩展 Object.prototype,因为它是所有对象的基础,污染范围最大。
四、同时使用两个库的工程化实践
在真实项目中,单纯解决 $ 冲突还不够,还需要从加载顺序、模块封装、原型污染规避等角度做整体安排。首先应明确一个原则:页面加载的所有脚本要么使用同一个 $ 约定,要么全部通过显式变量访问,避免对全局 $ 产生混合依赖。可以在公共脚本中声明 var $j = jQuery.noConflict(),然后所有新代码只使用 $j,旧代码继续使用 Prototype.js 的 $。
如果使用 RequireJS、Sea.js 等模块加载器,可以在配置阶段为两个库定义不同模块名,并在回调中接收各自的导出对象。此时两个库仍然会操作全局 $,模块内部可以通过局部参数隔离。现代打包工具如 webpack 也能通过 ProvidePlugin 或 resolve alias 控制全局变量注入,但需要确保最终生成的运行环境里对 $ 的引用与加载顺序匹配。
针对原型链污染,建议在项目启动阶段做一次全局检查,确认页面中是否有代码直接用 for in 遍历业务对象。如果有,应逐步改为 Object.keys 或 Object.prototype.hasOwnProperty.call 过滤。对于需要保存键值对的场景,优先使用 Map,或者用 Object.create(null) 生成没有继承属性的字典。使用 Prototype.js 的老项目还可以通过冻结 Object.prototype 来阻止后续代码继续添加属性,但冻结内置原型会影响 Prototype.js 自身工作,必须谨慎评估。
最后总结一下:$ 符号冲突和原型链污染是历史库混用时最常见的两类问题。前者通过 jQuery.noConflict 和作用域隔离可以稳定解决,后者则需要从编码习惯和对象遍历方式上持续规避。只要在加载顺序、别名约定、原型检测三个方面形成规范,jQuery 与 Prototype.js 完全可以在同一个页面中共存。
jQueryPrototype.js$符号修改时间:2026-09-27 15:30:36