# JavaScript 核心知识笔记
一、异步编程核心
1. Promise 基础
Promise 是 JavaScript 异步编程的基础,代表一个异步操作的最终完成(或失败)及其结果值。
new Promise():创建一个异步任务。执行器函数接收resolve和reject两个参数。then()/catch():用于处理Promise成功或失败的结果。then接收成功回调,catch接收失败回调。Promise.resolve():将一个值包装成一个已成功的Promise对象。Promise.reject():将一个值包装成一个已失败的Promise对象。Promise.all():接收一个Promise数组,等待所有Promise都成功,返回一个包含所有结果的数组。只要有一个失败,整体就失败。Promise.race():接收一个Promise数组,等待第一个完成的Promise(无论成功或失败),返回其结果。
2. async/await 语法糖
async/await 是基于Promise的语法糖,让异步代码写起来像同步代码一样清晰。
async函数:声明一个异步函数,它会隐式地返回一个Promise对象。await关键字:在async函数内部使用,用于等待一个Promise完成。await会暂停函数的执行,直到Promise状态变为fulfilled(成功)或rejected(失败)。成功时直接返回结果值,失败时需要用try...catch捕获。
// 示例:使用 async/await 替代 then/catch
async function fetchUserData() {
try {
const user = await fetch('/api/user').then(res => res.json());
const posts = await fetch(`/api/posts/${user.id}`).then(res => res.json());
console.log('用户和帖子数据:', { user, posts });
} catch (error) {
console.error('请求失败:', error);
}
}二、数组核心处理函数
数组方法是JavaScript中最常用的工具集,掌握它们是高效处理数据的关键。
2.1 every() - 全真才为真
- 作用:测试一个数组内的所有元素是否都能通过指定函数的测试。它返回一个布尔值。
场景:
- 表单校验:检查所有输入项是否都通过了校验。
- 权限校验:判断用户是否拥有全部所需的权限。
- 批量数据的合规性检查:如所有商品数量是否都大于0。
// 表单校验示例
const formFields = [
{ name: 'username', valid: true, value: '张三' },
{ name: 'email', valid: true, value: 'zhangsan@example.com' },
{ name: 'phone', valid: false, value: '123' }
];
const isFormValid = formFields.every(field => field.valid); // false2.2 some() - 一真即为真
- 作用:测试数组中是否至少有一个元素通过了指定函数的测试。它返回一个布尔值。
场景:
- 判断列表中是否存在未完成/异常的状态项。
- 快速判断搜索是否命中结果。
- 权限判断:只要用户拥有一个相关权限就能访问。
// 搜索命中示例
const users = [{ name: '张三' }, { name: '李四' }, { name: '王五' }];
const keywords = '张三';
const hasMatch = users.some(user => user.name.includes(keywords)); // true2.3 filter() - 筛选过滤
- 作用:创建一个新数组,其包含通过所提供函数实现的测试的所有元素。不会改变原数组。
场景:
- 数据筛选:根据条件过滤列表数据(如筛选出价格大于100的商品)。
- 清理无效数据:过滤掉数组中的
null、undefined、空字符串等。 - 数据分类:根据条件将原数组分成不同的子集。
// 过滤空值示例
const strings = ["苹果", "", "香蕉", " ", "橙子", null];
const validStrings = strings.filter(s => s && s.trim()); // ["苹果", "香蕉", "橙子"]2.4 map() - 一对一映射
- 作用:创建一个新数组,其结果是该数组中的每个元素调用一次提供的函数后的返回值。常用于数据转换。
场景:
- 接口数据适配:把后端返回的字段名(如
user_name)转成前端需要的(如name)。 - 渲染列表:把数据转成UI组件所需的格式。
- 批量修改数组中的每一项。
- 接口数据适配:把后端返回的字段名(如
// 接口数据适配示例
const apiData = [{ user_name: '张三', user_age: 25 }];
const frontData = apiData.map(item => ({
name: item.user_name,
age: item.user_age
}));
// 结果: [{ name: '张三', age: 25 }]2.5 reduce() - 万能汇总工具
- 作用:对数组中的每个元素执行一个reducer函数(升序执行),将其结果汇总为单个返回值。
reduce是函数式编程中最强大的工具之一。 参数:
- 回调函数:
(accumulator, currentValue) => newAccumulator - 初始值:
initialValue,为累加器提供初始值。
- 回调函数:
- 关键点:必须
return新的累加器,否则下次迭代时累加器为undefined。acc可以是数字、对象、数组等任何类型。 场景:
- 聚合统计:求和、求平均值、统计出现次数。
- 数据分组:把列表按某个字段分成不同的组。
- 复杂的数据变换:数组转对象、多层数据处理。
// 按类别分组并统计总价示例
const orderItems = [
{ category: '食品', price: 20 },
{ category: '服装', price: 100 },
{ category: '食品', price: 30 }
];
const totalByCategory = orderItems.reduce((acc, item) => {
acc[item.category] = (acc[item.category] || 0) + item.price;
return acc; // 必须 return acc
}, {}); // 初始值是一个空对象 {}
// 结果: { 食品: 50, 服装: 100 }2.6 find() & findIndex() - 精准查找
find():返回数组中第一个满足提供的测试函数的元素。找不到返回undefined。findIndex():返回数组中第一个满足提供的测试函数的元素的索引。找不到返回-1。- 与
indexOf的区别:indexOf只能用于简单值(如数字、字符串)的严格相等比较,而find/findIndex支持复杂的自定义判断条件。 - 场景:根据ID查找特定的用户/数据项,或找到元素位置以进行后续的删除/更新操作。
// 根据 ID 查找用户
const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }];
const user = users.find(user => user.id === 2); // { id: 2, name: '李四' }
const index = users.findIndex(user => user.id === 2); // 12.7 flat() & flatMap() - 数组扁平化
flat(depth):按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组。depth默认是1,可以传Infinity展开所有层级。flatMap():先执行map,再对结果执行flat(1),一步到位。场景:
- 处理多层嵌套的数据结构。
- 在
map操作后直接扁平化,如将句子数组拆分成单词数组。
// flatMap 示例:将句子拆分成单词
const lines = ["Hello world", "JavaScript is good"];
const words = lines.flatMap(line => line.split(' '));
// 结果: ["Hello", "world", "JavaScript", "is", "good"]2.8 includes() - 判断是否包含
- 作用:判断一个数组是否包含一个指定的值,根据情况返回
true或false。用于简单值的判断。 - 场景:检查用户角色、状态值等是否在允许的列表中。
const roles = ['admin', 'editor'];
const isAdmin = roles.includes('admin'); // true2.9 sort() - 排序(会修改原数组!)
- 作用:对数组的元素进行排序,并返回原数组。注意:
sort方法会改变原数组。默认排序顺序是将元素转换为字符串,然后比较它们的UTF-16代码单元值序列。 - 安全用法:使用
[...arr].sort()先拷贝,再排序,避免修改原数组。 - 数字排序:必须传入比较函数
(a, b) => a - b来升序排序,(a, b) => b - a来降序排序。
const nums = [3, 1, 2];
const sortedNums = [...nums].sort((a, b) => a - b); // 新数组 [1, 2, 3]
console.log(nums); // 原数组 [3, 1, 2],未改变2.10 reverse() - 反转数组(会修改原数组!)
- 作用:反转数组中的元素,并返回原数组。同样会改变原数组。如需不可变操作,先拷贝。
const arr = [1, 2, 3];
const reversedArr = [...arr].reverse(); // 新数组 [3, 2, 1]2.11 slice() - 截取数组
- 作用:返回一个新的数组对象,这一对象是一个由
start和end决定的原数组的浅拷贝(包括start,不包括end)。不会修改原数组。 - 场景:提取数组的子集,或浅拷贝整个数组
slice(0)或slice()。
const arr = [1, 2, 3, 4];
const newArr = arr.slice(1, 3); // [2, 3]2.12 splice() - 删除/插入/替换(会修改原数组!)
- 作用:通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。会修改原数组。应尽量避免在复杂逻辑中直接使用,或用不可变方式替代。
- 参数:
(start, deleteCount, item1, item2, ...)
const arr = [1, 2, 3];
const removed = arr.splice(1, 1); // 从索引1开始删1个,removed = [2], arr = [1, 3]
arr.splice(1, 0, 99); // 在索引1处插入99,arr = [1, 99, 3]三、对象核心处理函数
3.1 Object.keys() / values() / entries() - 对象遍历三剑客
Object.keys(obj):返回一个由对象的键名组成的数组。Object.values(obj):返回一个由对象的值组成的数组。Object.entries(obj):返回一个由对象的键值对组成的二维数组,每个内部项是[key, value]。场景:
- 遍历对象的属性,替代过去的
for...in。 - 把对象转成数组,方便使用
map、filter等数组方法处理。 - 渲染键值对形式的详情表格。
- 遍历对象的属性,替代过去的
// 对象过滤示例:只保留指定属性
const obj = { name: '张三', age: 25, gender: '男' };
const picked = Object.fromEntries(
Object.entries(obj).filter(([key]) => ['name', 'age'].includes(key))
);
// 结果: { name: '张三', age: 25 }3.2 Object.fromEntries() - 键值对转回对象
- 作用:把键值对列表(如
Map或entries返回的二维数组)转换成一个对象。是Object.entries()的逆操作。 场景:
- 解析URL查询参数。
- 过滤/转换对象的属性,先转成
entries处理,再转回来。 - 把
Map转成普通对象。
3.3 Object.assign() - 对象合并/浅拷贝
- 作用:将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
- 场景:对象合并。在现代JavaScript中,更推荐使用展开运算符
...来实现,因为它更简洁、更直观。
const obj1 = { a: 1 };
const obj2 = { b: 2 };
const newObj = Object.assign({}, obj1, obj2); // 等价于 const newObj = { ...obj1, ...obj2 };
// newObj: { a: 1, b: 2 }四、实用操作符与技巧
4.1 可选链 ?. 与空值合并 ??
- 可选链
?.:当访问嵌套对象的属性时,如果中间某个属性不存在(是null或undefined),不会报错,直接返回undefined。语法:obj?.prop、obj?.[expr]、func?.()。 - 空值合并
??:只有当左边的值是null或undefined时,才返回右边的默认值。与||的区别在于,??不会把0、''、false这些假值当成无效值。 - 场景:安全地访问深层嵌套属性,给表单字段设置默认值。
const user = { profile: { name: '张三' } };
console.log(user?.profile?.age); // undefined,不会报错
console.log(user?.address?.city); // undefined
const value = 0;
const defaultValue = value || 'default'; // 'default',因为 0 是假值
const safeDefault = value ?? 'default'; // 0,因为 0 不是 null/undefined4.2 剩余运算符与展开运算符 (...)
- 剩余运算符 (Rest):用在函数参数或解构赋值中,将剩余的参数或属性收集到一个数组或对象中。
- 展开运算符 (Spread):用在数组或对象字面量中,将一个可迭代对象(如数组)或对象“展开”到新的数组或对象中。
// 解构:收集剩余属性
const currentNode = { children: [], id: 1, name: 'root', type: 'folder' };
const { children, ...editMenuData } = currentNode;
// editMenuData 为 { id: 1, name: 'root', type: 'folder' }
// 对象合并
const obj1 = { a: 1 };
const obj2 = { b: 2, ...obj1 }; // { b: 2, a: 1 }
// 数组去重
const arr = [1, 2, 2, 3];
const uniqueArr = [...new Set(arr)]; // [1, 2, 3]4.3 深拷贝与浅拷贝
- 浅拷贝:只拷贝对象的第一层属性,如果属性值是引用类型,则拷贝的是其引用。方法:展开运算符
...、Object.assign()。 深拷贝:递归地拷贝对象的所有层级,生成一个完全独立的副本。
structuredClone():现代浏览器和Node.js推荐的原生API,支持大多数类型(如Date、Map、Set)。JSON.parse(JSON.stringify(obj)):简单粗暴,但只适用于纯数据对象(不含函数、undefined、循环引用等)。- Lodash
_.cloneDeep:工业级最稳方案。
五、函数式编程高级实战
5.1 函数组合 pipe / compose - 数据流水线
函数组合是将多个简单函数组合成一个复杂函数,数据从左到右(pipe)或从右到左(compose)依次流过每个函数。这是函数式编程的核心思想,能极大提升代码的可读性和可维护性。
// 从左到右执行(pipe,更符合人类阅读习惯,企业更常用)
const pipe = (...fns) => (initValue) => fns.reduce((acc, fn) => fn(acc), initValue);
// 从右到左执行(compose,经典函数式写法)
const compose = (...fns) => (initValue) => fns.reduceRight((acc, fn) => fn(acc), initValue);
// 实战场景:后端接口数据全链路处理
const apiData = [
{ goods_id: 1, goods_name: '手机', price: 5000, status: 1 },
{ goods_id: 2, goods_name: '充电器', price: 50, status: 1 },
{ goods_id: 3, goods_name: '耳机', price: 0, status: 0 } // 无效数据
];
// 步骤1:过滤有效数据(价格>0 且 状态=1)
const filterValid = (list) => list.filter(item => item.price > 0 && item.status === 1);
// 步骤2:字段映射(后端字段转前端字段)
const mapField = (list) => list.map(({ goods_id: id, goods_name: name, price }) => ({ id, name, price }));
// 步骤3:按价格排序
const sortByPrice = (list) => [...list].sort((a, b) => a.price - b.price);
// 步骤4:分组统计
const groupByPriceRange = (list) => list.reduce((acc, item) => {
const range = item.price < 100 ? '低价' : item.price < 500 ? '中价' : '高价';
acc[range] = [...(acc[range] || []), item];
return acc;
}, {});
// 把所有步骤串成流水线,代码线性、可读性拉满
const processGoodsData = pipe(filterValid, mapField, sortByPrice, groupByPriceRange);
// 直接使用
const result = processGoodsData(apiData);
console.log(result);
// 输出: { 高价: [ { id: 1, name: '手机', price: 5000 } ], 低价: [ { id: 2, name: '充电器', price: 50 } ] }5.2 柯里化 (Currying)
柯里化是将一个接受多个参数的函数转换为一系列接受单个参数的函数的技术。它允许你部分应用函数,预置一些参数。
const curry = (fn) => (...args) => {
if (args.length >= fn.length) {
return fn(...args);
} else {
return curry(fn.bind(null, ...args));
}
};
// 示例:一个计算价格的函数
const calcPrice = (price, tax, discount) => price * (1 + tax) * discount;
const curriedCalc = curry(calcPrice);
const priceWithTax = curriedCalc(100); // 预置价格100,返回一个新函数
const finalPrice = priceWithTax(0.1)(0.9); // 再传入税率0.1和折扣0.9,结果:99
console.log(finalPrice);六、异步编程高级实战
6.1 带限流的异步并发控制 (Async Pool)
解决批量请求并发过高导致服务端压力大或浏览器限制的问题。核心思想是始终维持最多 limit 个异步任务同时执行。
/**
* 异步任务并发控制
* @param {Array<Function>} tasks 返回 Promise 的任务函数数组
* @param {number} limit 最大并发数
* @returns {Promise<Array>} 所有任务的结果数组(按原顺序)
*/
const asyncPool = async (tasks, limit = 3) => {
const results = [];
const running = []; // 存放正在执行的 Promise
for (const task of tasks) {
// 包装任务,确保它是一个 Promise
const promise = Promise.resolve().then(() => task());
results.push(promise);
// 任务执行完后,从 running 数组中移除它
const finishPromise = promise.then(() => {
const index = running.indexOf(finishPromise);
if (index > -1) running.splice(index, 1);
});
running.push(finishPromise);
// 当并发数达到限制时,等待任意一个任务完成
if (running.length >= limit) {
await Promise.race(running);
}
}
// 等待所有任务完成
return Promise.all(results);
};
// 使用示例
const createTask = (id, delay) => () => new Promise(resolve => {
console.log(`任务 ${id} 开始`);
setTimeout(() => {
console.log(`任务 ${id} 结束`);
resolve(id);
}, delay);
});
const tasks = [
createTask(1, 1000),
createTask(2, 500),
createTask(3, 800),
createTask(4, 300),
createTask(5, 200)
];
asyncPool(tasks, 2).then(results => console.log('所有任务结果:', results));七、数据处理与性能优化
7.1 不可变数据更新 (Immutable Update)
在React/Vue等框架开发中,直接修改状态可能导致视图不更新或难以追踪Bug。必须遵循“不修改原数据”的原则。
- 改深层对象:用
...逐层展开复制。 - 改数组元素:用
map,在需要修改的元素上返回新对象。 - 删数组元素:用
filter,保留不需要删除的元素。 - 加数组元素:用
[...arr, newItem]。
// 原始状态
const state = {
user: {
name: '张三',
profile: { age: 25, city: '北京' }
},
posts: [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }]
};
// 1. 修改深层属性 (user.profile.city 改为 '上海')
const newState1 = {
...state,
user: {
...state.user,
profile: {
...state.user.profile,
city: '上海'
}
}
};
// 2. 修改数组元素 (将 id 为 2 的帖子标题改为 'New Title')
const newState2 = {
...state,
posts: state.posts.map(post =>
post.id === 2 ? { ...post, title: 'New Title' } : post
)
};
// 3. 删除数组元素 (删除 id 为 1 的帖子)
const newState3 = {
...state,
posts: state.posts.filter(post => post.id !== 1)
};
// 4. 添加数组元素 (在末尾添加新帖子)
const newPost = { id: 3, title: 'Post 3' };
const newState4 = {
...state,
posts: [...state.posts, newPost]
};7.2 函数记忆化 (Memoization)
缓存函数执行结果,对于相同参数,直接返回缓存值,避免重复计算。
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
};
// 一个昂贵的计算函数
const expensiveFibonacci = (n) => {
if (n <= 1) return n;
return expensiveFibonacci(n - 1) + expensiveFibonacci(n - 2);
};
const memoizedFibonacci = memoize(expensiveFibonacci);
console.time('第一次');
memoizedFibonacci(40); // 耗时较长
console.timeEnd('第一次');
console.time('第二次');
memoizedFibonacci(40); // 瞬间返回,从缓存读取
console.timeEnd('第二次');7.3 全局错误捕获
在生产环境中,需要捕获未处理的Promise异常和同步错误,防止应用崩溃,并向用户提供友好的提示或记录错误日志。
// 捕获未处理的 Promise rejection
window.addEventListener('unhandledrejection', (event) => {
console.error('未捕获的 Promise 错误:', event.reason);
// 在这里可以调用后端接口上报错误,或向用户展示错误提示
alert('系统出错了,请刷新页面重试。');
});
// 捕获未处理的同步错误
window.addEventListener('error', (event) => {
console.error('未捕获的同步错误:', event.error);
// 上报错误
});
// React 中的错误边界 (Error Boundary) 概念类似7.4 防抖 (Debounce) 与节流 (Throttle)
- 防抖 (Debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。适用于输入框搜索。
- 节流 (Throttle):规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。适用于滚动事件、
resize事件。
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 节流函数
function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
评论