PageSourceSearch

https://zhihuibei.pages.dev/js/utils.js

js zhihuibei.pages.dev collected 2026-10-03 06:02:00 UTC 7,347 bytes, 258 lines download raw bytes

1/**
2 * 工具函数模块
3 * 提供安全处理和通用工具函数
4 */
5
6'use strict';
7
8/**
9 * HTML 特殊字符转义
10 * 防止 XSS 攻击,将特殊字符转换为 HTML 实体
11 * @param {string} text - 需要转义的文本
12 * @returns {string} - 转义后的安全文本
13 */
14function escapeHtml(text) {
15    if (text === null || text === undefined) {
16        return '';
17    }
18    var str = String(text);
19    var div = document.createElement('div');
20    div.textContent = str;
21    return div.innerHTML;
22}
23
24/**
25 * 创建安全的 DOM 元素
26 * 避免使用 innerHTML,使用 createElement 创建元素
27 * @param {string} tag - HTML 标签名
28 * @param {Object} attributes - 元素属性对象
29 * @param {string} textContent - 文本内容(会自动转义)
30 * @returns {HTMLElement} - 创建的元素
31 */
32function createSafeElement(tag, attributes, textContent) {
33    var element = document.createElement(tag);
34    
35    // 设置属性
36    for (var key in attributes) {
37        if (attributes.hasOwnProperty(key)) {
38            var value = attributes[key];
39            if (key === 'className') {
40                element.className = value;
41            } else if (key === 'style' && typeof value === 'object') {
42                for (var styleKey in value) {
43                    if (value.hasOwnProperty(styleKey)) {
44                        element.style[styleKey] = value[styleKey];
45                    }
46                }
47            } else if (key.startsWith('on') && typeof value === 'function') {
48                // 事件处理器
49                var eventName = key.slice(2).toLowerCase();
50                element.addEventListener(eventName, value);
51            } else if (key === 'data' && typeof value === 'object') {
52                // data-* 属性
53                for (var dataKey in value) {
54                    if (value.hasOwnProperty(dataKey)) {
55                        element.dataset[dataKey] = value[dataKey];
56                    }
57                }
58            } else {
59                element.setAttribute(key, value);
60            }
61        }
62    }
63    
64    // 设置文本内容(自动转义)
65    if (textContent) {
66        element.textContent = textContent;
67    }
68    
69    return element;
70}
71
72/**
73 * 验证单词 ID 格式
74 * 确保 ID 符合 ebv2_数字 的格式
75 * @param {string} id - 单词 ID
76 * @returns {boolean} - 是否有效
77 */
78function isValidWordId(id) {
79    if (typeof id !== 'string') {
80        return false;
81    }
82    return /^ebv2_\d+$/.test(id);
83}
84
85/**
86 * 验证用户进度数据
87 * @param {Object} progress - 用户进度对象
88 * @returns {boolean} - 是否有效
89 */
90function isValidProgress(progress) {
91    if (!progress || typeof progress !== 'object') {
92        return false;
93    }
94    
95    // 检查 level 是否为 0-5 的整数
96    if (!Number.isInteger(progress.level) || progress.level < 0 || progress.level > 5) {
97        return false;
98    }
99    
100    // 检查 nextReview 是否为数字
101    if (typeof progress.nextReview !== 'number') {
102        return false;
103    }
104    
105    return true;
106}
107
108/**
109 * 使用加密安全的随机数生成器
110 * 替代不安全的 Math.random()
111 * @param {number} max - 最大值(不包含)
112 * @returns {number} - 0 到 max-1 之间的随机整数
113 */
114function getSecureRandomInt(max) {
115    if (max <= 0 || !Number.isInteger(max)) {
116        throw new Error('max must be a positive integer');
117    }
118    
119    var array = new Uint32Array(1);
120    crypto.getRandomValues(array);
121    return array[0] % max;
122}
123
124/**
125 * 洗牌算法(Fisher-Yates)
126 * 使用加密安全的随机数
127 * @param {Array} array - 需要洗牌的数组
128 * @returns {Array} - 洗牌后的新数组(不修改原数组)
129 */
130function shuffleArray(array) {
131    if (!Array.isArray(array)) {
132        throw new Error('Input must be an array');
133    }
134    
135    // 创建副本(避免使用展开运算符)
136    var result = array.slice();
137    for (var i = result.length - 1; i > 0; i--) {
138        var j = getSecureRandomInt(i + 1);
139        // 交换(避免使用解构赋值)
140        var temp = result[i];
141        result[i] = result[j];
142        result[j] = temp;
143    }
144    return result;
145}
146
147/**
148 * 防抖函数
149 * @param {Function} func - 需要防抖的函数
150 * @param {number} wait - 等待时间(毫秒)
151 * @returns {Function} - 防抖后的函数
152 */
153function debounce(func, wait) {
154    if (wait === undefined) wait = 300;
155    var timeout;
156    return function executedFunction() {
157        var args = Array.prototype.slice.call(arguments);
158        var later = function() {
159            clearTimeout(timeout);
160            func.apply(this, args);
161        };
162        clearTimeout(timeout);
163        timeout = setTimeout(later, wait);
164    };
165}
166
167/**
168 * 节流函数
169 * @param {Function} func - 需要节流的函数
170 * @param {number} limit - 限制时间(毫秒)
171 * @returns {Function} - 节流后的函数
172 */
173function throttle(func, limit) {
174    if (limit === undefined) limit = 300;
175    var inThrottle;
176    return function executedFunction() {
177        var args = Array.prototype.slice.call(arguments);
178        if (!inThrottle) {
179            func.apply(this, args);
180            inThrottle = true;
181            setTimeout(function() { inThrottle = false; }, limit);
182        }
183    };
184}
185
186/**
187 * 安全地解析 JSON
188 * 避免解析错误导致程序崩溃
189 * @param {string} jsonString - JSON 字符串
190 * @param {*} defaultValue - 解析失败时的默认值
191 * @returns {*} - 解析结果或默认值
192 */
193function safeJsonParse(jsonString, defaultValue) {
194    if (defaultValue === undefined) defaultValue = null;
195    try {
196        return JSON.parse(jsonString);
197    } catch (e) {
198        console.warn('[Utils] JSON parse error:', e.message);
199        return defaultValue;
200    }
201}
202
203/**
204 * 安全地序列化为 JSON
205 * 避免循环引用导致错误
206 * @param {*} value - 需要序列化的值
207 * @param {string} defaultValue - 序列化失败时的默认值
208 * @returns {string} - JSON 字符串或默认值
209 */
210function safeJsonStringify(value, defaultValue) {
211    if (defaultValue === undefined) defaultValue = '{}';
212    try {
213        return JSON.stringify(value);
214    } catch (e) {
215        console.warn('[Utils] JSON stringify error:', e.message);
216        return defaultValue;
217    }
218}
219
220/**
221 * 格式化日期
222 * @param {number} timestamp - 时间戳
223 * @returns {string} - 格式化后的日期字符串
224 */
225function formatDate(timestamp) {
226    if (!timestamp) return '-';
227    var date = new Date(timestamp);
228    return date.toLocaleDateString('zh-CN');
229}
230
231/**
232 * 格式化时间
233 * @param {number} timestamp - 时间戳
234 * @returns {string} - 格式化后的时间字符串
235 */
236function formatTime(timestamp) {
237    if (!timestamp) return '-';
238    var date = new Date(timestamp);
239    return date.toLocaleString('zh-CN');
240}
241
242// 导出模块
243if (typeof module !== 'undefined' && module.exports) {
244    module.exports = {
245        escapeHtml: escapeHtml,
246        createSafeElement: createSafeElement,
247        isValidWordId: isValidWordId,
248        isValidProgress: isValidProgress,
249        getSecureRandomInt: getSecureRandomInt,
250        shuffleArray: shuffleArray,
251        debounce: debounce,
252        throttle: throttle,
253        safeJsonParse: safeJsonParse,
254        safeJsonStringify: safeJsonStringify,
255        formatDate: formatDate,
256        formatTime: formatTime
257    };
258}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.