跳到主要内容

JavaScript常用经典代码对比

前言

JavaScript提供了多种数据结构和编程方法,在实际开发中,我们经常需要在不同的数据结构或方法之间做出选择。本文将详细对比JavaScript中常用的经典代码结构,帮助开发者理解它们的区别、适用场景和优缺点,从而在实际项目中做出更合适的选择。

一、数据结构对比

1. Map vs Set

MapSet是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 存储结构

特性MapSet
存储内容键值对(key-value)唯一的值(value)
键的类型任意值(函数、对象、基本类型等)无键,只有值
值的重复性值可以重复值不可重复
元素顺序按插入顺序迭代按插入顺序迭代
默认迭代器entries()返回键值对values()返回值

1.3 主要方法对比

操作MapSet
添加元素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.sizeset.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 基本特性对比

特性ObjectMap
键类型主要是字符串和Symbol任意类型(函数、对象、基本类型等)
键的顺序ES6之前无保证,ES6之后有一定规则按插入顺序
默认键原型链上的键可能被继承无默认键
大小获取需要手动计算直接通过map.size获取
迭代需通过其他方法直接可迭代
性能频繁添加/删除属性性能较差频繁添加/删除属性性能较好
序列化支持JSON.stringify不直接支持,需手动转换

2.2 方法对比

操作ObjectMap
创建const obj = {}const obj = new Object()const map = new Map()
添加属性/键值对obj[key] = valuemap.set(key, value)
获取值obj[key]obj.keymap.get(key)
删除属性/键值对delete obj[key]map.delete(key)
检查属性/键是否存在key in objobj.hasOwnProperty(key)map.has(key)
清空需要手动删除每个属性map.clear()
遍历for...inObject.keys/values/entriesfor...ofmap.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

ArraySetMap都是JavaScript中常用的集合类型,但它们各有特点和适用场景。

3.1 基本特性对比

特性ArraySetMap
存储内容有序的元素集合唯一值的集合键值对的集合
元素访问通过索引 array[index]通过 set.has(value) 检查存在性通过 map.get(key) 获取值
元素唯一性允许重复元素元素唯一值可以重复,但键唯一
元素顺序按插入顺序按插入顺序按插入顺序
查找复杂度O(n)O(1)O(1)
添加/删除复杂度取决于操作位置O(1)O(1)

3.2 方法对比

操作ArraySetMap
添加元素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) !== -1set.has(value)map.has(key)
清空array.length = 0set.clear()map.clear()
大小array.lengthset.sizemap.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

Promiseasync/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 功能对比

特性Promiseasync/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这门语言,编写出更加高效、优雅和可维护的代码。