软开(三):数组与对象操作
JavaScript 数组与对象操作学习报告
本报告重点讲解考试涉及的 JS 核心知识点,结合教材项目中的实际使用场景。
一、数组基础操作
1.1 数组合并与去重
考试题目:将两个无序数字数组 arr1、arr2 合并,要求去重、升序排列。
方法一:Set + 展开运算符(推荐)
let arr1 = [3, 1, 5, 2, 3];
let arr2 = [2, 4, 1, 6, 5];
// 步骤拆解:
// 1. [...arr1, ...arr2] 合并两个数组
// 2. new Set(...) 自动去重(Set 中每个值只出现一次)
// 3. [...new Set(...)] 把 Set 转回数组
// 4. .sort((a, b) => a - b) 升序排列(不写回调函数会按字符串排序,数字会出错!)
let result = [...new Set([...arr1, ...arr2])].sort((a, b) => a - b);
console.log(result); // [1, 2, 3, 4, 5, 6]
方法二:filter 去重
let merged = arr1.concat(arr2); // 合并
let unique = merged.filter((item, index) => merged.indexOf(item) === index); // 去重
let sorted = unique.sort((a, b) => a - b); // 升序
console.log(sorted); // [1, 2, 3, 4, 5, 6]
关于 sort 的注意事项:
// ❌ 错误:不传回调,sort 按字符串排序
[10, 2, 1, 20].sort(); // [1, 10, 2, 20] ← 错误!
// ✅ 正确:传入比较函数
[10, 2, 1, 20].sort((a, b) => a - b); // [1, 2, 10, 20] 升序
[10, 2, 1, 20].sort((a, b) => b - a); // [20, 10, 2, 1] 降序
二、数组深拷贝
考试题目:写出三种数组深拷贝的实现方法,并说明各自适用场景。
为什么需要深拷贝?
let arr = [{ name: '张三' }, { name: '李四' }];
// ❌ 浅拷贝:只复制引用,修改新数组会影响原数组!
let copy1 = arr; // 直接赋值
let copy2 = [...arr]; // 展开运算符(只是浅拷贝)
let copy3 = arr.slice(); // slice(只是浅拷贝)
copy2[0].name = '王五';
console.log(arr[0].name); // '王五' ← 原数组被改了!
// ✅ 深拷贝:完全独立的副本,修改新数组不影响原数组
方法一:JSON 序列化(最简单)
let original = [{ name: '张三', score: 88 }, { name: '李四', score: 95 }];
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy[0].name = '王五';
console.log(original[0].name); // '张三' ← 原数组未被影响 ✅
适用场景: 数据是纯 JSON 格式(只有普通对象、数组、字符串、数字、布尔值)。
局限性:
undefined、函数、Symbol会丢失Date对象会变成字符串- 不能处理循环引用
方法二:递归深拷贝(通用)
function deepClone(obj) {
// 基本类型直接返回
if (obj === null || typeof obj !== 'object') return obj;
// 处理数组
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
// 处理普通对象
let clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]); // 递归复制每个属性
}
}
return clone;
}
let original = [{ name: '张三', scores: [88, 90, 95] }];
let copy = deepClone(original);
copy[0].scores.push(100);
console.log(original[0].scores); // [88, 90, 95] ← 未受影响 ✅
适用场景: 需要处理 undefined、嵌套复杂结构的场景。
方法三:structuredClone(现代 API)
let original = [{ name: '张三', date: new Date() }];
let copy = structuredClone(original); // 浏览器原生 API,ES2022+
copy[0].name = '李四';
console.log(original[0].name); // '张三' ← 未受影响 ✅
console.log(original[0].date instanceof Date); // true ← Date 类型保留 ✅
适用场景: 现代浏览器环境,需要处理 Date、Map、Set 等特殊类型。不支持函数。
| 方法 | 简洁性 | 支持 Date | 支持函数 | 适用场景 |
|---|---|---|---|---|
| JSON.parse/stringify | ⭐⭐⭐ | ❌ | ❌ | 纯数据对象 |
| 递归深拷贝 | ⭐ | ✅ | ❌ | 复杂嵌套对象 |
| structuredClone | ⭐⭐⭐ | ✅ | ❌ | 现代环境 |
三、数组 map() 方法
考试题目:用 map() 遍历成绩数组,生成含等级的新数组。
3.1 map() 的基本语法
// map() 对数组的每个元素执行函数,返回一个新数组
// 原数组不变,新数组长度 = 原数组长度
let newArr = arr.map((item, index) => {
// item:当前元素
// index:当前下标(可选)
return 新元素; // 返回值组成新数组
});
3.2 考试题答案
const scoreList = [
{ name: "张三", subject: "语文", score: 88 },
{ name: "李四", subject: "数学", score: 95 },
{ name: "王五", subject: "英语", score: 76 },
{ name: "赵六", subject: "计算机", score: 100 }
];
const result = scoreList.map(item => {
// 根据分数判断等级
let level;
if (item.score >= 90) {
level = '优秀';
} else if (item.score >= 80) {
level = '良好';
} else if (item.score >= 60) {
level = '及格';
} else {
level = '不及格';
}
// 返回新对象(包含原有字段 + 新的 level 字段)
return {
name: item.name,
subject: item.subject,
score: item.score,
level: level
};
});
console.log(result);
// [
// { name: '张三', subject: '语文', score: 88, level: '良好' },
// { name: '李四', subject: '数学', score: 95, level: '优秀' },
// { name: '王五', subject: '英语', score: 76, level: '及格' },
// { name: '赵六', subject: '计算机', score: 100, level: '优秀' }
// ]
简化写法(三元运算符):
const result = scoreList.map(({ name, subject, score }) => ({
name,
subject,
score,
level: score >= 90 ? '优秀'
: score >= 80 ? '良好'
: score >= 60 ? '及格'
: '不及格'
}));
四、reduce() 方法
考试题目:用 reduce 计算每个订单的商品总金额,添加 totalAmount。
4.1 reduce() 的基本语法
// reduce() 把数组"归并"成一个值
arr.reduce((accumulator, currentItem) => {
return 新的累积值;
}, 初始值);
// accumulator:累积器(上一次回调的返回值,或初始值)
// currentItem:当前元素
4.2 简单示例
let numbers = [1, 2, 3, 4, 5];
// 求和
let sum = numbers.reduce((acc, curr) => acc + curr, 0);
console.log(sum); // 15
// 求最大值
let max = numbers.reduce((acc, curr) => curr > acc ? curr : acc, 0);
console.log(max); // 5
4.3 考试题答案
订单结构:
{
"orderid": 20010,
"customerid": "AHDTSM",
"items": [
{"discount": 0.03, "quantity": 100, "productid": 1001, "unitprice": 35.50},
{"discount": 0.00, "quantity": 50, "productid": 1002, "unitprice": 20.00}
],
"freight": 30.00
}
// 计算每个订单的商品总金额,添加 totalAmount 属性
const ordersWithTotal = orders.map(order => {
// 用 reduce 累加 items 中每个商品的金额
const totalAmount = order.items.reduce((sum, item) => {
// 商品金额 = 单价 × 数量 × (1 - 折扣)
const itemAmount = item.unitprice * item.quantity * (1 - item.discount);
return sum + itemAmount;
}, 0); // 初始值为 0
// 返回原订单对象 + totalAmount 字段
return {
...order, // 展开原订单的所有字段
totalAmount: parseFloat(totalAmount.toFixed(2)) // 保留两位小数
};
});
// 第一个订单的计算过程:
// 商品1:35.50 × 100 × (1 - 0.03) = 35.50 × 100 × 0.97 = 3443.50
// 商品2:20.00 × 50 × (1 - 0.00) = 20.00 × 50 × 1.00 = 1000.00
// totalAmount = 3443.50 + 1000.00 = 4443.50
五、客户年度销售额 Top10
考试题目:汇总每个客户2018年度的销售额,返回订单数量最多的Top10。
5.1 解题思路
1. 建立客户 ID → 客户信息的映射表(Map)
2. 遍历订单,筛选出 2018 年的订单
3. 按客户 ID 累加销售额和订单数量
4. 排序,取前 10
5.2 完整代码
// 假设已有 customers 数组和 orders 数组
// 第一步:建立客户 ID 到客户信息的映射表
// 方便后续快速通过 customerid 找到客户名称,不需要每次遍历
const customerMap = {};
customers.forEach(customer => {
customerMap[customer.customerid.trim()] = customer;
// 注意:customerid 可能有空格,trim() 去掉首尾空格
});
// customerMap 结构:
// {
// 'AHDTSM': { customerid: 'AHDTSM', companyname: '顶天商贸有限公司', ... },
// 'AHPPSP': { customerid: 'AHPPSP', companyname: '盼盼食品有限公司', ... },
// ...
// }
// 第二步:遍历订单,按客户统计2018年的销售额和订单数
const customerStats = {};
orders.forEach(order => {
// 筛选2018年的订单
const year = new Date(order.orderdate).getFullYear();
if (year !== 2018) return; // 不是2018年,跳过
const cid = order.customerid.trim(); // 去掉空格
// 如果该客户还没有统计记录,初始化
if (!customerStats[cid]) {
customerStats[cid] = {
customerid: cid,
companyname: customerMap[cid]?.companyname || '未知客户',
orderCount: 0, // 订单数量
totalSales: 0, // 总销售额
};
}
// 累加订单数量
customerStats[cid].orderCount += 1;
// 累加该订单的销售额(items 中每项的金额)
order.items.forEach(item => {
customerStats[cid].totalSales +=
item.unitprice * item.quantity * (1 - item.discount);
});
});
// 第三步:转成数组,按订单数量降序排列,取前10
const top10 = Object.values(customerStats)
.sort((a, b) => b.orderCount - a.orderCount) // 按订单数量降序
.slice(0, 10) // 取前10个
.map((item, index) => ({
rank: index + 1, // 名次
customerid: item.customerid,
companyname: item.companyname,
orderCount: item.orderCount,
totalSales: parseFloat(item.totalSales.toFixed(2))
}));
console.log(top10);
关键知识点:Object.values()
const obj = { a: 1, b: 2, c: 3 };
Object.values(obj); // [1, 2, 3] ← 把对象的所有值转成数组
Object.keys(obj); // ['a', 'b', 'c'] ← 把对象的所有键转成数组
Object.entries(obj); // [['a',1], ['b',2], ['c',3]] ← 键值对数组
六、树结构操作
考试题目:实现 insertNode(tree, parentId, newNode),在指定父节点下插入子节点,并更新 level、ancestor、isparentflag。
6.1 树结构数据格式
[
{
"id": "A",
"text": "饮料",
"level": 1,
"ancestor": "",
"isparentflag": 1,
"parentnodeid": "",
"children": [
{
"id": "A1",
"text": "非酒精饮料",
"level": 2,
"ancestor": "A#",
"isparentflag": 0,
"parentnodeid": "A",
"children": []
}
]
}
]
字段含义: | 字段 | 含义 | 示例 | |——|——|——| | id | 节点唯一标识 | "A1" | | level | 层级深度 | 根节点=1,子节点=2 | | ancestor | 所有祖先节点 ID,用 # 连接 | "A#" | | isparentflag | 是否有子节点:1=是,0=否 | 1 | | parentnodeid | 直接父节点的 ID | "A" |
6.2 完整实现
/**
* 在树中找到指定 parentId 的节点,并插入新子节点
* @param {Array} tree - 树形数组
* @param {string} parentId - 目标父节点的 ID
* @param {Object} newNode - 要插入的新节点(包含 id、text 等基本信息)
* @returns {boolean} - 是否插入成功
*/
function insertNode(tree, parentId, newNode) {
for (let node of tree) {
if (node.id === parentId) {
// 找到了父节点,准备插入
// 1. 计算新节点的层级 = 父节点层级 + 1
newNode.level = node.level + 1;
// 2. 计算新节点的祖先路径 = 父节点祖先 + 父节点ID + '#'
newNode.ancestor = node.ancestor + node.id + '#';
// 3. 设置新节点的父节点ID
newNode.parentnodeid = node.id;
// 4. 新节点默认没有子节点
newNode.isparentflag = 0;
if (!newNode.children) newNode.children = [];
// 5. 父节点现在有子节点了,更新 isparentflag
node.isparentflag = 1;
// 6. 确保父节点的 children 数组存在
if (!node.children) node.children = [];
// 7. 插入新节点
node.children.push(newNode);
return true; // 插入成功
}
// 没找到,递归在子节点里找
if (node.children && node.children.length > 0) {
let found = insertNode(node.children, parentId, newNode);
if (found) return true; // 子树里找到并插入了,停止递归
}
}
return false; // 没找到父节点
}
// 使用示例:
let tree = [
{
id: 'A', text: '饮料', level: 1, ancestor: '',
isparentflag: 1, parentnodeid: '',
children: [
{ id: 'A1', text: '非酒精饮料', level: 2, ancestor: 'A#', isparentflag: 0, parentnodeid: 'A', children: [] }
]
}
];
let newNode = { id: 'A1-1', text: '矿泉水' };
insertNode(tree, 'A1', newNode);
// 结果:newNode 被插入到 A1 节点下
// newNode = {
// id: 'A1-1',
// text: '矿泉水',
// level: 3, ← 自动计算
// ancestor: 'A#A1#', ← 自动计算
// parentnodeid: 'A1',← 自动设置
// isparentflag: 0, ← 新节点没有子节点
// children: []
// }
// A1 节点的 isparentflag 也被更新为 1
6.3 递归函数的理解
insertNode(tree, 'A1', newNode)
↓ 遍历第一层:找到 node.id='A',不是 'A1'
↓ A 有 children,递归进去
insertNode(A.children, 'A1', newNode)
↓ 遍历:找到 node.id='A1' ✅
↓ 执行插入逻辑
↓ return true
↑ 递归返回 true,外层停止继续遍历
七、常用数组方法速查表
| 方法 | 作用 | 返回值 | 是否修改原数组 |
|---|---|---|---|
map(fn) | 每个元素转换,生成新数组 | 新数组 | ❌ |
filter(fn) | 筛选满足条件的元素 | 新数组 | ❌ |
reduce(fn, init) | 累积归并成一个值 | 任意值 | ❌ |
find(fn) | 找第一个满足条件的元素 | 元素或 undefined | ❌ |
findIndex(fn) | 找第一个满足条件的下标 | 下标或 -1 | ❌ |
forEach(fn) | 遍历每个元素(无返回值) | undefined | ❌ |
some(fn) | 是否有元素满足条件 | boolean | ❌ |
every(fn) | 是否所有元素满足条件 | boolean | ❌ |
sort(fn) | 排序 | 排序后的数组 | ✅ 修改原数组 |
push(item) | 末尾添加元素 | 新长度 | ✅ |
splice(i, n) | 删除/插入元素 | 被删除的元素 | ✅ |
concat(arr) | 合并数组 | 新数组 | ❌ |
slice(i, j) | 截取子数组 | 新数组 | ❌ |
教材项目中频繁使用:
findIndex:在数组中找到某行的下标(如定位表格某行)filter:删除树节点(过滤掉要删除的节点)map:更新某个节点的数据forEach:遍历处理每个节点