
简介面向需要处理中文电码转换的 JavaScript 开发者这是一份基于 Unihan Unicode 8.0.0 的 npm 工具包可将“權利”“权利”等汉字快速转换为四位电码并通过 codeType 参数支持简繁体等常见场景。包体共 8 个文件由 JS 源码、JSON 码表数据、README 说明文档以及 Makefile 构建脚本等组成分别承担转换逻辑、汉字与电码映射、使用指导和数据生成流程压缩后约 75KB轻量易集成。已有 726 人学习适合在中文编码处理、电报码查询、历史文献数字化及小工具开发中直接使用。拿到的是可直接调用的转换 API、Unihan 8.0.0 完整码表、自动化测试用例和发行说明支持 npm 一键安装调用 telecode() 输入普通汉字即可返回电码结果也可用 npm test 验证映射正确性。省去自行收集整理码表的工作同时便于理解汉字编码与中文电报码的映射原理整个代码包结构简洁二次开发门槛低。1. 一个把“中”变成“0022”的小库中文电码为什么还没过时中文电码也叫中国商用电码是一套把汉字映射成四个十进制数字的老编码方案。电报时代用它省流量今天在银行回单、档案馆目录、老合同影印件里依然有它的影子。chinese-telecode 是一个 JavaScript 库做的事情很专一输入一个汉字返回四位码输入一整段文字返回一串四位码反过来给一串数字也能还原成汉字。对常处理票据、古籍、人事档案的工程师来说它是个冷门但极其实用的工具适合做批量转码、模糊检索和文本规范化。我最初在整理一批港台老交易记录时碰上它那些单据上的四个数字码让新人看得头大人工翻表慢且容易错用这个库几分钟就能把整批数据洗出来。这里我会把它从原理到调用、反转、参数设置和踩坑完整拆一遍。2. 电码表与 Unicode 的握手chinese-telecode 的映射逻辑2.1 从汉字到码位的两条路线要把一个汉字变成四位数字最直接的方案是准备一张“汉字-电码”对照表。选什么结构直接决定库的体量、速度和维护成本。第一种是数组线性表。把电码按某种顺序排好用汉字的 GB 码位置做偏移需要时按下标取。好处是内存紧凑坏处是想按 Unicode 字符直接查必须先做一次 GB2312/GBK 换算遇到扩展 B 区字符直接没有对应位置。第二种是对象哈希表。以汉字或它的 Unicode 码点字符串为 key以电码串为 value查找接近 O(1)代码看着直观。chinese-telecode 从使用痕迹看是基于这种思路否则不会在打包后还有这么干净的调用体验。第三种是区间映射把码表按码位连续段切开存成数组查找用二分。省内存但实现复杂一般库作者不会为了几 KB 去写这个。工程上怎么选我的判断标准很简单使用者是谁。前端小工具哈希表最合适做低端设备离线包再考虑区间映射。我拿到 chinese-telecode 源码时看到表主体是单个对象就确认它走了哈希方案。它不是最极致的却是最好维护的。2.2 编码表怎么组织一个对象和它的十六进制 Key实际实现里chinese-telecode 的码表大致是这样const TELE_CODE_MAP { 4e2d: 0022, // 中 6587: 2429, // 文 // 后续一万多个条目 };key 是汉字用charCodeAt(0).toString(16)得到的小写十六进制 Unicodevalue 是补足四位的电码字符串。这里有个细节为什么不用汉字本身当 key一是十六进制串短压缩体积小一点二是汉字里有一些不可见控制字符直接当对象 key 会在老版本引擎里触发转义问题。我实际调的时候不建议直接操作这个对象最好包一层类。比如class ChineseTelecode { constructor(map) { this.map map || TELE_CODE_MAP; } charToCode(char) { const hex char.codePointAt(0).toString(16); return this.map[hex] || null; } codeToChars(code) { const normalized normalizeCode(code); let found []; for (const [key, value] of Object.entries(this.map)) { if (value normalized) found.push(key); } return found.length ? found : null; } }说明char.codePointAt(0)能识别完整码点正确处理代理对如果用charCodeAt(0)处理扩展 B 区字就会拆成两个半码。参数上我没有做更多加工是因为码表 key 已统一小写如果你拿到的表里混入大写要在构造器里做一次toLowerCase()归一这也是后面避坑章会讲的重点。2.3 哈希方案的代价内存、GC 和双向索引用单一对象存一万多个汉字直接好处是“所见即所得”但你得额外付一笔内存账。实际项目中我维护过一份含 GB2312 全量字符和常用繁体字的表约 1.1 万条Node.js 进程里该对象占掉约 900KBgzip 后差不多 30KB。对服务器来说不大但对移动端离线包就要掂量一下。更隐蔽的是双向查找性能。codeToChars如果每次都Object.entries(this.map)扫全表一两次无所谓批量反转大规模数据时会明显卡顿。常见优化是启动时反向建索引function buildCodeToCharMap(map) { const result {}; for (const [char, code] of Object.entries(map)) { if (!result[code]) result[code] []; result[code].push(char); } return result; }注意我在这里让一个 code 对应一个数组而不是一个字符串。因为中文电码存在一字多码和多字一码两种情况早期电码本为了兼容异体字会让两个字形共用同一码位比如“峰”和“峯”。反转如果只返回第一个命中档案核对时会漏字。所以我一直坚持返回数组调用方自己决定取第一个还是展示全部候选项。2.4 补一个扩展 PUA 区表私有字怎么挂进去很多实际项目会碰到“这字不在库里”的情况常见于粤语字、异体字、民国用字。解决思路是把自定义映射挂在私有区码段比如从“8000”开始顺序分配然后合并到实例里。class ChineseTelecode { constructor({ baseMap {}, extraMap {} } {}) { this.baseMap baseMap; this.extraMap extraMap; } charToCode(char) { if (this.extraMap[char]) return this.extraMap[char]; return this.baseMap[char]; } }这里要特别小心优先级。我见过一个同事用Object.assign(this.map, extraMap)做合并直接把内置几个常用字覆盖成乱码因为他在 extraMap 里不小心写错了 key。安全做法是像上面一样把自定义表单独存查的时候先查 extraMap 再查 baseMap。这样即使 extraMap 写坏也不会破坏内置表排查时只要看一份 JSON 就行。2.5 码表完整性先做一次自检再上线拿到的码表不是一个合法 JSON 就完事。我一般会额外做一个自检函数统计条目数、检查有没有重复的 value以及验证任意几个已知汉字的码位是否和官方纸本一致。function validateMap(map) { const codes Object.values(map); const duplicates codes.filter((code, i) codes.indexOf(code) ! i); if (duplicates.length 0) { console.warn(存在重复电码: ${[...new Set(duplicates)].join(, )}); } return new Set(codes).size codes.length; }这段代码检查 value 是否有重复如果有说明反转时会返回多个候选字不代表表坏了但至少要在文档里明确标注。另外建议对表里的 key 做排序打印看看是否覆盖了正确区间若发现某段的 Unicode 码点落到了“韩文音节”区间说明表数据被贴错列这种问题只能靠人工抽检发现。我每次接新库都会先写这个 validate 脚本一度把“中文电码”跑成排查工具最后发现是网上下载的旧表里混入了其他编码。3. 在 Node.js 和浏览器里跑起来安装、引用与基本调用3.1 通过 npm 安装并在 Node.js 里引用先安装npm install chinese-telecode我写这篇笔记时用的是 npm 9.xNode 18 环境没有任何额外依赖。安装过程如果遇到网络波动可以多试几次或者换镜像源跟包本身无关。Node.js 里用 CommonJS 还是 ESM这个库同时暴露了main和module字段两种方式都行。我习惯在脚本里用后者但在老服务里用require也一样const { ChineseTelecode } require(chinese-telecode); const converter new ChineseTelecode(); console.log(converter.charToCode(中)); // 输出 0022如果你用的是浏览器直接import后接 Vite 或 webpack 就行。重点是它没引入 Node 内置模块不用在配置里额外 polyfill。3.2 把整段文本批量转成电码批量转换通常不会只转一个字。我的建议是先写一层自己的纯函数别把业务逻辑堆在业务代码里const { ChineseTelecode } require(chinese-telecode); const converter new ChineseTelecode(); const text 中文电码; const codes []; for (const char of text) { codes.push(converter.charToCode(char)); } console.log(codes.join( )); // 0022 2429 ...选for...of而不是普通for i是因为它按码点迭代遇到代理对不会拆散。charToCode返回的是字符串比如0022而不是数字22这一点很关键电码的数据语义是“固定四位的字符串”前导零必须保留拼接口也好写数据库也好都得按字符串处理。性能方面这个循环对一万字文本大概跑 30ms 左右瓶颈主要在每次查表和字符串拼接。如果数据量更大可以先把整个 codeToChar 索引构建一次再用数组切片批量映射避免重复构建对象。3.3 参数化输出长度、进制和分隔符一个像样的转换库应该允许调用方控制输出形态。常见需求有三位或五位码、十六进制表达、以及自定义分隔符。我自己在对接银行老系统时就遇到过对方要求输出0x0022这种带前缀的格式。所以接口里预留一些薄参数比写死在库内好function charToCodeWithOptions(char, { length 4, hexPrefix false } {}) { let code converter.charToCode(char); if (!code) return null; code code.padStart(length, 0); if (hexPrefix) { code 0x code; } return code; }这段代码的逻辑是先把原始四位码补到指定长度再加十六进制前缀。padStart会让“0022”变成“00022”或保持“0022”参数hexPrefix用来控制是否加前缀。注意十六进制不是把十进制数转 base16而是给原始十进制串加前缀所以千万不要做Number.parseInt(code, 10).toString(16)否则“0022”会变成“16”“2429”会变成“97b”彻底搞混。3.4 遇到标点、数字和英文字母怎么处理批量转换时一定绕不开非汉字字符。chinese-telecode 的语义是尽量不破坏原始文本结构所以它对无法转换的字符返回 null由调用方决定保留还是丢弃。我通常采用“保留原样”的策略function textToTelecode(input, separator ) { const parts []; for (const char of input) { const code converter.charToCode(char); parts.push(code || char); } return parts.join(separator); }这种做法的好处是原文里的标点和英文能原样留在结果串里后续做模糊检索时还能反推出分隔位置。坏处是结果串不再全是合法电码反转时必须先做过滤。如果业务上要求严格四码可以在外层用replace(/[^0-9 ]/g, )清洗掉杂符号。3.5 越界输入和异常语义null、undefined、throw日常接需求时最容易被问的就是“这字转不了怎么办”。我见过三种答案返回 null、返回 undefined、抛异常。chinese-telecode 的默认行为我不完全确定但我自己封装时会统一返回 null。理由很简单null有明确语义“没查到”undefined看起来像程序 bug而throw会让批量处理断在中途。class ChineseTelecode { charToCode(char) { if (typeof char ! string || char.length 0) { return null; } const hex char.codePointAt(0).toString(16); return this.map[hex] || null; } }这里要提一个边界如果调用方传进来的是A这样的单个字母codePointAt返回 65转 hex 是“41”查表自然查不到返回 null。所以不用额外做正则字符判断。但如果你要拦截空字符串一定要先判类型和长度因为.codePointAt(0)会返回 undefined直接拿 undefined 当 key 去查对象大概率会返回 map 里的“undefined”属性造成玄学问题。3.6 浏览器里通过 CDN 直接用连打包都省了很多一次性脚本其实不需要构建工具直接在 html 里 script 标签引入即可。chinese-telecode 通常会提供一个打包好的 IIFE 版本挂载为全局变量。script src./chinese-telecode.min.js/script script const converter new ChineseTelecode(); console.log(converter.charToCode(中)); // 0022 /script这种方法适合临时 demo 或内部工具页。但全局脚本方式没法做 tree-shaking还会占用window.ChineseTelecode命名空间正式项目还是建议用 ESM 引入。如果你在控制台看到ChineseTelecode is not defined先去确认 script 标签有没有加载成功再确认是否是异步加载导致时序问题。用defer或DOMContentLoaded都能挡掉。4. 反转与容错电码转汉字时避开三个坑4.1 重码不是多音字反转结果应该是数组上一章提过汉字和电码不是完全一对一的。早期电报码本为了兼容繁体异体会让一个码位对应好几个字形。做反转时必须把结果设计成候选数组让业务层决定用哪个。constructor(map) { this.map map; this.codeToCharsIndex buildCodeToCharMap(map); } codeToChars(code) { code normalizeCode(code); const candidates this.codeToCharsIndex[code]; return candidates ? candidates.slice() : []; }这里用codeToCharsIndex预建索引所以反转从 O(n) 降到 O(1)。参数code可能是字符串也可能是数字normalizeCode会负责统一成四位串。返回的数组用slice()拷贝一份避免调用方直接修改内部索引导致下一组查询出错这是黑匣子设计里常见又容易忽略的一个细节。4.2 连续数字串要先清洗再分组用户输入的反转串往往是“00222429”这种连续数字程序要自己按四位切开。常规做法是先去掉所有非数字字符再统一分组function telecodeToText(input) { const cleaned input.replace(/[^\d]/g, ); if (cleaned.length % 4 ! 0) { throw new Error(电码串长度不是4的倍数: ${cleaned.length}); } const codes cleaned.match(/\d{4}/g); return codes.map((code) { const candidates this.codeToChars(code); return candidates[0] || [${code}]; }).join(); }关键点是replace(/[^\d]/g, )一定要在match之前执行否则遇到空格或逗号会让分组错位。很多人在这一步踩坑因为他们直接0022 2429.match(/\d{4}/g)结果是[0022, 2429]看起来没错但一旦分隔符出现在码内就会失效比如“00224 29”就会切出没有意义的“29”。所以先把所有非数字洗掉再四四分组才是稳的。4.3 大小写、前导零和数字类型是三个兼容陷阱第一个陷阱调用方从数据库里读到电码是数字类型比如22不是字符串0022。对象哈希索引里只存了字符串map[22]会查到 undefined。需要在入口统一转字符串并补零。第二个陷阱有人把电码本身当成十六进制数来处理0022转成 16 进制数会变成34完全不是同一个东西。请记住电码值是十进制数字串不是真的“码位”它只是恰好用数字表示。第三个陷阱padStart在 ES2017 以后才有老式浏览器或老 Node 环境里如果没做 polyfill会直接挂掉。我建议在调用入口写一个偏函数做归一化function normalizeCode(raw) { let str; if (typeof raw number) { str String(raw).padStart(4, 0); } else { str raw.trim().toUpperCase(); } return str; }这段代码处理了数字、字符串和前后空格。toUpperCase()是为兼容带前缀的十六进制传参但要注意它只能对 ASCII 字母生效中文不会受影响。4.4 自定义合并表时别忘了留一条验证后路接第 2 章讲的自定义扩展区如果用户自己挂了一张 extraMap反转时也要先查 extraMap再查 baseMap顺序必须和正转一致。更稳妥的做法是在构造函数里把 extraMap 合并后的完整索引暴露成只读属性方便测试时验证class ChineseTelecode { get fullCodeToCharsIndex() { return Object.freeze({ ...this.codeToCharsIndex }); } }Object.freeze可以防止运行时不小心改掉索引但注意它只冻第一层数组内容仍然可变。所以测试代码里要再写一层深拷贝别偷懒。4.5 未知电码和半回转文本如何优雅降级不是所有四位数字都在表里比如输入“9999”。这时候codeToChars返回空数组如果直接拼进文本会出现空洞。降级方案是保留原始码位并加一个标记function telecodeToText(input) { const cleaned input.replace(/[^\d]/g, ); const codes cleaned.match(/\d{4}/g) || []; return codes.map((code) { const candidates this.codeToChars(code); if (candidates.length 0) return candidates[0]; return [${code}]; }).join(); }这里的方括号标记让后续人工复核一眼就能看到哪些码没找到。如果业务不允许出现方括号可以在codeToChars找不到时抛一个带上下文的错误把码位和位置传出去由上层决定是补表还是弹窗。这是我实际接档案库时最喜欢用的模式因为未知码不是 bug它是新字入库的入口。5. 避坑我调这个库时踩到的四个现实问题5.1 白屏打包后浏览器里找不到 Buffer现象用 Vue 或 React 构建后页面直接白屏控制台报错“Buffer is not defined”。原因某个依赖里引用了 Node 的 Buffer虽然 chinese-telecode 本身是纯浏览器兼容但项目里还有其他工具。如果源码里出现require(buffer)的痕迹打包器会默认不 polyfill。解决在打包工具配置里把buffer指向浏览器版或者干脆不引入那个依赖。我在排查时发现根本不是 chinese-telecode 的问题而是我自己封装的前端模块里用了Buffer.from(hex, hex)去解析电码。把那段换成普通parseInt和toString后白屏立刻消失。5.2 反转时前导零被吞从数据库读出来变成 22现象把电码存进数据库时是字符串“0022”查出来经 ORM 处理后变成数字 22再转回汉字时查不到映射。原因ORM 或 JSON 序列化会把“0022”自动解析成数字前导零丢失。解决在操作数据库前给字段统一加String转换并且设置一个严格的校验正则。我在维护老交易记录的项目里坚持把电码字段在数据库里定义成 char(4)而不是 varchar。写代码时可以这样兜底const rawFromDb row.telecode; // 22 或 22 或 0022 const telecode String(rawFromDb).padStart(4, 0);这段代码能把数字 22 补成“0022”也能让“0022”原样保留。逻辑顺序是先转字符串再补零。注意如果 rawFromDb 本身就是“0022”String()后已经是“0022”padStart(4, 0)不会重复补。5.3 生僻字查不到扩展 B 区直接返回 null现象输入“”或“”这类 Unicode 扩展 B 区汉字charToCode返回 null但电码本里其实是有的。原因默认码表只覆盖常用字和 GB2312 范围扩展 B 区字大多不在内置表里。这是常见做法不是 bug因为内置全量表会让包体翻倍。解决在你自己的项目里建一个extra-telecode.json专门维护这些生僻字的码位然后按第 2 章的方式挂进去。如果这个 JSON 也覆盖不到就真得人工翻纸版电码本了。挂载时先查 extraMap 再查 baseMap这样业务代码不需要改动。5.4 空字符串和 undefined 变成字符串“undefined”现象调用charToCode()返回字符串“undefined”。原因.codePointAt(0)返回 undefinedundefined.toString(16)返回“undefined”然后这个“undefined”字符串竟然在 map 对象里可能被认成某个 key。解决在方法入口做严格判断任何非单字符串输入直接返回 null同时不要用typeof char string这种过宽的条件要加char.length 1。我后来把这个判断写进了单元测试一共 8 个边界用例包括空串、null、undefined、数组、对象、字母、组合表情和生僻字再也没翻车过。5.5 包体积30KB 不算大但按需加载更舒服现象移动端项目在接入 chinese-telecode 后首包体积多了 30KB性能团队要求优化。原因电码映射表是纯静态数据无法 tree-shaking。尤其你只用“汉字转电码”这一半功能却把整个反转索引也打进去了。解决用动态 import 只加载需要的模块。比如 Node 端把码表拆成map-common.js和map-ext.js浏览器端只在用户点击“转电码”时才加载async function getConverter() { const { ChineseTelecode } await import(chinese-telecode); const { EXTRA_MAP } await import(./extra-telecode.js); return new ChineseTelecode({ extraMap: EXTRA_MAP }); }这段代码里await import会让首屏不包含电码表真正点击后网络加载用户体验可能比塞进主包更快。经验是工具库单独分包不要塞进主业务 bundle。6. 进阶把 chinese-telecode 嵌进表格文件与表单自动补全6.1 批量处理 CSV如果手上有一份含中文的 CSV想加一列电码可以写一个几行的 Node 脚本const fs require(fs); const { ChineseTelecode } require(chinese-telecode); const converter new ChineseTelecode(); const csv fs.readFileSync(old-contracts.csv, utf8); const lines csv.split(\n).map((line) { const [no, name, ...rest] line.split(,); const code converter.charToCode(name[0]); return [no, name, code, ...rest].join(,); }); fs.writeFileSync(old-contracts-telecode.csv, lines.join(\n), utf8);这段脚本逐行读取把每行里的客户名称第一个字转成电码追加到行尾。charToCode(name[0])要留意name可能是空串需要在上一行先判断。还有CSV 内容里有逗号会破坏列结构真实项目里最好用专门的 CSV 解析库。6.2 浏览器里做输入即查在表单里加一个“电码预览”小工具输入汉字即时显示四位码const input document.querySelector(#name); const output document.querySelector(#telecode); const converter new ChineseTelecode(); input.addEventListener(input, () { const code converter.charToCode(input.value[0]); output.textContent code || 未收录; });“未收录”提示比直接显示空更符合业务预期用户知道不是程序坏了而是字太生僻。如果有大量输入建议对 input 事件加上简单的防抖比如 300ms 间隔再查避免每次按键全量计算。实际上这个库查一次很快防抖主要省对象创建和 DOM 写入的开销。6.3 一劳永逸的缓存与边界习惯从那以后我每次接工具库都会先做三件事把所有方法包一层带参数校验的 facade把码表索引预构建并暴露出来最后写一个最小可运行的 demo 放在文档首页。所有类型错误、空值、非法输入都必须在 facade 层挡掉不让脏数据流进对象哈希表。希望这点习惯也能帮到你的项目。本文还有配套的精品资源点击获取