utils.js 说明文档

utils.js 说明文档

概述

utils.js 是一个通用前端工具类,约 500 行代码,零依赖。涵盖字符串、数组、对象、日期、数字、URL、Cookie、Storage、验证、防抖节流等日常开发中最常用的功能。

核心特点:

  • 📦 零依赖 --- 纯原生 JavaScript,不依赖任何第三方库
  • 🗂️ 分类清晰 --- 按功能模块组织,调用方式统一 Utils.xxx()
  • 🔌 多模块支持 --- 同时支持浏览器全局、CommonJS、AMD、ES Module 引入
  • 💡 开箱即用 --- 引入即用,无需配置,每个方法独立可拆
  • 📝 中文文档 --- 注释和文档均为中文,适合国内开发者

适用场景:

  • 不引入 lodash / underscore 等大型工具库的项目
  • 传统多页应用或纯静态页面
  • Node.js 后端 / Webpack / Vite 等现代构建工具项目
  • 需要轻量级常用函数的任何项目

快速开始

支持四种引入方式,项目中选择其一即可:

方式一:浏览器 <script> 标签(全局变量)

html 复制代码
<script src="utils.js"></script>
<script>
  Utils.formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss');
  // → "2026-08-11 14:30:00"

  Utils.deepClone({ a: 1, b: { c: 2 } });
  // → 深拷贝对象

  var fn = Utils.debounce(function() { console.log('search'); }, 300);
  input.addEventListener('input', fn);
</script>

方式二:CommonJS(require)

javascript 复制代码
var Utils = require('./utils.js');

Utils.formatDate(new Date(), 'yyyy-MM-dd');
Utils.throttle(handleScroll, 200);

方式三:AMD(require.js / define)

javascript 复制代码
require(['./utils.js'], function(Utils) {
  Utils.formatDate(new Date(), 'yyyy-MM-dd');
});

方式四:ES Module(import)

javascript 复制代码
import Utils from './utils.js';

Utils.formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss');
Utils.isEmail('test@example.com');

API 参考

一、类型判断

方法 说明 示例
Utils.isArray(val) 是否为数组 Utils.isArray([1,2])true
Utils.isObject(val) 是否为纯对象 Utils.isObject({a:1})true
Utils.isFunction(val) 是否为函数 Utils.isFunction(fn)true
Utils.isString(val) 是否为字符串 Utils.isString('abc')true
Utils.isNumber(val) 是否为数字(排除 NaN) Utils.isNumber(123)true
Utils.isBoolean(val) 是否为布尔值 Utils.isBoolean(true)true
Utils.isUndefined(val) 是否为 undefined Utils.isUndefined(x)true
Utils.isNull(val) 是否为 null Utils.isNull(null)true
Utils.isEmpty(val) 是否为空(null/undefined/''/[]/{} Utils.isEmpty({})true

二、字符串

Utils.trim(str)

去除首尾空白字符(包括全角空格和 \uFEFF),兼容没有 String.prototype.trim 的旧浏览器。

javascript 复制代码
Utils.trim('  hello world  ');
// → "hello world"

Utils.trim(null);
// → ""

Utils.capitalize(str)

首字母大写,其余字母小写。

javascript 复制代码
Utils.capitalize('hello');
// → "Hello"

Utils.capitalize('HELLO WORLD');
// → "Hello world"

Utils.camelCase(str)

将短横线、下划线、空格分隔的字符串转为驼峰命名。

javascript 复制代码
Utils.camelCase('hello-world');
// → "helloWorld"

Utils.camelCase('user_first_name');
// → "userFirstName"

Utils.camelCase('background color');
// → "backgroundColor"

Utils.kebabCase(str)

将驼峰或下划线/空格分隔的字符串转为短横线命名。

javascript 复制代码
Utils.kebabCase('helloWorld');
// → "hello-world"

Utils.kebabCase('user_first_name');
// → "user-first-name"

Utils.truncate(str, len, suffix)

截断字符串,超出指定长度时追加后缀。

参数 类型 默认值 说明
str String --- 原始字符串
len Number --- 最大长度
suffix String '...' 截断后缀
javascript 复制代码
Utils.truncate('这是一段很长很长的文本内容', 6);
// → "这是一段很长..."

Utils.truncate('hello world', 5, '...');
// → "hello..."

Utils.stripHtml(str)

移除字符串中的 HTML 标签。

javascript 复制代码
Utils.stripHtml('<p>Hello <b>World</b></p>');
// → "Hello World"

Utils.escapeHtml(str)

转义 HTML 特殊字符,防止 XSS 攻击。

javascript 复制代码
Utils.escapeHtml('<script>alert("xss")</script>');
// → "&lt;script&gt;alert(&quot;xss&quot;)&lt;&#x2F;script&gt;"

Utils.unescapeHtml(str)

反转义 HTML 实体。

javascript 复制代码
Utils.unescapeHtml('&lt;div&gt;Hello&lt;&#x2F;div&gt;');
// → "<div>Hello</div>"

Utils.padStart(str, len, padChar)

在字符串左侧填充字符至指定长度。

javascript 复制代码
Utils.padStart('7', 3, '0');
// → "007"

Utils.padStart('abc', 6, '-');
// → "---abc"

Utils.padEnd(str, len, padChar)

在字符串右侧填充字符至指定长度。

javascript 复制代码
Utils.padEnd('7', 3, '0');
// → "700"

三、数字

Utils.formatNumber(num, decimals)

保留指定位数小数。

参数 类型 默认值 说明
num Number --- 数字
decimals Number 2 小数位数
javascript 复制代码
Utils.formatNumber(3.14159, 2);
// → "3.14"

Utils.formatNumber(100);
// → "100.00"

Utils.formatCurrency(num, decimals, symbol)

格式化金额。

javascript 复制代码
Utils.formatCurrency(12345.6);
// → "¥ 12345.60"

Utils.formatCurrency(12345.6, 0, '$');
// → "$ 12346"

Utils.numberWithCommas(num)

添加千分位逗号。

javascript 复制代码
Utils.numberWithCommas(1234567.89);
// → "1,234,567.89"

Utils.random(min, max)

生成随机整数。

javascript 复制代码
Utils.random(1, 10);    // 1~10 之间的随机整数(含)
Utils.random(100);      // 0~100 之间
Utils.random();         // 0~1 之间浮点数

Utils.clamp(num, min, max)

将数值限制在范围内。

javascript 复制代码
Utils.clamp(150, 0, 100);
// → 100

Utils.clamp(-10, 0, 100);
// → 0

Utils.inRange(num, min, max)

判断数值是否在范围内。

javascript 复制代码
Utils.inRange(50, 0, 100);
// → true

Utils.inRange(-1, 0, 100);
// → false

四、数组

Utils.unique(arr)

数组去重(基于 indexOf)。

javascript 复制代码
Utils.unique([1, 2, 2, 3, 1]);
// → [1, 2, 3]

Utils.uniqueBy(arr, key)

对象数组按字段去重。

javascript 复制代码
var list = [{id:1}, {id:2}, {id:1}];
Utils.uniqueBy(list, 'id');
// → [{id:1}, {id:2}]

Utils.flatten(arr, deep)

展平数组。

javascript 复制代码
Utils.flatten([[1,2], [3,4]]);
// → [1, 2, 3, 4]

Utils.flatten([1, [2, [3, [4]]]], true);
// → [1, 2, 3, 4]

Utils.chunk(arr, size)

将数组按指定大小分块。

javascript 复制代码
Utils.chunk([1,2,3,4,5,6,7], 3);
// → [[1,2,3], [4,5,6], [7]]

Utils.shuffle(arr)

随机打乱数组(Fisher-Yates 洗牌算法),不改变原数组。

javascript 复制代码
Utils.shuffle([1, 2, 3, 4, 5]);
// → [3, 1, 5, 2, 4]  (随机)

Utils.range(start, end, step)

生成数字序列。

javascript 复制代码
Utils.range(5);
// → [0, 1, 2, 3, 4]

Utils.range(1, 10, 2);
// → [1, 3, 5, 7, 9]

Utils.groupBy(arr, key)

按字段分组。

javascript 复制代码
var users = [
  { name: 'Alice', role: 'admin' },
  { name: 'Bob', role: 'user' },
  { name: 'Charlie', role: 'admin' }
];
Utils.groupBy(users, 'role');
// → { admin: [{...}, {...}], user: [{...}] }

// 支持回调函数
Utils.groupBy(users, function(u) { return u.name[0]; });
// → { A: [...], B: [...], C: [...] }

Utils.sortBy(arr, key, desc)

按字段排序(不改变原数组)。

javascript 复制代码
Utils.sortBy(users, 'name');          // 升序
Utils.sortBy(users, 'name', true);    // 降序

Utils.remove(arr, item)

从数组中移除指定元素(直接修改原数组)。

javascript 复制代码
var a = [1, 2, 3, 4];
Utils.remove(a, 3);
// a → [1, 2, 4]

Utils.intersection(a, b)

取两个数组的交集。

javascript 复制代码
Utils.intersection([1,2,3], [2,3,4]);
// → [2, 3]

Utils.difference(a, b)

取差集(在 a 中但不在 b 中的元素)。

javascript 复制代码
Utils.difference([1,2,3], [2,3,4]);
// → [1]

五、对象

Utils.deepClone(obj)

深度克隆。支持 Date、RegExp、普通对象、数组。

javascript 复制代码
var obj = { a: 1, b: [1,2,{c:3}], d: new Date() };
var clone = Utils.deepClone(obj);
clone.b[2].c = 999;
// obj.b[2].c 仍为 3,互不影响

Utils.extend(target, src1, src2, ...)

浅合并,后面的覆盖前面的,直接修改 target

javascript 复制代码
Utils.extend({a:1}, {b:2}, {b:3});
// → {a:1, b:3}

Utils.extendDeep(target, src1, src2, ...)

深度合并。

javascript 复制代码
Utils.extendDeep(
  { a: { x: 1, y: 2 } },
  { a: { y: 999, z: 3 } }
);
// → { a: { x: 1, y: 999, z: 3 } }

Utils.pick(obj, keys)

从对象中挑选指定字段。

javascript 复制代码
Utils.pick({a:1, b:2, c:3, d:4}, ['a', 'c']);
// → { a: 1, c: 3 }

Utils.omit(obj, keys)

排除指定字段。

javascript 复制代码
Utils.omit({a:1, b:2, c:3, d:4}, ['a', 'c']);
// → { b: 2, d: 4 }

Utils.objectToQuery(obj)

对象转 URL 查询字符串。

javascript 复制代码
Utils.objectToQuery({ name: 'Alice', age: 28 });
// → "name=Alice&age=28"

Utils.objectToQuery({ a: 1, b: null, c: 3 });
// → "a=1&c=3"   (null 值自动忽略)

Utils.queryToObject(str)

URL 查询字符串解析为对象。

javascript 复制代码
Utils.queryToObject('name=Alice&age=28');
// → { name: 'Alice', age: '28' }

Utils.queryToObject();
// 自动解析当前页面 URL 参数 (location.search)

六、日期

Utils.formatDate(date, fmt)

格式化日期。

参数 类型 默认值 说明
date Date/String/Number --- 日期值
fmt String 'yyyy-MM-dd HH:mm:ss' 格式模板

格式占位符:

占位符 含义 示例
yyyy 年份四位数 2026
yy 年份两位数 26
MM 月份(01-12) 08
M 月份(无前导零) 8
dd 日期(01-31) 11
d 日期(无前导零) 11
HH 24小时制(00-23) 14
hh 12小时制(01-12) 02
mm 分钟(00-59) 30
ss 秒(00-59) 05
SSS 毫秒 123
t 上下午 AM / PM
javascript 复制代码
var d = new Date(2026, 7, 11, 14, 30, 5);

Utils.formatDate(d);
// → "2026-08-11 14:30:05"

Utils.formatDate(d, 'yyyy年MM月dd日');
// → "2026年08月11日"

Utils.formatDate(d, 'yyyy-MM-dd hh:mm:ss t');
// → "2026-08-11 02:30:05 PM"

Utils.formatDate(d, 'MM-dd');
// → "08-11"

Utils.formatDate(1702741234567);
// 时间戳也可以直接传入

Utils.timeAgo(date)

显示距离当前时间的相对描述。

javascript 复制代码
Utils.timeAgo(new Date(Date.now() - 30000));
// → "刚刚"

Utils.timeAgo(new Date(Date.now() - 30 * 60000));
// → "30 分钟前"

Utils.timeAgo(new Date(Date.now() - 5 * 3600000));
// → "5 小时前"

Utils.timeAgo(new Date(Date.now() - 3 * 86400000));
// → "3 天前"

Utils.timeAgo(new Date(Date.now() - 60 * 86400000));
// → "2 个月前"

Utils.timeAgo(new Date(Date.now() - 400 * 86400000));
// → "1 年前"

Utils.getDateDiff(start, end)

计算两个日期的时间差。

javascript 复制代码
var start = new Date('2026-08-01');
var end   = new Date('2026-08-11');
Utils.getDateDiff(start, end);
// → { days: 10, hours: 0, minutes: 0, seconds: 0, milliseconds: 864000000 }

Utils.addDays(date, days)

日期加减天数,返回新 Date 对象。

javascript 复制代码
Utils.addDays(new Date('2026-08-01'), 10);
// → Date(2026-08-11)

Utils.addDays(new Date('2026-08-01'), -5);
// → Date(2026-07-27)

Utils.isLeapYear(year)

判断是否为闰年。

javascript 复制代码
Utils.isLeapYear(2024);  // → true
Utils.isLeapYear(2026);  // → false
Utils.isLeapYear(new Date());  // 也可传入 Date 对象

Utils.daysInMonth(year, month)

获取指定年月的天数。

javascript 复制代码
Utils.daysInMonth(2026, 2);   // → 28
Utils.daysInMonth(2024, 2);   // → 29  (闰年)
Utils.daysInMonth(2026, 8);   // → 31

七、URL / 浏览器

Utils.parseUrl(url)

解析 URL 各组成部分。

javascript 复制代码
Utils.parseUrl('https://www.example.com:8080/path/page?a=1#top');
// → {
//   protocol: 'https:',
//   host: 'www.example.com:8080',
//   hostname: 'www.example.com',
//   port: '8080',
//   pathname: '/path/page',
//   search: '?a=1',
//   hash: '#top',
//   href: 'https://www.example.com:8080/path/page?a=1#top'
// }

Utils.getQueryParam(name, url)

获取 URL 中指定参数的值。

javascript 复制代码
// 当前页面 URL 为 ?name=Alice&age=28
Utils.getQueryParam('name');
// → "Alice"

Utils.getQueryParam('age', 'https://a.com?age=25&page=1');
// → "25"

Utils.addQueryParam(url, key, value)

给 URL 追加查询参数(已存在则替换)。

javascript 复制代码
Utils.addQueryParam('/page', 'page', '2');
// → "/page?page=2"

Utils.addQueryParam('/page?page=1', 'page', '2');
// → "/page?page=2"

Utils.addQueryParam('/page#top', 'sort', 'desc');
// → "/page?sort=desc#top"

Utils.removeQueryParam(url, key)

移除 URL 中指定查询参数。

javascript 复制代码
Utils.removeQueryParam('/page?a=1&b=2&c=3', 'b');
// → "/page?a=1&c=3"

Utils.isMobile()

检测是否为移动设备。

javascript 复制代码
if (Utils.isMobile()) {
  console.log('当前为手机浏览器');
}

Utils.isIE()

检测是否为 IE 浏览器。

javascript 复制代码
if (Utils.isIE()) {
  alert('请使用现代浏览器访问');
}

八、Cookie

Utils.cookie.set(name, value, days, path, domain)

设置 Cookie。

参数 类型 默认值 说明
name String --- Cookie 名称
value String --- Cookie 值
days Number undefined 过期天数(不传则为会话 Cookie)
path String '/' 有效路径
domain String '' 有效域名
javascript 复制代码
Utils.cookie.set('username', 'Alice', 7);
Utils.cookie.set('theme', 'dark', 30, '/', '.example.com');

Utils.cookie.get(name)

读取 Cookie。

javascript 复制代码
Utils.cookie.get('username');
// → "Alice"

Utils.cookie.get('nonexistent');
// → null

Utils.cookie.remove(name, path, domain)

删除 Cookie。

javascript 复制代码
Utils.cookie.remove('username');
Utils.cookie.remove('theme', '/', '.example.com');

九、Storage

Utils.storage.set(key, value, expires)

写入 localStorage,支持过期时间。

参数 类型 默认值 说明
key String --- 键名
value Any --- 值(自动 JSON 序列化)
expires Number undefined 过期毫秒数(不传则永不过期)
javascript 复制代码
Utils.storage.set('user', { name: 'Alice', role: 'admin' });

Utils.storage.set('token', 'abc123', 3600000);  // 1 小时后过期

Utils.storage.get(key)

读取 localStorage(自动 JSON 反序列化,过期则返回 null 并清除)。

javascript 复制代码
var user = Utils.storage.get('user');
// → { name: 'Alice', role: 'admin' }

Utils.storage.get('token');
// 如果过期 → null(已自动清除)

Utils.storage.remove(key) / Utils.storage.clear()

删除指定 key / 清空所有 localStorage。

javascript 复制代码
Utils.storage.remove('user');
Utils.storage.clear();

十、函数相关

Utils.debounce(fn, delay, immediate)

防抖 --- 高频事件停止触发 delay 毫秒后才执行。

参数 类型 默认值 说明
fn Function --- 要执行的函数
delay Number --- 延迟毫秒数
immediate Boolean false 是否立即执行一次
javascript 复制代码
// 搜索输入防抖
input.addEventListener('input', Utils.debounce(function(e) {
  console.log('搜索:', e.target.value);
}, 300));

// 立即执行模式(第一次立即触发,之后防抖)
btn.addEventListener('click', Utils.debounce(function() {
  console.log('提交');
}, 1000, true));

Utils.throttle(fn, delay)

节流 --- 每 delay 毫秒最多执行一次。

javascript 复制代码
// 滚动事件节流
window.addEventListener('scroll', Utils.throttle(function() {
  console.log('scrollTop:', Utils.getScrollTop());
}, 200));

// 按钮防连点
btn.addEventListener('click', Utils.throttle(function() {
  console.log('click');
}, 1000));

防抖 vs 节流:

防抖 (debounce) 节流 (throttle)
行为 停止触发后等待再执行 固定间隔持续执行
适用 搜索输入、窗口 resize 结束 滚动、鼠标移动、动画帧

Utils.once(fn)

创建只执行一次的函数。

javascript 复制代码
var init = Utils.once(function() {
  console.log('只执行一次');
});

init();  // → "只执行一次"
init();  // 无输出

Utils.memoize(fn)

创建带缓存结果的函数(基于参数 JSON 序列化做 key)。

javascript 复制代码
var factorial = Utils.memoize(function(n) {
  console.log('计算中...');
  if (n <= 1) return 1;
  return n * factorial(n - 1);
});

factorial(5);  // 计算中... x5
factorial(5);  // 直接从缓存返回

Utils.delay(fn, ms, ...args)

延迟执行函数。

javascript 复制代码
Utils.delay(function(name) {
  console.log('Hello, ' + name);
}, 2000, 'Alice');

// 2 秒后输出: "Hello, Alice"

十一、验证

所有验证方法在 Utils.validate 命名空间下。

方法 说明 示例
email(val) 邮箱格式 Utils.validate.email('test@example.com')true
phone(val) 手机号(中国大陆 1xx) Utils.validate.phone('13800138000')true
tel(val) 座机号 Utils.validate.tel('010-12345678')true
idCard(val) 身份证号(15位或18位) Utils.validate.idCard('110101199001011234')true
url(val) URL 格式 Utils.validate.url('https://example.com')true
number(val) 数字(整数或小数) Utils.validate.number('3.14')true
integer(val) 整数 Utils.validate.integer('100')true
chinese(val) 纯中文 Utils.validate.chinese('你好')true
ip(val) IPv4 地址 Utils.validate.ip('192.168.1.1')true
qq(val) QQ 号(5~11位) Utils.validate.qq('123456789')true
postalCode(val) 邮政编码(6位) Utils.validate.postalCode('100000')true
isEmpty(val) 是否为空 Utils.validate.isEmpty('')true
minLength(val, len) 最小长度 Utils.validate.minLength('abc', 2)true
maxLength(val, len) 最大长度 Utils.validate.maxLength('abc', 10)true
rangeLength(val, min, max) 长度范围 Utils.validate.rangeLength('abc', 1, 5)true
javascript 复制代码
// 表单验证示例
var rules = {
  email:    { validate: Utils.validate.email, msg: '邮箱格式不正确' },
  phone:    { validate: Utils.validate.phone, msg: '手机号格式不正确' },
  username: { validate: function(v) { return Utils.validate.rangeLength(v, 3, 20); }, msg: '用户名需 3-20 位' }
};

function checkField(field, value) {
  if (!rules[field].validate(value)) {
    return rules[field].msg;
  }
  return null;
}

十二、全局唯一 ID

Utils.uid(prefix)

生成短唯一 ID,格式:[前缀]时间戳_自增数_随机串

javascript 复制代码
Utils.uid();
// → "m5n8x2k_1_k3x9m2"

Utils.uid('user_');
// → "user_m5n8x2k_2_d8f3g1"

// 高并发下也不会重复(自增计数器保证唯一性)

Utils.guid()

生成标准 UUID(GUID)格式。

javascript 复制代码
Utils.guid();
// → "a3f1b2c4-5d6e-4f8a-9b0c-d1e2f3a4b5c6"

十三、颜色

Utils.color.random()

生成随机十六进制颜色。

javascript 复制代码
Utils.color.random();
// → "#a3f2c1"

document.body.style.backgroundColor = Utils.color.random();

Utils.color.hexToRgb(hex)

十六进制转 RGB 对象。

javascript 复制代码
Utils.color.hexToRgb('#ff6600');
// → { r: 255, g: 102, b: 0 }

Utils.color.hexToRgbStr(hex)

十六进制转 RGB 字符串。

javascript 复制代码
Utils.color.hexToRgbStr('#ff6600');
// → "rgb(255, 102, 0)"

Utils.color.hexToRgbStr('336699');
// → "rgb(51, 102, 153)"

Utils.color.rgbToHex(r, g, b)

RGB 转十六进制。

javascript 复制代码
Utils.color.rgbToHex(255, 102, 0);
// → "#ff6600"

十四、数学

javascript 复制代码
Utils.sum([1, 2, 3, 4, 5]);      // → 15
Utils.avg([1, 2, 3, 4, 5]);      // → 3
Utils.max([3, 1, 4, 1, 5]);      // → 5
Utils.min([3, 1, 4, 1, 5]);      // → 1
Utils.degToRad(180);              // → 3.14159...  (角度转弧度)
Utils.radToDeg(Math.PI);          // → 180          (弧度转角度)

十五、DOM

Utils.scrollToTop()

平滑滚动到页面顶部。

javascript 复制代码
Utils.scrollToTop();

Utils.getScrollTop()

获取当前页面滚动距离。

javascript 复制代码
Utils.getScrollTop();
// → 450

Utils.copyToClipboard(text)

复制文本到剪贴板。返回 Promise。

javascript 复制代码
Utils.copyToClipboard('Hello World').then(function() {
  console.log('复制成功');
}).catch(function() {
  console.log('复制失败');
});

// async/await 风格
await Utils.copyToClipboard('复制的文本');

Utils.getWindowSize()

获取视窗尺寸。

javascript 复制代码
Utils.getWindowSize();
// → { width: 1920, height: 1080 }

十六、异步相关

Utils.sleep(ms)

异步等待指定毫秒。返回 Promise。

javascript 复制代码
async function demo() {
  console.log('开始');
  await Utils.sleep(2000);
  console.log('2 秒后');
}

Utils.retry(fn, times, delay)

创建带重试机制的异步函数。

参数 类型 默认值 说明
fn Function --- 要重试的函数(需返回 Promise)
times Number 3 最大重试次数
delay Number 1000 重试间隔(毫秒)
javascript 复制代码
var fetchWithRetry = Utils.retry(function(url) {
  return fetch(url).then(function(res) {
    if (!res.ok) throw new Error('请求失败');
    return res.json();
  });
}, 5, 2000);

fetchWithRetry('/api/unstable-endpoint')
  .then(function(data) { console.log('成功:', data); })
  .catch(function(err) { console.error('5 次重试后仍失败', err); });

子命名空间一览

命名空间 说明
Utils.cookie.* Cookie 读写删
Utils.storage.* localStorage 封装(支持过期)
Utils.validate.* 表单验证方法集
Utils.color.* 颜色格式转换

模块规范

utils.js 采用 UMD(Universal Module Definition)包装,自动适配运行环境:

环境 检测条件 导出方式
浏览器 <script> 其他情况 window.Utils
CommonJS (Node.js) typeof module === 'object' module.exports
AMD (RequireJS) typeof define === 'function' && define.amd define(factory)

文件信息

复制代码
utils.js --- 主库文件(约 500 行,零依赖,UMD 模块)

兼容浏览器、Node.js、AMD、ES Module 等多种环境,无需任何构建工具可直接使用。

javascript 复制代码
/**
 * utils.js
 * 通用前端工具类
 *
 * 包含常用方法:
 *   字符串、数组、对象、日期、数字、URL、验证、防抖节流、Storage 等
 *
 * 浏览器引入:
 *   <script src="utils.js"></script>
 *   <script>
 *     Utils.deepClone(obj);
 *     Utils.formatDate(new Date(), 'yyyy-MM-dd HH:mm:ss');
 *     Utils.debounce(fn, 300);
 *   </script>
 *
 * CommonJS:
 *   var Utils = require('./utils.js');
 *
 * AMD:
 *   require(['./utils.js'], function(Utils) { ... });
 *
 * ES Module:
 *   import Utils from './utils.js';   // 或 import { deepClone, formatDate } from './utils.js'
 */
;(function(global, factory) {

  if (typeof exports === 'object' && typeof module === 'object') {
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    define(function() { return factory(); });
  } else {
    global.Utils = factory();
  }

}(typeof window !== 'undefined' ? window : this, function() {
  'use strict';

  var Utils = {};

  // ==================== 类型判断 ====================

  var _toString = Object.prototype.toString;

  function _getType(val) {
    return _toString.call(val).slice(8, -1).toLowerCase();
  }

  Utils.isArray = Array.isArray || function(val) {
    return _getType(val) === 'array';
  };

  Utils.isObject = function(val) {
    return val !== null && typeof val === 'object' && !Utils.isArray(val);
  };

  Utils.isFunction = function(val) {
    return typeof val === 'function';
  };

  Utils.isString = function(val) {
    return typeof val === 'string';
  };

  Utils.isNumber = function(val) {
    return typeof val === 'number' && !isNaN(val);
  };

  Utils.isBoolean = function(val) {
    return typeof val === 'boolean';
  };

  Utils.isUndefined = function(val) {
    return val === undefined;
  };

  Utils.isNull = function(val) {
    return val === null;
  };

  Utils.isEmpty = function(val) {
    if (val === null || val === undefined) return true;
    if (Utils.isArray(val) || Utils.isString(val)) return val.length === 0;
    if (Utils.isObject(val)) return Object.keys(val).length === 0;
    if (typeof val === 'number') return false;
    return !val;
  };

  // ==================== 字符串 ====================

  Utils.trim = function(str) {
    return str == null ? '' : String(str).replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
  };

  Utils.capitalize = function(str) {
    if (!str) return '';
    str = String(str);
    return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
  };

  Utils.camelCase = function(str) {
    return String(str).replace(/[-_\s]+(.)?/g, function(match, c) {
      return c ? c.toUpperCase() : '';
    });
  };

  Utils.kebabCase = function(str) {
    return String(str)
      .replace(/([a-z])([A-Z])/g, '$1-$2')
      .replace(/[\s_]+/g, '-')
      .toLowerCase();
  };

  Utils.truncate = function(str, len, suffix) {
    if (!str) return '';
    suffix = suffix || '...';
    return str.length > len ? str.slice(0, len) + suffix : str;
  };

  Utils.stripHtml = function(str) {
    return String(str).replace(/<[^>]+>/g, '');
  };

  Utils.escapeHtml = function(str) {
    var map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;' };
    return String(str).replace(/[&<>"'/]/g, function(m) { return map[m]; });
  };

  Utils.unescapeHtml = function(str) {
    var map = { '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#x27;': "'", '&#x2F;': '/' };
    return String(str).replace(/&(?:amp|lt|gt|quot|#x27|#x2F);/g, function(m) { return map[m] || m; });
  };

  Utils.padStart = function(str, len, padChar) {
    str = String(str);
    padChar = padChar || ' ';
    while (str.length < len) { str = padChar + str; }
    return str;
  };

  Utils.padEnd = function(str, len, padChar) {
    str = String(str);
    padChar = padChar || ' ';
    while (str.length < len) { str = str + padChar; }
    return str;
  };

  // ==================== 数字 ====================

  Utils.formatNumber = function(num, decimals) {
    if (!Utils.isNumber(num)) return num;
    decimals = decimals == null ? 2 : decimals;
    return Number(num).toFixed(decimals);
  };

  Utils.formatCurrency = function(num, decimals, symbol) {
    decimals = decimals == null ? 2 : decimals;
    symbol = symbol || '¥';
    return symbol + ' ' + Utils.formatNumber(num, decimals);
  };

  Utils.numberWithCommas = function(num) {
    var parts = Number(num).toFixed(2).split('.');
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
    return parts.join('.');
  };

  Utils.random = function(min, max) {
    if (min === undefined) return Math.random();
    if (max === undefined) { max = min; min = 0; }
    return Math.floor(Math.random() * (max - min + 1)) + min;
  };

  Utils.clamp = function(num, min, max) {
    return Math.min(Math.max(Number(num), min), max);
  };

  Utils.inRange = function(num, min, max) {
    return Number(num) >= min && Number(num) <= max;
  };

  // ==================== 数组 ====================

  Utils.unique = function(arr) {
    return arr.filter(function(item, index) {
      return arr.indexOf(item) === index;
    });
  };

  Utils.uniqueBy = function(arr, key) {
    var seen = {};
    return arr.filter(function(item) {
      var val = item[key];
      if (seen[val]) return false;
      seen[val] = true;
      return true;
    });
  };

  Utils.flatten = function(arr, deep) {
    if (deep) {
      return arr.reduce(function(flat, item) {
        return flat.concat(Utils.isArray(item) ? Utils.flatten(item, true) : item);
      }, []);
    }
    return arr.reduce(function(flat, item) {
      return flat.concat(item);
    }, []);
  };

  Utils.chunk = function(arr, size) {
    var result = [];
    for (var i = 0; i < arr.length; i += size) {
      result.push(arr.slice(i, i + size));
    }
    return result;
  };

  Utils.shuffle = function(arr) {
    var result = arr.slice();
    for (var i = result.length - 1; i > 0; i--) {
      var j = Math.floor(Math.random() * (i + 1));
      var temp = result[i];
      result[i] = result[j];
      result[j] = temp;
    }
    return result;
  };

  Utils.range = function(start, end, step) {
    step = step || 1;
    var result = [];
    if (end === undefined) {
      end = start;
      start = 0;
    }
    for (var i = start; i < end; i += step) {
      result.push(i);
    }
    return result;
  };

  Utils.groupBy = function(arr, key) {
    return arr.reduce(function(groups, item) {
      var val = typeof key === 'function' ? key(item) : item[key];
      (groups[val] || (groups[val] = [])).push(item);
      return groups;
    }, {});
  };

  Utils.sortBy = function(arr, key, desc) {
    return arr.slice().sort(function(a, b) {
      var valA = typeof key === 'function' ? key(a) : a[key];
      var valB = typeof key === 'function' ? key(b) : b[key];
      if (valA < valB) return desc ? 1 : -1;
      if (valA > valB) return desc ? -1 : 1;
      return 0;
    });
  };

  Utils.remove = function(arr, item) {
    var idx = arr.indexOf(item);
    if (idx > -1) arr.splice(idx, 1);
    return arr;
  };

  Utils.intersection = function(a, b) {
    return a.filter(function(item) { return b.indexOf(item) > -1; });
  };

  Utils.difference = function(a, b) {
    return a.filter(function(item) { return b.indexOf(item) === -1; });
  };

  // ==================== 对象 ====================

  Utils.deepClone = function(obj) {
    if (obj === null || typeof obj !== 'object') return obj;
    if (obj instanceof Date) return new Date(obj.getTime());
    if (obj instanceof RegExp) return new RegExp(obj);

    var clone = Utils.isArray(obj) ? [] : {};
    for (var key in obj) {
      if (obj.hasOwnProperty(key)) {
        clone[key] = Utils.deepClone(obj[key]);
      }
    }
    return clone;
  };

  Utils.extend = function(target) {
    var i = 1, len = arguments.length;
    for (; i < len; i++) {
      var src = arguments[i];
      if (src != null) {
        for (var key in src) {
          if (src.hasOwnProperty(key)) {
            target[key] = src[key];
          }
        }
      }
    }
    return target;
  };

  Utils.extendDeep = function(target) {
    var i = 1, len = arguments.length;
    for (; i < len; i++) {
      var src = arguments[i];
      if (src != null) {
        for (var key in src) {
          if (src.hasOwnProperty(key)) {
            if (Utils.isObject(src[key]) && Utils.isObject(target[key])) {
              Utils.extendDeep(target[key], src[key]);
            } else {
              target[key] = src[key];
            }
          }
        }
      }
    }
    return target;
  };

  Utils.pick = function(obj, keys) {
    var result = {};
    for (var i = 0; i < keys.length; i++) {
      var key = keys[i];
      if (key in obj) result[key] = obj[key];
    }
    return result;
  };

  Utils.omit = function(obj, keys) {
    var result = {};
    for (var k in obj) {
      if (obj.hasOwnProperty(k) && keys.indexOf(k) === -1) {
        result[k] = obj[k];
      }
    }
    return result;
  };

  Utils.objectToQuery = function(obj) {
    var parts = [];
    for (var key in obj) {
      if (obj.hasOwnProperty(key) && obj[key] != null) {
        parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
      }
    }
    return parts.join('&');
  };

  Utils.queryToObject = function(str) {
    var obj = {};
    var str2 = str || global.location.search.slice(1);
    if (!str2) return obj;
    var pairs = str2.split('&');
    for (var i = 0; i < pairs.length; i++) {
      var pair = pairs[i].split('=');
      if (pair[0]) {
        obj[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
      }
    }
    return obj;
  };

  // ==================== 日期 ====================

  Utils.formatDate = function(date, fmt) {
    if (!date) return '';
    if (typeof date === 'string' || typeof date === 'number') date = new Date(date);
    fmt = fmt || 'yyyy-MM-dd HH:mm:ss';

    var map = {
      'y+': date.getFullYear(),
      'M+': date.getMonth() + 1,
      'd+': date.getDate(),
      'H+': date.getHours(),
      'h+': date.getHours() % 12 || 12,
      'm+': date.getMinutes(),
      's+': date.getSeconds(),
      'S+': date.getMilliseconds(),
      't': date.getHours() < 12 ? 'AM' : 'PM'
    };

    for (var key in map) {
      if (new RegExp('(' + key + ')').test(fmt)) {
        var val = String(map[key]);
        if (key !== 't') {
          val = RegExp.$1.length === 1 ? val : Utils.padStart(val, RegExp.$1.length, '0');
        }
        fmt = fmt.replace(RegExp.$1, val);
      }
    }
    return fmt;
  };

  Utils.timeAgo = function(date) {
    if (typeof date === 'string' || typeof date === 'number') date = new Date(date);
    var diff = Date.now() - date.getTime();
    var minute = 60 * 1000;
    var hour = 60 * minute;
    var day = 24 * hour;
    var month = 30 * day;
    var year = 365 * day;

    if (diff < minute) return '刚刚';
    if (diff < hour) return Math.floor(diff / minute) + ' 分钟前';
    if (diff < day) return Math.floor(diff / hour) + ' 小时前';
    if (diff < month) return Math.floor(diff / day) + ' 天前';
    if (diff < year) return Math.floor(diff / month) + ' 个月前';
    return Math.floor(diff / year) + ' 年前';
  };

  Utils.getDateDiff = function(start, end) {
    if (typeof start === 'string' || typeof start === 'number') start = new Date(start);
    if (typeof end === 'string' || typeof end === 'number') end = new Date(end);
    var diff = end.getTime() - start.getTime();
    var day = 24 * 60 * 60 * 1000;
    return {
      days: Math.floor(diff / day),
      hours: Math.floor((diff % day) / (60 * 60 * 1000)),
      minutes: Math.floor((diff % (60 * 60 * 1000)) / (60 * 1000)),
      seconds: Math.floor((diff % (60 * 1000)) / 1000),
      milliseconds: diff
    };
  };

  Utils.addDays = function(date, days) {
    var d = new Date(date);
    d.setDate(d.getDate() + days);
    return d;
  };

  Utils.isLeapYear = function(year) {
    year = Utils.isNumber(year) ? year : new Date(year).getFullYear();
    return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
  };

  Utils.daysInMonth = function(year, month) {
    return new Date(year, month, 0).getDate();
  };

  // ==================== URL / 浏览器 ====================

  Utils.parseUrl = function(url) {
    url = url || global.location.href;
    var a = document.createElement('a');
    a.href = url;
    return {
      protocol: a.protocol,
      host: a.host,
      hostname: a.hostname,
      port: a.port,
      pathname: a.pathname,
      search: a.search,
      hash: a.hash,
      href: a.href
    };
  };

  Utils.getQueryParam = function(name, url) {
    url = url || global.location.href;
    var regex = new RegExp('[?&]' + name + '(?:=([^&#]*)|&|#|$)');
    var results = regex.exec(url);
    if (!results) return null;
    if (!results[1]) return '';
    return decodeURIComponent(results[1].replace(/\+/g, ' '));
  };

  Utils.addQueryParam = function(url, key, value) {
    var parts = url.split('#');
    var hash = parts[1] ? '#' + parts[1] : '';
    url = parts[0];

    var regex = new RegExp('([?&])' + key + '=[^&]*&?');
    var separator;

    if (regex.test(url)) {
      url = url.replace(regex, function(match, prefix) {
        return prefix + key + '=' + encodeURIComponent(value) + '&';
      });
    } else {
      separator = url.indexOf('?') > -1 ? '&' : '?';
      url += separator + key + '=' + encodeURIComponent(value);
    }

    return url + hash;
  };

  Utils.removeQueryParam = function(url, key) {
    var parts = url.split('#');
    var hash = parts[1] ? '#' + parts[1] : '';
    url = parts[0];

    var regex = new RegExp('([?&])' + key + '=[^&]*&?', 'g');
    url = url.replace(regex, function(match, prefix) {
      return match.charAt(match.length - 1) === '&' ? prefix : '';
    });
    url = url.replace(/[?&]$/, '');

    return url + hash;
  };

  Utils.isMobile = function() {
    return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(global.navigator && global.navigator.userAgent || '');
  };

  Utils.isIE = function() {
    return /MSIE|Trident/.test(global.navigator && global.navigator.userAgent || '');
  };

  // ==================== Cookie ====================

  Utils.cookie = {
    set: function(name, value, days, path, domain) {
      var expires = '';
      if (days) {
        var date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = '; expires=' + date.toUTCString();
      }
      path = path ? '; path=' + path : '; path=/';
      domain = domain ? '; domain=' + domain : '';
      document.cookie = name + '=' + encodeURIComponent(value || '') + expires + path + domain;
    },

    get: function(name) {
      var cookies = document.cookie.split(';');
      for (var i = 0; i < cookies.length; i++) {
        var c = Utils.trim(cookies[i]);
        if (c.indexOf(name + '=') === 0) {
          return decodeURIComponent(c.substring(name.length + 1));
        }
      }
      return null;
    },

    remove: function(name, path, domain) {
      Utils.cookie.set(name, '', -1, path, domain);
    }
  };

  // ==================== Storage ====================

  Utils.storage = {
    set: function(key, value, expires) {
      var data = { value: value };
      if (expires) data.expires = Date.now() + expires;
      try {
        localStorage.setItem(key, JSON.stringify(data));
        return true;
      } catch (e) {
        return false;
      }
    },

    get: function(key) {
      try {
        var data = JSON.parse(localStorage.getItem(key));
        if (!data) return null;
        if (data.expires && Date.now() > data.expires) {
          localStorage.removeItem(key);
          return null;
        }
        return data.value;
      } catch (e) {
        return null;
      }
    },

    remove: function(key) {
      localStorage.removeItem(key);
    },

    clear: function() {
      localStorage.clear();
    }
  };

  // ==================== 函数相关 ====================

  Utils.debounce = function(fn, delay, immediate) {
    var timer = null;
    return function() {
      var context = this;
      var args = arguments;
      if (timer) clearTimeout(timer);
      if (immediate) {
        var callNow = !timer;
        timer = setTimeout(function() { timer = null; }, delay);
        if (callNow) fn.apply(context, args);
      } else {
        timer = setTimeout(function() { fn.apply(context, args); }, delay);
      }
    };
  };

  Utils.throttle = function(fn, delay) {
    var lastTime = 0;
    return function() {
      var now = Date.now();
      if (now - lastTime >= delay) {
        fn.apply(this, arguments);
        lastTime = now;
      }
    };
  };

  Utils.once = function(fn) {
    var called = false;
    return function() {
      if (!called) {
        called = true;
        return fn.apply(this, arguments);
      }
    };
  };

  Utils.memoize = function(fn) {
    var cache = {};
    return function() {
      var key = JSON.stringify(arguments);
      if (cache[key] === undefined) {
        cache[key] = fn.apply(this, arguments);
      }
      return cache[key];
    };
  };

  Utils.delay = function(fn, ms) {
    var args = Array.prototype.slice.call(arguments, 2);
    return setTimeout(function() { fn.apply(null, args); }, ms);
  };

  // ==================== 验证 ====================

  Utils.validate = {
    email: function(val) {
      return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(val);
    },

    phone: function(val) {
      return /^1[3-9]\d{9}$/.test(val);
    },

    tel: function(val) {
      return /^(\d{3,4}-)?\d{7,8}$/.test(val);
    },

    idCard: function(val) {
      return /^\d{15}$|^\d{17}[\dXx]$/.test(val);
    },

    url: function(val) {
      return /^(https?:\/\/)?([\w-]+\.)+[\w-]+(\/[\w-./?%&=]*)?$/.test(val);
    },

    number: function(val) {
      return /^-?\d+(\.\d+)?$/.test(val);
    },

    integer: function(val) {
      return /^-?\d+$/.test(val);
    },

    chinese: function(val) {
      return /^[\u4e00-\u9fa5]+$/.test(val);
    },

    ip: function(val) {
      return /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/.test(val);
    },

    qq: function(val) {
      return /^[1-9]\d{4,10}$/.test(val);
    },

    postalCode: function(val) {
      return /^\d{6}$/.test(val);
    },

    isEmpty: function(val) {
      return Utils.isEmpty(val);
    },

    minLength: function(val, len) {
      return String(val).length >= len;
    },

    maxLength: function(val, len) {
      return String(val).length <= len;
    },

    rangeLength: function(val, min, max) {
      var len = String(val).length;
      return len >= min && len <= max;
    }
  };

  // ==================== 全局唯一 ID ====================

  var _uidCounter = 0;

  Utils.uid = function(prefix) {
    prefix = prefix || '';
    return prefix + Date.now().toString(36) + '_' + (++_uidCounter).toString(36) + '_' + Math.random().toString(36).slice(2, 8);
  };

  Utils.guid = function() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
      var r = Math.random() * 16 | 0;
      var v = c === 'x' ? r : (r & 0x3 | 0x8);
      return v.toString(16);
    });
  };

  // ==================== 颜色 ====================

  Utils.color = {
    random: function() {
      return '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0');
    },

    hexToRgb: function(hex) {
      var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
      return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
      } : null;
    },

    hexToRgbStr: function(hex) {
      var rgb = Utils.color.hexToRgb(hex);
      return rgb ? 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')' : '';
    },

    rgbToHex: function(r, g, b) {
      return '#' + [r, g, b].map(function(x) {
        var hex = Number(x).toString(16);
        return hex.length === 1 ? '0' + hex : hex;
      }).join('');
    }
  };

  // ==================== 数学 ====================

  Utils.sum = function(arr) {
    return arr.reduce(function(s, n) { return s + n; }, 0);
  };

  Utils.avg = function(arr) {
    return arr.length ? Utils.sum(arr) / arr.length : 0;
  };

  Utils.max = function(arr) {
    return Math.max.apply(null, arr);
  };

  Utils.min = function(arr) {
    return Math.min.apply(null, arr);
  };

  Utils.degToRad = function(deg) {
    return deg * Math.PI / 180;
  };

  Utils.radToDeg = function(rad) {
    return rad * 180 / Math.PI;
  };

  // ==================== DOM ====================

  Utils.scrollToTop = function() {
    global.scrollTo({ top: 0, behavior: 'smooth' });
  };

  Utils.getScrollTop = function() {
    return document.documentElement.scrollTop || document.body.scrollTop;
  };

  Utils.copyToClipboard = function(text) {
    if (global.navigator && global.navigator.clipboard) {
      return navigator.clipboard.writeText(text);
    }
    var textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.style.position = 'fixed';
    textarea.style.opacity = '0';
    document.body.appendChild(textarea);
    textarea.select();
    document.execCommand('copy');
    document.body.removeChild(textarea);
    return Promise.resolve();
  };

  Utils.getWindowSize = function() {
    return {
      width: global.innerWidth || document.documentElement.clientWidth,
      height: global.innerHeight || document.documentElement.clientHeight
    };
  };

  // ==================== 异步相关 ====================

  Utils.sleep = function(ms) {
    return new Promise(function(resolve) {
      setTimeout(resolve, ms);
    });
  };

  Utils.retry = function(fn, times, delay) {
    times = times || 3;
    delay = delay || 1000;
    return function() {
      var args = arguments;
      var context = this;
      var attempts = 0;

      function tryFn() {
        try {
          var result = fn.apply(context, args);
          if (result && typeof result.then === 'function') {
            return result.then(function(res) { return res; }).catch(function(err) {
              attempts++;
              if (attempts >= times) return Promise.reject(err);
              return Utils.sleep(delay).then(tryFn);
            });
          }
          return result;
        } catch (err) {
          attempts++;
          if (attempts >= times) throw err;
          return new Promise(function(resolve) { setTimeout(resolve, delay); }).then(tryFn);
        }
      }

      return tryFn();
    };
  };

  // ==================== 版本号 ====================

  Utils.version = '2.0.0';

  return Utils;

}));
相关推荐
zyplayer-doc3 小时前
VuePress类静态文档站和动态知识库怎么选:两种技术路线的适用场景
javascript·人工智能·后端·安全·智能手机
leisoo80974 小时前
100GBA股股票数据怎么存ClickHouseRedisMySQLJSON完整对比
大数据·linux·服务器·开发语言·python
不会代码的小猴4 小时前
21. 泛型编程上
开发语言·c++·笔记·算法
一只旭宝5 小时前
细讲C加加【9】C++ std::function与std::bind详解|仿函数、绑定器、类成员绑定、占位符、成员偏移指针
开发语言·c++·算法
coder!mq7 小时前
说几个常见的语法糖?
java·开发语言·算法
gugucoding7 小时前
38. 【Java】Stream API(下):高级操作与性能
java·开发语言
Lhan.zzZ7 小时前
在 Visual Studio 2022 中打造可扩展的动态链接库模块:从零搭建到原理解析
开发语言·c++·visual studio
心平气和量大福大8 小时前
android-实例-蒲公英-更新与安装-5-签名与生成APK
android·java·开发语言
码哥DFS8 小时前
构造函数、实例对象、对象原型 ----三者关系
开发语言·javascript·原型模式