Day 6|ES6+ 语法:把现代 JS 写顺手,从解构到 map / filter / reduce

今天小项目的效果如下

昨天我们用 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 新手必踩的坑 + 调试

  1. map 回调忘了 return :arr.map(x => { x * 2 }) 得到一坨 undefined------箭头函数一旦加了 {},就必须手写 return。单表达式别加花括号。
  2. sort 原地改 + 数字排序:见第五节,拷贝后再排、数字一定给比较函数。
  3. reduce 不设初始值 :[1,2,3].reduce((s,x)=>s+x) 没问题,但空数组 [].reduce(...) 不带初始值会直接报错。养成习惯永远给第二个参数 (0 / [] / {})。
  4. forEach 里想 break?做不到 :要中途退出用 for...of + break,或先 find / some(它们遇到匹配会短路)。
  5. Promise 吞掉错误 :async 函数里 await 一个会失败的请求却不写 try/catch,错误会悄悄变成"未处理的 Promise 拒绝",页面啥反应都没有。凡是 await,默认配 try/catch。
  6. 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>&copy; 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 分钟)

  1. 打开页面,看"正在加载..."约 0.7 秒后看板出现------这就是 await fetchOrders() 在等 Promise。在 boot() 里 await 前后各加一句 console.log('前'/'后'),看它们的先后顺序。
  2. 点"数码 / 家居 / 运动"分类标签:观察 KPI、占比条、Top 榜整体重算------背后是 filter + 重新 render。
  3. 把 topSellers 的 n 从 5 改成 3,看 Top 榜只剩 3 条;再故意去掉 [...list] 的展开,直接 list.sort(),观察原数据 ALL 被改乱(理解"sort 原地修改")。
  4. 在 summarize 里再补一个字段 maxSingle:用 reduce 求"单价最高的商品"。验证:结果应是 4K 显示器(¥1299)。
  5. 把 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 类型完整描述一遍。


相关推荐
铁皮饭盒1 小时前
CPU就行, 老照片变清晰, 本地部署582mb腾讯的模型, 本地部署, 过节了给长辈们变个小魔术
前端·javascript·后端
其实防守也摸鱼1 小时前
Linux文本处理命令理论学习
linux·服务器·前端·数据库·sql·学习·安全
涛涛ing2 小时前
Chrome DevTools MCP 正式可用:AI 终于能自己开浏览器调试你的页面了
前端
after you2 小时前
【无标题】
前端·javascript
酷炫码神2 小时前
Day 6|ES6+ 语法:把现代 JS 写顺手,从解构到 map / filter / reduce
前端·javascript·es6
lemon_sjdk2 小时前
从一段真实的 Electron IM 前端代码,看高级前端的工程化修养
前端·electron·状态模式·im
IT_陈寒3 小时前
Vite性能优化完全指南
前端·人工智能·后端
燕落南枝3 小时前
HTML 表单入门:表单容器、文本框与密码框属性详解
前端·microsoft·html
TYC93 小时前
Mac创建Vue3+TypeScript或者React+TypeScript项目
前端·javascript·typescript