每当看到发在 FCC 成都社区群里的技术文章,水歌都忍不住去指出它的不足。
今天评注的文章题为《一批提升你工作效率的 JS 工具方法》,文中的 60 个方法与上次评注的“24 个 ES 方法”类似,不够简洁、优雅,与最新 ECMAScript、DOM 标准有些差距,有些“复制粘贴老文章片段”的感觉。
接下来,我就按功能类别来对一些有必要优化的工具方法一一重构。
数据校验
完全基于正则表达式的检验规则其实可以不用封装成函数,全放在独立的模块中,导入后直接 /regexp/.test(data)
即可。
电邮地址
export const Email = /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/;
// 原文:/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+((.[a-zA-Z0-9_-]{2,3}){1,2})$/
「注解」
\w
即为[a-zA-Z0-9_]
[]
表示一个字符范围,就是一个整体,无需()
包围- Gmail 等服务商还支持形如
[email protected]
这样的用户别名邮箱 (.[a-zA-Z0-9_-]{2,3}){1,2}
只适用于前些年常见的.cn
、.com.cn
一类根域名,近几年新增的.name
、.info
、.club
、.camp
等域名就失效了,形如vip.xxmail.com
的多级域名也不适用
手机号码
其实以下只适用于中国大陆手机号,其它国家手机号似乎与固定电话号之间没有明显的区分。
export const Mobile = /^1[3-9]\d{9}$/;
// 原文:/^1[0-9]{10}$/
「注解」
\d
即为[0-9]
- 中国大陆手机号第二位目前没有 1、2
固话号码
中国大陆固定电话号码“区号 + 机号”始终为 11 位。
export const Phone = /^((0\d{2}-)?\d{8}|(0\d{3}-)?\d{7})$/;
// 原文:/^([0-9]{3,4}-)?[0-9]{7,8}$/
网址
export const URL = /^\w+:\/\/\S+$/;
// 原文:/^http[s]?:\/\/.*/
「注解」
- URL 协议不仅包括
http
、https
,还有ftp
(文件传输)、file
(本机文件系统)、ed2k
(电驴 2000)等各种各样的网络协议 - URL 主机名、路径可以是 Unicode 中各种可见字符,但遇到空白符就结束
日期格式
判断是否为合法的日期格式除了用正则之外,还可利用 Date
构造函数内部的算法:
export const isDate = raw => !isNaN(+new Date(raw));
对于无法解析为日期的数据,date.toString()
会返回“Invalid Date”,date.getTime()
对应的返回值则是 NaN
。而算数运算符会调用对象的 valueOf()
方法,date.valueOf()
的返回值又与 date.getTime()
相同。
汉字
“汉字”在计算机领域的学名叫中日韩统一表意文字(俗称 CJK),在 2017 年 6 月发布的 Unicode 10 标准中,它有了代码级明确的指代:
export const HanZi = /\p{Unified_Ideograph}/u;
最佳实践
- 学习:正则分析器 RegExr、Regex101
- 前端:HTML 5 表单校验 API
- 后端:基于装饰器的数据校验
数据转换
阿拉伯数字转中文
ECMA-402 标准(ECMAScript 国际化 API)把各语言之间的数据格式转换算法都封装好了,我们引入 polyfill 就可以直接用:
export const toChineseNumber = raw =>
new Intl.NumberFormat("zh-Hans-u-nu-hanidec").format(raw);
数据类型
判断一个值的类型,用比较构造函数名或类名的方式兼容性比较差,因为线上环境通常是压缩后的代码,自定义的函数名、类名不再是原名,应用开发者一般也不会实现 Symbol.toStringTag
getter 类成员,导致 Object.prototype.toString.call()
只会返回默认值 [object Object]
。
JavaScript
综上,我们应该利用 JavaScript 原型继承,来统一判断“值的类型归属”:
export const isType = (value, constructor) =>
Object(value) instanceof constructor;
「注解」
Object
构造函数会返回所有基本值的包装对象
TypeScript
下面,我再给出一个 TypeScript 的实现,让类型推断更加准确:
export function isType<T>(
value: T,
constructor: { new (...data: any[]): T }
): value is T {
return Object(value) instanceof constructor;
}
import { isType } from "./utility";
let test;
if (isType(test, Number)) console.log(test!.toFixed(2));
浏览器检测
以下使用 globalThis
是为了兼容浏览器主线程、Web Worker、Node.js、Deno 等不同 JavaScript 运行时环境。
品牌
export const isBrowserVendor = (
name,
UA = globalThis.navigator?.userAgent || ""
) => UA.toLowerCase().includes(name);
爬虫
export const isRobot = (UA = globalThis.navigator?.userAgent || "") =>
/bot|spider|crawler/i.test(UA);
去除 HTML 标签
正则表达式
以下使用了 non-greedy(非贪婪模式)来提升性能,并规避正文中可能出现的示例代码没完全转译尖括号,导致删除错误。
export const removeHtmlTag = raw => raw.replace(/<[\s\S]+?>/g, "");
DOM API
下面再提供一种借助 DOM 引擎的实现:
const box = document.createElement("template");
export function removeHtmlTag(raw) {
box.innerHTML = raw;
return box.content.textContent;
}
URL 参数追加
URL()
、URLSearchParams()
在浏览器主线程、Web Worker、Node.js 10+、Deno 均全局可用。
export function appendQuery(path, data, base = globalThis.location.href) {
const URI = new URL(path, base);
const { searchParams } = URI;
for (const key in data) searchParams.append(key, data[key]);
return URI + "";
}
W3C、ECMA 标准
还有一些可以用新标准(部分为提案)直接实现的特性,集中罗列如下:
.trim()
、.trimStart()
、.trimEnd()
(原文第 53 条).includes()
(原文第 42 条)Array.from()
(原文第 48 条)- 数组去重(原文第 44 条)
- 动态
import
(原文第 27 条) element.classList
(原文第 29 ~ 31 条)saveAs()
(原文第 28 条)text-transform
(原文第 54 条)
开源库
水歌把日常开发中积累的各种工具方法,用 TypeScript 写成一个 Web 开源工具库 —— https://web-cell.dev/web-utility/ ,欢迎大家使用、改进!~