Appearance
手写题
问题 1:实现一个函数柯里化 add
- add(1,2,3).valueOf() // 6
- add(1,2)(3)(4,5).valueOf() // 15
- add(1)(2)(3)(4,5,6)(7).valueOf() // 28
js
const curry = (...args1) => {
let params = args1
const addFn = (...args2) => {
params = params.concat(args2)
return addFn
}
addFn.valueOf = () => {
return params.reduce((pre, cur) => {
return pre + cur
}, 0)
}
return addFn
}
const add = curry(1, 2, 3)
console.log(add(4, 5, 6)(7).valueOf()) // 28问题 2:实现响应式数据 + 依赖收集
要求:编写 constructor,让其实现数据修改触发 render,且同步变更需要合并。
js
class Component {
data = { name: '' }
constructor() {}
render() {
console.log(`render - name:${this.proxyData.name}`)
}
}
// 要求以下代码需要触发render, 且同步变更需要合并。
const com = new Component()
com.proxyData.name = 'a'
com.proxyData.name = 'b'
com.proxyData.name = 'wifi'
// 上面三个执行完后,第一次触发 render
setTimeout(() => {
com.proxyData.name = 'hello wifi'
})
// 定时器执行完后,第二次触发 render解析:通过 Proxy 触发响应式,加上 Promise 实现依赖收集。
js
class Component {
data = { name: '' }
pending = false
constructor() {
this.proxyData = new Proxy(this.data, {
set: (target, key, value) => {
// target => sthis.data
target[key] = value
if (!this.pending) {
this.pending = true
// 第一次和第二次触发,相差一个微任务,只需要在微任务中重新执行render就行
Promise.resolve().then(() => {
this.render()
})
}
}
})
}
render() {
this.pending = false
console.log(`render - name:${this.proxyData.name}`)
}
}
// 要求以下代码需要触发render, 且同步变更需要合并。
const com = new Component()
com.proxyData.name = 'a'
com.proxyData.name = 'b'
com.proxyData.name = 'wifi'
// 上面三个执行完后,第一次触发 render
setTimeout(() => {
com.proxyData.name = 'hello wifi'
})
// 定时器执行完后,第二次触发 render问题 3:手写 instanceof 操作符
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
js
/**
* 思路:
* 1、通过 Object.getPrototypeOf 获取 obj 的原型
* 2、循环判断 objProtoType 是否和 constructor 的原型相等
* 2.1、如果相等就返回 true
* 2.2、如果不相等 就重新赋值一下 obj 的原型 进入下一次循环
* 3、判断是 objProtoType 是否为空 如果为空就说明不存在 返回 false
*/
function myInstanceof(obj, constructor) {
// 检查obj是否为对象或函数
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
return false
}
// 获取构造函数的原型对象 let proto = obj.__proto__
let proto = Object.getPrototypeOf(obj)
// 检查构造函数的原型链是否包含给定的构造函数的原型
while (proto !== null) {
if (proto === constructor.prototype) {
return true
}
// obj.__proto__.__proto__ ...
proto = Object.getPrototypeOf(proto)
}
return false
}
class A {}
class B extends A {}
class C extends B {}
let obj = new C()
console.log(myInstanceof(obj, A))问题 4:手写 Object.create
Object.create():创建一个新的对象,将传入的对象原型指向新对象并返回。
js
function MyObjectCreate(obj) {
// object 必须是对象或 null,否则抛错
if (typeof obj !== 'object' && typeof obj !== 'function' && obj !== null) {
throw new TypeError('Object prototype may only be an Object or null')
}
// 核心代码
function Fn() {}
Fn.prototype = obj
return new Fn()
}
const person = { name: 'wifi' }
const p = MyObjectCreate(person)
console.log(p.__proto__ === person) // true(原型绑定成功)问题 5:使用 setTimeout 实现 setInterval
js
function MySetInterval(callback, delay) {
const interval = () => {
callback()
setTimeout(interval, delay)
}
setTimeout(interval, delay)
}
MySetInterval(() => {
console.log(111)
}, 1000)js
function MySetInterval(callback, delay) {
let timerId = null
const interval = () => {
callback()
timerId = setTimeout(interval, delay)
}
timerId = setTimeout(interval, delay)
return {
clear() {
clearTimeout(timerId)
}
}
}
const { clear } = MySetInterval(() => {
console.log(111)
}, 1000)问题 6:发布订阅模式
js
class EventEmitter {
constructor() {
this.emit = {}
}
// 订阅事件
on(eventName, callback) {
if (!this.emit[eventName]) {
this.emit[eventName] = []
}
this.emit[eventName].push(callback)
}
// 发布事件
emits(eventName, ...args) {
if (!this.emit[eventName]) {
return new Error(eventName + 'event is not find')
}
this.emit[eventName].forEach((cb) => cb(...args))
}
// 取消订阅
off(eventName, callback) {
if (!this.emit[eventName]) {
return new Error(eventName + 'event is not find')
}
if (callback) {
this.emit[eventName] = this.emit[eventName].filter(
(cb) => cb !== callback
)
} else {
delete this.emit[eventName]
}
}
}
// 使用示例
const emitter = new EventEmitter()
// 订阅事件
emitter.on('message', (message) => {
console.log(`收到消息: ${message}`)
})
// 发布事件
emitter.emits('message', '你好,世界!')
emitter.emits('message', '你好,世界2!')
// 取消订阅事件
emitter.off('message')
// 再次发布事件,此时回调函数不会被执行
emitter.emits('message', '再见,世界!')js
const emitter = (function () {
var deps = {}
return {
on: function (type, cb) {
deps[type] = deps[type] || []
deps[type].push(cb)
},
emit: function (type, ...rest) {
deps[type] instanceof Array &&
deps[type].forEach((cb) => cb.apply(null, rest))
},
off: function (type, cb) {
if (!deps[type]) return
let index = deps[type].findIndex((item) => item === cb)
if (index !== -1) {
deps[type].splice(index, 1)
}
}
}
})()
const handle1 = (data) => {
console.log('test 1', data)
}
emitter.on('test', handle1)
emitter.on('test', (data) => {
console.log('test 2', data)
})
setTimeout(() => {
emitter.emit('test', 'hello world')
emitter.off('test', handle1) // 取消订阅
emitter.emit('test', 'hello world') // 当 off 了handle1 后,handle1 不会再执行了
}, 1000)问题 7:手写 节流、防抖、深浅拷贝、new、call、apply、bind、promise、setTimeout、setInterval 校正、并发请求
这些问题在对应 JS 部分 和 Promise 部分有。
问题 8:手写 LRU 缓存算法
LRU 缓存算法是一种常用的缓存算法,它基于最久未使用的原则,即当缓存空间满了时,会淘汰最近最不常用的缓存项。
js
class LRUCache {
constructor(capacity) {
// 最大容量
this.capacity = capacity
this.cache = new Map()
}
has(key) {
return this.cache.has(key)
}
get(key) {
const value = this.cache.get(key)
// 删除并重新插入,确保是最新加入的
this.cache.delete(key)
this.cache.set(key, value)
return value
}
put(key, value) {
if (this.cache.has(key)) {
// 如果有值,先删除,再新增
this.cache.delete(key)
} else if (this.cache.size >= this.capacity) {
// 没有值,如果容量已满,删除最老的缓存
// this.cache.keys() 返回一个迭代器对象,该迭代器可以遍历 Map 中所有的键
// 迭代器对象具有 next() 方法,调用该方法会返回一个包含 value 和 done 属性的对象
// this.cache.keys().next().value 用来获取迭代器中第一个键的值
this.cache.delete(this.cache.keys().next().value)
// 或者将 Map 的 keys 转换为数组,取第一个元素
// const oldestKey = Array.from(this.cache.keys())[0]
// this.cache.delete(oldestKey)
}
this.cache.set(key, value)
}
}问题 9:排序算法
js
/**
* 冒泡排序:相邻元素两两比较,大的往后冒
* 时间复杂度 O(n²)
*/
function bubbleSort(arr) {
const result = [...arr]
const n = result.length
for (let i = 0; i < n - 1; i++) {
let swapped = false
for (let j = 0; j < n - 1 - i; j++) {
if (result[j] > result[j + 1]) {
;[result[j], result[j + 1]] = [result[j + 1], result[j]]
swapped = true
}
}
if (!swapped) break
}
return result
}js
/**
* 快速排序:选基准分区,递归排序
* 时间复杂度 平均 O(n log n)
*/
function quickSort(arr) {
if (arr.length <= 1) return [...arr]
const pivot = arr[Math.floor(arr.length / 2)]
const left = arr.filter((x) => x < pivot)
const mid = arr.filter((x) => x === pivot)
const right = arr.filter((x) => x > pivot)
return [...quickSort(left), ...mid, ...quickSort(right)]
}问题 10:数组操作
1. 数组扁平化
flat:将多维数组转换为一维数组
js
function flattenRecursive(arr) {
const result = []
for (const item of arr) {
if (Array.isArray(item)) {
result.push(...flattenRecursive(item))
} else {
result.push(item)
}
}
return result
}js
function flattenStack(arr) {
const stack = [...arr]
const result = []
while (stack.length) {
const top = stack.pop()
if (Array.isArray(top)) {
stack.push(...top)
} else {
result.unshift(top)
}
}
return result
}js
const nested = [1, [2, [3, [4, 5]], 6], 7]
console.log('原数组:', JSON.stringify(nested))
console.log('递归扁平化:', flattenRecursive(nested))
console.log('堆栈扁平化:', flattenStack(nested))
// 原数组: [1,[2,[3,[4,5]],6],7]
// 递归扁平化: [
// 1, 2, 3, 4,
// 5, 6, 7
// ]
// 堆栈扁平化: [
// 1, 2, 3, 4,
// 5, 6, 7
// ]2. 数组去重
js
// 方法一:Set(最常用)
function unique1(arr) {
return [...new Set(arr)]
}
// 方法二:Map(根据指定键值去重)
function uniqueBy(arr, keyFn = (x) => x) {
const seen = new Map()
return arr.filter((item) => {
const k = keyFn(item)
if (seen.has(k)) return false
seen.set(k, true)
return true
})
}3. 实现 filter
实现 Array.prototype.filter:返回通过回调测试的元素组成的新数组
js
Array.prototype.myFilter = function (callback) {
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function')
}
const result = []
for (let i = 0; i < this.length; i++) {
// this指向调用他的元素,也就是下面的nums数组
if (callback(this[i])) {
result.push(this[i])
}
}
return result
}
// ========== 示例 ==========
const nums = [1, 2, 3, 4, 5, 6]
console.log('原数组:', nums)
console.log(
'偶数:',
nums.myFilter((x) => x % 2 === 0)
)
console.log(
'大于3:',
nums.myFilter((x) => x > 3)
)
const users = [
{ name: 'Alice', age: 20 },
{ name: 'Bob', age: 17 },
{ name: 'Carol', age: 22 }
]
console.log(
'成年人:',
users.myFilter((u) => u.age >= 18)
)4. 实现 map
js
Array.prototype.myMap = function (callback) {
const result = []
for (let i = 0; i < this.length; i++) {
// this 指向调用它的数组
result.push(callback(this[i]))
}
return result
}
// ========== 示例 ==========
const nums = [1, 2, 3, 4, 5]
console.log('原数组:', nums)
console.log(
'平方:',
nums.myMap((x) => x * x)
)
console.log(
'转字符串:',
nums.myMap((x) => String(x))
)5. 实现 reduce
js
Array.prototype.myReduce = function (callback, initialValue) {
let acc = initialValue
let i = 0
// 没传初始值时,用第一个元素作为初始值
if (acc === undefined) {
acc = this[0]
i = 1
}
for (; i < this.length; i++) {
acc = callback(acc, this[i])
}
return acc
}
// ========== 示例 ==========
const nums = [1, 2, 3, 4, 5]
console.log(
'求和:',
nums.myReduce((a, b) => a + b, 0)
)问题 11:二叉树的前中后序遍历
具体可以在【算法】部分的【二叉树基础知识】部分查看。
js
/**
* 二叉树节点
*/
function TreeNode(val, left = null, right = null) {
this.val = val
this.left = left
this.right = right
}
/** 前序遍历:根 -> 左 -> 右 */
function preOrder(root, result = []) {
if (!root) return result
result.push(root.val)
preOrder(root.left, result)
preOrder(root.right, result)
return result
}
/** 中序遍历:左 -> 根 -> 右 */
function inOrder(root, result = []) {
if (!root) return result
inOrder(root.left, result)
result.push(root.val)
inOrder(root.right, result)
return result
}
/** 后序遍历:左 -> 右 -> 根 */
function postOrder(root, result = []) {
if (!root) return result
postOrder(root.left, result)
postOrder(root.right, result)
result.push(root.val)
return result
}
// ========== 示例 ==========
// 1
// / \
// 2 3
// / \
// 4 5
const root = new TreeNode(
1,
new TreeNode(2, new TreeNode(4), new TreeNode(5)),
new TreeNode(3)
)
console.log('前序遍历:', preOrder(root)) // [1, 2, 4, 5, 3]
console.log('中序遍历:', inOrder(root)) // [4, 2, 5, 1, 3]
console.log('后序遍历:', postOrder(root)) // [4, 5, 2, 3, 1]问题 12:驼峰命名转下划线命名
js
function pascalToSnake(str) {
let result = ''
for (let i = 0; i < str.length; i++) {
const char = str[i]
const isUpper = char >= 'A' && char <= 'Z'
if (isUpper && i > 0) {
result += '_'
}
result += isUpper ? char.toLowerCase() : char
}
return result
}问题 13:url 解析
这个题在解析参数的时候会换着考,例如:
&这个符号会写成&- 参数的 value 会对中文进行编码,例如:
你好会写成%E4%BD%A0%E5%A5%BD - 嵌套 key 的话会写成
key[subkey]这样的形式
js
// console.log(parseSmartUrl('https://maps.test.com/map?zoom=12&points=1&points=3&filter[type]=park&loaded=true'));/* 输出:
// {
// zoom: 12,
// points: [1,3],
// filter: { type: "park" },
// loaded: true
// }
// */
function parseSmartUrl(url) {
const query = url.split('?')[1]
if (!query) return {}
const params = query.split('&')
const result = {}
for (const param of params) {
const [key, value] = param.split('=')
const depStart = key.split('').findIndex((v) => v === '[')
const depEnd = key.split('').findIndex((v) => v === ']')
if (depStart !== -1 && depEnd !== -1) {
const depKey = key.slice(depStart + 1, depEnd)
const newKey = key.slice(0, depStart)
result[newKey] = {
[depKey]: value
}
continue
}
if (result.hasOwnProperty(key)) {
if (Array.isArray(result[key])) {
result[key].push(value)
} else {
result[key] = [result[key], value]
}
} else {
result[key] = value
}
}
return result
}js
/**
* 实现一个函数 parseQueryString(url),将 URL 的查询字符串解析成对象。
*
* 功能要求:
*
* 1. 支持基本解析:
* 例如:?a=1&b=2
* 输出:{ a: '1', b: '2' }
*
* 2. 支持 decodeURIComponent:
* 例如:?name=%E5%BC%A0%E4%B8%89
* 输出:{ name: '张三' }
*
* 3. 支持重复 key 自动合并为数组:
* 例如:?a=1&a=2
* 输出:{ a: ['1', '2'] }
*
* 4. 支持空值:
* ?a=&b
* 输出:{ a: '', b: '' }
*
* 示例用例:
* parseQueryString('https://www.test.com?a=1&a=2&name=%E5%BC%A0%E4%B8%89&empty=')
*
* 输出结果:
* {
* a: ['1', '2'],
* name: '张三',
* empty: ''
* }
*/
function parseQueryString(url) {
const queryString = url.split('?')[1]
if (!queryString) {
return {}
}
const paramsString = queryString.replaceAll('&', '&')
const params = paramsString.split('&')
const result = {}
for (const param of params) {
const [key, value] = param.split('=')
const decodeKey = decodeURIComponent(key)
const decodeValue = decodeURIComponent(value)
if (result.hasOwnProperty(decodeKey)) {
if (Array.isArray(result[decodeKey])) {
result[decodeKey].push(decodeValue)
} else {
// 第二次遇到
result[decodeKey] = [result[decodeKey], decodeValue]
}
} else {
// 没有key
result[decodeKey] = decodeValue
}
}
return result
}问题 14:转为树
1. 数组转树
假设每一项结构为:{ id, parentId, ...其它字段 },parentId 为 null 或 undefined 时表示根节点。
js
function arrayToTree(list) {
const nodeMap = new Map()
const roots = []
// 先拷贝一份,避免直接修改入参
for (const item of list) {
const clone = { ...item, children: [] }
nodeMap.set(clone['id'], clone)
}
// nodeMap.values():[Map Iterator] {
// { id: 1, parentId: null, name: '根 1', children: [] },
// { id: 2, parentId: 1, name: '子节点 1-1', children: [] },
// { id: 3, parentId: 1, name: '子节点 1-2', children: [] },
// { id: 4, parentId: 2, name: '子节点 1-1-1', children: [] },
// { id: 5, parentId: null, name: '根 2', children: [] }
// }
for (const node of nodeMap.values()) {
const parentId = node['parentId']
if (parentId == null || !nodeMap.has(parentId)) {
// 根节点
roots.push(node)
} else {
const parent = nodeMap.get(parentId)
parent['children'].push(node)
}
}
return roots
}
// ===== 示例 =====
const flatList = [
{ id: 1, parentId: null, name: '根 1' },
{ id: 2, parentId: 1, name: '子节点 1-1' },
{ id: 3, parentId: 1, name: '子节点 1-2' },
{ id: 4, parentId: 2, name: '子节点 1-1-1' },
{ id: 5, parentId: null, name: '根 2' }
]
const tree = arrayToTree(flatList)
console.dir(tree, { depth: null })2. 树转数组
默认 children 字段为子节点数组,将整棵树拍平成一维数组。
js
function treeToArray(roots) {
const result = []
const stack = Array.isArray(roots) ? [...roots] : [roots]
while (stack.length) {
const node = stack.pop()
if (!node) continue
const { children, ...rest } = node
result.push(rest)
if (Array.isArray(children) && children.length) {
// 为了保持从上到下,从左到右的顺序,这里倒序入栈
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i])
}
}
}
return result
}js
// ===== 示例 =====
const sampleTree = [
{
id: 1,
parentId: null,
name: '根 1',
children: [
{
id: 2,
parentId: 1,
name: '子节点 1-1',
children: [{ id: 4, parentId: 2, name: '子节点 1-1-1', children: [] }]
},
{
id: 3,
parentId: 1,
name: '子节点 1-2',
children: []
}
]
},
{
id: 5,
parentId: null,
name: '根 2',
children: []
}
]
const flat = treeToArray(sampleTree)
console.log(flat)3. 路径字符串转树
- 输入:一组类似文件路径的字符串,例如:
['src/index.js', 'src/utils/helper.js', 'test/index.test.js'] - 输出:树结构:
js
[
{
name: 'src',
children: [
{ name: 'index.js', children: [] },
{
name: 'utils',
children: [{ name: 'helper.js', children: [] }]
}
]
},
...
]js
function pathStringsToTree(paths) {
const root = {}
for (const raw of paths) {
// 'src/index.js' → ['src', 'index.js']
const parts = raw.split('/')
let current = root
for (const part of parts) {
// 如果当前这一层还没有这个节点(比如还没有 src),就先建一个空对象
if (!current[part]) {
current[part] = {}
}
// 然后进入下一层,继续找下一层
// 例如:处理 'src/index.js' 后:root = { src: { 'index.js': {} } }
current = current[part]
}
}
// 将嵌套对象转成需要的 { name, children } 树形结构
function toNodes(obj) {
// [key, value] => [name, childrenObj]
return Object.entries(obj).map(([name, childrenObj]) => ({
name,
children: toNodes(childrenObj)
}))
}
return toNodes(root)
}
// ===== 示例 =====
const pathList = [
'src/index.js',
'src/utils/helper.js',
'src/components/Button.jsx',
'test/index.test.js',
'README.md'
]
const tree = pathStringsToTree(pathList)
console.dir(tree, { depth: null })问题 15:红绿灯循环
红灯亮 3 秒 → 绿灯亮 2 秒 → 黄灯亮 1 秒 → 循环往复
示例输出(每轮):
md
现在是 红 灯,持续 3000ms
(3 秒后)
现在是 绿 灯,持续 2000ms
(2 秒后)
现在是 黄 灯,持续 1000ms
(1 秒后)
下一轮...用 Promise / async-await 实现「按顺序、可循环」的亮灯逻辑
js
// ---------- 工具:延迟 N 毫秒(sleep 工具函数) ----------
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
// ---------- 核心:亮某灯并持续 duration 毫秒 ----------
async function light(color, duration) {
console.log(`现在是 ${color} 灯,持续 ${duration}ms`)
await sleep(duration)
}
// ---------- 无限循环:红 → 绿 → 黄,一直重复 ----------
async function trafficLightLoop() {
while (true) {
await light('红', 3000)
await light('绿', 2000)
await light('黄', 1000)
}
}
trafficLightLoop()问题 16:CodingMan
题目:实现一个支持链式调用的 CodingMan
CodingMan 算法的经典规则通常是:
- 支持链式调用,如
CodingMan('Tom').sleep(3).eat('dinner'),表示- 先输出 Hi! This is Tom!,
- 等待 3 秒后输出 Wake up after 3 seconds!
- 再输出 Eat dinner~
- 链式调用的执行顺序严格遵循调用顺序,且 sleep 会阻塞后续操作的执行
js
class _CodingMan {
taskQueue = []
next() {
const task = this.taskQueue.shift()
if (task) {
task()
}
}
constructor(name) {
this.sayHi(name)
// 把 next() 放入「宏任务队列」
// 异步启动队列,确保所有链式方法的任务都入队后再执行
setTimeout(() => {
this.next()
}, 0)
}
sayHi(name) {
this.taskQueue.push(() => {
console.log(`Hi! This is ${name}!`)
this.next()
})
return this
}
sleep(delay) {
this.taskQueue.push(async () => {
setTimeout(() => {
console.log(`Wake up after ${delay} seconds!`)
this.next()
}, delay * 1000)
})
return this
}
eat(food) {
this.taskQueue.push(() => {
console.log(`Eat ${food}~`)
this.next()
})
return this
}
}
function CodingMan(name) {
return new _CodingMan(name)
}
CodingMan('Tom').sleep(3).eat('dinner')问题 17:Promisify
这是一个Node.js中常用的工具函数,用于将基于回调的异步函数转换为返回Promise的函数。
js
/**
* @param {Function} fn 形如 (arg1, arg2, ..., callback) 的函数
* @returns {Function} 返回一个新的函数,调用后得到 Promise
*/
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn.call(this, ...args, (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
})
})
}
}
// ===== 示例 1:模拟异步回调函数 =====
function fakeAsyncAdd(a, b, callback) {
setTimeout(() => {
if (typeof a !== 'number' || typeof b !== 'number') {
callback(new Error('参数必须是数字'))
} else {
callback(null, a + b)
}
}, 300)
}
const addAsync = promisify(fakeAsyncAdd)
addAsync(1, 2)
.then((sum) => console.log('1 + 2 =', sum))
.catch((err) => console.error('出错:', err.message))问题 18:retry 重试 + 超时控制
retry 是一个返回 Promise 的函数。会在总超时时间内,按间隔多次重试,直到成功或失败。
js
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
function retry(fn) {
// 最大重试次数(不含第一次执行)
const retries = 3
// 每次重试间隔(毫秒)
const interval = 1000
// 总超时时间(毫秒)
const timeout = 5000
const start = Date.now()
// 重新运行
async function rerun(max) {
try {
return await fn()
} catch (error) {
// 失败重试
const elapsed = Date.now() - start
const timeLeft = timeout - elapsed
if (timeLeft <= 0 || max <= 0) {
// 总时间耗尽或没有重试次数了
throw err
}
const waitTime = Math.min(interval, timeLeft)
await sleep(waitTime)
return rerun(max - 1)
}
}
return rerun(retries)
}
// test
let failCount = 0
async function failTask() {
// 前两次失败,第三次成功
failCount++
console.log(`执行第 ${failCount} 次`)
if (failCount < 3) {
throw new Error('模拟失败')
}
return '成功结果'
}
retry(failTask)
.then((res) => {
console.log('retry 成功:', res)
})
.catch((err) => {
console.error('retry 失败:', err.message)
})