使用 JSONP 解决跨域数据请求时,服务端返回的并不是纯 JSON 文本,而是一段 JavaScript 函数调用。函数名必须由前端提前定义并注入到全局作用域,同时通过 URL 参数告诉服务端。jQuery 在内部负责了这个函数名的创建、注册和清理工作。如果没有自定义 jsonpCallback 参数,返回给服务端的 callback 值看起来像是一串随机数字,例如 jQuery3710123_1699999999999。这个字符串从何而来、是否每次都是重新随机、不同请求之间如何保证不冲突,下面从源码角度拆开分析。

一、JSONP 回调函数名为什么必须唯一
JSONP 的本质是利用 <script> 标签不受同源策略限制的特点,动态创建一个脚本元素去加载远程接口。服务端收到请求后,会把真正的数据包在一个函数调用里返回,例如 handleData({"id":1})。浏览器执行这段脚本时,会去全局作用域查找名为 handleData 的函数并调用它。因此,前端必须先挂载一个同名的全局函数,否则请求完成后会直接报错。
如果同一个页面同时发出多个 JSONP 请求,并且它们都使用同一个回调函数名,就会出现严重的覆盖问题。后一个请求把 window.handleData 重新赋值后,前一个请求的响应回来时也会调用这个新函数,导致数据串扰、回调执行异常。并发越多,现象越隐蔽。jQuery 解决这个问题的思路是为每个请求分配一个独立且不易重复的函数名,并保证请求结束后能够清理干净。
从设计上看,这个函数名需要同时满足三个条件:全局唯一、不容易被业务代码误用、后续可以安全回收。jQuery 并没有每次请求都去调用 Math.random(),而是采用前缀随机生成、后缀自增计数的方式,在性能和唯一性之间取得了平衡。
二、jQuery 默认 jsonpCallback 的生成算法
jQuery 在脚本加载模块中维护了一个全局前缀 jQuery.expando。这个前缀在页面存活期间只生成一次,它由版本号和随机小数拼接后去掉所有非数字字符得到。可以用下面的代码模拟这个生成过程:
// 模拟 jQuery 内部生成 expando 的思路 var version = "3.7.1"; var expando = "jQuery" + (version + Math.random()).replace(/\D/g, ""); console.log(expando); // 输出类似 jQuery3710123,后面的数字包含版本号和随机部分
这里需要注意的是,version + Math.random() 并不是数学加法,而是字符串拼接。版本号 "3.7.1" 加上一个随机小数后,得到类似 "3.7.10.428571" 的字符串,再通过正则 /\D/g 去掉非数字字符,最终剩下的就是一组纯数字。不同页面实例初始化的随机数不同,因此 jQuery.expando 通常不会相同。
有了稳定前缀之后,jQuery 在默认 ajaxSetup 中定义了一个 jsonpCallback 函数。每次发起 JSONP 请求时,如果用户没有传入自定义名称,jQuery 就会执行这个默认函数。简化后的逻辑如下:
var nonce = { guid: Date.now() };
var oldCallbacks = [];
jQuery.ajaxSetup({
jsonp: "callback",
jsonpCallback: function() {
var callback = oldCallbacks.pop() || (jQuery.expando + "_" + (nonce.guid++));
this[callback] = true;
return callback;
}
});这段逻辑中,nonce.guid 的初始值是 Date.now(),也就是页面初始化时的一个毫秒级时间戳。每次生成回调名时,后缀会自增 1。因此同一个页面先后发起的请求,回调名大致是:
jQuery3710123_1700000000000 jQuery3710123_1700000000001 jQuery3710123_1700000000002
可以看到,所谓随机其实只发生在 jQuery.expando 的前缀创建阶段。对于同一次页面会话,后续回调名主要依靠时间戳加自增序号保证唯一。这种设计避免了高频请求下 Math.random() 可能出现的碰撞,也让回调名具备可预测的递增特征,便于调试和日志追踪。
三、自定义 jsonpCallback 的两种传参方式
如果业务需要控制回调函数名,可以在 $.ajax() 中通过 jsonpCallback 参数显式指定。jQuery 支持两种形式:直接传字符串,或者传一个函数。传字符串时,这个名字会被原样拼接到请求 URL 上,同时挂载到 window 对象。示例代码如下:
$.ajax({
url: "https://ipipp.com/api/user",
dataType: "jsonp",
jsonp: "cb",
jsonpCallback: "handleUser",
success: function(data) {
console.log(data);
}
});上面的请求最终会向 https://ipipp.com/api/user?cb=handleUser 发起加载。服务端必须读取 cb 参数,并返回 handleUser({...}) 这样的脚本。如果服务端写死了别的名字,或者返回纯 JSON,前端都会执行失败。这里 jsonp 参数用于指定 URL 中的查询参数名,默认是 callback,示例里改成 cb 是为了更贴合部分接口习惯。
如果希望在回调名前再加上动态时间戳,可以使用函数形式:
jsonpCallback: function() {
return "custom_" + Date.now();
}传入函数时,jQuery 会立即调用它并取返回值作为最终回调名。这种写法保留了自定义前缀,又加入时间信息避免部分缓存问题。但要注意,如果函数每次返回同一个固定字符串,依然会出现前面提到的并发覆盖问题。因此除非能保证同一时刻只有一个同名请求,否则不建议返回固定值。
另外,如果同时设置了 jsonp: false 或 jsonpCallback: false,jQuery 就不会再为该请求注入回调机制。这种情况下通常意味着服务端已经处理好了脚本调用,或者请求已经退化为普通 script 加载,需要开发者自己保证执行正确。
四、回调函数名的释放、复用与服务端配合
每个 JSONP 请求完成后,jQuery 都必须清理挂在 window 上的临时函数,否则长时间运行的页面会残留大量无效全局属性。清理逻辑大致如下:
jqXHR.always(function() {
if (overwritten === undefined) {
jQuery(window).removeProp(callbackName);
} else {
window[callbackName] = overwritten;
}
if (callbackName) {
oldCallbacks.push(callbackName);
}
});这里有一个细节:如果 window[callbackName] 在请求前已经有值,说明业务代码本来就定义过同名函数。jQuery 会在请求结束后恢复原来的引用,避免破坏页面已有逻辑。如果原本没有定义,则直接删除临时属性。对于默认生成的随机回调名,jQuery 还会把它放入 oldCallbacks 数组中,后续新的 JSONP 请求会优先从这个数组里取出旧名字复用。这样做一方面减少了 window 属性的频繁创建,另一方面也能降低部分浏览器内存压力。
服务端配合随机回调名时,只需要从查询参数中读取名称即可。以 PHP 为例,可以这样返回:
$callback = isset($_GET['callback']) ? $_GET['callback'] : 'callback';
header('Content-Type: application/javascript');
$data = array("code" => 200, "message" => "ok");
echo $callback . '(' . json_encode($data) . ');';这里不能把 $callback 写死为某个固定名称,否则当 jQuery 使用随机生成的回调名时,脚本返回的函数调用名和前端已经挂载的函数名不匹配,请求会失败。正确做法是始终读取 URL 中的 callback 或约定参数,再拼接到输出里。
随机回调名还有一个容易被忽略的好处:每次请求的 URL 都不同,可以绕过一部分浏览器或中间代理对相同 URL 的缓存策略。如果业务上不想让 JSONP 结果被缓存,使用默认的随机名是简单有效的方案。反过来,如果希望命中缓存,则需要显式传入固定的 jsonpCallback,并确保服务端允许缓存 application/javascript 响应。
整体来看,jQuery 对 jsonpCallback 的处理可以概括为:初始化时生成带随机数的一次性前缀,后续通过时间戳加自增序号生成完整名称;请求结束后自动清理并复用旧名称;用户自定义时则直接采用传入值。这套机制在保证唯一性的同时控制了性能开销,是早期前端跨域方案中非常成熟的一层封装。

