← 返回手写题目录

🔄 数据类型互转 Top 10

URL参数 · 数组对象 · 树列表 · Map对象 · 金额 · CSV · 表单 · querystring

URL 与查询字符串

1URL 参数字符串 → 对象(parseQueryString)

场景:从 ?name=Tom&age=18&tags=a&tags=b 解析成 { name:'Tom', age:18, tags:['a','b'] }。

"?name=Tom&age=18&tags=a&tags=b" { name:'Tom', age:18, tags:['a','b'] }
// 手写实现 function parseQueryString(url) { const result = {} // ① 提取 ? 后面的部分 const queryStr = url.includes('?') ? url.split('?')[1] : url if (!queryStr) return result // ② 按 & 分割每组参数 queryStr.split('&').forEach(pair => { const [key, value] = pair.split('=') const decoded = decodeURIComponent(value?.replace(/\+/g, ' ')) if (result[key] !== undefined) { // 已存在 → 转成数组 result[key] = Array.isArray(result[key]) ? [...result[key], decoded] : [result[key], decoded] } else { result[key] = decoded } }) return result } parseQueryString('?name=Tom&age=18&tags=a&tags=b') // { name:'Tom', age:'18', tags:['a','b'] } // 生产推荐:用原生 URLSearchParams Object.fromEntries(new URLSearchParams('?name=Tom&age=18'))
注意:要 decodeURIComponent 解码中文和特殊字符;要把 + 还原成空格;重复 key 要合并成数组。
2对象 → URL 参数字符串(stringifyQueryString)

场景:发起 GET 请求前把参数对象拼到 URL 后面。axios 就是这么做的。

{ name:'Tom', age:18, tags:['a','b'] } "?name=Tom&age=18&tags=a&tags=b"
function stringifyQueryString(obj) { if (!obj || Object.keys(obj).length === 0) return '' const parts = [] Object.entries(obj).forEach(([key, value]) => { if (value === undefined || value === null) return // 跳过 null/undefined const values = Array.isArray(value) ? value : [value] values.forEach(v => { // encodeURIComponent 编码,避免中文/特殊符号 parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`) }) }) return '?' + parts.join('&') } stringifyQueryString({ name: 'Tom 李', age: 18, tags: ['a', 'b'] }) // '?name=Tom%20%E6%9D%8E&age=18&tags=a&tags=b'
细节:① null/undefined 跳过 ② 数组拆成多个同名 key ③ 必须 encode。漏一个都会出 bug。

数组对象与树结构

3数组 ↔ 对象互转

场景:表头+行数据 → 对象数组;对象按 key 提取成数组;数组转成以 id 为 key 的 map。

// ① 表格数据:表头数组 + 行数组 → 对象数组 function tableToObject(headers, rows) { return rows.map(row => row.reduce((obj, val, i) => (obj[headers[i]] = val, obj), {}) ) } const headers = ['name', 'age', 'city'] const rows = [['Tom', 18, '北京'], ['Jerry', 20, '上海']] tableToObject(headers, rows) // [{name:'Tom',age:18,city:'北京'}, {name:'Jerry',age:20,city:'上海'}] // ② 反操作:对象数组 → 表头+行(导出 CSV 用) function objectToTable(arr) { const headers = Object.keys(arr[0]) const rows = arr.map(item => headers.map(h => item[h])) return { headers, rows } } // ③ 数组 → 以 id 为 key 的 map(业务高频!) const arrayToMap = arr => arr.reduce((map, item) => (map[item.id] = item, map), {}) arrayToMap([{id:1,name:'a'},{id:2,name:'b'}]) // { '1':{id:1,name:'a'}, '2':{id:2,name:'b'} } // ④ 对象 → 数组(取键值对) Object.entries({a:1, b:2}) // [['a',1],['b',2]] Object.values({a:1, b:2}) // [1,2]
实战:从后端拿到列表后,arrayToMap 转 map 方便按 id 查找,比每次 find 高效(O(1) vs O(n))。
4树结构 ↔ 扁平列表(深度遍历)

场景:菜单树要存数据库(转扁平带 pid);后端返回树要展示成列表(转扁平)。前端必考。

{id:1, children:[{id:2, children:[{id:3}]}]} [{id:1,pid:null},{id:2,pid:1},{id:3,pid:2}]
// ① 树 → 扁平列表(带 pid 关系) function treeToFlat(tree, pid = null) { const result = [] tree.forEach(node => { const { children, ...rest } = node result.push({ ...rest, pid }) if (children?.length) { result.push(...treeToFlat(children, node.id)) // 递归,pid 传当前 id } }) return result } // ② 扁平列表 → 树(用 Map,O(n)) function flatToTree(list) { const map = {}, tree = [] list.forEach(item => map[item.id] = { ...item, children: [] }) list.forEach(item => { if (item.pid && map[item.pid]) { map[item.pid].children.push(map[item.id]) } else { tree.push(map[item.id]) } }) return tree } // 测试双向转换 const tree = [{id:1,name:'a',children:[{id:2,name:'b',children:[{id:3,name:'c'}]}]}] const flat = treeToFlat(tree) // [{id:1,name:'a',pid:null}, {id:2,name:'b',pid:1}, {id:3,name:'c',pid:2}] flatToTree(flat) // 还原成原 tree
关键:递归时把当前节点 id 作为子节点的 pid 传下去,建立父子关系。这是和普通树遍历最大的区别。
5Map ↔ Object 互转

场景:JSON 不支持 Map,传输时要转 Object;接收后再转回 Map 保留插入顺序。

// Object → Map const objToMap = obj => new Map(Object.entries(obj)) // Map → Object const mapToObj = map => Object.fromEntries(map) // 用例 const obj = { name: 'Tom', age: 18 } const map = objToMap(obj) // Map(2) { 'name' => 'Tom', 'age' => 18 } map.set('gender', 'male') mapToObj(map) // { name:'Tom', age:18, gender:'male' } // 进阶:嵌套对象也要转 Map(深转换) function deepObjToMap(obj) { if (typeof obj !== 'object' || obj === null) return obj const map = new Map() Object.entries(obj).forEach(([k, v]) => { map.set(k, typeof v === 'object' ? deepObjToMap(v) : v) }) return map }
Map vs Object:Map 的 key 可以是任意类型(包括对象)、保留插入顺序、有 size 属性、增删快。需要频繁增删键值对时用 Map 更合适。

格式化转换

6数字金额互转:分 ↔ 元、千分位

场景:后端数据库存"分"(整数),前端展示"元";输入框输入元,提交时转分。支付业务必考。

// 元 → 分(乘 100,四舍五入避免浮点误差) function yuanToFen(yuan) { return Math.round(Number(yuan) * 100) } yuanToFen(19.99) // 1999 yuanToFen(0.1 + 0.2) // 30,避免 0.30000004 的坑 // 分 → 元(除以 100,保留两位小数) function fenToYuan(fen) { return (Number(fen) / 100).toFixed(2) } fenToYuan(1999) // '19.99' // 分 → 元 + 千分位(最终展示) function formatFen(fen) { const yuan = (Number(fen) / 100).toFixed(2) // 千分位正则 return yuan.replace(/\B(?=(\d{3})+(?!\d))/g, ',') } formatFen(123456789) // '1,234,567.89' // 完整版:分 → "¥1,234.56" const money = fen => '¥' + formatFen(fen)
浮点坑0.1 + 0.2 = 0.30000000000000004,必须用 Math.round 处理。这是支付代码出错率最高的地方。
7对象数组 ↔ CSV 字符串互转

场景:导出 Excel 报表时把数据转 CSV;读取 CSV 文件转成对象数组。后台管理高频。

// 对象数组 → CSV function toCSV(arr, columns) { const header = columns.map(c => c.title).join(',') const rows = arr.map(item => columns.map(c => { const val = item[c.key] // 含逗号或换行的值要用引号包裹 return typeof val === 'string' && (val.includes(',') || val.includes('\n')) ? `"${val}"` : val }).join(',') ) return [header, ...rows].join('\n') } const data = [ { name: 'Tom, Jr', age: 18, city: '北京' }, { name: 'Jerry', age: 20, city: '上海' } ] const cols = [ { key: 'name', title: '姓名' }, { key: 'age', title: '年龄' }, { key: 'city', title: '城市' } ] toCSV(data, cols) // '姓名,年龄,城市\n"Tom, Jr",18,北京\nJerry,20,上海' // CSV → 对象数组 function fromCSV(csvText) { const lines = csvText.trim().split('\n') const headers = lines[0].split(',') return lines.slice(1).map(line => { const values = line.split(',') return headers.reduce((obj, h, i) => (obj[h] = values[i], obj), {}) }) }
细节:CSV 里含逗号或换行的值必须用双引号包裹,否则会破坏列分隔。复杂的 CSV 解析建议用 papaparse 库。
8表单序列化(FormData / form → object)

场景:提交 form 表单前把所有字段聚合成一个对象;Vue/React 表单提交。

// 方法1:原生 FormData 转对象 function formDataToObject(formData) { return Object.fromEntries(formData.entries()) } // 方法2:DOM form 元素 → 对象(不用 FormData 时手写) function formToObject(form) { const result = {} const formData = new FormData(form) for (const [key, value] of formData.entries()) { if (result[key] !== undefined) { // 同名(多选框、复选框)→ 数组 result[key] = Array.isArray(result[key]) ? [...result[key], value] : [result[key], value] } else { result[key] = value } } return result } // 反向:对象 → FormData(提交 multipart/form-data) function objectToFormData(obj) { const formData = new FormData() Object.entries(obj).forEach(([key, value]) => { if (Array.isArray(value)) { value.forEach(v => formData.append(key, v)) } else { formData.append(key, value) } }) return formData } // 用例 const formEl = document.querySelector('form') const data = formToObject(formEl) await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) })
实战:FormData 自动处理复选框多选、文件上传。手写要考虑同名 key 合并、文件类型、空值过滤。
9阿拉伯数字转中文(123 → 一百二十三)

场景:合同金额、发票、收据需要中文大写金额。"壹仟贰佰叁拾肆元整"。

// 数字 → 中文(小写) function numberToChinese(num) { const digits = '零一二三四五六七八九' const units = ['', '十', '百', '千', '万', '十', '百', '千', '亿'] const str = String(num) let result = '' for (let i = 0; i < str.length; i++) { const digit = Number(str[i]) const unit = units[str.length - 1 - i] if (digit === 0) { if (!result.endsWith('零')) result += '零' } else { result += digits[digit] + unit } } return result.replace(/零+$/, '').replace(/^一十/, '十') } numberToChinese(123) // '一百二十三' numberToChinese(1005) // '一千零五' // 金额 → 中文大写(合同/发票用) function toRMB(num) { const digits = '零壹贰叁肆伍陆柒捌玖' const units = ['元', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿'] const [intPart, decPart] = num.toFixed(2).split('.') let result = '' for (let i = 0; i < intPart.length; i++) { const d = Number(intPart[i]) const u = units[intPart.length - 1 - i] if (d === 0) result += '零' else result += digits[d] + u } result = result.replace(/零+$/, '元').replace(/零+/g, '零') // 角分 if (decPart === '00') result += '整' else result += digits[decPart[0]] + '角' + (decPart[1] !== '0' ? digits[decPart[1]] + '分' : '') return result } toRMB(1234.56) // '壹仟贰佰叁拾肆元伍角陆分'
面试价值:考字符串处理 + 边界条件。难点是连续零的合并、单位对齐。"一十"开头要简化为"十"。
10querystring 完整版(嵌套对象支持)

场景:qs 库的核心功能。支持 a[b][c]=1 这种嵌套对象语法。

// 简化版 qs.stringify:{a:{b:1}} → 'a[b]=1' function qsStringify(obj, prefix = '') { const parts = [] Object.entries(obj).forEach(([key, value]) => { const fullKey = prefix ? `${prefix}[${key}]` : key if (value && typeof value === 'object' && !Array.isArray(value)) { // 嵌套对象 → 递归 parts.push(...qsStringify(value, fullKey).split('&')) } else { parts.push(`${encodeURIComponent(fullKey)}=${encodeURIComponent(value)}`) } }) return parts.join('&') } qsStringify({ user: { name: 'Tom', age: 18 }, tags: ['a', 'b'] }) // 'user[name]=Tom&user[age]=18&tags[0]=a&tags[1]=b' // 简化版 qs.parse:'a[b][c]=1' → {a:{b:{c:1}}} function qsParse(str) { const result = {} str.replace(/^\?/, '').split('&').forEach(pair => { const [rawKey, rawVal] = pair.split('=') const value = decodeURIComponent(rawVal) // 拆出 a[b][c] → ['a','b','c'] const keys = decodeURIComponent(rawKey).split(/[\[\]]/).filter(Boolean) keys.reduce((acc, k, i) => { if (i === keys.length - 1) acc[k] = value else acc[k] = acc[k] || {} return acc[k] }, result) }) return result } qsParse('user[name]=Tom&user[age]=18') // { user: { name:'Tom', age:'18' } }
核心思路:嵌套用 [key] 表示层级;解析时用正则 /[\[\]]/ 拆出各级 key,reduce 逐层建对象。生产环境直接用 qs 库。
🎯 类型转换复习重点:① URL ↔ 对象(必考)② 树 ↔ 列表(前端必考)③ 金额元/分转换(支付业务)④ querystring 嵌套(高级题)。
转换题本质是字符串处理 + 递归。把字符串的 split/join/reduce 三个 API 用熟,转换题基本都能解。