如何手写 AJAX 请求和 JSONP?它们有什么区别?
简化版
手写 AJAX 通常用 XMLHttpRequest 封装 Promise,监听 readystatechange 或 load/error/timeout,处理状态码、超时、请求头和响应解析。JSONP 则是动态插入 script 标签,通过回调函数接收数据,只支持 GET,本质不是 XHR,也不能读取 HTTP 状态细节。
详细版
基础 AJAX:
function ajax({ url, method = 'GET', data, headers = {}, timeout = 0 }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.timeout = timeout
Object.entries(headers).forEach(([key, value]) => xhr.setRequestHeader(key, value))
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) resolve(xhr.responseText)
else reject(new Error(`HTTP ${xhr.status}`))
}
xhr.onerror = () => reject(new Error('network error'))
xhr.ontimeout = () => reject(new Error('timeout'))
xhr.send(data ?? null)
})
}
JSONP 通过约定回调名:服务端返回 callbackName({...}),前端提前把函数挂到 window 上。它能绕过同源限制加载脚本,但安全和能力边界都比 CORS 差。
完整版教学
一、AJAX 与 JSONP 的本质差异
| 对比项 | AJAX/XHR | JSONP |
|---|---|---|
| 底层能力 | 浏览器网络请求 API | script 标签加载脚本 |
| 跨域 | 受同源策略与 CORS 控制 | 利用脚本可跨域加载 |
| 方法 | 支持多种 HTTP 方法 | 通常只支持 GET |
| 状态码 | 可读取 | 很难可靠读取 |
| 安全性 | 可配合 CORS、凭证、头部 | 执行远端脚本,风险更高 |
AJAX 是请求数据,JSONP 是执行代码。这个区别非常关键,因为 JSONP 返回内容一旦被加载,就会在当前页面上下文执行。
易错点:JSONP 不是“特殊 AJAX”,它没有使用 XHR,也没有真正绕过浏览器安全模型,只是利用了脚本资源加载规则。
二、手写 Promise 版 AJAX
function ajax(options) {
const {
url,
method = 'GET',
data = null,
headers = {},
timeout = 0,
responseType = '',
} = options
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open(method, url, true)
xhr.timeout = timeout
xhr.responseType = responseType
for (const [key, value] of Object.entries(headers)) {
xhr.setRequestHeader(key, value)
}
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseType ? xhr.response : xhr.responseText)
} else {
reject(new Error(`HTTP ${xhr.status}`))
}
}
xhr.onerror = () => reject(new Error('network error'))
xhr.ontimeout = () => reject(new Error('timeout'))
xhr.onabort = () => reject(new Error('aborted'))
xhr.send(data)
})
}
onload 表示请求完成,但不代表业务成功,所以还要检查状态码。onerror 常表示网络层失败、DNS 失败或跨域被拦截,很多时候拿不到精确原因。
三、GET 参数和 JSON 请求体
function buildQuery(params) {
const search = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) search.append(key, value)
})
return search.toString()
}
ajax({
url: '/api/user',
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ name: 'Tom' }),
})
GET 请求通常把参数拼到 URL,POST JSON 请求需要设置 Content-Type 并序列化对象。真实封装还要处理重复 key、数组参数、编码和后端约定。
四、手写 JSONP
function jsonp(url, params = {}, { callbackKey = 'callback', timeout = 5000 } = {}) {
return new Promise((resolve, reject) => {
const callbackName = `__jsonp_${Date.now()}_${Math.random().toString(16).slice(2)}`
const script = document.createElement('script')
const timer = setTimeout(() => cleanup(new Error('timeout')), timeout)
function cleanup(error, data) {
clearTimeout(timer)
delete window[callbackName]
script.remove()
if (error) reject(error)
else resolve(data)
}
window[callbackName] = data => cleanup(null, data)
const query = buildQuery({ ...params, [callbackKey]: callbackName })
script.src = `${url}${url.includes('?') ? '&' : '?'}${query}`
script.onerror = () => cleanup(new Error('script load error'))
document.head.appendChild(script)
})
}
服务端需要返回类似 __jsonp_xxx({"name":"Tom"}) 的 JavaScript。前端函数名必须唯一,结束后要清理全局函数、定时器和 script 节点,否则会污染全局作用域。
五、状态码、错误和超时边界
AJAX 可以区分 HTTP 404、HTTP 500、超时和主动 abort。JSONP 加载的是脚本,脚本是否执行回调才是主要信号,很多状态码细节浏览器不会暴露给脚本逻辑。
AJAX: open -> send -> load/error/timeout/abort -> inspect status
JSONP: create script -> load remote JS -> remote JS calls callback -> cleanup
如果 JSONP 的脚本加载成功但服务端返回的不是预期回调,Promise 会等到超时才失败。因此 JSONP 必须设置超时。
六、安全与现代替代方案
JSONP 会执行第三方返回的 JavaScript,一旦服务端被污染,攻击代码就能运行在当前页面权限下。现代跨域请求优先使用 CORS,服务端通过响应头授权来源、方法和请求头,前端继续使用 fetch 或 XHR。
| 场景 | 推荐 |
|---|---|
| 新项目跨域 API | CORS + fetch |
| 老接口只支持 JSONP | 临时 JSONP 封装 |
| 携带凭证的跨域请求 | 明确 CORS 凭证策略 |
| 不可信第三方数据源 | 避免 JSONP |
面试中提到 JSONP 时,要同时说出它的历史价值和安全局限,不能只写出动态 script。
七、常见误区与追问
- 误区:JSONP 支持 POST。 它靠 script src 发起资源加载,通常只能 GET。
- 误区:AJAX 的
onload就代表成功。 还要检查 HTTP 状态码和业务响应。 - 误区:JSONP 比 CORS 更安全。 JSONP 执行远端脚本,风险通常更高。
- 追问:为什么 JSONP 要清理全局函数? 避免命名污染和内存泄漏。
- 追问:跨域失败为什么拿不到详细错误? 浏览器为了安全会隐藏很多跨域失败细节。
- 追问:如何取消 AJAX? 调用
xhr.abort(),并处理onabort。
八、加强记忆
AJAX 是“用浏览器请求 API 拿数据”,JSONP 是“把远端响应当脚本执行”。手写 AJAX 抓住 open、header、send、load/error/timeout/abort 和状态码;手写 JSONP 抓住唯一回调名、script 注入、超时、错误和清理。现代项目优先 CORS,JSONP 只作为历史兼容题理解。