你不知道的 10个JavaScript 高级技巧
前言
JavaScript 是一门功能强大的语言,它隐藏了许多可以提高开发效率和代码整洁度的特性。这里有 10 个你可能不知道的 JavaScript 高级技巧,它们可以显著提升你的编码技能。
1. 带别名的解构
解构允许你将数组中的值或对象中的属性解包到不同的变量中。别名让你可以在这个过程中重命名变量,这在处理来自外部来源(如 API)的数据时特别有用。
使用场景: 从 API 获取数据时,你希望为属性赋予更有意义的名称,以便提高代码可读性和可维护性。
const apiResponse = { first_name: 'John', user_age: 30, address: { city: 'New York', zip: '10001' } };
const { first_name: firstName, user_age: age, address: { city: hometown, zip: postalCode } } = apiResponse;
console.log(firstName); // John
console.log(age); // 30
console.log(hometown); // New York
console.log(postalCode); // 10001
为什么要用它: 它可以使变量名更具自我解释性和直观性,从而提高代码可读性和可维护性。通过使用别名,你可以避免命名冲突,并提高代码的清晰度,使其更容易处理复杂的数据结构。
2. 柯里化
柯里化是将一个接受多个参数的函数转换为一系列只接受一个参数的函数的过程。这种技术允许你创建更灵活和可重用的函数,这在函数式编程中特别有用。
使用场景: 创建可重用且可配置的函数来应用折扣。你无需为不同的折扣百分比编写单独的函数,而是可以创建一个柯里化函数。
const applyDiscount = (discount) => (price) => price - (price * discount / 100);
const tenPercentOff = applyDiscount(10);
const twentyPercentOff = applyDiscount(20);
console.log(tenPercentOff(100)); // 90
console.log(twentyPercentOff(100)); // 80
const applyTax = (taxRate) => (price) => price + (price * taxRate / 100);
const applyTenPercentTax = applyTax(10);
console.log(applyTenPercentTax(100)); // 110
console.log(applyTenPercentTax(twentyPercentOff(100))); // 88
为什么要用它: 它使你能够在函数中预设参数,从而编写更模块化和可组合的代码。这可以大大简化高度可重用实用程序函数的创建,使你的代码库更简洁,更易于维护。柯里化在需要部分应用函数或使用不同配置重用函数的情况下特别有用。
3. 防抖和节流
防抖和节流是控制函数执行频率的技术。它们对于优化事件处理程序,防止过度调用函数,从而降低性能特别有用。
防抖:
防抖确保只有在自上次调用函数以来经过一定时间后,才会再次调用函数。这对于搜索输入字段等场景非常有用,你希望在用户停止输入后再发出 API 调用。
使用场景: 优化搜索输入字段以减少 API 调用次数。这可以防止服务器过载,并通过仅在用户完成输入后才启动搜索来改善用户体验。
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
const search = debounce((query) => {
console.log(`Searching for ${query}`);
// 假设这里有一个 API 调用
}, 300);
document.getElementById('searchInput').addEventListener('input', (event) => {
search(event.target.value);
});
为什么要用它: 减少不必要的函数调用次数,通过确保只有在用户停止执行触发操作后才调用函数来提高性能和用户体验。这对于涉及网络请求或大量计算的操作特别有用。
节流:节流确保在指定的时间段内最多调用一次函数。这对于滚动事件等场景非常有用,你希望限制函数调用的频率。
使用场景:优化滚动事件处理以提高性能。这可以防止浏览器因过多的事件调用而过载,从而确保更流畅、更灵敏的交互。
function throttle(func, interval) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= interval) {
lastCall = now;
func.apply(this, args);
}
};
}
const handleScroll = throttle(() => {
console.log('Scrolled');
// 假设这里有复杂的计算或 DOM 更新
}, 300);
window.addEventListener('scroll', handleScroll);
为什么要用它: 通过确保以受控的间隔调用函数来防止性能问题,减少浏览器负载并提供更好的用户体验。节流对于可以频繁触发的事件监听器(例如滚动或调整大小事件)特别有用。
4. 记忆化
记忆化是一种优化技术,它涉及缓存昂贵函数调用的结果,并在再次出现相同输入时返回缓存的结果。这可以显著提高计算量大的函数的性能,特别是那些频繁使用相同参数调用的函数。
使用场景: 提高递归函数(如斐波那契数列计算)的性能。如果没有记忆化,对斐波那契函数的每次调用都会多次冗余地计算相同的值,导致时间复杂度呈指数级增长。
const memoize = (fn) => {
const cache = {};
return (...args) => {
const key = JSON.stringify(args);
if (!cache[key]) {
cache[key] = fn(...args);
}
return cache[key];
};
};
const fibonacci = memoize((n) => {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(40)); // 102334155
为什么要用它: 避免冗余计算,显著提高具有重复输入的函数的性能。记忆化可以将低效的重复计算转换为可管理的线性时间操作,使其成为优化性能密集型任务的必要技术。
5. 代理
Proxy 对象允许你为另一个对象创建代理,使你能够拦截和重新定义基本操作,例如属性查找、赋值、枚举、函数调用等。这提供了一种强大的方式来向对象添加自定义行为。
使用场景: 在对象属性访问和赋值时进行验证和日志记录。例如,你可以强制执行类型约束并记录访问尝试,从而提供更好的控制和调试功能。
const user = {
name: 'John',
age: 30
};
const handler = {
get: (target, prop) => {
console.log(`Getting ${prop}`);
return target[prop];
},
set: (target, prop, value) => {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('Age must be a number');
}
console.log(`Setting ${prop} to ${value}`);
target[prop] = value;
return true;
}
};
const proxyUser = new Proxy(user, handler);
console.log(proxyUser.name); // Getting name, John
proxyUser.age = 35; // Setting age to 35
// proxyUser.age = '35'; // Throws TypeError
为什么要用它: 允许为对象操作(例如验证、日志记录等)自定义行为,从而增强对对象操作的控制。代理还可以用于实现复杂的逻辑,例如访问控制和数据绑定。这使得它们成为管理和扩展对象行为的多功能工具。
6. 生成器
生成器是可以退出并稍后重新进入的函数,在重新进入之间保持其上下文和变量绑定。它们对于实现迭代器和以同步方式处理异步任务非常有用。
使用场景: 为自定义对象遍历实现迭代器。生成器提供了一种简单的方法来定义自定义迭代行为,从而更容易遍历复杂的数据结构。
function* objectEntries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
const user = { name: 'John', age: 30, city: 'New York' };
for (let [key, value] of objectEntries(user)) {
console.log(`${key}: ${value}`);
}
// name: John
// age: 30
// city: New York
为什么要用它: 提供了一个强大的工具,用于实现自定义迭代器和简化异步工作流程。生成器使处理复杂的迭代逻辑和异步过程变得更容易,从而编写出更易读、更易维护的代码。它们还可以使用 co
等库以更直接、线性方式管理异步操作。
7. 充分利用控制台
使用场景: 改进调试复杂对象的日志记录。console.table
、console.group
和 console.time
等控制台方法可以提供更结构化、更 informative
的调试信息。
// 基本日志记录
console.log('简单日志');
console.error('这是一个错误');
console.warn('这是一个警告');
// 记录表格数据
const users = [
{ name: 'John', age: 30, city: 'New York' },
{ name: 'Jane', age: 25, city: 'San Francisco' },
];
console.table(users);
// 分组日志
console.group('用户详细信息');
console.log('用户 1: John');
console.log('用户 2: Jane');
console.groupEnd();
// 计时代码执行
console.time('计时器');
for (let i = 0; i < 1000000; i++) {
// 一些繁重的计算
}
console.timeEnd('计时器');
为什么要用它: 增强调试信息的可见性和组织性,使其更容易诊断和解决问题。正确使用控制台方法可以通过提供清晰、有条理和详细的日志来显著提高调试过程的效率。
8. 使用 structuredClone 进行结构化克隆
使用新的 structuredClone
方法深度克隆对象。与传统的浅拷贝不同,结构化克隆会创建对象的深拷贝,确保嵌套的对象也被拷贝。这种方法避免了 JSON.parse(JSON.stringify(obj))
的局限性,后者无法处理某些数据类型,例如函数、undefined
和循环引用。
使用场景: 创建复杂对象的深拷贝。这在需要复制对象以进行不应改变原始数据的操作时非常有用。
const obj = {
a: 1,
b: { c: 2 },
date: new Date(),
arr: [1, 2, 3],
nestedArr: [{ d: 4 }]
};
const clonedObj = structuredClone(obj);
console.log(clonedObj);
// { a: 1, b: { c: 2 }, date: 2023-06-08T00:00:00.000Z, arr: [1, 2, 3], nestedArr: [{ d: 4 }] }
console.log(clonedObj === obj); // false
console.log(clonedObj.b === obj.b); // false
console.log(clonedObj.date === obj.date); // false
console.log(clonedObj.arr === obj.arr); // false
console.log(clonedObj.nestedArr[0] === obj.nestedArr[0]); // false
为什么要用它: 提供了一种内置的、高效的深度克隆对象的方法,避免了手动实现深拷贝的陷阱和复杂性。与 JSON.parse(JSON.stringify(obj))
等替代方案相比,这种方法更可靠,并且可以更好地处理复杂的数据结构。
9. 自调用函数
自调用函数,也称为立即执行函数表达式 (IIFE),是在创建后自动执行的函数。它们对于封装代码以避免污染全局作用域非常有用,这对于维护简洁和模块化的代码至关重要。
使用场景: 封装代码以避免污染全局作用域。这种技术在块级作用域(let
和 const
)不可用的旧 JavaScript 环境中,或者在需要立即执行初始化逻辑的情况下特别有用。
(function() {
const privateVar = '这是私有的';
console.log('自调用函数立即运行');
// 初始化代码
})();
// 私有变量无法从外部访问
// console.log(privateVar); // ReferenceError: privateVar is not defined
为什么要用它: 通过避免全局变量并在不留下全局作用域痕迹的情况下执行初始化代码来帮助维护干净的代码。这种方法可以防止大型代码库中的冲突,并确保更好地封装功能,从而提高代码的可维护性并避免副作用。
10. 带标签的模板字面量
带标签的模板字面量允许你自定义模板字面量的处理方式。它们对于创建专门的模板非常有用,例如国际化、清理 HTML 或生成动态 SQL 查询。
使用场景: 清理 HTML 模板中的用户输入以防止 XSS 攻击。此技术可确保将用户生成的内容安全地插入 DOM 中,而不会执行任何恶意脚本。
function sanitize(strings, ...values) {
return strings.reduce((result, string, i) => {
let value = values[i - 1];
if (typeof value === 'string') {
value = value.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
return result + value + string;
});
}
const userInput = '<script>alert("xss")</script>';
const message = sanitize`用户输入: ${userInput}`;
console.log(message); // 用户输入: <script>alert("xss")</script>
为什么要用它: 提供了一种强大的机制来控制和自定义模板字面量的输出,从而实现更安全、更灵活的模板创建。带标签的模板字面量可用于强制执行安全性、格式化字符串和生成动态内容,从而增强代码的鲁棒性和多功能性。
总结
JavaScript 是一门功能丰富的语言,可以帮助你编写更简洁、更高效的代码。通过将这些高级技巧融入你的编码实践中,你可以提高你的生产力并增强代码的可读性。从带别名的解构到柯里化、防抖、节流等等,这些技巧可以使你的代码更简洁、更高效。祝你编程愉快!