
做开发这些年我几乎每天都会碰到一个特别不起眼但特别容易让人崩溃的问题前端拿到接口返回的 JSON 是下划线命名的user_name后端代码里写的是驼峰风格的userName数据库字段又是另一种风格配置文件里还可能冒出个user-name。你在联调接口时是不是也经常为这种事来回折腾今天这篇就专门聊聊驼峰命名变量转换方法从原理、手写实现到框架层面的全局配置一次讲透。驼峰命名camelCase是 JavaScript、Java、C# 等语言里最常见的变量命名风格但它在 HTTP 接口、JSON 数据、数据库字段、配置文件这些边界上经常会和 snake_case、kebab-case 撞在一起。不管你是后端被前端追着改字段名还是前端被后端文档里五花八门的命名搞得晕头转向只要学会一套靠谱的转换思路以后这种破事基本能一劳永逸。这篇文章我会先从核心原理讲起再给各语言可复制的代码最后盘点那些我踩过无数次的坑。1. 命名风格不统一程序员每天都在给变量“改名”1.1 一个真实的联调现场三种命名风格同台先还原一个特别典型的场景。后端用 Java 写接口按阿里巴巴开发规范成员变量喜欢写成userName、orderId、createdAt这种驼峰风格。可是数据库设计规范通常要求字段用下划线比如user_name、order_id、created_at。到了前端JavaScript 代码习惯上又用驼峰可后端接口返回的数据经过序列化之后可能变成user_name因为框架配置了 snake_case也可能变成userName完全取决于后端怎么配置。这时候如果有第三方系统接入比如要对接一个老平台的协议字段是user-name这种短横线风格那情况就更热闹了。同一份数据在数据库、后端、接口、前端四个环节就有四种不同的名字每次联调改字段名改到后面都不知道自己在改什么东西。我见过最夸张的一次一个订单模块的字段映射文件里写了三百多行手工map每行就是put(user_name, user.getUserName())这种代码看得人头皮发麻。这种问题的本质不是某一种命名规范不好而是不同环境有各自的历史惯性和生态偏好。数据库用下划线是因为 SQL 关键字、传统建模工具都倾向于这种风格Java 用驼峰是因为语言规范推荐、IDE 支持好前端用驼峰则是因为 JavaScript 语言本身最常见的风格就是 camelCase。你没有本事让所有环节全部统一那就必须熟练掌握转换方法在边界处把它们翻译过来。1.2 驼峰命名的定义与使用边界驼峰命名分两种小驼峰lowerCamelCase和大驼峰PascalCase。小驼峰首字母小写后续单词首字母大写比如userName、paymentOrderDetail是变量和方法的主流风格大驼峰首字母也大写比如UserName、PaymentOrderDetail主要用于类名、接口名、组件名。容易忽略的是驼峰命名不仅仅是“字母大小写”的问题它还隐含了一个信息单词边界是通过大写字母来标记的。这意味着你想把userName转成user_name本质上是先“拆词”再“按新规则拼接”。很多人一上来就写正则替换大写字母加下划线遇到userName没问题一遇到XMLParser、HTTPResponse这种连续大写缩写就炸了拆出来变成x_m_l_parser完全没法用。所以先搞懂“拆词”才是转换方法里最核心的部分后面我会专门用一个小节来讲。另外驼峰命名的使用边界比很多人以为的要广。变量名要用驼峰函数名要用驼峰类名要用大驼峰这大家都知道。但前端组件的属性名、后端接口的请求参数名、Redis 缓存 key 的命名、配置中心的配置项命名这些场景里驼峰和 snake_case 的混用同样非常常见。你在处理这些数据时同样需要转换方法的支撑。1.3 命名转换在不同岗位中的侧重点后端同学最常遇到的是 ORM 框架的字段映射问题MyBatis 里一个map-underscore-to-camel-case配置就能解决大部分问题前端同学最常遇到的是接口数据字段风格不统一的处理几乎家家都会写一个convertKeys工具函数客户端同学则要面对不同平台的序列化差异像是 Objective-C 风格、Swift 风格、Java Bean 风格的转换。不同岗位对转换方法的需求深度不一样但核心的拆词与拼接算法是一样的学会了通用的那套走到哪都能用。2. 拆词是转换的核心别急着写正则2.1 先从“拆词”开始理解转换本质如果用一句话概括驼峰变量转换方法那就是格式化拆分单词 重新组合。举个例子paymentOrderDetail这个词我们需要先把payment、Order、Detail三个词拆开然后再根据目标风格决定是否加分隔符、是否调整大小写。转成payment_order_detail中间加了下划线全部小写转成PaymentOrderDetail三个词首字母都大写中间无缝拼接。拆词的基本规律很简单在一个小写字母或数字后面紧跟着大写字母的位置通常就是一个单词边界。userName中r是小写N是大写所以r和N之间是边界orderId中r和I之间是边界。用正则表达就是[a-z0-9]后面跟[A-Z]的位置。但连续大写的情况要单独处理。比如XMLParserXMLP这一段都是大写如果只按“大写后面是边界”去拆可能拆成X M L Parser显然不对。正确思路是当一串连续大写字母后面跟着一个大写字母加小写字母的组合时边界在最后一个大写字母之前。XMLParser应该拆成XML和Parser边界在L和P之间。正则就是[A-Z]后面跟[A-Z][a-z]的位置。2.2 边界情况数字、首字母缩写、空字符串数字边界是另一个很容易翻车的地方。user2Name应该拆成user2和Name还是user、2、Name按大多数团队的约定数字和后面的大写字母之间是边界数字和前面的小写字母一般不算边界也就是user2保留为一个整体。但是在一些框架的转换规则里2可能被当成独立单词转出来变成user_2_name。这种细微差别一般不会导致程序出错但会让你和别人联调时对不上字段名非常烦人。还有首字母缩写的处理。userID转成 snake_case 应该是user_id而不是user_i_d。这里的难点在于“ID”这种缩写全是字母大写拆词算法需要把它们当成一个完整的单词。上面提到的连续大写规则能处理一部分情况但如果你手写解析逻辑时没有考虑“全大写单词后面直接结束”的场景就很容易拆错。空字符串和纯数字字符串也是边界情况。写工具函数时一定要记得处理否则在高阶函数里调用时遇到空串会返回很奇怪的结果。我在自己写的工具类里一般会加一个边界判断如果输入为空或者不包含任何字母直接原样返回。2.3 一份可以直接用的拆词思路与伪代码下面这套拆词思路我在多个语言里都用过逻辑一致效率也够1. 在小写字母或数字 与 大写字母 之间插入空格 2. 在一串连续大写字母 与 大写字母小写字母组合 之间插入空格 3. 把所有非字母数字的字符替换成空格 4. 统一转成小写或按需保留大小写信息 5. 去掉首尾空格按空格拆分得到单词数组用 JavaScript 写出来就是下面这段代码不长但能把 90% 的命名转换场景都覆盖住function splitWords(str) { return str .replace(/([a-z0-9])([A-Z])/g, $1 $2) .replace(/([A-Z])([A-Z][a-z])/g, $1 $2) .replace(/[^a-zA-Z0-9]/g, ) .trim() .split(/\s/); }核心是前两个正则尤其是第二个。([A-Z])([A-Z][a-z])匹配的是“一串大写字母后面紧跟一个大写字母加小写字母”比如XMLParser中的XMLP替换后变成XML Parser。如果你直接用单一规则在大写字母前加分隔符那么连续缩写这关永远过不了。3. 常见编程语言的驼峰转换实现3.1 JavaScript手写一套健壮的转换工具JavaScript 是前端处理字段命名的重灾区因为接口返回的数据往往跟内部代码风格不一致。有了splitWords这个基础函数剩下的转换就是简单地拼接。下面是驼峰转 snake_case 和 snake_case 转驼峰的完整实现function camelToSnake(str) { return splitWords(str) .join(_) .toLowerCase(); } function camelToKebab(str) { return splitWords(str) .join(-) .toLowerCase(); } function camelToPascal(str) { const words splitWords(str); return words .map(w w.charAt(0).toUpperCase() w.slice(1)) .join(); }注意splitWords里已经把所有单词统一转成小写了所以后面不需要额外处理大小写。如果需要 snake_case 转驼峰逻辑也简单function snakeToCamel(str) { return str .toLowerCase() .replace(/_([a-z])/g, (_, c) c.toUpperCase()); } function snakeToPascal(str) { const camel snakeToCamel(str); return camel.charAt(0).toUpperCase() camel.slice(1); }这里有个小细节splitWords里的第二行replace(/[^a-zA-Z0-9]/g, )会把下划线、短横线、空格都统一替换成空格所以不管输入是user_name、user-name还是user name拆出来的结果都是[user, name]这就是为什么我强调拆词是核心。任何命名风格只要你能拆出单词数组想转成什么风格都行。3.2 Pythonsnake_case 与 camelCase 互转Python 世界里 snake_case 是主流但经常要处理外部接口传来的驼峰字段。Python 的标准库re足够完成这个任务不需要引第三方包import re def split_words(name: str) - list[str]: name re.sub(r([a-z0-9])([A-Z]), r\1 \2, name) name re.sub(r([A-Z])([A-Z][a-z]), r\1 \2, name) name re.sub(r[^a-zA-Z0-9], , name) return name.strip().lower().split() def camel_to_snake(name: str) - str: return _.join(split_words(name)) def snake_to_camel(name: str) - str: parts name.split(_) return parts[0] .join(p.title() for p in parts[1:])Python 的str.title()会把name变成Name刚好符合单词首字母大写的需求。如果你想转大驼峰可以直接在snake_to_camel的基础上把首字母也大写。在实际项目中Python 后端做数据清洗时经常遇到嵌套 JSON 的 key 需不需要递归转换的问题。我建议写一个递归函数来处理整个字典结构而不是只转换顶层 keydef convert_keys(data, converter): if isinstance(data, dict): return {converter(k): convert_keys(v, converter) for k, v in data.items()} elif isinstance(data, list): return [convert_keys(item, converter) for item in data] else: return data这样一行convert_keys(payload, camel_to_snake)就能把整个返回体里所有层级的 key 都转成蛇形。3.3 Java 与 C# 中的转换套路Java 生态里最简单的方式是直接引入 Google 的 Guava 库CaseFormat类就是专门干这个的。LOWER_CAMEL、LOWER_UNDERSCORE、UPPER_UNDERSCORE、LOWER_HYPHEN这些枚举对应了常见的几种命名风格用法如下import com.google.common.base.CaseFormat; String snake user_name; String camel CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, snake); // 结果userName String pascal CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, snake); // 结果UserNameGuava 对连续大写的处理做得还不错实测CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, http_response)会得到httpResponse符合预期。C# 里微软没有内置这么便捷的类但你可以用System.Text.Json自带的命名策略比如JsonNamingPolicy.CamelCase如果不满足需求可以用正则实现和前面 JavaScript 版本一样的逻辑。提示比起手写转换逻辑Java 项目优先考虑 Guava因为它在边界情况上经过了大厂多年验证远比你自己写的正则健壮。C# 项目如果版本较新System.Text.Json的命名策略已经能覆盖大多数常见需求。4. 框架层全局转换改一处省百处4.1 前端把接口字段统一转驼峰前端拿到接口数据后如果后端返回的是 snake_case而你代码里又坚持用驼峰那最省事的方案是在请求拦截器里统一做转换。用 axios 举个例子在响应拦截器里递归转换所有 keyimport axios from axios; function convertKeys(obj, converter) { if (Array.isArray(obj)) { return obj.map(item convertKeys(item, converter)); } if (obj ! null typeof obj object) { return Object.keys(obj).reduce((acc, key) { const newKey converter(key); acc[newKey] convertKeys(obj[key], converter); return acc; }, {}); } return obj; } axios.interceptors.response.use(response { response.data convertKeys(response.data, snakeToCamel); return response; });这样所有接口返回的字段在自己的业务代码里就都是一致的驼峰风格了。但这里有一点必须提醒如果你在拦截器里做了全局转换那所有联调对接方都要知道你做了这件事否则别人对照接口文档逐个核对字段名时会对你产生怀疑。我建议在项目文档里明确写一行“前端请求/响应拦截器统一将 snake_case 转为 camelCase”。另外转换后的 key 变化会不会影响依赖字段名的第三方库比如某些表格组件、埋点 SDK、可视化配置是直接读原始字段名的转换后可能拿不到数据。所以全局转换前一定要排查项目里的硬编码字段名引用。4.2 Spring Boot 后端全局配置 Jackson后端最常用的是 Spring Boot 生态。如果你的接口对外统一要求 snake_case但代码里依然写驼峰属性不需要在每一处 DTO 上加JsonProperty全局配置 Jackson 的命名策略即可Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - builder.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); } }或者在application.properties里一行搞定spring.jackson.property-naming-strategySNAKE_CASE这样所有 Controller 返回的 JSON 字段都会自动变成user_name这种风格。但要注意如果你引入了第三方 SDK而它的内部类也用了 SNake_CASE 策略那就可能出现嵌套对象字段风格不统一的情况。我实际项目里就遇到过自己项目的接口全是 snake_case结果链路追踪 SDK 返回的 trace 信息字段是驼峰前端那叫一个乱。最终只能给那个 SDK 单独加配置或者写一个专用的序列化器。同理如果接口接收前端传来的 camelCase 字段也可以依赖这层配置自动反序列化到 Java 的驼峰属性不需要手动写一堆JsonProperty别名。4.3 数据库查询与 ORM 字段映射MyBatis 里开启驼峰映射的配置非常简单mybatis: configuration: map-underscore-to-camel-case: true开启后user_name查询结果会自动映射到userName属性上。这个配置解决了从数据库到 Java 对象的字段映射问题不用在你的 XML 里写一堆resultMap去做别名字段。Hibernate / JPA 里也有类似的命名策略配置spring.jpa.hibernate.naming.physical-strategy可以指定物理命名策略让实体属性自动映射到下划线字段。不过这种自动映射有一个反直觉的地方当你依赖这种隐式映射时代码可读性会下降。新人接手项目看到查询结果集直接注入userName属性会好奇userName是从哪个数据库字段映射过来的必须翻配置才能明白。所以我个人的习惯是项目小、约定统一时开启自动映射项目大、字段多时缓存一个生成好的resultMap或者用代码生成器生成映射文件比隐式规则更容易维护。5. 高频翻车现场这些问题我几乎每次都会遇到5.1 连续大写缩写被拆得一塌糊涂这是新手最容易踩的坑。用最简单的正则str.replace(/([A-Z])/g, _$1)去转驼峰遇到userName能正确输出user_name但遇到XMLParser就会得到x_m_l_parser。这种错误非常隐蔽因为单测如果只覆盖了userName这类常规输入根本发现不了问题。解决办法就是前面反复强调的拆词两步走。我经常在评审代码时看到同事写了个看似没问题、实际一跑就错的转换函数每次都要和他们解释“连续大写字母要当做一个整体单词”。建议大家拿到这段代码后一定要把XMLParser、HTTPResponse、userID这几个经典例子加进单测里。5.2 递归转换时遇到循环引用前端做全局转换时如果数据结构里存在循环引用对象里直接或间接引用了自身你的convertKeys函数就会无限递归最终栈溢出。这在处理一些复杂的图表配置、树形数据时很容易触发。解决办法是可以加一个WeakSet记录已经处理过的对象遇到循环引用直接跳过function convertKeys(obj, converter, seen new WeakSet()) { if (obj null || typeof obj ! object) return obj; if (seen.has(obj)) return obj; seen.add(obj); if (Array.isArray(obj)) { return obj.map(item convertKeys(item, converter, seen)); } return Object.keys(obj).reduce((acc, key) { acc[converter(key)] convertKeys(obj[key], converter, seen); return acc; }, {}); }这个技巧我是在一次处理图形化编辑器的节点数据时发现的当时整个页面直接白屏控制台报超出最大调用栈排查了半天才意识到是循环引用的问题。所以如果你也要写递归转换记得把WeakSet的反循环机制一并考虑进去。5.3 性能问题高频转换时慎用正则如果只是接口返回时转换一次性能完全不用纠结。但在某些场景下转换函数的调用频率会非常高比如在列表页每一行的render函数里都做字段转换或者读取大量配置项时逐条转换 key。正则每调用一次都要编译、执行性能确实不算好。我有一次在数据大屏项目里对几千条实时推送的数据做字段转换页面帧率明显下降。排查下来发现是响应拦截器里每次都对完整数据做了一次递归转换而数据又是每隔几百毫秒推送一次。优化方案有两个一是缓存转换结果用Map把已经转换过的字段名存起来命中的直接返回二是不要在运行时做字段转换在一开始的接入层就把字段名约定好或者让后端直接把字段输出为前端需要的格式。对于字段名变化的频率很低、但调用频率很高的场景缓存方案是最实用的。const cache new Map(); function cachedCamelToSnake(key) { if (!cache.has(key)) { cache.set(key, camelToSnake(key)); } return cache.get(key); }5.4 特殊场景工业软件与仿真平台的命名约定这里多说一个不太常见但很有意思的场景。在一些工业软件、仿真建模工具里变量的命名风格同样影响巨大。比如仿真软件里定义的“弹塑性应变变量”、有限元模型里的中间变量导出到外部程序时可能因为大小写风格不匹配导致迭代计算不收敛或者变量读取为空。还有不少设备上位机或组态软件里的变量定义脚本里对某个变量做“置位、复位、二次确认”操作时如果变量的命名风格在导入导出过程中被改掉了脚本就会找不到目标变量。这不是标准的编程语言问题但处理思路是一样的先搞清楚目标系统接受什么命名风格再用自动转换工具批量清洗变量名。工业环境里我见过有人导出几百个变量名到 Excel 里通过公式批量做驼峰与下划线互转再导回去效率比手动改高得多。想到这里我才意识到命名转换这件事真的不只属于 Web 开发凡是涉及跨系统数据交换的领域都会遇到。6. 工具库速查与使用建议6.1 各语言常用的命名转换工具库手写实现适合学习和应对简单场景但生产环境里我更推荐直接用成熟的工具库少维护一行代码就少一个潜在的坑。下面是我常用的工具库清单语言工具库核心功能使用建议JavaScriptlodash_.camelCase()、_.snakeCase()、_.kebabCase()前端项目基本都带 lodash直接用就行JavaScriptchange-case支持更多命名风格包括 Pascal、Constant需要多种风格切换时非常方便Pythoninflectioncamelize()、underscore()轻量适合数据清洗脚本Pythonhumps递归转换 dict 的 key处理嵌套 JSON 时最省事JavaGuavaCaseFormat四种风格互转只要项目已引入 Guava 就直接用GostrcaseToCamel()、ToSnake()简单直接无外部依赖这里面我想重点夸一下 lodash 的_.camelCase它对连续大写、数字边界的处理已经相当成熟实测_.camelCase(XML_HTTP_REQUEST)返回xmlHttpRequest完全在线。如果你的项目刚好已经引入了 lodash真的没必要再手写一套。6.2 从工具库源码里学到的三个小技巧如果你有时间建议抽空翻一翻工具库的源码。我自己从change-case的源码里学到不少处理细节这里分享三个收获第一工具库通常会做 Unicode 字符支持。很多手写实现只处理 ASCII 字母遇到中文、重音字符时行为不可控。工具库一般用String.prototype.normalize先做 NFC 标准化再结合 Unicode 属性来判断字符类别这样处理德语、法语里的重音字母才不会出错。如果项目需要处理多语言文本这点很重要。第二很多成熟的实现会区分“单词是否完整大写”。比如HTTPResponse和HttpResponse拆出来的结果就不一样前者拆成HTTP和Response后者拆成Http和Response。这背后的逻辑是如果整个单词都是大写它会被当作首字母缩写词处理如果只有首字母大写则当作普通单词处理。手写实现时这个细节很容易被忽略。第三工具库通常有专门的性能优化。lodash 的camelCase内部会先检查字符串里是否真的包含大写字母不含大写字母就直接走快速路径返回避免不必要的正则运算。这个思路很实用可以直接抄到自己的工具函数里。说到底驼峰命名变量转换方法不是一个高深的技术但它渗透在写代码、联调、数据交换的每一个角落。掌握一套通用的拆词思路学会至少一个工具库的用法知道哪些场景需要全局配置、哪些场景必须手动处理你在遇到字段命名问题时就不会再手忙脚乱。我个人在实际操作中的体会是最稳妥的方案不是在某一个环节死磕转换而是在项目初期就把命名约定写进规范文档里让不同系统之间默认使用同一种风格。真到了需要转换的时候优先用框架配置和工具库手写实现只作为兜底方案。这样既保证了效率也减少了因为命名差异引发的低级故障。下次再有人拿着一堆字段名让你帮忙“统一风格”你可以直接把这篇的思路和代码丢给他省下来的时间干点啥不好。