现代 JavaScript 日常手册:数据、异步、模块与 DOM 安全
本文目录
日常写 JavaScript,最常见的问题不是记不住某个关键字,而是没有明确数据形态、异步边界、错误归属和资源生命周期。下面按实际代码审查顺序整理一份速查:先确认值,再组织状态,然后处理并发,最后把副作用收进清晰边界。
声明与值语义
- 默认使用
const;只有变量本身需要重新指向其他值时才用let。它们都不能让对象内容自动不可变,const config = {}仍可修改属性。 - 避免
var:它是函数作用域并有提升行为,容易让循环和条件块里的变量共享意外状态。 - 用严格比较
===/!==,显式处理null、undefined、空字符串、0和NaN。条件判断中的 truthy/falsy 规则可能把合法的0或空字符串误判为“没有值”。 - 原始值赋值复制值;对象与数组赋值复制的是引用。浅拷贝不会递归复制嵌套对象,更新嵌套状态时要明确哪些层需要新引用。
- 对外部 JSON、URL 参数、表单和
postMessage数据先做运行时校验。TypeScript 类型在编译后不存在,不能替代边界验证。
const input = { profile: { name: 'Haoge' } };
const next = { ...input, profile: { ...input.profile, name: 'Guest' } };
console.log(input.profile.name); // Haoge
console.log(next.profile.name); // Guest
数组与对象
map转换每项并返回新数组,filter筛选,find返回第一个匹配项,some/every表达存在性与全量条件。forEach用于有副作用的遍历,不返回转换结果;不要写array.forEach(async ...)后就以为循环已等待。用for...of顺序等待,或用Promise.all并发。reduce适合有明确 accumulator 的归并;若初始值和状态机很难读,普通循环更清楚。for...of遍历值;for...in遍历可枚举属性键,还可能看到原型链上的键,不应用来遍历数组元素。- 对象键可能触发原型相关风险。处理不可信键时考虑
Map、Object.hasOwn()和允许列表,不要盲目把输入合并进权限或配置对象。
const results = await Promise.allSettled(tasks.map((task) => task.run()));
const failed = results.filter((result) => result.status === 'rejected');
Promise.all 有任一拒绝就整体拒绝;allSettled 保留每项结果。并发本身也要有上限:数千个任务一次性启动会打满网络、服务端或内存,应使用带队列的并发池。
异步、取消和错误
- Promise 代表未来结果,不会阻塞当前线程。
await让当前 async 函数暂停后续逻辑,不会让整个浏览器主线程停住。 - 用
try/catch捕获可恢复错误,并在有意义的边界增加上下文;不要静默吞错,也不要在多层重复记录同一错误。 - 用
AbortController取消被页面、搜索词或用户动作淘汰的请求。取消后仍需区分AbortError与真实网络失败,并清理 loading 状态。 - 同时发起的独立请求可并行;依赖上一步结果的操作应顺序执行。写请求要考虑超时、重试、幂等键和用户可见结果,不能因网络错误盲目重放。
- 定时器、事件监听器、Observer、Worker、WebSocket 和媒体资源都需要明确释放点;组件卸载或路由离开时移除监听、取消请求、断开观察器并停止任务。
const controller = new AbortController();
try {
const response = await fetch('/api/search?q=observer', {
signal: controller.signal,
credentials: 'same-origin',
});
if (!response.ok) throw new Error('Search request failed: ' + response.status);
const data = await response.json();
renderResults(data);
} catch (error) {
if (error.name !== 'AbortError') showRequestError(error);
}
// 搜索条件变化或页面离开时:controller.abort();
实际请求层应限制响应体大小、校验响应结构并处理认证与权限状态;HTTP 404/500 不会自动让 fetch() Promise reject。
模块与依赖边界
ES Modules 用 export / import 表达依赖,模块默认在严格模式下执行。命名导出适合多个明确能力;默认导出适合模块唯一主实体。避免循环依赖、全局变量和在模块加载时执行不可控副作用。只在确实需要时动态 import(),并验证分包后的加载失败与路由回退。
包管理器锁文件需要提交,依赖升级要看变更日志、兼容范围、许可和供应链风险。不要为了一个很小的格式化函数引入体积大、长期无人维护的依赖;也不要为了省依赖重写复杂日期、加密或协议算法。
DOM 更新与 XSS
用户内容默认作为文本显示,优先使用 textContent、框架的安全文本插值和显式属性赋值。不要把不可信字符串交给 innerHTML、eval、Function 或未经约束的 URL 导航。确实需要富文本时使用成熟 sanitizer 和允许列表,并补充 CSP;CSP 是纵深防御,不是净化器。
事件监听优先使用稳定函数引用或 AbortSignal 统一清理。事件委托适合大量动态子项,但要检查 event.target 的预期元素和最近业务容器。DOM、网络响应和缓存内容都按不可信数据处理。
排错时的快速检查
- 变量当前是原始值、对象引用、Promise、DOM 节点还是流?在哪个时间点可用?
- 数据从哪里进入,是否经过 schema 校验、授权和长度限制?是否发生浅拷贝或共享引用?
- 异步操作是否有取消、超时、并发上限、错误 UI 和清理?写操作重试是否幂等?
- 页面卸载、切换账号、重复点击和慢网下,旧回调能否覆盖新状态?
- 动态内容会进入 HTML、URL、选择器、日志或第三方服务吗?是否需要编码、净化、脱敏或白名单?
参考:MDN JavaScript Guide、MDN Language Overview、MDN Promises、MDN JavaScript Modules、ECMAScript Specification。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。