JavaScript常用经典代码对比
前言
JavaScript提供了多种数据结构和编程方法,在实际开发中,我们经常需要在不同的数据结构或方法之间做出选择。本文将详细对比JavaScript中常用的经典代码结构,帮助开发者理解它们的区别、适用场景和优缺点,从而在实际项目中做出更合适的选择。
一、数据结构对比
1. Map vs Set
Map和Set是ES6引入的两种新的数据结构,它们都属于集合类型,但在存储和操作数据方面有显著区别。
1.1 基本概念与定义
- Map:是一种键值对的集合,类似于对象,但键可以是任何数据类型(不限于字符串和Symbol)。
- Set:是一种值的集合,其中的值是唯一的(不会有重复的值)。
// Map的创建
const map = new Map();
const mapFromArray = new Map([['key1', 'value1'], ['key2', 'value2']]);
// Set的创建
const set = new Set();
const setFromArray = new Set([1, 2, 3, 3, 4]); // 结果是 {1, 2, 3, 4}
1.2 存储结构
| 特性 | Map | Set |
|---|---|---|
| 存储内容 | 键值对(key-value) | 唯一的值(value) |
| 键的类型 | 任意值(函数、对象、基本类型等) | 无键,只有值 |
| 值的重复性 | 值可以重复 | 值不可重复 |
| 元素顺序 | 按插入顺序迭代 | 按插入顺序迭代 |
| 默认迭代器 | entries()返回键值对 | values()返回值 |
1.3 主要方法对比
| 操作 | Map | Set |
|---|---|---|
| 添加元素 | map.set(key, value) | set.add(value) |
| 获取元素 | map.get(key) | set.has(value)(检查存在性) |
| 删除元素 | map.delete(key) | set.delete(value) |
| 检查存在性 | map.has(key) | set.has(value) |
| 清空集合 | map.clear() | set.clear() |
| 获取大小 | map.size | set.size |
| 遍历 | map.forEach((value, key, map) => {}) | set.forEach((value, value2, set) => {}) |
注意:Set的forEach方法中,回调函数的前两个参数都是值,这是为了保持与Map API的一致性。
// Map示例
const map = new Map();
map.set('name', 'John');
map.set(1, 'One');
map.set({id: 1}, 'Object key');
console.log(map.get('name')); // 'John'
console.log(map.has(1)); // true
console.log(map.size); // 3
map.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// Set示例
const set = new Set();
set.add('apple');
set.add('banana');
set.add('apple'); // 重复值,不会被添加
console.log(set.has('banana')); // true
console.log(set.size); // 2
set.forEach(value => {
console.log(value);
});
1.4 适用场景
-
Map适用场景:
- 需要存储键值对数据时
- 键不是字符串类型时
- 需要维护键值对的插入顺序时
- 需要频繁查找、添加和删除键值对时
- 需要将对象作为键时
-
Set适用场景:
- 需要存储唯一值的集合时
- 需要去重时
- 需要检查一个值是否存在于集合中时
- 需要按插入顺序迭代元素时
- 需要进行集合操作(如并集、交集)时
// Map实际应用场景:缓存数据
function createCache() {
const cache = new Map();
return {
get: function(key) {
return cache.get(key);
},
set: function(key, value) {
cache.set(key, value);
},
has: function(key) {
return cache.has(key);
},
clear: function() {
cache.clear();
}
};
}
// Set实际应用场景:数组去重
function uniqueArray(arr) {
return [...new Set(arr)];
}
// 集合操作示例
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// 并集
const union = new Set([...setA, ...setB]); // {1, 2, 3, 4, 5, 6}
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x))); // {3, 4}
// 差集
const difference = new Set([...setA].filter(x => !setB.has(x))); // {1, 2}
2. Object vs Map
Object是JavaScript中最基本的数据结构之一,而Map是ES6引入的新数据结构。虽然两者都可以存储键值对,但它们有很多重要的区别。
2.1 基本特性对比
| 特性 | Object | Map |
|---|---|---|
| 键类型 | 主要是字符串和Symbol | 任意类型(函数、对象、基本类型等) |
| 键的顺序 | ES6之前无保证,ES6之后有一定规则 | 按插入顺序 |
| 默认键 | 原型链上的键可能被继承 | 无默认键 |
| 大小获取 | 需要手动计算 | 直接通过map.size获取 |
| 迭代 | 需通过其他方法 | 直接可迭代 |
| 性能 | 频繁添加/删除属性性能较差 | 频繁添加/删除属性性能较好 |
| 序列化 | 支持JSON.stringify | 不直接支持,需手动转换 |
2.2 方法对比
| 操作 | Object | Map |
|---|---|---|
| 创建 | const obj = {} 或 const obj = new Object() | const map = new Map() |
| 添加属性/键值对 | obj[key] = value | map.set(key, value) |
| 获取值 | obj[key] 或 obj.key | map.get(key) |
| 删除属性/键值对 | delete obj[key] | map.delete(key) |
| 检查属性/键是否存在 | key in obj 或 obj.hasOwnProperty(key) | map.has(key) |
| 清空 | 需要手动删除每个属性 | map.clear() |
| 遍历 | for...in 或 Object.keys/values/entries | for...of 或 map.forEach |
// Object示例
const obj = {
name: 'John',
age: 30
};
obj['job'] = 'Developer';
console.log(obj.name); // 'John'
console.log('age' in obj); // true
console.log(Object.keys(obj)); // ['name', 'age', 'job']
delete obj.age;
console.log(obj); // {name: 'John', job: 'Developer'}
// Map示例
const map = new Map();
map.set('name', 'John');
map.set('age', 30);
map.set('job', 'Developer');
console.log(map.get('name')); // 'John'
console.log(map.has('age')); // true
console.log(map.size); // 3
map.delete('age');
console.log(map.size); // 2
2.3 适用场景
-
Object适用场景:
- 需要简单的键值存储,且键主要是字符串时
- 需要使用对象的原型方法时
- 需要将数据序列化为JSON时
- 需要定义方法和属性的组合体时
- 需要使用解构赋值时
-
Map适用场景:
- 需要使用非字符串类型作为键时
- 需要频繁添加和删除键值对时
- 需要保持键值对的插入顺序时
- 需要跟踪数据大小(条目数量)时
- 需要避免原型链污染时
// Object实际应用场景:配置对象
const config = {
apiUrl: 'https://api.example.com',
timeout: 3000,
retryAttempts: 3,
logging: true
};
// Map实际应用场景:存储DOM元素映射
function setupElementHandlers() {
const elementHandlers = new Map();
document.querySelectorAll('.interactive').forEach(el => {
const handler = () => {
console.log(`Element ${el.id} clicked`);
};
el.addEventListener('click', handler);
elementHandlers.set(el, handler);
});
// 清理函数
return function cleanup() {
elementHandlers.forEach((handler, el) => {
el.removeEventListener('click', handler);
});
elementHandlers.clear();
};
}
3. Array vs Set vs Map
Array、Set和Map都是JavaScript中常用的集合类型,但它们各有特点和适用场景。
3.1 基本特性对比
| 特性 | Array | Set | Map |
|---|---|---|---|
| 存储内容 | 有序的元素集合 | 唯一值的集合 | 键值对的集合 |
| 元素访问 | 通过索引 array[index] | 通过 set.has(value) 检查存在性 | 通过 map.get(key) 获取值 |
| 元素唯一性 | 允许重复元素 | 元素唯一 | 值可以重复,但键唯一 |
| 元素顺序 | 按插入顺序 | 按插入顺序 | 按插入顺序 |
| 查找复杂度 | O(n) | O(1) | O(1) |
| 添加/删除复杂度 | 取决于操作位置 | O(1) | O(1) |
3.2 方法对比
| 操作 | Array | Set | Map |
|---|---|---|---|
| 添加元素 | array.push(value) 或 array.unshift(value) | set.add(value) | map.set(key, value) |
| 获取元素 | array[index] | set.has(value) (无直接获取方法) | map.get(key) |
| 删除元素 | array.pop(), array.shift(), array.splice(index, 1) | set.delete(value) | map.delete(key) |
| 检查存在性 | array.includes(value) 或 array.indexOf(value) !== -1 | set.has(value) | map.has(key) |
| 清空 | array.length = 0 | set.clear() | map.clear() |
| 大小 | array.length | set.size | map.size |
// Array示例
const array = [1, 2, 3, 3, 4];
array.push(5);
console.log(array[0]); // 1
console.log(array.includes(3)); // true
console.log(array.length); // 5
// Set示例
const set = new Set([1, 2, 3, 3, 4]);
set.add(5);
console.log(set.has(3)); // true
console.log(set.size); // 5
// Map示例
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
map.set('d', 4);
console.log(map.get('a')); // 1
console.log(map.has('b')); // true
console.log(map.size); // 4
3.3 适用场景
-
Array适用场景:
- 需要按索引访问元素时
- 需要存储相同类型或相关数据的有序集合时
- 需要使用数组特有的方法(如map、filter、reduce等)时
- 需要进行排序操作时
- 需要支持重复元素时
-
Set适用场景:
- 需要存储唯一值时
- 需要频繁检查值是否存在时
- 需要去重操作时
- 需要进行集合运算(并集、交集、差集)时
- 不关心元素的顺序,只关心元素是否存在时
-
Map适用场景:
- 需要存储键值对数据时
- 需要使用非字符串类型作为键时
- 需要保持键值对的插入顺序时
- 需要频繁添加和删除键值对时
- 需要建立映射关系时
// Array实际应用场景:数据处理管道
const data = [1, 2, 3, 4, 5, 6];
const result = data
.filter(num => num % 2 === 0) // [2, 4, 6]
.map(num => num * 2) // [4, 8, 12]
.reduce((sum, num) => sum + num, 0); // 24
// Set实际应用场景:标签管理
function TagManager() {
const tags = new Set();
return {
addTag: function(tag) {
tags.add(tag);
},
removeTag: function(tag) {
tags.delete(tag);
},
hasTag: function(tag) {
return tags.has(tag);
},
getAllTags: function() {
return [...tags];
}
};
}
// Map实际应用场景:计算词频
function countWords(text) {
const words = text.toLowerCase().match(/\w+/g) || [];
const wordCount = new Map();
words.forEach(word => {
const count = wordCount.get(word) || 0;
wordCount.set(word, count + 1);
});
return wordCount;
}
二、数组方法对比
4. map() vs forEach()
**map()和forEach()**是JavaScript数组中两个常用的迭代方法,但它们有明显的区别和不同的用途。
4.1 基本概念
- map():创建一个新数组,其结果是该数组中的每个元素都调用一次提供的函数后的返回值。
- forEach():对数组中的每个元素执行一次提供的函数,但不返回新数组。
4.2 语法和返回值
// map()语法
const newArray = array.map(callback(currentValue, index, array) => {
// 返回处理后的值
}, thisArg);
// forEach()语法
array.forEach(callback(currentValue, index, array) => {
// 执行操作,无返回值
}, thisArg);
- map() 返回一个新数组,数组长度与原数组相同
- forEach() 没有返回值(返回undefined)
4.3 功能对比
| 特性 | map() | forEach() |
|---|---|---|
| 返回值 | 新数组 | undefined |
| 链式调用 | 支持(可与filter、reduce等链式调用) | 不支持(返回undefined) |
| 中断迭代 | 不能通过return中断(但可以使用some或every) | 不能中断(除非抛出异常) |
| 性能 | 通常比forEach()稍慢(因为创建新数组) | 通常比map()稍快 |
| 用途 | 数据转换 | 数据遍历和副作用操作 |
// map()示例:将数组中的每个数字加倍
const numbers = [1, 2, 3, 4, 5];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // [2, 4, 6, 8, 10]
// forEach()示例:打印数组中的每个元素
const names = ['John', 'Alice', 'Bob'];
names.forEach(name => {
console.log(name);
});
// 输出:
// John
// Alice
// Bob
// 链式调用示例
const result = numbers
.map(num => num * 2)
.filter(num => num > 5)
.reduce((sum, num) => sum + num, 0);
console.log(result); // 6 + 8 + 10 = 24
4.4 适用场景
-
map()适用场景:
- 需要将数组中的每个元素转换为新值时
- 需要基于原数组创建新数组时
- 需要进行链式调用处理数据时
- 数据映射操作
-
forEach()适用场景:
- 需要对数组中的每个元素执行操作但不需要返回新数组时
- 需要执行副作用(如日志记录、DOM操作等)时
- 不关心返回值时
- 简单的数据遍历
// map()实际应用场景:格式化数据
const users = [
{ id: 1, name: 'John', age: 30 },
{ id: 2, name: 'Alice', age: 25 },
{ id: 3, name: 'Bob', age: 35 }
];
const userNames = users.map(user => user.name);
console.log(userNames); // ['John', 'Alice', 'Bob']
const userInfo = users.map(user => ({
userId: user.id,
displayName: `${user.name} (${user.age})`
}));
// forEach()实际应用场景:DOM操作
function renderUsers(users) {
const container = document.getElementById('users-container');
container.innerHTML = '';
users.forEach(user => {
const userElement = document.createElement('div');
userElement.textContent = `${user.name}, ${user.age} years old`;
container.appendChild(userElement);
});
}
5. filter() vs find()
**filter()和find()**都是数组中用于查找元素的方法,但它们的返回结果和用途有所不同。
5.1 基本概念
- filter():创建一个新数组,包含所有通过所提供函数实现的测试的元素。
- find():返回数组中满足提供的测试函数的第一个元素的值,如果没有找到符合条件的元素,则返回undefined。
5.2 语法和返回值
// filter()语法
const newArray = array.filter(callback(element, index, array) => {
// 返回布尔值,表示是否包含该元素
}, thisArg);
// find()语法
const element = array.find(callback(element, index, array) => {
// 返回布尔值,表示是否为要查找的元素
}, thisArg);
- filter() 返回一个新数组,包含所有满足条件的元素
- find() 返回第一个满足条件的元素,如果没有找到则返回undefined
5.3 功能对比
| 特性 | filter() | find() |
|---|---|---|
| 返回值 | 包含所有满足条件的元素的新数组 | 第一个满足条件的元素或undefined |
| 停止条件 | 总是遍历整个数组 | 找到第一个匹配项就停止 |
| 性能 | 当只需要一个匹配项时效率较低 | 当只需要一个匹配项时效率较高 |
| 用途 | 筛选多个元素 | 查找单个元素 |
// filter()示例:筛选出所有偶数
const numbers = [1, 2, 3, 4, 5, 6, 7, 8];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8]
// find()示例:查找第一个大于5的数
const firstGreaterThanFive = numbers.find(num => num > 5);
console.log(firstGreaterThanFive); // 6
// 查找不存在的元素
const notFound = numbers.find(num => num > 10);
console.log(notFound); // undefined
5.4 适用场景
-
filter()适用场景:
- 需要获取数组中所有满足条件的元素时
- 需要基于条件筛选数据时
- 需要创建一个符合特定条件的子集时
- 多结果查询
-
find()适用场景:
- 需要查找数组中第一个满足条件的元素时
- 需要根据ID或唯一标识符查找特定元素时
- 只关心是否存在至少一个匹配项时
- 单结果查询
// filter()实际应用场景:筛选活跃用户
const users = [
{ id: 1, name: 'John', active: true },
{ id: 2, name: 'Alice', active: false },
{ id: 3, name: 'Bob', active: true },
{ id: 4, name: 'Charlie', active: true }
];
const activeUsers = users.filter(user => user.active);
console.log(activeUsers);
// [{ id: 1, name: 'John', active: true }, { id: 3, name: 'Bob', active: true }, { id: 4, name: 'Charlie', active: true }]
// find()实际应用场景:根据ID查找用户
function findUserById(users, id) {
return users.find(user => user.id === id);
}
const user = findUserById(users, 2);
console.log(user); // { id: 2, name: 'Alice', active: false }
6. reduce() vs reduceRight()
**reduce()和reduceRight()**都是数组中用于归约操作的方法,它们的主要区别在于迭代的方向。
6.1 基本概念
- reduce():对数组中的每个元素按顺序执行一个由您提供的reducer函数,将其结果汇总为单个返回值。
- reduceRight():与reduce()类似,但从数组的右侧开始,从右到左遍历数组。
6.2 语法和返回值
// reduce()语法
const result = array.reduce(callback(accumulator, currentValue, index, array) => {
// 返回新的accumulator值
}, initialValue);
// reduceRight()语法
const result = array.reduceRight(callback(accumulator, currentValue, index, array) => {
// 返回新的accumulator值
}, initialValue);
- 两者都返回一个累积计算的结果
- 区别在于迭代的方向:reduce()从左到右,reduceRight()从右到左
6.3 功能对比
| 特性 | reduce() | reduceRight() |
|---|---|---|
| 迭代方向 | 从左到右(索引0开始) | 从右到左(最后一个索引开始) |
| 用途 | 一般的归约操作 | 需要从右到左处理的特殊归约操作 |
| 结果 | 取决于回调函数和初始值 | 取决于回调函数和初始值,但顺序相反 |
// reduce()示例:计算数组元素的总和
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 15
// reduceRight()示例:反转字符串
const strArray = ['h', 'e', 'l', 'l', 'o'];
const reversedStr = strArray.reduceRight((accumulator, currentValue) => accumulator + currentValue, '');
console.log(reversedStr); // 'olleh'
// 不同方向的reduce对比
const array = [1, 2, 3, 4];
const leftToRight = array.reduce((acc, curr) => acc + curr, 0); // 0+1+2+3+4 = 10
const rightToLeft = array.reduceRight((acc, curr) => acc + curr, 0); // 0+4+3+2+1 = 10
console.log(leftToRight); // 10
console.log(rightToLeft); // 10
// 但在某些情况下结果会不同
const diffLeftToRight = array.reduce((acc, curr) => acc - curr, 0); // 0-1-2-3-4 = -10
const diffRightToLeft = array.reduceRight((acc, curr) => acc - curr, 0); // 0-4-3-2-1 = -10
console.log(diffLeftToRight); // -10
console.log(diffRightToLeft); // -10
// 对于非交换操作,结果会不同
const divisionLeftToRight = array.reduce((acc, curr) => acc / curr, 100); // 100/1/2/3/4 ≈ 4.1667
const divisionRightToLeft = array.reduceRight((acc, curr) => acc / curr, 100); // 100/4/3/2/1 ≈ 4.1667
console.log(divisionLeftToRight); // ~4.1667
console.log(divisionRightToLeft); // ~4.1667
6.4 适用场景
-
reduce()适用场景:
- 计算总和、平均值等统计数据
- 将数组转换为对象
- 合并数组元素
- 数据分组
- 一般的归约操作
-
reduceRight()适用场景:
- 需要从右到左处理数据的场景
- 嵌套数组的扁平化(从内到外)
- 构建嵌套对象
- 字符串反转
- 任何需要从末尾开始处理的操作
// reduce()实际应用场景:数据分组
const users = [
{ name: 'John', age: 25, department: 'HR' },
{ name: 'Alice', age: 30, department: 'IT' },
{ name: 'Bob', age: 35, department: 'IT' },
{ name: 'Charlie', age: 28, department: 'HR' },
{ name: 'David', age: 40, department: 'Finance' }
];
const usersByDepartment = users.reduce((groups, user) => {
const department = user.department;
if (!groups[department]) {
groups[department] = [];
}
groups[department].push(user);
return groups;
}, {});
console.log(usersByDepartment);
// {
// HR: [{ name: 'John', age: 25, department: 'HR' }, { name: 'Charlie', age: 28, department: 'HR' }],
// IT: [{ name: 'Alice', age: 30, department: 'IT' }, { name: 'Bob', age: 35, department: 'IT' }],
// Finance: [{ name: 'David', age: 40, department: 'Finance' }]
// }
// reduceRight()实际应用场景:嵌套数组扁平化
const nestedArray = [1, [2, [3, [4, 5]]]];
function flatten(array) {
return array.reduceRight((acc, curr) => {
return Array.isArray(curr) ? [...flatten(curr), ...acc] : [curr, ...acc];
}, []);
}
const flattened = flatten(nestedArray);
console.log(flattened); // [1, 2, 3, 4, 5]
三、异步编程对比
7. Promise vs async/await
Promise和async/await都是JavaScript中处理异步操作的机制,async/await是基于Promise的语法糖,使异步代码看起来更像同步代码。
7.1 基本概念
- Promise:表示一个异步操作的最终完成(或失败)及其结果值。
- async/await:ES2017引入的语法糖,基于Promise,使异步代码的编写和阅读更加直观。
7.2 语法对比
// Promise语法
function fetchData() {
return new Promise((resolve, reject) => {
// 异步操作
if (/* 操作成功 */) {
resolve(result);
} else {
reject(error);
}
});
}
fetchData()
.then(result => {
// 处理成功结果
return processResult(result);
})
.then(processedResult => {
// 处理处理后的结果
})
.catch(error => {
// 处理错误
})
.finally(() => {
// 无论成功失败都会执行
});
// async/await语法
async function fetchData() {
try {
const result = await fetchData();
const processedResult = await processResult(result);
// 处理处理后的结果
return processedResult;
} catch (error) {
// 处理错误
} finally {
// 无论成功失败都会执行
}
}
7.3 功能对比
| 特性 | Promise | async/await |
|---|---|---|
| 语法复杂度 | 链式调用,嵌套较深时可读性差 | 线性代码,可读性更好 |
| 错误处理 | 使用catch()方法 | 使用try/catch语句 |
| 调试难度 | 较难(需要在then回调中调试) | 较易(与同步代码调试方式相同) |
| 返回值 | 总是返回Promise | 异步函数返回Promise |
| 适用场景 | 简单的异步操作链 | 复杂的异步流程控制 |
// Promise示例:顺序获取数据
function fetchUserPosts(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => {
return fetch(`/api/users/${userId}/posts`)
.then(response => response.json())
.then(posts => {
return { user, posts };
});
})
.catch(error => {
console.error('Error fetching user posts:', error);
throw error;
});
}
// async/await示例:顺序获取数据
async function fetchUserPosts(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
const user = await userResponse.json();
const postsResponse = await fetch(`/api/users/${userId}/posts`);
const posts = await postsResponse.json();
return { user, posts };
} catch (error) {
console.error('Error fetching user posts:', error);
throw error;
}
}
// Promise示例:并行获取数据
function fetchMultipleResources() {
return Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(([users, posts, comments]) => {
return { users, posts, comments };
})
.catch(error => {
console.error('Error fetching resources:', error);
throw error;
});
}
// async/await示例:并行获取数据
async function fetchMultipleResources() {
try {
const [usersResponse, postsResponse, commentsResponse] = await Promise.all([
fetch('/api/users'),
fetch('/api/posts'),
fetch('/api/comments')
]);
const [users, posts, comments] = await Promise.all([
usersResponse.json(),
postsResponse.json(),
commentsResponse.json()
]);
return { users, posts, comments };
} catch (error) {
console.error('Error fetching resources:', error);
throw error;
}
}
7.4 适用场景
-
Promise适用场景:
- 简单的异步操作
- 需要组合多个Promise时(使用Promise.all、Promise.race等)
- 需要与不支持async/await的旧代码兼容时
- 处理简单的异步链式调用
-
async/await适用场景:
- 复杂的异步流程控制
- 需要顺序执行多个异步操作时
- 希望代码更接近同步代码风格时
- 需要更清晰的错误处理(try/catch)时
- 提高代码可读性和可维护性
// Promise实际应用场景:图片加载器
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
img.src = url;
});
}
// 加载多个图片
function loadImages(urls) {
return Promise.all(urls.map(url => loadImage(url)));
}
// async/await实际应用场景:表单提交处理
async function handleSubmit(event) {
event.preventDefault();
const form = event.target;
const formData = new FormData(form);
try {
// 显示加载状态
showLoading(true);
// 验证表单
await validateForm(formData);
// 提交表单数据
const response = await submitFormData(formData);
// 处理响应
showSuccessMessage(response.message);
// 重置表单
form.reset();
} catch (error) {
// 显示错误信息
showErrorMessage(error.message);
} finally {
// 隐藏加载状态
showLoading(false);
}
}
8. fetch() vs XMLHttpRequest
fetch()和XMLHttpRequest都是JavaScript中用于发送HTTP请求的API,但fetch()是现代的API,提供了更简洁和强大的功能。
8.1 基本概念
- XMLHttpRequest:传统的发送HTTP请求的API,已存在很长时间。
- fetch():ES6引入的现代HTTP请求API,基于Promise,提供了更简洁的接口。
8.2 语法对比
// XMLHttpRequest语法
function makeRequest(method, url, data) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = () => {
reject(new Error('Network error'));
};
xhr.send(data ? JSON.stringify(data) : null);
});
}
// fetch()语法
function makeRequest(method, url, data) {
const options = {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : null
};
return fetch(url, options)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response.json();
});
}
8.3 功能对比
| 特性 | fetch() | XMLHttpRequest |
|---|---|---|
| 基于Promise | 是 | 否(需要手动封装) |
| 错误处理 | 只在网络错误时reject,HTTP错误码仍resolve | 可以自定义错误处理逻辑 |
| 流式处理 | 支持(通过Body流) | 有限支持 |
| 跨域请求 | 默认不发送cookie(需设置credentials) | 默认发送cookie |
| 进度事件 | 支持(通过Response.body.getReader()) | 更好的进度事件支持 |
| 中止请求 | 通过AbortController | 通过xhr.abort() |
| 浏览器支持 | 现代浏览器(需polyfill支持IE) | 所有浏览器 |
// XMLHttpRequest示例:带进度监控的文件上传
function uploadFile(file) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
xhr.open('POST', '/api/upload');
// 监听进度事件
xhr.upload.addEventListener('progress', event => {
if (event.lengthComputable) {
const percentComplete = (event.loaded / event.total) * 100;
console.log(`Upload progress: ${percentComplete}%`);
}
});
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = () => {
reject(new Error('Upload failed'));
};
xhr.send(formData);
});
}
// fetch()示例:带取消功能的请求
function fetchWithCancel(url) {
const controller = new AbortController();
const signal = controller.signal;
const fetchPromise = fetch(url, { signal })
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
throw error;
}
});
return {
promise: fetchPromise,
cancel: () => controller.abort()
};
}
// 使用示例
const { promise, cancel } = fetchWithCancel('/api/data');
// 5秒后取消请求
setTimeout(cancel, 5000);
promise
.then(data => console.log(data))
.catch(error => console.error(error));
8.4 适用场景
-
fetch()适用场景:
- 现代Web应用开发
- 需要与Promise和async/await结合使用时
- 需要流式处理响应时
- 不需要支持旧浏览器时
- 希望使用更简洁的API时
-
XMLHttpRequest适用场景:
- 需要支持IE等旧浏览器时
- 需要精细控制上传/下载进度时
- 需要更灵活的请求配置时
- 处理复杂的跨域场景时
- 维护旧项目代码时
// fetch()实际应用场景:API客户端
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(endpoint) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'GET',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
async post(endpoint, data) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
credentials: 'include'
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
// 其他HTTP方法...
}
// XMLHttpRequest实际应用场景:进度条实现
function createUploadProgressBar(fileInput, progressElement) {
fileInput.addEventListener('change', function() {
const file = this.files[0];
if (!file) return;
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file);
xhr.upload.addEventListener('progress', function(event) {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
progressElement.style.width = `${percent}%`;
progressElement.textContent = `${Math.round(percent)}%`;
}
});
xhr.open('POST', '/upload');
xhr.send(formData);
});
}
四、函数式编程对比
9. 函数声明 vs 函数表达式 vs 箭头函数
JavaScript中有多种定义函数的方式,包括函数声明、函数表达式和箭头函数,它们各有特点和适用场景。
9.1 基本概念
- 函数声明:使用function关键字声明的函数,具有函数提升特性。
- 函数表达式:将函数赋值给变量的方式,不具有函数提升特性。
- 箭头函数:ES6引入的函数简写形式,不绑定自己的this、arguments等。
9.2 语法对比
// 函数声明
function functionName(parameters) {
// 函数体
return result;
}
// 函数表达式
const functionName = function(parameters) {
// 函数体
return result;
};
// 箭头函数
const functionName = (parameters) => {
// 函数体
return result;
};
// 箭头函数简写(单行表达式,自动返回)
const functionName = (parameters) => expression;
// 单个参数可以省略括号
const functionName = parameter => expression;
9.3 功能对比
| 特性 | 函数声明 | 函数表达式 | 箭头函数 |
|---|---|---|---|
| 函数提升 | 是 | 否(变量提升但未初始化) | 否 |
| this绑定 | 动态绑定(取决于调用方式) | 动态绑定 | 静态绑定(继承自外层作用域) |
| arguments对象 | 有 | 有 | 无(可使用rest参数) |
| 构造函数 | 可以用作构造函数 | 可以用作构造函数 | 不能用作构造函数 |
| prototype属性 | 有 | 有 | 无 |
| yield关键字 | 支持 | 支持 | 不支持 |
| 简洁性 | 标准语法 | 较简洁 | 最简洁 |
// 函数声明(具有提升特性)
console.log(add(2, 3)); // 5 (不会报错,因为函数被提升了)
function add(a, b) {
return a + b;
}
// 函数表达式(不具有提升特性)
console.log(subtract(5, 3)); // TypeError: subtract is not a function
const subtract = function(a, b) {
return a - b;
};
// 箭头函数(不绑定自己的this)
const obj = {
value: 42,
regularMethod: function() {
console.log(this.value); // 42
const innerFunction = function() {
console.log(this.value); // undefined(在非严格模式下是全局对象)
};
innerFunction();
const arrowFunction = () => {
console.log(this.value); // 42(继承自regularMethod的this)
};
arrowFunction();
}
};
obj.regularMethod();
// arguments对象对比
function regularFunction() {
console.log(arguments); // 包含所有参数的类数组对象
}
const arrowFunc = () => {
// console.log(arguments); // ReferenceError: arguments is not defined
// 可以使用rest参数代替
};
const restFunction = (...args) => {
console.log(args); // 包含所有参数的数组
};
9.4 适用场景
-
函数声明适用场景:
- 需要在定义之前使用函数时
- 创建命名函数,便于调试
- 函数体较大,需要清晰的结构
- 需要用作构造函数时
-
函数表达式适用场景:
- 创建匿名函数或用作回调函数
- 需要闭包时
- 模块化开发中导出函数
- 按需创建函数
-
箭头函数适用场景:
- 简短的回调函数
- 需要保持外层作用域this的场景
- 数组方法中的回调(map、filter等)
- 不需要自己的this、arguments的场景
- 需要简洁代码时
// 函数声明实际应用场景:工具函数
function validateEmail(email) {
const re = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
return re.test(email);
}
function formatDate(date) {
if (!(date instanceof Date)) {
date = new Date(date);
}
return date.toLocaleDateString('zh-CN');
}
// 函数表达式实际应用场景:闭包
function createCounter() {
let count = 0;
return {
increment: function() {
return ++count;
},
decrement: function() {
return --count;
},
getCount: function() {
return count;
}
};
}
// 箭头函数实际应用场景:数组操作
const users = [
{ name: 'John', age: 30 },
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 35 }
];
// 过滤出年龄大于30的用户
const olderUsers = users.filter(user => user.age > 30);
// 将用户数组映射为用户名数组
const userNames = users.map(user => user.name);
// 计算所有用户的平均年龄
const averageAge = users.reduce((sum, user) => sum + user.age, 0) / users.length;
// 保持this上下文
class Timer {
constructor() {
this.seconds = 0;
// 使用箭头函数保持this上下文
this.interval = setInterval(() => {
this.seconds++;
console.log(`Seconds passed: ${this.seconds}`);
}, 1000);
}
stop() {
clearInterval(this.interval);
}
}
10. 闭包 vs 类
闭包和类都是JavaScript中用于创建封装和状态管理的机制,但它们的实现方式和适用场景有所不同。
10.1 基本概念
- 闭包:函数可以访问其词法作用域之外的变量,即使函数在其定义的作用域之外执行。
- 类:ES6引入的语法糖,用于创建对象和实现继承,基于原型继承机制。
10.2 语法对比
// 使用闭包实现封装
function createPerson(name, age) {
// 私有变量
let _name = name;
let _age = age;
// 返回公共方法
return {
getName: function() {
return _name;
},
getAge: function() {
return _age;
},
setName: function(name) {
_name = name;
},
setAge: function(age) {
if (age >= 0) {
_age = age;
}
},
greet: function() {
console.log(`Hello, my name is ${_name} and I am ${_age} years old.`);
}
};
}
// 使用类实现封装
class Person {
// 私有字段(ES2022特性)
#name;
#age;
constructor(name, age) {
this.#name = name;
this.#age = age;
}
// 公共方法
getName() {
return this.#name;
}
getAge() {
return this.#age;
}
setName(name) {
this.#name = name;
}
setAge(age) {
if (age >= 0) {
this.#age = age;
}
}
greet() {
console.log(`Hello, my name is ${this.#name} and I am ${this.#age} years old.`);
}
}
10.3 功能对比
| 特性 | 闭包 | 类 |
|---|---|---|
| 封装机制 | 基于函数作用域 | 基于类和私有字段(ES2022) |
| 继承实现 | 手动实现组合和委托 | 使用extends关键字 |
| 实例化 | 调用工厂函数 | 使用new关键字 |
| 内存占用 | 每个实例都有独立的函数副本 | 方法共享(通过原型) |
| 性能 | 创建实例较快,但方法不共享 | 创建实例稍慢,但方法共享 |
| 可读性 | 对于复杂对象可能较差 | 更符合传统OOP习惯,可读性较好 |
| 静态方法 | 需要手动实现 | 原生支持static关键字 |
// 闭包示例:计数器
function createCounter() {
let count = 0;
return {
increment: function() {
return ++count;
},
reset: function() {
count = 0;
},
getCount: function() {
return count;
}
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1.increment()); // 1
console.log(counter1.increment()); // 2
console.log(counter2.increment()); // 1 (独立的状态)
// 类示例:计数器
class Counter {
#count = 0;
increment() {
return ++this.#count;
}
reset() {
this.#count = 0;
}
getCount() {
return this.#count;
}
}
const counterA = new Counter();
const counterB = new Counter();
console.log(counterA.increment()); // 1
console.log(counterA.increment()); // 2
console.log(counterB.increment()); // 1 (独立的状态)
// 闭包实现继承(组合优于继承)
function createEmployee(name, age, position, salary) {
// 组合Person
const person = createPerson(name, age);
// 添加新的私有变量
let _position = position;
let _salary = salary;
// 返回扩展的对象
return {
// 继承person的方法
...person,
// 添加新方法
getPosition: function() {
return _position;
},
getSalary: function() {
return _salary;
},
setSalary: function(salary) {
if (salary > 0) {
_salary = salary;
}
},
// 覆盖方法
greet: function() {
console.log(`Hello, my name is ${person.getName()}, I am ${person.getAge()} years old, and I work as a ${_position}.`);
}
};
}
// 类实现继承
class Employee extends Person {
#position;
#salary;
constructor(name, age, position, salary) {
super(name, age);
this.#position = position;
this.#salary = salary;
}
getPosition() {
return this.#position;
}
getSalary() {
return this.#salary;
}
setSalary(salary) {
if (salary > 0) {
this.#salary = salary;
}
}
// 覆盖方法
greet() {
console.log(`Hello, my name is ${this.getName()}, I am ${this.getAge()} years old, and I work as a ${this.#position}.`);
}
}
10.4 适用场景
-
闭包适用场景:
- 创建简单的封装对象
- 实现私有变量
- 函数式编程风格
- 不需要复杂继承关系时
- 创建工厂函数
- 模块化模式
-
类适用场景:
- 面向对象编程风格
- 需要复杂继承关系时
- 创建多个具有相同属性和方法的对象
- 需要使用 instanceof 检查类型时
- 需要静态方法和属性时
- 更符合团队的OOP编程习惯
// 闭包实际应用场景:模块化模式
const Calculator = (function() {
// 私有函数
function validateNumber(n) {
return typeof n === 'number' && !isNaN(n);
}
// 公共API
return {
add: function(a, b) {
if (validateNumber(a) && validateNumber(b)) {
return a + b;
}
throw new Error('Both arguments must be numbers');
},
subtract: function(a, b) {
if (validateNumber(a) && validateNumber(b)) {
return a - b;
}
throw new Error('Both arguments must be numbers');
},
multiply: function(a, b) {
if (validateNumber(a) && validateNumber(b)) {
return a * b;
}
throw new Error('Both arguments must be numbers');
},
divide: function(a, b) {
if (validateNumber(a) && validateNumber(b)) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
throw new Error('Both arguments must be numbers');
}
};
})();
// 类实际应用场景:游戏实体系统
class GameObject {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.isActive = true;
}
update(deltaTime) {
// 基本更新逻辑,可以被子类覆盖
}
render(context) {
// 基本渲染逻辑,可以被子类覆盖
context.fillRect(this.x, this.y, this.width, this.height);
}
// 检测碰撞
collidesWith(other) {
return this.x < other.x + other.width &&
this.x + this.width > other.x &&
this.y < other.y + other.height &&
this.y + this.height > other.y;
}
}
// 继承GameObject创建Player类
class Player extends GameObject {
constructor(x, y) {
super(x, y, 32, 32);
this.speed = 200; // 像素/秒
this.score = 0;
}
update(deltaTime) {
// 玩家特定的更新逻辑
// 处理输入、移动等
}
render(context) {
// 玩家特定的渲染逻辑
context.fillStyle = '#4CAF50';
super.render(context);
// 渲染分数等信息
}
collect(item) {
this.score += item.value;
console.log(`Score: ${this.score}`);
}
}
总结
本文详细对比了JavaScript中常用的经典代码结构,包括数据结构(Map vs Set、Object vs Map、Array vs Set vs Map)、数组方法(map() vs forEach()、filter() vs find()、reduce() vs reduceRight())、异步编程(Promise vs async/await、fetch() vs XMLHttpRequest)以及函数式编程(函数声明 vs 函数表达式 vs 箭头函数、闭包 vs 类)等方面。
在实际开发中,选择合适的代码结构对于提高代码质量、性能和可维护性至关重要。没有绝对的最佳选择,只有最适合特定场景的选择。开发者应该根据项目需求、团队习惯和个人偏好,结合各种代码结构的特点和适用场景,做出明智的选择。
通过深入理解这些代码结构的区别和联系,开发者可以更加灵活地运用JavaScript这门语言,编写出更加高效、优雅和可维护的代码。