今天小项目的效果如下

昨天我们用 JS 把 MiniMall "点亮"了------能搜索、能加购物车、能校验表单。但如果你回看那段代码,会发现里面其实已经偷偷用了一堆"现代语法":
filter、map、解构、模板字符串、??。它们零散地散落各处,你可能是一边抄一边用。今天专门把这些 ES6+ 语法 系统串一遍:解构、展开、
map/filter/reduce、Map/Set、Promise/async-await、模块化、class。学完今天,你处理"列表数据"(商品、订单、日志)会像呼吸一样自然,也为明天正式调后端接口打好了地基。文末同样有一个完整
index.html:一个 MiniMall 运营数据看板 ------用map/filter/reduce把一堆订单跑出排序、分组、销售额统计、Top 榜,数据还是用async/await模拟"异步加载"的。全程零框架,复制保存即可打开。
一、先站在全局看:ES6+ 到底解决了什么
ES6(2015 年发布,之后每年一版,统称 ES2015+ / ES6+)不是"另一门语言",而是给 JavaScript 补齐了一批"其它语言早就有"的能力。今天的内容可以归成四条主线:
| 主线 | 解决什么 | 关键词 |
|---|---|---|
| 取数据更顺手 | 从对象/数组里优雅地"拿"和"放" | 解构、展开/剩余、可选链、空值合并 |
| 处理列表更清晰 | 把"循环 + if + push"变成声明式 | map / filter / reduce / find / sort |
| 异步不再绕 | 告别"回调地狱",像写同步一样写异步 | Promise、async / await |
| 组织代码更清爽 | 拆分文件、封装结构 | import / export、class、模块作用域 |
一句话定位:ES6+ 让"处理数据"和"组织代码"这两件事,从'能跑就行'升级到'好读好维护'。 今天的练习全部围绕"列表数据",因为这正是前端 90% 的日常------后端给你一坨 JSON 数组,你要把它变成页面上的表格、图表、统计。
二、解构进阶:优雅地"从数据里拿东西"
昨天你见过 const { title, price } = product。解构远不止于此。
js
const order = {
id: 1001,
customer: { name: '张三', vip: true },
items: [
{ sku: 'A1', name: '耳机', price: 299, qty: 1 },
{ sku: 'B2', name: '键盘', price: 199, qty: 2 },
],
status: 'paid',
};
// 1. 对象解构 + 重命名 + 默认值
const { id: orderId, status = 'unknown', coupon = '无' } = order;
// orderId=1001, status='paid', coupon='无'(字段不存在才用默认值)
// 2. 嵌套解构:一层层往里拿
const { customer: { name, vip } } = order; // name='张三', vip=true
// 3. 数组解构:按位置拿
const [firstItem, secondItem] = order.items; // 第 1、2 个
const [a, , c] = [10, 20, 30]; // 用逗号跳过:a=10, c=30
const [head, ...tail] = order.items; // 剩余运算符:head=第1个, tail=其余数组
// 4. 交换两个变量(不用临时变量!)
let x = 1, y = 2;
[x, y] = [y, x]; // x=2, y=1
函数参数里解构,是最实用的场景------调用方传一个大对象,函数签名里直接声明要哪几个字段:
js
function renderOrder({ id, customer: { name }, items = [] }) {
console.log(`订单 #${id} · ${name} · ${items.length} 件`);
}
renderOrder(order); // 订单 #1001 · 张三 · 2 件
为什么好用?函数参数顺序不再重要、缺字段有默认值兜底、看签名就知道这个函数依赖哪些数据。React/Vue 里组件的 props、Vue 的
setup,本质都在用这一招。
三、展开与剩余:... 的三张面孔
... 出现在左边 是"解构收集"(上面见过),出现在右边/调用处是"展开"。三种高频用法:
js
// 1. 展开数组/对象:合并、拷贝、改字段
const oldList = [1, 2, 3];
const newList = [...oldList, 4, 5]; // [1,2,3,4,5] 拼接
const copy = [...oldList]; // 浅拷贝数组
const updated = { ...order, status: 'shipped' }; // 浅拷贝对象 + 改一个字段
// ⚠️ 浅拷贝:updated.customer 和 order.customer 指向同一个对象
// 2. 去重(配合 Set,第六节细讲)
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1,2,3]
// 3. 剩余参数:函数接收不定数量参数
function sum(...nums) { // nums 是数组
return nums.reduce((s, n) => s + n, 0);
}
sum(1, 2, 3, 4); // 10
// 4. 函数调用时展开实参
Math.max(...[5, 12, 3]); // 12(等价 Math.max(5,12,3))
浅拷贝的坑要记牢 :
{ ...obj }只复制第一层。嵌套对象仍是引用,改了updated.customer.name会把原order.customer.name也改掉。需要深拷贝时用structuredClone(obj)(现代浏览器内置)或JSON.parse(JSON.stringify(obj))(简单场景,会丢函数/undefined)。
四、map / filter / reduce:处理列表的"三件套"(今天的核心)
这是今天最重要的一节。规则只有一个:凡是你想"遍历数组、边循环边判断边 push",先停下来,问自己能不能用这三件套替代。
js
const products = [
{ id: 1, name: '耳机', cat: '数码', price: 299, sales: 320, stock: 10 },
{ id: 2, name: '键盘', cat: '数码', price: 199, sales: 150, stock: 0 },
{ id: 3, name: '保温杯', cat: '家居', price: 89, sales: 800, stock: 50 },
{ id: 4, name: '台灯', cat: '家居', price: 129, sales: 260, stock: 20 },
{ id: 5, name: '跑鞋', cat: '运动', price: 459, sales: 90, stock: 5 },
];
map:一个一个变(数量不变,换个形状)
js
const names = products.map(p => p.name); // ['耳机','键盘',...]
const labels = products.map(p => `${p.name}(${p.cat})`);
filter:挑出符合的(数量变少)
js
const inStock = products.filter(p => p.stock > 0); // 有货的
const richSales = products.filter(p => p.sales > 200); // 销量>200
reduce:汇聚成一个值(累加/汇总/分组)
js
const totalValue = products.reduce((s, p) => s + p.price * p.stock, 0); // 库存总值
const maxSales = products.reduce((m, p) => p.sales > m.sales ? p : m); // 销量冠军
reduce 的高级玩法------分组(group by),这是后端/BI 里的常客:
js
const byCat = products.reduce((acc, p) => {
(acc[p.cat] ||= []).push(p); // ||= 该键不存在时初始化成空数组
return acc;
}, {});
// { 数码:[耳机,键盘], 家居:[保温杯,台灯], 运动:[跑鞋] }
链式组合 = 声明式数据流水线的威力:
js
// 需求:找出"有货的商品,按销售额从高到低,取前 3 名的名字"
const top3 = products
.filter(p => p.stock > 0)
.map(p => ({ ...p, revenue: p.price * p.sales }))
.sort((a, b) => b.revenue - a.revenue)
.slice(0, 3)
.map(p => p.name);
对比一下:同样这段逻辑用
for+if+ 临时变量写,要十几行且容易写错;链式写出来就是"数据流水"------先过滤、再加字段、再排序、再取头部、最后取名字,读起来几乎就是中文需求本身。这就是"声明式"的价值。
reduce心法 :把它想成一个"雪球"。你给它一个初始值(雪球核,上面的0/{}/ 第一个商品),然后它滚过数组每个元素,每滚一个就把雪球更新一次,最后交还给你。map/filter能做的,reduce理论都能做,但别硬用------能一眼看懂哪个用哪个。
五、其它高频数组方法:补齐工具箱
js
products.find(p => p.id === 3); // 找第一个 → 对象或 undefined
products.findIndex(p => p.stock === 0); // 找下标 → 数字,找不到 -1
products.some(p => p.stock === 0); // 有没有任一缺货 → true
products.every(p => p.price > 50); // 是否全都 > 50 → true
products.includes(x); // 判断"是否包含某元素"(基本类型用)
// sort ------ ⚠️ 原地改变原数组!且默认按字符串排(数字会出错)
[10, 1, 100].sort(); // [1, 10, 100] 结果正确
[10, 1, 100].sort((a, b) => a - b); // ✅ 数字排序必须给比较函数
[...products].sort((a, b) => a.price - b.price); // ✅ 先展开拷贝,别污染原数组
// flat:把嵌套数组拍平
[1, [2, [3]]].flat(Infinity); // [1, 2, 3]
两个必须刻进脑子的坑 :①
sort会原地修改 原数组,想保留原序就先[...arr]拷一份再排;②sort不带比较函数时按字符编码 排,[10,1,100]恰好对了是运气,[30,100,9]会得到[100,30,9]------数字排序永远写(a,b)=>a-b(升序)/(a,b)=>b-a(降序)。
六、Map 与 Set:比 {}/[] 更合适的容器
js
// Set:自动去重,判断"是否存在"极快
const cats = new Set(products.map(p => p.cat)); // {'数码','家居','运动'}
cats.has('数码'); // true
[...cats]; // 转回数组 ['数码','家居','运动']
// Map:键可以是任意类型(对象也行),且保持插入顺序,自带 size
const stockById = new Map(products.map(p => [p.id, p.stock]));
stockById.get(3); // 50
stockById.set(1, 0); // 修改
stockById.size; // 5
什么时候用谁:
- 要去重 、要快速判断"有没有" →
Set(new Set(arr).has(x)比arr.includes(x)在大数组上快得多)。 - 要做计数/索引/分组统计 、键不是字符串 →
Map(对象也能凑,但Map语义更清晰、size更直观、删除更干净)。
js
// 用 Map 做一次"分类计数"
const countByCat = new Map();
for (const p of products) countByCat.set(p.cat, (countByCat.get(p.cat) || 0) + 1);
七、异步编程:从 Promise 到 async / await(为明天调接口做准备)
昨天末尾我们埋了个伏笔:setTimeout、网络请求这类"要等一会儿才有结果"的操作,是异步的------它们不会卡住后面的代码,而是"稍后再来告诉你结果"。今天正式认识它。
第一步:Promise------一个"未来才会有结果"的容器 ,三种状态:pending(等待中)→ fulfilled(成功,带值)/ rejected(失败,带原因)。
js
function loadOrders() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const ok = true;
if (ok) resolve([{ id: 1 }, { id: 2 }]); // 成功:交出数据
else reject(new Error('网络挂了')); // 失败:交出原因
}, 800);
});
}
// 老写法:.then / .catch
loadOrders()
.then(orders => console.log('拿到', orders))
.catch(err => console.error(err))
.finally(() => console.log('无论成败都会执行,用来关 loading'));
第二步:async / await------把 Promise 写得像同步(今天的重点):
js
async function renderDashboard() {
showLoading(true);
try {
const orders = await loadOrders(); // ← 像同步赋值一样拿到结果
const total = orders.length;
render(orders);
} catch (err) {
showError(err.message); // await 失败会走 catch,比 .catch 直观
} finally {
showLoading(false);
}
}
并发:要同时发多个请求,别串行 await:
js
// ❌ 串行:一个等完再等下一个,慢
const a = await fetchA();
const b = await fetchB();
// ✅ 并行:Promise.all 一起发,全部成功才有结果
const [a, b] = await Promise.all([fetchA(), fetchB()]);
// Promise.allSettled:不管成败,全部等完返回各自状态(一个失败不拖垮其它)
await只能写在async函数里(顶层模块除外)。它的作用就是把"等 Promise 兑现"这件事,从回调链变成一行赋值,代码立刻线性、可读、能配合try/catch。明天讲 TypeScript + Axios 调后端接口,用的就是async/await,今天务必练熟。
八、模块化:import / export 把代码拆干净
一个页面的 JS 越写越长,就该拆分文件。ES6 模块用 export 暴露、import 引入:
js
// ===== utils.js =====
export const money = n => '¥' + n.toFixed(2); // 命名导出
export function sum(arr) { return arr.reduce((s, n) => s + n, 0); }
export default class Chart { /* ... */ } // 默认导出(每文件最多一个)
// ===== main.js =====
import Chart, { money, sum } from './utils.js'; // 默认 + 命名一起引
import * as utils from './utils.js'; // 整包引:utils.money(...)
和今天小项目的关系 :真实项目里,
loadOrders()(数据)、render()(渲染)、money()(格式化)本该各放一个文件用import组织。今天的博客 demo 为了方便"复制打开即运行",把它们都写在一个<script type="module">里------但你在代码里能看到我用注释分块模拟了模块边界。明天开始,我们会正式拆文件、用打包工具。
<script type="module">有个附带好处:天然带defer(等 HTML 解析完再执行),且有自己的模块作用域,变量不会污染全局。
九、class 与对象增强:组织"有行为的数据"
需要"同一类东西、共享方法"时(比如多个购物车实例、多个图表组件),用 class:
js
class Cart {
#items = {}; // # 开头 = 私有字段,外部访问不到
add(id, qty = 1) { this.#items[id] = (this.#items[id] || 0) + qty; return this; } // 链式
count() { return Object.values(this.#items).reduce((s, q) => s + q, 0); }
get isEmpty() { return this.count() === 0; } // getter:像属性一样访问
}
const cart = new Cart().add('A1').add('B2', 2); // 链式调用
cart.isEmpty; // false
对象字面量的几个现代简写:
js
const name = '耳机', price = 299;
const p = {
name, // 属性名简写(等价 name: name)
price,
show() { return `${this.name} ¥${this.price}`; }, // 方法简写
[`sku_${Date.now()}`]: '自动生成键', // 计算属性名
};
// Object 家族工具方法(处理字典超好用)
Object.keys(p); // 所有键 → 数组
Object.values(p); // 所有值 → 数组
Object.entries(p); // [键,值] 数组对 → 常用于 Object.fromEntries 反向组装
Object.assign({}, p); // 浅合并(等价 { ...p })
十、ES6 新手必踩的坑 + 调试
map回调忘了return:arr.map(x => { x * 2 })得到一坨undefined------箭头函数一旦加了{},就必须手写return。单表达式别加花括号。sort原地改 + 数字排序:见第五节,拷贝后再排、数字一定给比较函数。reduce不设初始值 :[1,2,3].reduce((s,x)=>s+x)没问题,但空数组[].reduce(...)不带初始值会直接报错。养成习惯永远给第二个参数 (0/[]/{})。forEach里想break?做不到 :要中途退出用for...of+break,或先find/some(它们遇到匹配会短路)。Promise吞掉错误 :async函数里await一个会失败的请求却不写try/catch,错误会悄悄变成"未处理的 Promise 拒绝",页面啥反应都没有。凡是await,默认配try/catch。const不是"常量不可变" :const a = [1]; a.push(2)合法(地址没变)。const只保证引用不变,不保证内容不变。
调试和昨天一样:console.table(products) 把对象数组直接渲染成表;异步逻辑里在 await 前后各打一行 console.log,看执行顺序对不对;try/catch 里 console.error(err) 打出真实错误对象,别只打 err.message 丢了堆栈。
今日小项目:MiniMall 运营数据看板(map / filter / reduce 实战)
目标 :模拟运营后台------用 async/await 拉一批订单,再用三件套跑出各种统计与图表,全程原生 ES6+。
重点练 :async/await 异步加载 + loading、map/filter/reduce 链式、reduce 分组、Set 去重分类、sort 拷贝后排序、模板字符串渲染看板卡片。
用法 :复制全部内容 → 存成 index.html → 双击打开 → 看"加载中..."后看板自动算出:总销售额、订单数、客单价、分类销售额占比条、销量 Top5 榜、可按分类切换筛选。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MiniMall - Day6 运营数据看板</title>
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{--primary:#6366f1;--danger:#dc2626;--ok:#059669;--text:#1f2937;--muted:#6b7280;--line:#e5e7eb;--bg:#f3f4f6}
body{font-family:system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;
color:var(--text);background:var(--bg);line-height:1.6;min-height:100dvh}
.container{width:100%;max-width:1100px;margin:0 auto;padding:24px 16px}
h1{font-size:1.4rem;font-weight:700}
h1 i{font-style:normal;color:var(--primary)}
.sub{color:var(--muted);font-size:.9rem;margin:2px 0 18px}
.toolbar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:18px}
.chip{padding:6px 16px;background:#fff;border:1px solid var(--line);border-radius:20px;cursor:pointer;font-size:.9rem}
.chip.active{background:var(--primary);color:#fff;border-color:var(--primary)}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin-bottom:18px}
.kpi{background:#fff;border-radius:12px;padding:18px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
.kpi__label{color:var(--muted);font-size:.85rem}
.kpi__value{font-size:1.7rem;font-weight:800;margin-top:4px}
.kpi__value.money{color:var(--danger)}
.kpi__value.ok{color:var(--ok)}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:720px){.grid-2{grid-template-columns:1fr}}
.panel{background:#fff;border-radius:12px;padding:18px;box-shadow:0 1px 3px rgba(0,0,0,.06)}
.panel__title{font-size:1rem;font-weight:600;margin-bottom:14px;padding-bottom:10px;border-bottom:1px solid #f3f4f6}
.bar-row{display:flex;align-items:center;gap:10px;margin-bottom:12px;font-size:.9rem}
.bar-row .cat{width:48px;color:var(--muted)}
.bar-track{flex:1;height:12px;background:#f3f4f6;border-radius:6px;overflow:hidden}
.bar-fill{height:100%;background:linear-gradient(90deg,#6366f1,#8b5cf6);border-radius:6px}
.bar-row .amt{min-width:80px;text-align:right;font-weight:600}
.rank{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid #f6f7f9;font-size:.92rem}
.rank:last-child{border-bottom:none}
.rank__no{width:22px;height:22px;border-radius:50%;background:#eef2ff;color:var(--primary);
display:flex;align-items:center;justify-content:center;font-size:.8rem;font-weight:700}
.rank__no.top{background:var(--primary);color:#fff}
.rank__name{flex:1}
.rank__meta{color:var(--muted);font-size:.82rem}
.rank__rev{font-weight:700;color:var(--danger)}
.loading{text-align:center;color:var(--muted);padding:60px 0}
.spin{display:inline-block;width:18px;height:18px;border:2px solid #d1d5db;border-top-color:var(--primary);
border-radius:50%;animation:sp .7s linear infinite;vertical-align:-4px;margin-right:8px}
@keyframes sp{to{transform:rotate(360deg)}}
footer{text-align:center;color:#9ca3af;font-size:.85rem;padding:22px}
</style>
</head>
<body>
<div class="container">
<h1><i>Mini</i>Mall · 运营数据看板</h1>
<p class="sub">Day 6 配套演示 · async/await 加载 + map / filter / reduce 实时统计(纯原生 ES6+)</p>
<div class="toolbar" id="toolbar"><span class="loading"><span class="spin"></span>正在加载订单...</span></div>
<div id="app"><div class="loading"><span class="spin"></span>正在统计...</span></div></div>
</div>
<footer>© 2026 MiniMall · 《企业级平台技术底座》Day 6 配套演示 · 纯原生 JavaScript ES6+</footer>
<script type="module">
/* ===================== 1. 模块:数据层(模拟后端接口) ===================== */
// 真实项目里这里是 axios.get('/api/orders'),今天用 Promise + setTimeout 模拟"异步拉数据"
function fetchOrders() {
return new Promise((resolve) => {
setTimeout(() => resolve([
{ id: 1001, name: '无线降噪耳机', cat: '数码', price: 299, qty: 320 },
{ id: 1002, name: '机械键盘', cat: '数码', price: 199, qty: 150 },
{ id: 1003, name: '4K 显示器', cat: '数码', price: 1299, qty: 40 },
{ id: 1004, name: '保温杯', cat: '家居', price: 89, qty: 800 },
{ id: 1005, name: '护眼台灯', cat: '家居', price: 129, qty: 260 },
{ id: 1006, name: '乳胶枕', cat: '家居', price: 159, qty: 120 },
{ id: 1007, name: '专业跑鞋', cat: '运动', price: 459, qty: 90 },
{ id: 1008, name: '瑜伽垫', cat: '运动', price: 79, qty: 540 },
{ id: 1009, name: '哑铃套装', cat: '运动', price: 239, qty: 60 },
]), 700);
});
}
/* ===================== 2. 模块:工具函数(对应 utils.js) ===================== */
const money = n => '¥' + n.toLocaleString('zh-CN', { maximumFractionDigits: 0 });
const $ = (sel) => document.querySelector(sel);
/* ===================== 3. 模块:统计逻辑(纯函数,全是三件套) ===================== */
// 给每条订单补一个"销售额"字段 revenue
const withRevenue = (orders) => orders.map(o => ({ ...o, revenue: o.price * o.qty }));
// 汇总 KPI:reduce 一锅出
const summarize = (list) => ({
revenue: list.reduce((s, o) => s + o.revenue, 0),
count: list.length,
units: list.reduce((s, o) => s + o.qty, 0),
avg: list.length ? list.reduce((s, o) => s + o.revenue, 0) / list.length : 0,
});
// 按分类分组统计销售额:reduce + ||= 累加
const revenueByCat = (list) => list.reduce((acc, o) => {
acc[o.cat] = (acc[o.cat] || 0) + o.revenue;
return acc;
}, {});
// 销量 Top N:filter/map/sort/slice 链式
const topSellers = (list, n = 5) =>
[...list].sort((a, b) => b.qty - a.qty).slice(0, n); // 拷贝后排序,不污染原数组
// 分类集合:Set 自动去重
const categories = (list) => [...new Set(list.map(o => o.cat))];
/* ===================== 4. 模块:渲染层 ===================== */
function render(data) {
const s = summarize(data);
const cats = categories(data);
const byCat = revenueByCat(data);
const maxCat = Math.max(...Object.values(byCat));
const tops = topSellers(data, 5);
$('#app').innerHTML = `
<div class="kpis">
<div class="kpi"><div class="kpi__label">总销售额</div><div class="kpi__value money">${money(s.revenue)}</div></div>
<div class="kpi"><div class="kpi__label">订单条数</div><div class="kpi__value">${s.count}</div></div>
<div class="kpi"><div class="kpi__label">总销量</div><div class="kpi__value">${s.units.toLocaleString('zh-CN')} 件</div></div>
<div class="kpi"><div class="kpi__label">平均单条销售额</div><div class="kpi__value ok">${money(s.avg)}</div></div>
</div>
<div class="grid-2">
<div class="panel">
<div class="panel__title">各分类销售额占比</div>
${Object.entries(byCat)
.sort((a, b) => b[1] - a[1])
.map(([cat, amt]) => `
<div class="bar-row">
<span class="cat">${cat}</span>
<div class="bar-track"><div class="bar-fill" style="width:${(amt / maxCat * 100).toFixed(0)}%"></div></div>
<span class="amt">${money(amt)}</span>
</div>`).join('')}
</div>
<div class="panel">
<div class="panel__title">销量 Top 5 爆款</div>
${tops.map((o, i) => `
<div class="rank">
<span class="rank__no ${i === 0 ? 'top' : ''}">${i + 1}</span>
<span class="rank__name">${o.name}</span>
<span class="rank__meta">${o.cat} · 售 ${o.qty}</span>
<span class="rank__rev">${money(o.revenue)}</span>
</div>`).join('')}
</div>
</div>`;
$('#toolbar').innerHTML = `<span class="chip active" data-cat="all">全部(${data.length})</span>` +
cats.map(c => `<span class="chip" data-cat="${c}">${c}</span>`).join('');
}
/* ===================== 5. 模块:交互 + 启动(async / await) ===================== */
let ALL = []; // 全量数据缓存
async function boot() {
try {
const raw = await fetchOrders(); // ← 异步等待,像同步一样接结果
ALL = withRevenue(raw);
render(ALL);
} catch (err) {
$('#app').innerHTML = `<div class="loading">加载失败:${err.message}</div>`;
}
}
// 分类筛选(事件委托,只绑一次)
$('#toolbar').addEventListener('click', (e) => {
const chip = e.target.closest('.chip');
if (!chip) return;
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
const cat = chip.dataset.cat;
render(cat === 'all' ? ALL : ALL.filter(o => o.cat === cat));
});
boot(); // 顶层 await 在 module 里也可直接用;这里用 async 函数更直观
</script>
</body>
</html>
这个页面覆盖了今天的全部要点
✅ async / await + try/catch/finally,用 Promise+setTimeout 模拟接口与 loading
✅ map 补字段、filter 筛选、reduce 汇总 KPI 与按分类分组(acc[cat] ||= ...)
✅ 链式流水线:filter → map → sort → slice → map(Top 榜)
✅ Set 去重得到分类集合、Map/Object.entries 做分类占比与排序
✅ sort 先 [...list] 拷贝再排、数字比较函数 (a,b)=>b-a
✅ 解构(const { price, qty }、Object.entries 里的 [cat, amt])、展开({ ...o, revenue })
✅ 模板字符串 + 模板内条件渲染做看板卡片
✅ 模块化思想:数据 / 工具 / 统计 / 渲染分块注释,<script type="module"> 自带作用域与 defer
✅ 事件委托(分类筛选只绑一次)、classList.toggle/add/remove
动手任务(15 分钟)
- 打开页面,看"正在加载..."约 0.7 秒后看板出现------这就是
await fetchOrders()在等Promise。在boot()里await前后各加一句console.log('前'/'后'),看它们的先后顺序。 - 点"数码 / 家居 / 运动"分类标签:观察 KPI、占比条、Top 榜整体重算------背后是
filter+ 重新render。 - 把
topSellers的n从 5 改成 3,看 Top 榜只剩 3 条;再故意去掉[...list]的展开,直接list.sort(),观察原数据ALL被改乱(理解"sort 原地修改")。 - 在
summarize里再补一个字段maxSingle:用reduce求"单价最高的商品"。验证:结果应是4K 显示器(¥1299)。 - 把
revenueByCat里的acc[o.cat] = (acc[o.cat] || 0) + o.revenue改成用Map实现,render里相应改成Object.fromEntries(map),体会Map与对象字典的互换。
明天做什么
Day 7:TypeScript 基础 ------ 今天你处理的全是"凭经验知道结构"的裸数据:o.revenue 到底存不存在、cat 拼错了也不报错,全靠自觉。明天给 JS 加上类型 :用 interface/type 把今天这份"订单"结构定义清楚,让 o.reveneu(拼错)在写代码时就被编辑器标红;定义统一的 API 返回类型,为第 13 天的 Axios 封装做准备。小项目:把今天看板的数据结构用 TS 类型完整描述一遍。