期末复习

软开(三):数组与对象操作

2026 年 06 月 29 日 约 8185 字 · 21 分钟 期末复习

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 类型保留 ✅

适用场景: 现代浏览器环境,需要处理 DateMapSet 等特殊类型。不支持函数。

方法简洁性支持 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:遍历处理每个节点