11 個你可以用原生 JavaScript 解決的問題,無需任何JS庫

當(dāng)原生 JavaScript 已擁有你所需的一切時,請停止尋找 JavaScript 庫。
過去十幾年,JavaScript 經(jīng)歷了翻天覆地的變化。過去需要 jQuery、Lodash 或 Moment.js 等繁重庫才能實(shí)現(xiàn)的功能,如今只需優(yōu)雅的原生解決方案即可實(shí)現(xiàn)。
然而,許多開發(fā)者仍然本能地依賴外部庫,這不僅給應(yīng)用程序增加了不必要的負(fù)擔(dān),也錯失了編寫更高性能、更易于維護(hù)的代碼的機(jī)會。
今天這篇文章,我將分享 11 個常見的 JavaScript編程問題,你可以完全使用原生 JavaScript 來解決。
每個解決方案都進(jìn)行了詳盡的解釋,旨在讓您有信心擺脫對庫的依賴。無論您是構(gòu)建初創(chuàng) MVP 還是優(yōu)化企業(yè)應(yīng)用程序,這些原生方法都能為您提供良好的服務(wù)。
讓我們深入探討每個 JavaScript 開發(fā)人員遇到的問題——以及讓您重新思考解決方案。
1. 不受 JSON 限制的深度克隆對象
問題 :你需要創(chuàng)建一個復(fù)雜對象的完整副本,包括嵌套對象、數(shù)組,甚至函數(shù)或日期。常見的 JSON.parse(JSON.stringify()) 技巧在處理函數(shù)、日期、未定義值和循環(huán)引用時會失敗。
重要性:深度克隆對于狀態(tài)管理、撤銷功能和防止函數(shù)式編程模式中的不必要變異至關(guān)重要。
原生解決方案 :
function deepClone(obj, seen = new WeakMap()) {
// 處理原語和 null
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 處理循環(huán)引用
if (seen.has(obj)) {
return seen.get(obj);
}
// 處理 Date 對象
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// 處理數(shù)組
if (Array.isArray(obj)) {
const arrCopy = [];
seen.set(obj, arrCopy);
obj.forEach((item, index) => {
arrCopy[index] = deepClone(item, seen);
});
return arrCopy;
}
// 處理正則表達(dá)式
if (obj intentof RegExp) {
return new RegExp(obj.source, obj.flags);
}
// 處理對象
const objCopy = {};
seen.set(obj, objCopy);
Object.keys(obj).forEach(key => {
objCopy[key] = deepClone(obj[key], seen);
});
return objCopy;
}
// 使用示例
const original = {
name: 'John',
birthDate: new Date('1990-01-01'),
hobbies: ['reading', 'coding'],
address: {
street: '123 Main St',
city: 'Anytown'
},
greet: function() { return `Hello, I'm ${this.name}`; }
};
const cloned = deepClone(original);
cloned.address.city = 'New City'; // 原始地址保持不變有效原因:這個解決方案使用 WeakMap 來跟蹤已經(jīng)克隆的對象,防止循環(huán)引用導(dǎo)致的無限循環(huán)。
它能正確處理所有 JavaScript 數(shù)據(jù)類型,包括函數(shù)和日期,比基于 JSON 的方法更健壯。
2. 去抖動函數(shù)調(diào)用以提高性能
問題 :你需要限制函數(shù)執(zhí)行的頻率,通常用于搜索輸入、滾動事件或調(diào)整大小處理器。如果沒有防抖,這些函數(shù)會不斷觸發(fā),導(dǎo)致性能問題和不必要的 API 調(diào)用。
重要性:一個沒有使用防抖功能的搜索輸入,在用戶輸入“JavaScript”時可能會觸發(fā)數(shù)百次 API 請求。防抖確保函數(shù)只在用戶停止輸入后執(zhí)行,從而顯著提升性能和用戶體驗(yàn)。
原生解決方案 :
function debounce(func, delay, immediate = false) {
let timeoutId;
return function debounced(...args) {
const callNow = immediate && !timeoutId;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
timeoutId = null;
if (!immediate) {
func.apply(this, args);
}
}, delay);
if (callNow) {
func.apply(this, args);
}
};
}
// 使用示例
const expensiveSearch = debounce((query) => {
console.log(`正在搜索:${query}`);
// 此處調(diào)用 API
}, 300);
const saveDocument = debounce((document) => {
console.log('正在保存文檔...');
// 在此處保存邏輯
}, 1000, true); // 立即執(zhí)行
// 事件監(jiān)聽器
document.getElementById('search').addEventListener('input', (e) => {
expensiveSearch(e.target.value);
});工作原理:防抖函數(shù)在每次調(diào)用時都會重置計(jì)時器,只有在指定的延遲時間過去且沒有新的調(diào)用時才會執(zhí)行。`immediate`參數(shù)允許執(zhí)行領(lǐng)先邊的操作,適用于需要立即執(zhí)行但不應(yīng)重復(fù)的操作,如保存。
3. 節(jié)流功能,確保性能平穩(wěn)
問題 :與防抖不同,有時你需要確保函數(shù)以固定間隔執(zhí)行,而不管它被調(diào)用的頻率如何。這對于滾動動畫、進(jìn)度跟蹤或 API 調(diào)用速率限制至關(guān)重要。
重要性 :沒有節(jié)流的滾動事件監(jiān)聽器每秒可能觸發(fā)數(shù)百次,導(dǎo)致動畫卡頓和性能差。節(jié)流功能確保以受控的速率平滑、一致地執(zhí)行。
原生解決方案 :
function throttle(func, limit) {
let inThrottle;
let lastFunc;
let lastRan;
return function throttled(...args) {
if (!inThrottle) {
func.apply(this, args);
lastRan = Date.now();
inThrottle = true;
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(() => {
if ((Date.now() - lastRan) >= limit) {
func.apply(this, args);
lastRan = Date.now();
}
}, limit - (Date.now() - lastRan));
}
};
}
// 使用示例
const updateScrollPosition = throttle((e) => {
const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
document.getElementById('progress').style.width = `${scrollPercent}%`;
}, 16); // ~60fps
const trackAnalytics = throttle((event) => {
console.log('Tracking event:', event);
// Analytics API 調(diào)用
}, 1000);
window.addEventListener('scroll', updateScrollPosition);工作原理 :這個節(jié)流實(shí)現(xiàn)確保函數(shù)在第一次調(diào)用時立即執(zhí)行,然后保持一致的執(zhí)行速率。它比簡單的基于計(jì)時器的方法更復(fù)雜,確保如果需要,最后一次調(diào)用總會被執(zhí)行。
4. 解析無依賴關(guān)系的 URL 參數(shù)
問題:您需要提取并處理各種格式的 URL 參數(shù)。內(nèi)置功能URLSearchParams對于現(xiàn)代瀏覽器來說已經(jīng)足夠強(qiáng)大,但您通常需要更靈活的解析、驗(yàn)證和類型轉(zhuǎn)換功能。
重要性:每個 Web 應(yīng)用程序都需要處理用于路由、過濾、分頁和狀態(tài)管理的 URL 參數(shù)。強(qiáng)大的解析解決方案可以消除錯誤并提供更好的開發(fā)者體驗(yàn)。
原生解決方案 :
class URLParamsParser {
constructor(url = window.location.href) {
this.url = new URL(url);
this.params = new URLSearchParams(this.url.search);
}
get(key, defaultValue = null) {
return this.params.get(key) || defaultValue;
}
getNumber(key, defaultValue = 0) {
const value = this.params.get(key);
const parsed = parseFloat(value);
return isNaN(parsed) ? defaultValue : parsed;
}
getBoolean(key, defaultValue = false) {
const value = this.params.get(key);
if (value === null) return defaultValue;
return value.toLowerCase() === 'true' || value === '1';
}
getArray(key, separator = ',', defaultValue = []) {
const value = this.params.get(key);
return value ? value.split(separator).map(item => item.trim()) : defaultValue;
}
getObject() {
const obj = {};
for (const [key, value] of this.params.entries()) {
obj[key] = value;
}
return obj;
}
set(key, value) {
this.params.set(key, value);
return this;
}
remove(key) {
this.params.delete(key);
return this;
}
toString() {
return this.params.toString();
}
updateURL(pushState = true) {
const newURL = `${this.url.pathname}?${this.toString()}`;
if (pushState) {
history.pushState({}, '', newURL);
} else {
history.replaceState({}, '', newURL);
}
}
}
// Usage examples
const parser = new URLParamsParser('https://example.com?page=2&active=true&tags=js,react,node');
console.log(parser.getNumber('page')); // 2
console.log(parser.getBoolean('active')); // true
console.log(parser.getArray('tags')); // ['js', 'react', 'node']
// Update URL
parser.set('page', 3).remove('active').updateURL();工作原理:這個解決方案封裝了原生的 URLSearchParams API,提供了便捷的類型轉(zhuǎn)換方法和可鏈?zhǔn)讲僮鳌K芴幚砣笔?shù)等邊緣情況,同時提供合理的默認(rèn)值,并保持與復(fù)雜參數(shù)結(jié)構(gòu)的靈活性。
5. 日期格式化與操作變得簡單
問題 :在 JavaScript 中處理日期傳統(tǒng)上很痛苦,導(dǎo)致許多開發(fā)者會使用 Moment.js 或 date-fns 等庫。你需要可靠的日期格式化、解析和操作,而不需要額外負(fù)擔(dān)。
重要性 :日期處理在 Web 應(yīng)用中無處不在——從顯示時間戳到計(jì)算時間差。原生解決方案性能更優(yōu),且能顯著減小包體積。
原生解決方案 :
class DateHelper {
constructor(date = new Date()) {
this.date = new Date(date);
}
static create(date) {
return new DateHelper(date);
}
format(options = {}) {
const defaults = {
year: 'numeric',
month: 'long',
day: 'numeric'
};
return this.date.toLocaleDateString('en-US', { ...defaults, ...options });
}
formatTime(options = {}) {
const defaults = {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
return this.date.toLocaleTimeString('en-US', { ...defaults, ...options });
}
formatRelative() {
const now = new Date();
const diffMs = now - this.date;
const diffSecs = Math.floor(diffMs / 1000);
const diffMins = Math.floor(diffSecs / 60);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffSecs < 60) return 'just now';
if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
return this.format();
}
addDays(days) {
const newDate = new Date(this.date);
newDate.setDate(newDate.getDate() + days);
return new DateHelper(newDate);
}
addMonths(months) {
const newDate = new Date(this.date);
newDate.setMonth(newDate.getMonth() + months);
return new DateHelper(newDate);
}
startOfDay() {
const newDate = new Date(this.date);
newDate.setHours(0, 0, 0, 0);
return new DateHelper(newDate);
}
endOfDay() {
const newDate = new Date(this.date);
newDate.setHours(23, 59, 59, 999);
return new DateHelper(newDate);
}
isSameDay(otherDate) {
const other = new Date(otherDate);
return this.date.toDateString() === other.toDateString();
}
isToday() {
return this.isSameDay(new Date());
}
toISO() {
return this.date.toISOString();
}
valueOf() {
return this.date.getTime();
}
}
// Usage examples
const date = DateHelper.create('2024-01-15');
console.log(date.format()); // "January 15, 2024"
console.log(date.formatTime()); // "12:00:00 AM"
console.log(date.formatRelative()); // "2 days ago"
const nextWeek = date.addDays(7);
console.log(nextWeek.format()); // "January 22, 2024"
console.log(date.isToday()); // false
console.log(date.startOfDay().toISO()); // "2024-01-15T00:00:00.000Z"工作原理 :該解決方案通過 Intl.DateTimeFormat API 在底層實(shí)現(xiàn),借助 toLocaleDateString 和 toLocaleTimeString,提供靈活的格式化選項(xiàng)。鏈?zhǔn)?API 使日期操作直觀,同時保持原始日期不變。
6. 基于真正隨機(jī)性的數(shù)組改組
問題 :你需要隨機(jī)打亂數(shù)組,用于游戲、隨機(jī)化內(nèi)容或 A/B 測試。使用 Math.random() 排序的樸素方法無法提供真正的隨機(jī)性,且可能存在偏差。
重要性 :正確的洗牌算法能確保公平的隨機(jī)化,這對于游戲、調(diào)查以及任何需要真正隨機(jī)性的應(yīng)用至關(guān)重要。不恰當(dāng)?shù)南磁瓶赡芤肫睿瑥亩绊懹脩趔w驗(yàn)或業(yè)務(wù)邏輯。
原生解決方案 :
class ArrayUtils {
// Fisher-Yates 隨機(jī)排序算法 - 無偏且高效
static shuffle(array) {
const shuffled = [...array]; // 創(chuàng)建副本以避免數(shù)據(jù)突變
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled;
}
// 加權(quán)隨機(jī)排序 - 每個元素都有一個權(quán)重來決定選擇概率
static weightedShuffle(items, weights) {
if (items.length !== weights.length) {
throw new Error('元素和權(quán)重?cái)?shù)組的長度必須相同');
}
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
const result = [];
const workingItems = [...items];
const workingWeights = [...weights];
while (workingItems.length > 0) {
const random = Math.random() * workingWeights.reduce((sum, w) => sum + w, 0);
let currentWeight = 0;
for (let i = 0; i < workingItems.length; i++) {
currentWeight += workingWeights[i];
if (random <= currentWeight) {
result.push(workingItems[i]);
workingItems.splice(i, 1);
workingWeights.splice(i, 1);
break;
}
}
}
return result;
}
// 不重復(fù)地隨機(jī)抽取 n 個元素
static sample(array, n) {
if (n >= array.length) return this.shuffle(array);
const shuffled = this.shuffle(array);
return shuffled.slice(0, n);
}
// 將數(shù)組分塊為指定大小的較小數(shù)組
static chunk(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
// 按鍵分組函數(shù)
static groupBy(array, keyFn) {
return array.reduce((groups, item) => {
const key = keyFn(item);
if (!groups[key]) groups[key] = [];
groups[key].push(item);
return groups;
}, {});
}
}
// 使用示例
const cards = ['A?', 'K?', 'Q?', 'J?', '10?', '9?', '8?', '7?'];
const shuffledCards = ArrayUtils.shuffle(cards);
console.log(shuffledCards); // 每次隨機(jī)排序
// 加權(quán)打亂,得到不同的概率
const prizes = ['Common', 'Rare', 'Epic', 'Legendary'];
const weights = [50, 30, 15, 5]; // 普通卡的概率是傳奇卡的 10 倍
const weightedResult = ArrayUtils.weightedShuffle(prizes, weights);
// 隨機(jī)物品示例
const randomPlayers = ArrayUtils.sample(['Alice', 'Bob', 'Charlie', 'David', 'Eve'], 3);
console.log(randomPlayers); // 3 個隨機(jī)玩家
// 按年齡段對用戶分組
const users = [
{ name: 'John', age: 25 },
{ name: 'Jane', age: 35 },
{ name: 'Bob', age: 28 }
];
const groupedByDecade = ArrayUtils.groupBy(users, user =>
Math.floor(user.age / 10) * 10 + 's'
);工作原理 :Fisher-Yates 洗牌算法在數(shù)學(xué)上被證明能產(chǎn)生無偏結(jié)果,為每種排列提供相等的概率。該類還提供了額外的實(shí)用方法來補(bǔ)充洗牌功能,使其成為數(shù)組操作需求的全面解決方案。
7. 動態(tài)內(nèi)容的模板字符串引擎
問題 :你需要從模板生成動態(tài)內(nèi)容,類似于 Handlebars 或 Mustache,但無需完整模板庫的開銷。這在電子郵件模板、HTML 生成或配置文件中很常見。
重要性 :模板引擎對于分離邏輯與呈現(xiàn)、創(chuàng)建可復(fù)用的內(nèi)容結(jié)構(gòu)至關(guān)重要。一個輕量級的原生解決方案可以在無需外部依賴的情況下處理大多數(shù)用例。
原生解決方案 :
class TemplateEngine {
constructor(options = {}) {
this.options = {
openTag: '{{',
closeTag: '}}',
helpers: {},
...options
};
this.registerHelper('if', this.ifHelper.bind(this));
this.registerHelper('each', this.eachHelper.bind(this));
this.registerHelper('unless', this.unlessHelper.bind(this));
}
registerHelper(name, fn) {
this.options.helpers[name] = fn;
return this;
}
compile(template) {
return (data = {}) => this.render(template, data);
}
render(template, data = {}) {
const { openTag, closeTag } = this.options;
const regex = new RegExp(`${this.escapeRegex(openTag)}\\s*([^}]+)\\s*${this.escapeRegex(closeTag)}`, 'g');
return template.replace(regex, (match, expression) => {
return this.evaluateExpression(expression.trim(), data);
});
}
evaluateExpression(expression, data) {
// Handle helpers
if (expression.includes(' ')) {
const parts = expression.split(' ');
const helperName = parts[0];
if (this.options.helpers[helperName]) {
const args = parts.slice(1).map(arg => this.getValue(arg, data));
return this.options.helpers[helperName](...args, data);
}
}
// Handle simple variable substitution
return this.getValue(expression, data) || '';
}
getValue(path, data) {
if (path.startsWith('"') && path.endsWith('"')) {
return path.slice(1, -1); // String literal
}
if (!isNaN(path)) {
return Number(path); // Number literal
}
return path.split('.').reduce((obj, key) => obj && obj[key], data);
}
escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
// Built-in helpers
ifHelper(condition, data) {
return condition ? '<!-- if-true -->' : '<!-- if-false -->';
}
eachHelper(array, data) {
if (!Array.isArray(array)) return '';
return '<!-- each-placeholder -->';
}
unlessHelper(condition, data) {
return !condition ? '<!-- unless-true -->' : '<!-- unless-false -->';
}
}
// Enhanced version with block helpers
class AdvancedTemplateEngine extends TemplateEngine {
render(template, data = {}) {
// First pass: handle block helpers
template = this.handleBlockHelpers(template, data);
// Second pass: handle regular expressions
return super.render(template, data);
}
handleBlockHelpers(template, data) {
const blockRegex = /{{#(\w+)\s+([^}]+)}}([\s\S]*?){{\/\1}}/g;
return template.replace(blockRegex, (match, helperName, args, content) => {
if (helperName === 'each') {
const arrayPath = args.trim();
const array = this.getValue(arrayPath, data);
if (!Array.isArray(array)) return '';
return array.map((item, index) => {
const itemData = { ...data, this: item, '@index': index };
return this.render(content, itemData);
}).join('');
}
if (helperName === 'if') {
const condition = this.getValue(args.trim(), data);
return condition ? this.render(content, data) : '';
}
if (helperName === 'unless') {
const condition = this.getValue(args.trim(), data);
return !condition ? this.render(content, data) : '';
}
return match;
});
}
}
// Usage examples
const engine = new AdvancedTemplateEngine();
// Register custom helpers
engine.registerHelper('uppercase', (str) => str.toUpperCase());
engine.registerHelper('currency', (amount) => `$${amount.toFixed(2)}`);
const template = `
<h1>Welcome, {{ name }}!</h1>
<p>Your balance is {{ currency balance }}</p>
{{#if isVip}}
<div class="vip-section">
<h2>VIP Benefits</h2>
<ul>
{{#each benefits}}
<li>{{ uppercase this }}</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#unless isActive}}
<div class="inactive-warning">
Please activate your account.
</div>
{{/unless}}
`;
const data = {
name: 'John Doe',
balance: 1234.56,
isVip: true,
isActive: false,
benefits: ['priority support', 'exclusive offers', 'early access']
};
const compiled = engine.compile(template);
const result = compiled(data);
console.log(result);
// Outputs formatted HTML with all variables replaced and conditionals processed工作原理:此模板引擎使用正則表達(dá)式解析模板語法,并以遞歸方式求值表達(dá)式。塊輔助系統(tǒng)支持循環(huán)和條件等復(fù)雜邏輯,同時輔助系統(tǒng)還提供了自定義格式化函數(shù)的可擴(kuò)展性。
8. 具有過期時間和類型安全的本地存儲
問題 :localStorage 僅存儲字符串且沒有內(nèi)置的過期機(jī)制。你需要自動序列化/反序列化、過期日期和類型安全以實(shí)現(xiàn)更好的數(shù)據(jù)管理。
重要性 :大多數(shù)應(yīng)用程序需要具有智能清理和類型保留的持久客戶端存儲。如果沒有合適的抽象,你最終會在整個應(yīng)用程序中遇到過時數(shù)據(jù)和類型轉(zhuǎn)換錯誤。
原生解決方案 :
class StorageManager {
constructor(prefix = 'app_') {
this.prefix = prefix;
this.isSupported = this.checkSupport();
}
checkSupport() {
try {
const test = '__storage_test__';
localStorage.setItem(test, test);
localStorage.removeItem(test);
return true;
} catch (e) {
return false;
}
}
set(key, value, expirationMinutes = null) {
if (!this.isSupported) return false;
const item = {
value,
type: typeof value,
timestamp: Date.now(),
expiration: expirationMinutes ? Date.now() + (expirationMinutes * 60 * 1000) : null
};
try {
localStorage.setItem(this.prefix + key, JSON.stringify(item));
return true;
} catch (e) {
console.warn('Storage quota exceeded:', e);
this.cleanup();
try {
localStorage.setItem(this.prefix + key, JSON.stringify(item));
return true;
} catch (e2) {
return false;
}
}
}
get(key, defaultValue = null) {
if (!this.isSupported) return defaultValue;
try {
const itemStr = localStorage.getItem(this.prefix + key);
if (!itemStr) return defaultValue;
const item = JSON.parse(itemStr);
// Check expiration
if (item.expiration && Date.now() > item.expiration) {
this.remove(key);
return defaultValue;
}
return item.value;
} catch (e) {
console.warn('Error parsing stored item:', e);
this.remove(key);
return defaultValue;
}
}
remove(key) {
if (!this.isSupported) return false;
localStorage.removeItem(this.prefix + key);
return true;
}
has(key) {
return this.get(key) !== null;
}
clear() {
if (!this.isSupported) return false;
Object.keys(localStorage)
.filter(key => key.startsWith(this.prefix))
.forEach(key => localStorage.removeItem(key));
return true;
}
cleanup() {
if (!this.isSupported) return 0;
let cleaned = 0;
const keys = Object.keys(localStorage)
.filter(key => key.startsWith(this.prefix));
keys.forEach(key => {
try {
const itemStr = localStorage.getItem(key);
const item = JSON.parse(itemStr);
if (item.expiration && Date.now() > item.expiration) {
localStorage.removeItem(key);
cleaned++;
}
} catch (e) {
// Remove corrupted items
localStorage.removeItem(key);
cleaned++;
}
});
return cleaned;
}
size() {
if (!this.isSupported) return 0;
return Object.keys(localStorage)
.filter(key => key.startsWith(this.prefix))
.length;
}
usage() {
if (!this.isSupported) return { used: 0, total: 0 };
let used = 0;
Object.keys(localStorage)
.filter(key => key.startsWith(this.prefix))
.forEach(key => {
used += key.length + localStorage.getItem(key).length;
});
// Rough estimate of localStorage limit (usually 5-10MB)
return {
used,
usedFormatted: this.formatBytes(used),
estimated: this.formatBytes(used * 2) // Very rough estimate
};
}
formatBytes(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// Batch operations
setMultiple(items, expirationMinutes = null) {
const results = {};
Object.entries(items).forEach(([key, value]) => {
results[key] = this.set(key, value, expirationMinutes);
});
return results;
}
getMultiple(keys) {
const results = {};
keys.forEach(key => {
results[key] = this.get(key);
});
return results;
}
}
// Usage examples
const storage = new StorageManager('myApp_');
// Set items with different types
storage.set('user', { name: 'John', id: 123 }, 60); // Expires in 1 hour
storage.set('settings', { theme: 'dark', notifications: true });
storage.set('lastLogin', new Date().toISOString(), 24 * 60); // Expires in 1 day
// Get items (automatically typed)
const user = storage.get('user');
const theme = storage.get('settings').theme;
const nonExistent = storage.get('missing', 'default value');
// Batch operations
const multipleItems = storage.getMultiple(['user', 'settings', 'lastLogin']);
storage.setMultiple({
'temp1': 'value1',
'temp2': 'value2'
}, 30); // All expire in 30 minutes
// Maintenance
console.log('Storage size:', storage.size());
console.log('Storage usage:', storage.usage());
console.log('Cleaned items:', storage.cleanup());
// Auto-cleanup on page load
window.addEventListener('load', () => {
storage.cleanup();
});工作原理 :這個解決方案將 localStorage 包裝在智能序列化、自動類型保留和過期處理中。清理機(jī)制防止存儲膨脹,而批量操作提高了多個相關(guān)操作的性能。
9. 具有指數(shù)退避的健壯異步重試邏輯
問題 :網(wǎng)絡(luò)請求和異步操作可能由于臨時問題而失敗。你需要具有指數(shù)退避、抖動和可配置條件的智能重試邏輯,以優(yōu)雅地處理瞬態(tài)故障。
重要性 :強(qiáng)大的錯誤處理將生產(chǎn)級應(yīng)用與脆弱的原型區(qū)分開來。合理的重試邏輯能提升用戶體驗(yàn)和系統(tǒng)可靠性,尤其在分布式系統(tǒng)或不穩(wěn)定的網(wǎng)絡(luò)環(huán)境下。
原生解決方案 :
class RetryManager {
constructor(options = {}) {
this.options = {
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 30000,
backoffFactor: 2,
jitter: true,
retryCondition: (error) => true,
onRetry: (attempt, error, delay) => {},
...options
};
}
async execute(asyncFunction, ...args) {
let lastError;
for (let attempt = 1; attempt <= this.options.maxAttempts; attempt++) {
try {
const result = await asyncFunction(...args);
return result;
} catch (error) {
lastError = error;
// Check if we should retry this error
if (!this.options.retryCondition(error)) {
throw error;
}
// Don't delay after the last attempt
if (attempt === this.options.maxAttempts) {
break;
}
const delay = this.calculateDelay(attempt);
this.options.onRetry(attempt, error, delay);
await this.sleep(delay);
}
}
throw lastError;
}
calculateDelay(attempt) {
const exponentialDelay = this.options.baseDelay * Math.pow(this.options.backoffFactor, attempt - 1);
const cappedDelay = Math.min(exponentialDelay, this.options.maxDelay);
if (this.options.jitter) {
// Add random jitter (±25% of the delay)
const jitterRange = cappedDelay * 0.25;
const jitter = (Math.random() - 0.5) * 2 * jitterRange;
return Math.max(0, cappedDelay + jitter);
}
return cappedDelay;
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Convenience method for HTTP requests
static async retryFetch(url, options = {}, retryOptions = {}) {
const retryManager = new RetryManager({
retryCondition: (error) => {
// Retry on network errors or 5xx server errors
if (error.name === 'TypeError') return true; // Network error
if (error.status >= 500) return true; // Server error
if (error.status === 429) return true; // Too many requests
return false;
},
...retryOptions
});
return retryManager.execute(async () => {
const response = await fetch(url, options);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
throw error;
}
return response;
});
}
}
// Specialized retry managers for common scenarios
class DatabaseRetryManager extends RetryManager {
constructor(options = {}) {
super({
maxAttempts: 5,
baseDelay: 500,
retryCondition: (error) => {
// Retry on connection errors, timeouts, and deadlocks
const retryableErrors = ['ConnectionError', 'TimeoutError', 'DeadlockError'];
return retryableErrors.some(errorType => error.name.includes(errorType));
},
onRetry: (attempt, error, delay) => {
console.log(`Database operation failed (attempt ${attempt}), retrying in ${delay}ms:`, error.message);
},
...options
});
}
}
class APIRetryManager extends RetryManager {
constructor(options = {}) {
super({
maxAttempts: 3,
baseDelay: 1000,
maxDelay: 10000,
retryCondition: (error) => {
// Don't retry client errors (4xx), except rate limiting
if (error.status >= 400 && error.status < 500 && error.status !== 429) {
return false;
}
return true;
},
onRetry: (attempt, error, delay) => {
console.log(`API call failed (attempt ${attempt}), retrying in ${delay}ms:`, error.message);
},
...options
});
}
}
// Usage examples
// Basic retry for any async function
const retryManager = new RetryManager({
maxAttempts: 3,
baseDelay: 1000,
onRetry: (attempt, error, delay) => {
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms`);
}
});
async function unreliableFunction() {
if (Math.random() < 0.7) {
throw new Error('Random failure');
}
return 'Success!';
}
try {
const result = await retryManager.execute(unreliableFunction);
console.log(result);
} catch (error) {
console.log('All retry attempts failed:', error.message);
}
// HTTP requests with retry
async function fetchWithRetry() {
try {
const response = await RetryManager.retryFetch('https://api.example.com/data', {
method: 'GET',
headers: { 'Authorization': 'Bearer token' }
}, {
maxAttempts: 5,
baseDelay: 2000
});
const data = await response.json();
return data;
} catch (error) {
console.error('Request failed after all retries:', error);
throw error;
}
}
// Database operations with specialized retry logic
const dbRetry = new DatabaseRetryManager();
async function saveUser(userData) {
return dbRetry.execute(async () => {
// Simulate database operation
if (Math.random() < 0.3) {
const error = new Error('Database connection failed');
error.name = 'ConnectionError';
throw error;
}
return { id: 123, ...userData };
});
}
// API calls with rate limiting awareness
const apiRetry = new APIRetryManager({
maxAttempts: 5,
baseDelay: 2000,
onRetry: (attempt, error, delay) => {
if (error.status === 429) {
console.log(`Rate limited, waiting ${delay}ms before retry ${attempt}`);
}
}
});
async function callAPI(endpoint, data) {
return apiRetry.execute(async () => {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) {
const error = new Error(`API Error: ${response.statusText}`);
error.status = response.status;
throw error;
}
return response.json();
});
}工作原理 :該解決方案實(shí)現(xiàn)了行業(yè)標(biāo)準(zhǔn)的重試模式,包括指數(shù)退避、抖動以防止雷鳴群集問題,以及可配置的重試條件。專門的處理器能以適當(dāng)?shù)哪J(rèn)值處理數(shù)據(jù)庫操作和 API 調(diào)用等常見場景。
10. 全面的表單驗(yàn)證引擎
問題 :表單驗(yàn)證對用戶體驗(yàn)和數(shù)據(jù)完整性至關(guān)重要,但若不依賴庫來創(chuàng)建靈活、可重用的驗(yàn)證系統(tǒng),則需要精心設(shè)計(jì)以處理不同字段類型、自定義規(guī)則和實(shí)時反饋。
重要性 :糟糕的表單驗(yàn)證會導(dǎo)致用戶不滿,并產(chǎn)生不良數(shù)據(jù)。一個強(qiáng)大的驗(yàn)證系統(tǒng)可以改善用戶體驗(yàn),減少服務(wù)器負(fù)載,并確保整個應(yīng)用程序中的數(shù)據(jù)質(zhì)量。
原生解決方案 :
class FormValidator {
constructor(options = {}) {
this.options = {
validateOnInput: true,
validateOnBlur: true,
showErrorsImmediately: false,
errorClass: 'error',
validClass: 'valid',
...options
};
this.validators = new Map();
this.customRules = new Map();
this.errors = new Map();
this.initializeBuiltInRules();
}
initializeBuiltInRules() {
// Built-in validation rules
this.addRule('required', (value) => {
if (Array.isArray(value)) return value.length > 0;
return value !== null && value !== undefined && String(value).trim() !== '';
}, 'This field is required');
this.addRule('email', (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return !value || emailRegex.test(value);
}, 'Please enter a valid email address');
this.addRule('min', (value, min) => {
if (typeof value === 'string') return value.length >= min;
if (typeof value === 'number') return value >= min;
return true;
}, 'Value is too short/small');
this.addRule('max', (value, max) => {
if (typeof value === 'string') return value.length <= max;
if (typeof value === 'number') return value <= max;
return true;
}, 'Value is too long/large');
this.addRule('pattern', (value, pattern) => {
if (!value) return true;
const regex = new RegExp(pattern);
return regex.test(value);
}, 'Invalid format');
this.addRule('numeric', (value) => {
return !value || /^\d+$/.test(value);
}, 'Only numbers are allowed');
this.addRule('alpha', (value) => {
return !value || /^[a-zA-Z]+$/.test(value);
}, 'Only letters are allowed');
this.addRule('alphanumeric', (value) => {
return !value || /^[a-zA-Z0-9]+$/.test(value);
}, 'Only letters and numbers are allowed');
this.addRule('url', (value) => {
if (!value) return true;
try {
new URL(value);
return true;
} catch {
return false;
}
}, 'Please enter a valid URL');
this.addRule('phone', (value) => {
if (!value) return true;
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/;
return phoneRegex.test(value.replace(/[\s\-\(\)]/g, ''));
}, 'Please enter a valid phone number');
}
addRule(name, validator, defaultMessage) {
this.customRules.set(name, { validator, defaultMessage });
return this;
}
validateField(element, rules) {
const value = this.getFieldValue(element);
const fieldErrors = [];
for (const rule of rules) {
const { name, params = [], message } = this.parseRule(rule);
const ruleConfig = this.customRules.get(name);
if (!ruleConfig) {
console.warn(`Unknown validation rule: ${name}`);
continue;
}
const isValid = ruleConfig.validator(value, ...params);
if (!isValid) {
fieldErrors.push(message || ruleConfig.defaultMessage);
break; // Stop at first error
}
}
const fieldName = element.name || element.id;
if (fieldErrors.length > 0) {
this.errors.set(fieldName, fieldErrors);
this.showFieldError(element, fieldErrors[0]);
} else {
this.errors.delete(fieldName);
this.showFieldSuccess(element);
}
return fieldErrors.length === 0;
}
validateForm(form) {
const elements = this.getFormElements(form);
let isValid = true;
elements.forEach(element => {
const rules = this.getElementRules(element);
if (rules.length > 0) {
const fieldValid = this.validateField(element, rules);
if (!fieldValid) isValid = false;
}
});
return isValid;
}
getFieldValue(element) {
switch (element.type) {
case 'checkbox':
return element.checked;
case 'radio':
const radioGroup = document.querySelectorAll(`input[name="${element.name}"]`);
const checked = Array.from(radioGroup).find(r => r.checked);
return checked ? checked.value : '';
case 'select-multiple':
return Array.from(element.selectedOptions).map(opt => opt.value);
case 'file':
return element.files;
default:
return element.value;
}
}
getFormElements(form) {
return Array.from(form.querySelectorAll('input, select, textarea'))
.filter(el => !el.disabled && el.type !== 'submit' && el.type !== 'button');
}
getElementRules(element) {
const rulesAttr = element.getAttribute('data-rules');
if (!rulesAttr) return [];
return rulesAttr.split('|').filter(rule => rule.trim());
}
parseRule(ruleString) {
const [name, ...paramsPart] = ruleString.split(':');
const params = paramsPart.length > 0 ? paramsPart[0].split(',') : [];
// Handle custom messages
let message = null;
const messageMatch = ruleString.match(/\[(.+)\]$/);
if (messageMatch)
{
message = messageMatch[1];
}
return {
name: name.trim(),
params: params.map(p => {
const trimmed = p.trim();
if (!isNaN(trimmed)) return Number(trimmed);
return trimmed;
}),
message
};
}
showFieldError(element, message) {
element.classList.remove(this.options.validClass);
element.classList.add(this.options.errorClass);
this.updateErrorMessage(element, message);
}
showFieldSuccess(element) {
element.classList.remove(this.options.errorClass);
element.classList.add(this.options.validClass);
this.clearErrorMessage(element);
}
updateErrorMessage(element, message) {
const errorId = `${element.id || element.name}-error`;
let errorElement = document.getElementById(errorId);
if (!errorElement) {
errorElement = document.createElement('div');
errorElement.id = errorId;
errorElement.className = 'validation-error';
element.parentNode.insertBefore(errorElement, element.nextSibling);
}
errorElement.textContent = message;
errorElement.style.display = 'block';
}
clearErrorMessage(element) {
const errorId = `${element.id || element.name}-error`;
const errorElement = document.getElementById(errorId);
if (errorElement) {
errorElement.style.display = 'none';
}
}
bindToForm(form) {
const elements = this.getFormElements(form);
elements.forEach(element => {
if (this.options.validateOnInput) {
element.addEventListener('input', () => {
if (this.options.showErrorsImmediately || this.errors.has(element.name || element.id)) {
const rules = this.getElementRules(element);
if (rules.length > 0) {
this.validateField(element, rules);
}
}
});
}
if (this.options.validateOnBlur) {
element.addEventListener('blur', () => {
const rules = this.getElementRules(element);
if (rules.length > 0) {
this.validateField(element, rules);
}
});
}
});
form.addEventListener('submit', (e) => {
e.preventDefault();
if (this.validateForm(form)) {
// Form is valid, proceed with submission
if (this.options.onSuccess) {
this.options.onSuccess(form, this.getFormData(form));
}
} else {
// Form has errors
if (this.options.onError) {
this.options.onError(form, this.errors);
}
}
});
return this;
}
getFormData(form) {
const formData = new FormData(form);
const data = {};
for (const [key, value] of formData.entries()) {
if (data[key]) {
if (Array.isArray(data[key])) {
data[key].push(value);
} else {
data[key] = [data[key], value];
}
} else {
data[key] = value;
}
}
return data;
}
getErrors() {
return Object.fromEntries(this.errors);
}
hasErrors() {
return this.errors.size > 0;
}
clearErrors() {
this.errors.clear();
document.querySelectorAll('.validation-error').forEach(el => {
el.style.display = 'none';
});
}
}
// Usage examples
const validator = new FormValidator({
validateOnInput: true,
validateOnBlur: true,
onSuccess: (form, data) => {
console.log('Form submitted successfully:', data);
},
onError: (form, errors) => {
console.log('Form has errors:', errors);
}
});
// Add custom validation rules
validator.addRule('strongPassword', (value) => {
if (!value) return true;
const hasUpper = /[A-Z]/.test(value);
const hasLower = /[a-z]/.test(value);
const hasNumber = /\d/.test(value);
const hasSpecial = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);
const isLongEnough = value.length >= 8;
return hasUpper && hasLower && hasNumber && hasSpecial && isLongEnough;
}, 'Password must contain at least 8 characters with uppercase, lowercase, number, and special character');
validator.addRule('confirmPassword', (value, originalFieldName) => {
const originalField = document.querySelector(`[name="${originalFieldName}"]`);
return originalField ? value === originalField.value : false;
}, 'Passwords do not match');
// Bind to form
const form = document.getElementById('registrationForm');
validator.bindToForm(form);
// HTML structure:
/*
<form id="registrationForm">
<input
type="text"
name="username"
data-rules="required|min:3|max:20|alphanumeric"
placeholder="Username"
/>
<input
type="email"
name="email"
data-rules="required|email"
placeholder="Email"
/>
<input
type="password"
name="password"
data-rules="required|strongPassword"
placeholder="Password"
/>
<input
type="password"
name="confirmPassword"
data-rules="required|confirmPassword:password"
placeholder="Confirm Password"
/>
<input
type="url"
name="website"
data-rules="url"
placeholder="Website (optional)"
/>
<button type="submit">Register</button>
</form>
*/工作原理 :這個 DOM 工具庫結(jié)合了類似 jQuery 風(fēng)格的鏈?zhǔn)讲僮鞯谋憷砸约艾F(xiàn)代 JavaScript 特性,如 Promise、Intersection Observer 和 CSS 動畫。它為完整框架提供了一個輕量級替代方案,同時保持了出色的性能和瀏覽器兼容性。
結(jié)論:擁抱原生 JavaScript 的力量
JavaScript 已經(jīng)發(fā)展為一個功能強(qiáng)大的成熟語言,配備了 API 和特性,可以消除對外部庫的需求。今天分享的11個原生解決方案,只是很小很小的一部分,希望能夠?qū)δ阌兴鶐椭?/span>
優(yōu)點(diǎn):
- 性能優(yōu)勢 :原生解決方案通常比庫替代方案更快,且內(nèi)存占用更小
- 減少依賴 :依賴更少意味著更少的安全漏洞和更易于維護(hù)
- 更深入的理解 :編寫原生解決方案能加深你對 JavaScript 基礎(chǔ)的理解
- 長期穩(wěn)定性 :原生 API 比可能無人維護(hù)的第三方庫更穩(wěn)定
何時使用這些解決方案:
- 構(gòu)建輕量級應(yīng)用程序,其中包大小很重要
- 在依賴策略嚴(yán)格的環(huán)境中工作
- 學(xué)習(xí)和理解核心 JavaScript 概念
- 創(chuàng)建可重用的組件,這些組件不應(yīng)依賴于特定庫
何時考慮使用庫:
- 開發(fā)速度比包體積更重要的復(fù)雜應(yīng)用程序
- 已經(jīng)在特定生態(tài)系統(tǒng)中投入大量資源的團(tuán)隊(duì)
- 當(dāng)你需要的功能需要花費(fèi)大量時間來實(shí)施和正確測試時
現(xiàn)代網(wǎng)絡(luò)平臺非常強(qiáng)大。通過掌握這些原生方法,你將成為一名更全面的開發(fā)者,能夠明智地決定何時使用庫,何時利用平臺本身。
當(dāng)然,我們的目標(biāo)不是完全避開所有庫,而是了解平臺本身能實(shí)現(xiàn)什么。掌握了這些知識,你可以做出更好的架構(gòu)決策,編寫更高效、更易于維護(hù)的代碼。



























