JavaScript 函数与字符统计:参数、Unicode 和 Map 的正确用法
本文目录
函数是有输入、输出和副作用边界的模块。旧练习常把参数重新赋值、隐式全局和普通对象当字典混在一起;现代写法应明确数据如何传递、字符按什么单位计数,以及结果如何处理。
参数是局部绑定
形参是函数局部名称。给形参重新赋值不会改写调用方的原始变量;传入对象时,形参持有指向同一对象的引用值副本,因此修改其属性会影响调用方看到的对象。若函数的任务只是统计文本,就不要改写参数、DOM 或全局状态。
默认参数仅在实参缺失或为 undefined 时启用,显式传入 null 不会触发默认值。rest 参数 ...items 收集剩余实参为数组;它与展开运算符在语法位置上相反。对未知输入先做运行时校验,TypeScript 声明不能验证 JSON、URL 或数据库实际返回值。
先选定“字符”的定义
JavaScript 字符串以 UTF-16 码元保存。text.length 统计码元;for...of 迭代 Unicode 码点,能将多数代理对表示的字符作为一个单位;但肤色修饰、组合音标和家庭 emoji 等由多个码点组成的组合序列,用户通常会感知为一个字符。产品需求应明确是字节、码元、码点、字素簇还是词。
Unicode 允许同一个可见字母用组合符或预组合形式表示。若产品把规范等价文本视为相同,可先用 normalize('NFC');不要无条件删掉标点、空格或兼容字符,因为这会改变原文语义。需要按用户看到的字素簇计数时,在支持环境使用 Intl.Segmenter,并为缺少支持的环境选兼容策略。
使用 Map 统计码点
普通对象带有原型链,不应直接把任意用户字符当属性名存计数。Map 没有这类原型键冲突,语义也更适合键值映射。以下示例明确统计“归一化后的 Unicode 码点”,不是用户感知的字素簇:
function countCodePoints(input) {
const text = String(input).normalize('NFC');
const counts = new Map();
for (const codePoint of text) {
counts.set(codePoint, (counts.get(codePoint) ?? 0) + 1);
}
return counts;
}
countCodePoints('😀a😀').get('😀'); // 2 个码点
function countGraphemes(input, locale = 'zh') {
if (typeof Intl.Segmenter !== 'function') {
throw new Error('当前环境不支持字素簇分段');
}
const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });
const counts = new Map();
for (const { segment } of segmenter.segment(input.normalize('NFC'))) {
counts.set(segment, (counts.get(segment) ?? 0) + 1);
}
return counts;
}
迭代 for...of 和 Array.from 都按码点工作,不应拿它们代替字素簇分段。若要限制社交平台昵称长度,使用产品定义的算法,并检查客户端和服务端采用相同 Unicode 版本与归一化策略;更重要的是服务器再次执行限制。
复杂度与结果呈现
对长度为 n 的输入遍历一次,计数过程平均为 O(n) 时间,额外空间取决于不同字符种类数。面对超大文本仍要限制输入字节数和处理时间,避免把无限输入交给同步主线程。
返回值可用 Map、数组条目或明确 DTO。将统计结果写回页面时,使用文本节点或框架默认转义,不要用 HTML 拼字符串。日志也不要收集用户全文;测试覆盖空字符串、代理对、组合符、emoji 组合、重复字符和恶意超长输入。
检查清单
- 需求说的“一个字符”具体是码元、码点还是字素簇?
- 输入是否需要 NFC,是否会改变业务上有意义的内容?
- 用户控制的任意字符是否进入对象属性键?
- 统计逻辑是否会修改输入或写入共享状态?
- 大文本是否有大小限制、性能预算和安全输出方式?
参考:MDN String.normalize、MDN Intl.Segmenter、MDN for...of、MDN Map、Unicode grapheme cluster boundaries。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。