你写的原生极简写法,确实会造成内存泄漏,而且有三处泄漏隐患。
一、为什么会内存泄漏
1. <script> 标签一直留在 DOM 里
const script = document.createElement('script');
script.src = 'xxx?callback=getData';
document.body.appendChild(script);
请求完成后,这个 script 元素还挂在 body 里,不会自动删除 ,多次调用就会堆积大量无用 script 节点 → DOM 节点泄漏。
2. 全局回调函数 getData 常驻全局
window.getData 定义后一直存在,不会自动销毁。多次 JSONP 请求、复用同一个回调名,容易变量污染、回调错乱,且全局引用不释放 → 内存占用累积。
3. 没有事件解绑
如果给 script 绑了 onload/onerror,不手动置空解绑,也会形成引用链无法回收。
二、怎么彻底解决(防泄漏完整版)
核心三件事:
-
请求完成 / 失败后 立即移除 script 标签
-
删除全局回调函数
-
清空事件、解除引用
// 全局回调
window.getData = function(res) {
console.log('拿到数据', res);
// 关键:业务处理完,立刻清理
cleanUp();
};let script;
let timer;// 发起JSONP
function startJsonp() {
script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=getData';// 超时兜底
timer = setTimeout(() => {
console.log('请求超时');
cleanUp();
}, 5000);// 加载失败
script.onerror = function() {
console.log('请求失败');
cleanUp();
};document.body.appendChild(script);
}// 统一清理:杜绝内存泄漏
function cleanUp() {
// 1. 清除超时定时器
clearTimeout(timer);// 2. 移除DOM中的script节点
if (script && script.parentNode) {
script.parentNode.removeChild(script);
}// 3. 解绑事件,切断引用
script.onload = null;
script.onerror = null;// 4. 删除全局回调函数
delete window.getData;// 5. 变量置空,方便GC回收
script = null;
}// 调用
startJsonp();
三、最佳实践(彻底避免全局污染 + 内存泄漏)
用随机唯一回调名,自动清理,不污染全局
function jsonp(url, timeout = 5000) {
return new Promise((resolve, reject) => {
// 随机唯一回调名,防止冲突
const cbName = 'jsonp_cb_' + Date.now() + '_' + Math.random().toString(36);
// 挂载全局回调
window[cbName] = (data) => {
resolve(data);
clean();
};
const script = document.createElement('script');
script.src = `${url}?callback=${cbName}`;
let timer = setTimeout(() => {
reject(new Error('jsonp超时'));
clean();
}, timeout);
script.onerror = () => {
reject(new Error('jsonp请求失败'));
clean();
};
function clean() {
clearTimeout(timer);
// 移除节点
script.parentNode && script.parentNode.removeChild(script);
// 解绑事件
script.onerror = null;
// 删除全局回调
delete window[cbName];
// 置空引用
script = null;
}
document.body.appendChild(script);
});
}
// 使用
jsonp('https://api.example.com/data')
.then(res => console.log(res))
.catch(err => console.error(err));
四、总结
- 你原来的写法:会内存泄漏,DOM 节点堆积 + 全局函数不回收;
- 解决方案必须做:删 script 节点 + 删全局回调 + 清定时器 + 解绑事件 + 变量置空;
- 项目里直接用上面 Promise 封装版,零泄漏、无全局污染、可复用
-
JSONP 只支持 GET 请求,不支持 POST/PUT/DELETE
-
后端必须配合 :返回格式为
回调函数名(数据),例如:getData({ name: "测试", age: 20 }) -
相比 CORS,JSONP 兼容性更好(支持 IE 低版本)
-
封装版使用 Promise ,支持
then/catch异步处理
JSONP 不是 AJAX,它就是一段可执行的 JS 代码。 浏览器拿到后端返回的内容,会直接当作 JS 执行。
1. 你前端写的代码
// 前端告诉后端:我的回调函数叫 getData
const script = document.createElement('script');
script.src = "https://api.com/data?callback=getData";
document.body.appendChild(script);
你定义了一个全局函数:
function getData(data) {
console.log(data);
}
2. 后端必须返回什么?
**后端不能返回 JSON!**不能返回:
{ "name": "小明", "age": 20 }
必须返回一段 JS 函数调用代码:
getData({ "name": "小明", "age": 20 });
3. 为什么必须这样?
因为 <script src="..."> 拿到内容后,会直接执行!
浏览器拿到后端返回的内容后,相当于:
<script>
getData({ "name": "小明", "age": 20 });
</script>
它会立刻执行你定义的 getData 函数,并把数据传进去!
这就是 JSONP 的原理。
4. 用生活例子理解
- 你(前端)给快递员(后端)说:"送到后请喊我的名字:getData"
- 快递员(后端)必须喊:"getData,你的快递到了!"
- 你听到名字,就知道快递来了(拿到数据)
如果后端不喊你的名字,直接扔快递,你根本不知道是给谁的。
5. 后端返回错误会发生什么?
如果后端返回普通 JSON:
{ "name": "小明" }
浏览器会报错:
Uncaught SyntaxError: Unexpected token ':'
因为它把 JSON 当 JS 执行,语法不合法。
6. 后端怎么写?(示例)
Node.js 后端
app.get('/data', (req, res) => {
const callbackName = req.query.callback; // 拿到前端传的 getData
const data = { name: "小明", age: 20 };
// 返回:getData({...})
res.send(`${callbackName}(${JSON.stringify(data)})`);
});
PHP 后端
$callback = $_GET['callback'];
$data = array("name"=>"小明","age"=>20);
echo $callback . '(' . json_encode($data) . ')';
Java 后端
String callback = request.getParameter("callback");
out.print(callback + "({name:'小明',age:20})");
全部都是:回调名 + (+ 数据 +)
7. 最终总结(最关键)
- JSONP = 动态创建 script 标签
- 后端返回的不是数据,是一段 JS 函数调用代码
- 格式必须是:回调函数名 (数据)
- 浏览器执行这段代码,你就拿到数据了