盐汽水的海洋

JavaScript / ES6

avatar

盐汽水

目标:2–3 小时快速具备可实战能力。按“概念 → 示例 → 练习 → 自测”结构组织,聚焦高频必会内容。


1) 概念:核心概念与必须掌握的 API/配置(80/20 精选)

1.1 语言与语法(必知 16 条)

  1. let / const:两者都是块级作用域;默认用 const,只有需要重赋值时用 let,避免 var

  2. 解构赋值:从对象/数组中按结构取值更简洁;支持默认值与重命名({a: x=0})。

  3. 展开/剩余 ...:浅拷贝/合并对象数组或收集剩余参数;记住只是复制。

  4. 模板字符串:多行 + ${expr} 插值;避免繁琐的字符串拼接与转义。

  5. 箭头函数:不绑定自己的 this,更适合回调;对象方法或需要 this 时用常规函数。

  6. this 规则:与“调用方式”相关;可用 call/apply/bind 显式绑定,箭头函数捕获外层 this

  7. 模块 import/export:浏览器与 Node 原生支持 ES Modules;Node 侧需 package.json"type":"module"

  8. 类与原型class 是原型语法糖;优先组合而非深层继承。

  9. 迭代与 for...of:遍历可迭代对象(数组、Map、Set);对象键值用 Object.entries()

  10. 可选链 ?. / 空值合并 ??:安全访问深层属性并为 null/undefined 提供默认值。

  11. 默认参数 / 剩余参数:用默认参数代替函数内手动判空;用 ...args 代替 arguments

  12. Promise 基础:异步结果的占位;.then/.catch/.finally 组织回调与错误。

  13. async/await:同步写法处理异步;配合 try/catch 统一错误处理。

  14. 并发组合Promise.all 并行、allSettled 容错、race/any 竞速;注意失败传播与回滚策略。

  15. 错误处理:抛出 new Error(msg);使用 try/catch/finally,记录上下文信息。

  16. 事件循环:微任务(Promise 回调)先于宏任务(setTimeout);理解顺序有助定位异步问题。

1.2 标准库与 Web API(必会 14 条)

  1. Arraymap/filter/reduce/find/some/every/flat/includes/sortsort 需自定义比较函数确保数值排序正确。

  2. Objectkeys/values/entries/fromEntries/assign/hasOwn;遍历时过滤掉原型链属性。

  3. Map/Set/WeakMap/WeakSet:Map 键可为任意值;Set 去重;Weak* 适合与对象弱关联防止内存泄漏。

  4. 字符串/正则includes/startsWith/endsWith/trim/padStart;正则用于匹配/替换。

  5. 数值/MathNumber.isNaN/parseInt/parseFloat/toFixed;注意浮点误差(如 0.1+0.2)。

  6. 日期/国际化Date 基础;用 Intl.DateTimeFormat/NumberFormat 做本地化。

  7. JSONJSON.parse/stringify;可用“替换器/还原器”控制序列化。

  8. structuredClone:原生深拷贝(不克隆函数/DOM 节点);优于 JSON Hack。

  9. URL/参数URLURLSearchParams 解析/构造链接和查询参数。

  10. Fetchfetch(url,{method,headers,body});HTTP 非 2xx 不抛错,需手动检查 res.ok

  11. 取消/超时AbortController + signal 取消请求;组件卸载时要 abort。

  12. StoragelocalStorage/sessionStorage 仅存字符串;结合 JSON 存取对象。

  13. DOM 操作querySelectorclassListclosestappend/removetextContent/value

  14. 事件addEventListener、事件委托(监听父节点)与 stopPropagation/preventDefault

1.3 配置与工程规范(实战 6 条)

  1. 模块化启动:浏览器 <script type="module">;Node 设 "type":"module"(仅 ES 模块)。

  2. 本地服务器:用 VS Code Live Server 或 npx serve 起 HTTP,避免 file:// 导致模块/跨域问题。

  3. ESLint + Prettier:开启 eslint:recommended 与常用规则(no-undef/no-unused-vars);统一代码风格。

  4. 浏览器兼容:用 browserslist 声明目标;老环境再考虑 Babel/打包器。

  5. 导出风格:优先具名导出(便于重构与 Tree Shaking);默认导出仅限单一主对象。

  6. 官方文档优先:遇到歧义先查 MDNECMAScript 规范;网络 API 参考 WHATWG 文档。


2) 场景:典型应用与设计取舍(做决策时看这里)

  • 列表渲染Array.map + 模板字符串快速输出;有用户输入时避免 innerHTML,优先 textContentcreateElement 以防 XSS。

  • 事件处理:大量动态项使用事件委托(绑在容器上);少量静态项逐个绑定更直观。

  • 状态更新:小型页面直接改数组/对象;多人协作或需要撤销/时间旅行时倾向不可变更新.../map),成本是拷贝开销。

  • 网络请求:现代浏览器优先 fetch;若需拦截器/旧浏览器兼容可选 axios。

  • 并发请求:能并行就用 Promise.all;部分失败仍要继续用 allSettled

  • 取消/切换页:组件卸载时用 AbortController 取消请求;否则可能更新已卸载节点。

  • 数据结构:键不是字符串或需保持插入顺序用 Map,需快速去重/查重用 Set;简单键值仍可用对象/数组。

  • 模块组织:入口(副作用) + 工具(纯函数) + 业务模块;模块拆分提可读性,但过度拆分增加维护成本。

  • 深拷贝/合并:首选 structuredClone 与展开运算;需要自定义规则时考虑库(如 lodash)权衡体积。

  • 安全:DOM 写入优先 textContent;必须插入 HTML 时用可信模板或 DOMPurify 一类的净化器。

    • *

3) 示例:从零开始的最小可运行项目(含注释与运行步骤)

功能:极简 Todo(新增/完成/删除/过滤),数据持久化到 localStorage,首屏尝试拉取 3 条远端样例(失败则本地回退)。
亮点:使用 ES 模块、async/await、事件委托、AbortControllerMap/Set 可选。

3.1 目录结构

es6-mini/
├─ index.html
├─ main.js
├─ storage.js
└─ utils.js

3.2 运行步骤(任选其一)

  1. 推荐:在该目录执行 npx serve -p 5173python3 -m http.server 5173,浏览器打开 http://localhost:5173/

  2. VS Code:安装 Live Server 插件,右键 index.html 选择 “Open with Live Server”。

直接双击 index.html 在部分浏览器下会受 file:// 限制导致模块导入失败,故建议走本地服务器。

3.3 代码

index.html

<!doctype html>
<html lang="zh">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>ES6 最小可用 Todo</title>
  <style>
    body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial; margin: 2rem; }
    .row { display:flex; gap:.5rem; margin-bottom: 1rem; }
    input[type="text"] { flex:1; padding:.5rem; }
    button { padding:.5rem .8rem; cursor:pointer; }
    ul { list-style: none; padding: 0; margin: 0; }
    li { display:flex; align-items:center; gap:.5rem; padding:.4rem 0; border-bottom: 1px solid #eee; }
    li.done span { text-decoration: line-through; color: #888; }
    .muted { color:#666; font-size:.9rem; }
  </style>
</head>
<body>
  <h1>Todo(ES6 模块)</h1>

  <form id="add-form" class="row">
    <input id="todo-input" type="text" placeholder="要做什么..." required />
    <button type="submit">添加</button>
    <select id="filter">
      <option value="all">全部</option>
      <option value="active">未完成</option>
      <option value="done">已完成</option>
    </select>
  </form>

  <ul id="list"></ul>
  <p class="muted"><span id="count">0</span> 项 · 双击可编辑,勾选切换完成</p>

  <script type="module" src="./main.js"></script>
</body>
</html>

utils.js

// 小工具:选择器、事件委托、ID 生成
export const qs = (sel, el = document) => el.querySelector(sel);

export const on = (el, type, selectorOrHandler, handlerMaybe) => {
  // 支持直接绑定或事件委托
  if (typeof selectorOrHandler === "function") {
    el.addEventListener(type, selectorOrHandler);
    return;
  }
  const selector = selectorOrHandler;
  const handler = handlerMaybe;
  el.addEventListener(type, (e) => {
    const target = e.target.closest(selector);
    if (target && el.contains(target)) handler(e, target);
  });
};

// 稳健的 ID:优先原生随机 UUID,回退时间戳+随机数
export const uid = () =>
  (crypto?.randomUUID?.() ?? `id_${Date.now()}_${Math.random().toString(36).slice(2)}`);

storage.js

const KEY = "todos-v1";

export const loadTodos = () => {
  try {
    return JSON.parse(localStorage.getItem(KEY) || "[]");
  } catch {
    return [];
  }
};

export const saveTodos = (todos) => {
  localStorage.setItem(KEY, JSON.stringify(todos));
};

main.js

import { qs, on, uid } from "./utils.js";
import { loadTodos, saveTodos } from "./storage.js";

// ---- 状态 ----
let todos = loadTodos(); // [{id,title,done}]
let filter = "all";

// ---- DOM ----
const $list = qs("#list");
const $form = qs("#add-form");
const $input = qs("#todo-input");
const $filter = qs("#filter");
const $count = qs("#count");

// ---- 初始化:首屏尝试获取 3 条远端样例(失败则本地回退)----
(async function bootstrap() {
  if (todos.length === 0) {
    try {
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), 3500); // 超时取消
      const res = await fetch("https://jsonplaceholder.typicode.com/todos?_limit=3", { signal: ctrl.signal });
      clearTimeout(timer);
      if (res.ok) {
        const data = await res.json();
        todos = data.map(d => ({ id: uid(), title: d.title, done: d.completed }));
        saveTodos(todos);
      } else {
        fallback();
      }
    } catch {
      fallback();
    }
  }
  render();
})();

function fallback() {
  todos = [
    { id: uid(), title: "学习 ES6 基础", done: false },
    { id: uid(), title: "用 fetch 拉数据", done: true },
    { id: uid(), title: "把数据存到 localStorage", done: false },
  ];
  saveTodos(todos);
}

// ---- 渲染 ----
function render() {
  const frag = document.createDocumentFragment();
  const filtered = todos.filter(t =>
    filter === "active" ? !t.done : filter === "done" ? t.done : true
  );
  for (const t of filtered) {
    const li = document.createElement("li");
    li.dataset.id = t.id;
    if (t.done) li.classList.add("done");

    const cb = document.createElement("input");
    cb.type = "checkbox";
    cb.checked = t.done;
    cb.setAttribute("aria-label", "切换完成");

    const span = document.createElement("span");
    span.textContent = t.title;
    span.contentEditable = "true"; // 双击后可直接编辑(简化演示)

    const del = document.createElement("button");
    del.type = "button";
    del.textContent = "删除";
    del.className = "del";

    li.append(cb, span, del);
    frag.append(li);
  }
  $list.replaceChildren(frag);
  $count.textContent = String(todos.length);
}

// ---- 事件:添加 ----
on($form, "submit", (e) => {
  e.preventDefault();
  const title = $input.value.trim();
  if (!title) return;
  todos.unshift({ id: uid(), title, done: false });
  saveTodos(todos);
  $input.value = "";
  render();
});

// ---- 事件:过滤 ----
on($filter, "change", () => {
  filter = $filter.value;
  render();
});

// ---- 事件:列表(委托) 勾选/删除/编辑 ----
on($list, "change", "li input[type=checkbox]", (e, target) => {
  const id = target.closest("li").dataset.id;
  const item = todos.find(t => t.id === id);
  if (item) {
    item.done = target.checked;
    saveTodos(todos);
    render();
  }
});

on($list, "click", "li .del", (e, btn) => {
  const id = btn.closest("li").dataset.id;
  todos = todos.filter(t => t.id !== id);
  saveTodos(todos);
  render();
});

on($list, "blur", "li span", (e, span) => {
  // 简易编辑:失焦时保存;防 XSS 使用 textContent,而非 innerHTML
  const id = span.closest("li").dataset.id;
  const item = todos.find(t => t.id === id);
  if (item) {
    item.title = span.textContent.trim();
    saveTodos(todos);
    render();
  }
});

为什么这套示例足够“实战”?
覆盖了模块化、异步请求、取消/超时、DOM 事件委托、状态管理、持久化、输入安全(textContent)和基本可访问性(aria/label)。


4) 常见坑与调试技巧(踩坑少一半)

4.1 常见坑(如何避免)

  • var 提升与闭包循环for (var i...) 里的异步回调共享同一 i;用 let 或创建块级作用域。

  • this 迷惑:回调里的 this 往往不是你以为的对象;需要 this 的方法用常规函数并 bind,回调多用箭头函数。

  • 浮点误差0.1 + 0.2 !== 0.3;使用整数放大再缩小或 toFixed/Math.round 控制显示。

  • fetch 不会为 404/500 抛错:先检查 res.okres.json();用 try/catch 包裹。

  • 未取消的请求:组件卸载后仍回调更新 DOM;用 AbortController 或在回调中检查“已卸载”标记。

  • for...in 枚举原型链:产生意外键;仅在真正需要时使用,或配合 Object.hasOwn() / hasOwnProperty

  • sort 默认字典序[2, 10].sort() 得到 [10, 2];数值排序用 (a,b) => a - b

  • 深浅拷贝混淆:展开 ... 是浅拷贝;深层对象用 structuredClone

  • ||?? 混用0/''/false 是“有效值”但被 || 视作假;用 ?? 仅针对 null/undefined

  • CORS:跨域被浏览器拦截;需服务端正确设置 Access-Control-Allow-Origin,前端无法单方面解决。

  • 时间与时区Date 默认为本地时区;序列化用 toISOString(),显示用 Intl.DateTimeFormat

  • 内存泄漏:忘记解绑事件监听/定时器;使用事件委托或记录并统一清理。

4.2 调试技巧(提效 3 倍)

  • DevTools 断点:右键行号设条件断点;在代码中用 debugger 精确停住。

  • Console 家族console.table/dir/groupCollapsed/time/assert 让日志更有结构。

  • 网络面板:看请求头/响应体/时序;用复制 fetch 作为重放模板。

  • DOM 断点:在元素上设“属性修改/节点移除”断点,定位谁在改你的 DOM。

  • 性能与内存performance.now() 粗测耗时;快照检查泄漏(观察监听器与闭包)。

  • Node 调试node --inspect,用 Chrome DevTools 附加调试。

    • *

5) 练习清单(分层 3×3)

入门(掌握基本语法与 API)

  1. map/filter/reduce:给定一组订单,算总金额与未付款列表,输出为字符串模板。

  2. 写一个 request(url, {timeout}):使用 fetch + AbortController,超时自动取消并抛错。

  3. DOM 小练习:输入框+按钮,把输入项追加到列表;用事件委托完成删除。

巩固(综合使用异步、模块与存储)

  1. 分页列表:拉取分页数据(如 GitHub 用户仓库),实现“上一页/下一页”,并缓存到 sessionStorage

  2. 设置面板:用 URLSearchParams 读写筛选条件到地址栏,实现“可分享的筛选”。

  3. 模块拆分:把通用工具与业务逻辑拆成两个模块,使用具名导出并写 3 条单元“自测”断言。

进阶(性能与工程化思维)

  1. 并发控制:实现 limit(promiseFactories, max),限制并发下载数量。

  2. 防抖/节流:实现通用 debouncethrottle,用在搜索输入与滚动事件。

  3. 数据结构选择:用 Set 去重大列表,再用 Mapid 快速查找、更新与合并。


6) 自测(Checklist + 闭卷题)

6.1 自测清单(打 √)

  • 我能解释 let/constvar 的差别,并始终默认使用 const

  • 我能在代码中正确使用解构、展开、模板字符串与默认参数。

  • 我能用 async/await + try/catch 写出一次网络请求并处理非 2xx。

  • 我知道何时用 Promise.all / allSettled / race,以及失败传播的影响。

  • 我能在 DOM 中用事件委托管理列表项的点击/勾选/删除。

  • 我能安全地把用户输入插入页面(textContent 而非 innerHTML)。

  • 我能解释微任务/宏任务的执行顺序,并推断一段代码的打印顺序。

  • 我知道在 Node 中如何启用 ES 模块("type":"module")。

  • 我会用 console.table、条件断点与 debugger 快速定位问题。

6.2 闭卷题(含标准答案)

题 1:事件循环顺序

console.log(1);
setTimeout(() => console.log(2));
Promise.resolve().then(() => console.log(3));
console.log(4);

答案1, 4, 3, 2
解释:同步先执行(1、4),微任务(Promise 回调 3)先于宏任务(setTimeout 2)。


题 2:?? 与默认参数、解构

const cfg = { retry: 0, timeout: undefined };
const { retry = 3, timeout = 5000 } = cfg;
const maxRetry = cfg.retry ?? 5;
console.log(retry, timeout, maxRetry);

答案:输出 0 5000 0
解释:解构默认值在属性为 undefined 时生效(timeout→5000);retry 已有值 0 不用默认;?? 只在 null/undefined 时用默认,因此 0 ?? 5 得 0。


题 3:箭头函数的 this

const counter = {
  n: 0,
  incLater() {
    setTimeout(() => { console.log(this.n++); }, 0);
  }
};
counter.incLater();

答案:打印 0(随后 n 变为 1)。
解释:箭头函数不绑定自己的 this,捕获外层 incLaterthis(即 counter)。


附:推荐官方文档(查阅优先)

  • MDN:JavaScript 语言基础、内置对象与 Web API(关键词直达:Promise、fetch、Map/Set、URL、Storage、Intl、structuredClone、事件)。

  • ECMAScript 规范:语言行为的最终解释(遇到边界/兼容性问题时参考)。

  • WHATWG/Fetch:网络请求与流式处理的详细说明。

  • ESLinteslint:recommended 规则与最佳实践;Prettier 风格化说明。

建议随用随查:用 MDN 作为 API 真值表,工程争议用 ESLint 规则与团队规范统一口径。祝你上手顺滑,直接用本示例改造即可投入小功能开发!


闽ICP备2021014815号-2
powered by emlog sitemap