ES6 放着不管是不可能的。前端项目、Node.js 后端、小程序、Electron 桌面端,甚至自动化脚本,到处都在用 let/const、箭头函数、Promise、class、import/export。一个 JavaScript 开发者如果现在还在到处写 var 加回调嵌套,写起来难受,别人 review 代码也难受。
这次要梳理的这套 ES6 必学实用教程,对应的是 JavaScript 高级阶段最常被问到、也最实用的一批知识点:变量与作用域、函数与对象语法、面向对象、异步编程、模块化工程化。内容并不追求把 ES6 的每个边角特性都搬出来,而是按“日常工作真正用得到”的标准来组织。
值得重点关注的功能点有这几块:块级作用域 let/const、箭头函数与 this、ES6 class 与继承、Promise 与 async/await、import/export 模块化。另外解构赋值、模板字符串、展开运算符、Map/Set、Proxy 这些高频特性也会一并覆盖。学习门槛不高:一台能装 Node.js 的电脑就行,浏览器 F12 控制台也能直接验证。
本文会把整套知识梳理成可运行、可自测的教程,读者跟着搭一个项目跑一遍,基本能覆盖日常开发 80% 的写法。适合三类人:有一定 JS 基础想进阶的前端开发者、准备面试的求职者、想把 Node.js 后端和自动化脚本写得更规范的同学。
1. ES6 核心能力速览
| 能力项 | 说明 |
|---|---|
| 语言版本 | ES6(ECMAScript 2015)及后续高频现代语法 |
| 核心内容 | let/const、箭头函数、解构、模板字符串、class、Promise、async/await、import/export、Map/Set、Proxy |
| 运行环境 | 现代浏览器开发者工具、Node.js(建议 18 LTS 或更高) |
| 是否需要转译 | 现代环境可直接运行;旧浏览器或旧运行时需 Babel 转译 |
| 工程化配合 | npm、ESLint、Prettier,构建工具可选 |
| 学习门槛 | 需要 JS 基础:变量、函数、数组、对象、回调 |
| 适合人群 | 前端开发、Node.js 开发、面试准备、自动化脚本编写 |
| 不适合场景 | 完全零基础初学者;需要兼容 IE 且没有构建工具的旧项目 |
这套内容最大的优势不是“语法新”,而是把所有写法往工程化方向推了一把。变量有了块级作用域,函数有了更短的箭头形式,异步有了 Promise 和 async/await,代码拆分有了模块系统。学完之后回头看旧代码,最容易产生的感受就是:原来很多问题不是程序员能力不够,而是语言工具没给到位。
2. 适用场景与学习边界
ES6 解决的是 JavaScript 长期积累的四个问题:一是 var 声明的变量存在函数作用域和变量提升,容易造成全局污染;二是普通函数的 this 指向不稳定,回调里经常丢掉外层上下文;三是异步逻辑依赖回调嵌套,代码一旦复杂就变成回调地狱;四是代码拆分靠全局函数和 script 标签顺序,根本没有模块边界。
这套 ES6 教程适合已经有基础的人群。变量怎么声明、函数怎么定义、数组和对象怎么操作、回调函数是什么,这些内容最好先过一遍,再进入 ES6 学习会顺畅很多。如果完全零基础,直接学 let/const 和 class 也不是不行,但对变量提升、作用域、this 这些概念理解会不够深,后续排错容易卡住。
不适用的情况也要说清楚。如果手上是 2015 年之前的浏览器环境,比如 IE11 及更低版本,ES6 语法需要 Babel 转译成 ES5 才能运行,团队还需要引入构建流程和 polyfill 机制。如果只是维护几百行的旧项目,也没有升级计划,强行把代码改成 ES6 风格反而会增加 review 成本。这种项目更适合“新代码用新语法,老代码逐步迁移”的策略。
学习边界也很明确:ES6 不是背语法,而是解决问题。每种语法都有它最适合的场景。比如箭头函数适合短回调和固定 this 的场景,不适合作为构造函数;class 适合表达实体概念,但不必所有代码都套面向对象;Promise 适合异步流程,但如果你只有一层回调,用不用都行。理解了边界,才算真正掌握。
3. 环境准备与前置条件
写 ES6 不需要重型 IDE,但需要一个能执行 JavaScript 的环境。推荐三个地方:
第一,Node.js 是最主要的验证环境。建议安装 LTS 版本,本文示例以 Node.js 18 及以上为例,因为后面的自动化测试脚本用到了官方内置的 node:test 模块。安装完成后打开终端验证:
node -v npm -v能输出版本号就说明环境可用。没有 Node.js 的,直接到官网下载对应系统的安装包,一路默认安装即可。
第二,浏览器开发者工具是学习 DOM 和浏览器 API 的好地方。Chrome、Edge、Firefox 都支持,按 F12 打开 Console 面板,可以直接输入 ES6 语法,回车执行。模板字符串、解构、Promise 这些语法在控制台里试起来非常快,不用建文件和启动进程。
第三,VS Code 作为编辑器足够用。装一个 ESLint 插件和一个 Prettier 插件,代码自动格式化和基础语法检查就有了。ESLint 的默认规则里,no-var 和 prefer-const 是最直接体现 ES6 风格的规则,配置好之后会一直提醒你“这里不要用 var,这里可以用 const”。
磁盘和内存方面没有特殊要求,几百 MB 的 Node.js 安装空间就够了。如果只是跟练本文的项目,不需要额外下载框架。整套练习工程的体积可以控制在一个很小的范围,比装一个 IDE 全家桶轻量得多。
4. 搭建一个 ES6 练习项目
先建一个干净的目录。终端里执行:
mkdir es6-practice cd es6-practice npm init -ynpm init -y 会生成一个默认的 package.json。这一步之后,需要手动改一下入口和模块规范,把 package.json 里加上"type": "module"。原因很直接:ES6 的 import/export 默认在 Node 的 CommonJS 模块系统里不生效,声明 type 为 module 之后,Node 才会把 .js 文件当作 ES 模块处理。
package.json 大致像这样:
{ "name": "es6-practice", "version": "1.0.0", "type": "module", "scripts": { "start": "node src/main.js", "test": "node --test" } }然后建一个建议的目录结构:
es6-practice/ ├─src/ │ ├─01-scope.js │ ├─02-function.js │ ├─03-class.js │ ├─04-async.js │ ├─utils.js │ └─main.js ├─test/ │ └─es6.test.js └─package.jsonsrc 放功能示例代码,test 放自动化测试脚本,main.js 作为入口。这个目录习惯在真实项目里也通用,长期维护时不会出现所有代码堆在同一个文件里的情况。
运行示例统一用一个命令:
node src/01-scope.js测试统一用:
node --test这种配置方式有几个好处:依赖少,不用 webpack 和 vite 也能跑;文件路径清晰,每学一个特性就新增一个文件;测试脚本可复用,后面验证一堆特性时不用手动盯着终端看输出。
5. ES6 核心语法:变量、函数与对象操作
这一部分覆盖的是 ES6 里出现频率最高的语法点。不需要一次记住全部细节,先跑通示例,再放到项目中反复使用。
5.1 let/const 与块级作用域
var 最大的问题是变量提升和函数作用域。同一个函数内部,不管在哪个块级位置声明 var 变量,都会被提升到函数顶部,而且没有块级隔离。
if (true) { var a = 1; let b = 2; } console.log(a); // 1,var 穿透了 if 块 try { console.log(b); } catch (err) { console.log('b 不可访问:', err.message); // b is not defined }let 和 const 都是块级作用域,语句块外面访问不到块内变量。const 比 let 更严格,声明时必须赋值,而且不能重新赋值。
const user = { name: '张三' }; user.name = '李四'; // 允许,对象属性可以改 try { user = { name: '王五' }; } catch (err) { console.log('重新赋值报错:', err.message); }要注意,const 保护的是“绑定关系”,不是“对象内容”。对象本身是可以修改的,想彻底冻结一个对象,需要 Object.freeze。面试里经常考这一点,实际项目里也经常有人误解。
let/const 还有一个特性叫暂时性死区。变量声明之前访问会抛 ReferenceError,而不是像 var 一样得到 undefined。
try { console.log(x); } catch (err) { console.log('暂时性死区:', err.message); } let x = 10;所以最稳妥的实践是:默认用 const,只有确实需要重新赋值时才改用 let。var 在 ES6 项目里基本应该消失。
5.2 箭头函数与 this
箭头函数是 ES6 最直观的语法变化之一,写法短,而且 this 行为完全不同。
const names = ['张三', '李四', '王五']; const upperNames = names.map(name => name.toUpperCase()); console.log(upperNames);箭头函数的 this 不会在调用时重新绑定,它会捕获定义时外层作用域的 this。这一点在回调函数里非常有用,不用再写var self = this或者.bind(this)。
const counter = { count: 0, start() { setInterval(() => { this.count++; console.log(this.count); }, 1000); } };这里的箭头函数把 start 方法里的 this 固定下来了,setInterval 回调里的 this 也指向 counter。如果换成普通函数,this 会指向全局对象或者变成 undefined,就得额外处理。
箭头函数也有三个不能碰的场景。第一,不能作为构造函数,new 一个箭头函数会直接报错;第二,函数内部没有自己的 arguments 对象,要用剩余参数代替;第三,需要动态 this 的对象方法不应该写箭头函数,否则 this 会指向外层而非调用对象。
5.3 默认参数、rest 参数与展开运算符
默认参数解决的是“参数没传怎么办”的问题。ES5 时代一般写a = a || 0,但要么只能处理假值,要么语义不明确。ES6 直接写在函数签名里:
function greet(name = '陌生人', greeting = '你好') { return `${greeting},${name}`; } console.log(greet()); console.log(greet('张三', '早上好'));rest 参数把剩余参数收集成一个数组。注意 arguments 不是真数组,rest 参数是真数组,可以直接用 map、filter 这些数组方法。
function collect(first, ...rest) { return [first, rest.length, rest[0]]; } console.log(collect('a', 'b', 'c', 'd'));展开运算符正好和 rest 相反,把数组或对象展开成单个元素。数组拼接、对象合并都变得非常干净:
const arr1 = [1, 2]; const arr2 = [3, 4]; const merged = [...arr1, ...arr2]; const base = { id: 1, name: '张三' }; const user = { ...base, age: 18 };展开运算符最常用的地方就是不可变更新。React 和 Vue 生态里,复制对象时几乎都用{ ...prev, key: newValue }这种写法,而不是直接修改原对象。
5.4 解构赋值
解构赋值是从数组或对象里提取值的快捷方式。数组解构按位置提取:
const [first, second, third] = [10, 20, 30]; console.log(first, second, third); const [head, ...tail] = [1, 2, 3, 4]; console.log(head, tail);对象解构按属性名提取:
const user = { name: '张三', age: 25, city: '上海' }; const { name, age, city } = user; console.log(name, age, city);对象解构可以重命名,也可以给默认值,这在函数参数里特别实用:
function show({ name = '匿名', age = 0 }) { console.log(`${name} ${age}`); } show({ name: '李四', age: 30 }); show({});解构的默认值只会在值严格等于 undefined 时生效。如果你解构的对象本身是 undefined,那必须给整个参数做默认值,否则会报错:
function safeShow({ name = '匿名' } = {}) { console.log(name); } safeShow();日常开发中解构最常见的用法是接收后端接口返回的数据、取出路由参数、分离函数返回的多项结果。配合 rest 参数,还能做“排除某个字段”的操作:
const { password, ...safeUser } = user; console.log(safeUser);5.5 模板字符串
模板字符串用反引号代替单双引号,支持换行和变量插值。
const name = '张三'; const age = 25; const message = `你好,我叫 ${name},今年 ${age} 岁。`; console.log(message);多行文本以前要靠数组 join 或者字符串拼接,模板字符串直接写即可:
const text = `第一行 第二行 第三行`; console.log(text);模板字符串里的${}不只是变量,任何表达式都可以放,比如函数调用、三元表达式、数组 join:
const items = ['a', 'b', 'c']; console.log(`列表:${items.join(', ')}`);总结一下,ES6 这一部分语法的共同特点就是降低表达成本。原来写五行的逻辑,现在两行搞定;原来容易写错的作用域和 this,现在有了更安全的默认写法。这一部分学完之后,应该能流畅地把老代码里的 var 改成 const/let,把匿名回调改成箭头函数,把手工拼接改成模板字符串。
6. ES6 面向对象:class 与继承
JavaScript 原本是基于原型的面向对象语言,ES6 的 class 并没有改变这个底层机制,而是把它包装成更接近 Java、C# 的写法。理解这一点很重要:class 本质还是函数,实例还是通过原型链查找方法,但写起来清晰了很多。
6.1 class 基础语法
class 里的 constructor 就是构造函数,实例方法不需要 function 关键字,直接写在类体里:
class User { constructor(name, age) { this.name = name; this.age = age; } greet() { return `你好,我是 ${this.name},今年 ${this.age} 岁`; } } const user = new User('张三', 25); console.log(user.greet());类和构造函数的重要区别是:class 不能直接调用,必须用 new 创建实例。直接调用 User() 会报错,这一点比构造函数更安全,能在早期发现错误。
6.2 静态方法、getter/setter 与私有字段
静态方法直接用 static 修饰,通过类名调用,不需要实例。
class User { static createGuest() { return new User('游客', 0); } } const guest = User.createGuest(); console.log(guest.greet());getter 和 setter 可以拦截属性的读取和赋值。注意约定上属性名带下划线是对外隐藏的,ES6 的私有字段则用 # 前缀,这是真正的私有,外部无法访问。
class Account { #password; constructor(username, password) { this.username = username; this.#password = password; } get password() { return this.#password; } set password(value) { if (value.length < 6) { throw new Error('密码太短'); } this.#password = value; } checkPassword(input) { return input === this.#password; } } const account = new Account('admin', 'secret123'); console.log(account.checkPassword('secret123')); console.log(account.password);这里 getter/setter 的价值不是“看起来优雅”,而是给字段访问加了一层校验和拦截逻辑。以后想给 password 加规则,只要改 setter 就行,不需要改所有调用方。
6.3 继承与 super
extends 实现继承,子类 constructor 里必须调用 super(),并且要在使用 this 之前调用:
class Admin extends User { constructor(name, age, permissions) { super(name, age); this.permissions = permissions; } manage() { return `${this.name} 管理权限:${this.permissions.join(', ')}`; } } const admin = new Admin('李四', 30, ['user:edit', 'user:delete']); console.log(admin.greet()); console.log(admin.manage());子类可以重写父类方法,也可以在重写的方法里调用 super.method() 复用父类逻辑:
class Admin extends User { greet() { return `[管理员] ${super.greet()}`; } } console.log(admin.greet());6.4 面向对象设计的注意点
ES6 提供了 class,但并不意味着所有代码都应该变成类和继承。面向对象真正要考虑的三件事是封装、复用和边界清晰。
- 封装:内部状态不要全部暴露,该用私有字段就私有,该提供方法就提供方法。
- 复用:优先考虑组合而不是继承。一个 User 和一个 Admin 如果只是权限不同,组合一个 role 对象往往比继承更简单。
- 边界:一个类只负责一个明确概念。比如 User 类管用户信息,不要让它同时管登录日志和订单记录。
实际开发中,class 最常用在领域模型、组件基类、自定义错误类型、工具类封装这些地方。如果只是几个纯函数集合,用普通函数和对象就够了,不需要强行套 class。
7. ES6 异步编程:Promise 与 async/await
异步是 JavaScript 里最容易写错的部分。ES6 引入了 Promise,ES2017 又在 Promise 基础上加了 async/await 语法糖,让异步代码看起来像同步代码。
7.1 Promise 基础
Promise 表示一个尚未完成但最终会有结果的操作。它有 pending、fulfilled、rejected 三种状态,状态一旦确定就不能再改变。
function loadUser() { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ id: 1, name: '张三' }); }, 1000); }); } loadUser() .then(user => { console.log(user); }) .catch(err => { console.error(err); }) .finally(() => { console.log('请求结束'); });Promise 最大的价值在于链式调用。多个异步任务可以串联,不依赖缩进和嵌套:
loadUser() .then(user => loadPosts(user.id)) .then(posts => loadComments(posts[0].id)) .then(comments => console.log(comments)) .catch(err => console.error(err));每一个 .then 都会返回新的 Promise,所以链可以继续接下去,错误会一路传递到最近的 .catch。
7.2 async/await 写法
async 函数内部可以使用 await,await 会暂停函数执行,等待 Promise 完成,然后把 resolve 的值返回出来。
async function main() { try { const user = await loadUser(); console.log(user); return user; } catch (err) { console.error('加载失败:', err.message); } } main();用 async/await 改写一个多步骤流程,代码结构非常接近同步逻辑:
async function loadDashboard() { const user = await loadUser(); const posts = await loadPosts(user.id); const comments = await loadComments(posts[0].id); return comments; }注意两点。第一,async 函数执行后本身也是 Promise,调用它的时候可以继续 .then 或者 await。第二,await 后面必须是 Promise,如果不是,Node 会把它包装成立即 resolved 的 Promise,这在大多数情况下没问题。
7.3 并发与失败处理
多个互相独立的异步操作不应该用 await 顺序执行,那样会浪费等待时间。用 Promise.all 并行开始:
async function loadAll() { const [user, posts, settings] = await Promise.all([ loadUser(), loadPosts(), loadSettings() ]); return { user, posts, settings }; }Promise.all 有一个特点:任何一个 Promise 失败,整体就会立即走到 catch。如果希望每个任务的结果都保留下来,无论成功失败,用 Promise.allSettled:
const results = await Promise.allSettled([ loadUser(), loadBroken() ]); for (const result of results) { if (result.status === 'fulfilled') { console.log('成功:', result.value); } else { console.log('失败:', result.reason.message); } }实际项目里,批量并发请求、多任务并行初始化、失败不中断的同步任务,都可以用 allSettled 来兜底。后端接口批量调用、报表生成、定时任务采集数据这类场景尤其适合。
异步编程还有一个常见坑:用 async 函数时,里面的 try/catch 只能捕获 await 之前的错误,如果返回的是没有被 await 的 Promise,错误会变成未处理的 rejection:
function bad() { try { return Promise.reject('失败'); } catch (err) { // 这里捕获不到,因为错误发生在 Promise 内部 } }正确写法是把 return 的部分也 await,或者在调用方加 catch。ES6 的异步学习绕不开这些细节,建议每个示例都在本地改动之后跑一遍,观察错误进入 catch 的路径。
8. ES6 模块化:import/export 与工程化
模块化是大型项目的基础能力。ES6 提供了标准化的 import/export 语法,把“文件即模块”的边界定义得清清楚楚。
8.1 命名导出与默认导出
模块里通过 export 把变量、函数、类暴露出去,其他文件用 import 引入。
// src/utils.js export function formatTime(ms) { return `${(ms / 1000).toFixed(2)}s`; } export const VERSION = '1.0.0'; export default function logger(message) { console.log(`[LOG] ${message}`); }同一个模块可以有很多命名导出,但只能有一个默认导出。引入时可以这样写:
// src/main.js import logger, { formatTime, VERSION } from './utils.js'; logger('系统启动'); console.log(formatTime(1200)); console.log(VERSION);注意 Node 环境下,import 路径要写完整文件名,包括 .js 后缀。这是 ES Module 的相对导入规则,和 CommonJS 的 require 行为不一样。
8.2 动态 import
静态 import 在文件加载时就会执行。如果某个模块体积很大,或者只在用户触发某个操作时才需要,可以用动态 import。它返回一个 Promise:
const { default: report } = await import('./report.js'); report();动态 import 非常适合路由懒加载、大组件按需加载、工具库按功能分包这类场景。前端工程化里很多性能优化其实不是去压缩代码,而是把不那么关键的代码延迟到需要的时候再加载。
8.3 Node 与浏览器的模块差异
浏览器里使用 ES6 模块,要在 script 标签上加 type="module":
<script type="module" src="/src/main.js"></script>Node 环境里,想让 .js 文件使用 ES Module,最简单的方式是在 package.json 里加"type": "module"。如果不想改全局配置,也可以把文件后缀改成 .mjs。
模块化还涉及一个实际运维问题:循环依赖。两个模块互相 import,在大型项目中容易出现初始化顺序异常。遇到这种情况,优先考虑重构抽离公共依赖,不要试图在 import 层面绕过去。
整体来看,import/export 把 JavaScript 从“一堆 script 标签靠全局变量通信”推进到了“每个文件都有明确边界,显式声明依赖”的阶段。这也是为什么现代前端框架都要求 ES6 模块化作为基础,TypeScript 编译后输出的也是 ES Module 形式。
9. ES6 批量自测:自动化验证脚本
学习 ES6 不能只看不跑。把测试脚本写成自动化断言,比手动打开终端看输出要可靠得多。从 Node.js 18 开始,Node 内置了 test 模块,不需要额外安装 Jest 或者 Mocha 就能写单元测试。
下面是一个覆盖多个 ES6 特性的测试文件:
// test/es6.test.js import { test } from 'node:test'; import assert from 'node:assert/strict'; test('let 是块级作用域', () => { let value = 1; { let value = 2; } assert.equal(value, 1); }); test('const 不能重新赋值', () => { const name = 'es6'; assert.throws(() => { name = 'es7'; }, TypeError); }); test('箭头函数在模块顶层不绑定 this', () => { const fn = () => this; assert.equal(fn(), undefined); }); test('class 继承与 super', () => { class Animal { constructor(name) { this.name = name; } speak() { return 'sound'; } } class Dog extends Animal { speak() { return `${this.name} says wang`; } } const dog = new Dog('旺财'); assert.equal(dog.speak(), '旺财 says wang'); }); test('async/await 获取 Promise 结果', async () => { const getNumber = () => Promise.resolve(42); const num = await getNumber(); assert.equal(num, 42); });运行测试:
node --testNode 会扫描当前工程里符合测试文件命名规则的文件并依次执行。输出里能看到每个测试用的名称、耗时和结论。如果某个特性行为被改错了,很快就定位到文件行号。这种方式很适合做“语法特性批量验证”:学一个特性,补一个用例,长期积累下来就是一套针对性很强的 ES6 回归测试。
如果 Node 版本低于 18,node:test 不可用,可以退一步写一个普通断言脚本:
node test/run.js// test/run.js import assert from 'node:assert/strict'; const result1 = 1 + 1; assert.equal(result1, 2); console.log('基础断言通过');这套自测习惯的收益不止在学 ES6 阶段。真实项目里,把模块的输入输出用边界用例固定下来,重构时就能大胆改代码,不用担心改坏别人依赖的功能。
10. ES6 常见问题与排查清单
ES6 语法的报错信息并不复杂,但新手很容易被同一个错误反复卡住。下面把常见问题整理成表格,按现象查原因。
10.1 启动与语法类问题
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Import 语句报错:Cannot use import statement outside a module | 文件被当作 CommonJS 模块解析 | 查看 package.json 是否有 type 字段 | 添加"type": "module"或将文件改为 .mjs |
| 变量访问报错:Cannot access 'x' before initialization | 踩了暂时性死区 | 找出变量声明位置和访问顺序 | 先声明后使用,避免提前访问 |
| const 重新赋值报 TypeError | 对常量绑定了新值 | 确认赋值目标是否与声明变量一致 | 改为 let,或修改对象属性而不是重新赋值 |
| 箭头函数后使用 new 报错 | 箭头函数不能当构造函数 | 检查是否写了 new 箭头函数 | 改为普通函数或 class |
| 解构 undefined 报错 | 解构目标本身是 undefined | 检查数据源是否存在 | 给函数参数加默认值= {} |
10.2 运行结果与 this 类问题
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 箭头函数里的 this 不是组件实例 | 箭头函数在定义时捕获外层 this | 确认定义位置和作用域链 | 在普通方法内部使用箭头函数,或在类字段中绑定 |
| class 子类报错:Must call super constructor | constructor 没有先调用 super | 看子类构造函数第一行 | 在使用 this 前调用 super(...) |
| Promise 报错未捕获:UnhandledPromiseRejection | await 的位置不对,或没有 catch | 检查返回的 Promise 是否被处理 | 用 try/catch 包住 await,或手动 .catch |
| async/await 拿到的是 Promise 对象而不是值 | 忘了在最外层 await | 打印返回值看类型 | 在 async 函数内部 await,或调用处 .then |
| 模块之间循环依赖导致初始化异常 | 两个模块互相 import | 使用状态时打印依赖加载顺序 | 抽离公共依赖,减少互相 import |
还有一个容易被忽略的坑:在 Node ESM 环境里,模块顶层 this 是 undefined,不是全局对象。如果在模块顶层写console.log(this),看到 undefined 是正常行为。如果使用的是浏览器 script 标签方式加载,顶层 this 是 window,两种环境表现不一样,排错时先确认自己在哪个环境。
11. 最佳实践与下一步学习建议
11.1 推荐学习顺序
课程和文章的顺序可以这样安排,按依赖关系递进:
- let/const 与块级作用域,先打变量基础。
- 模板字符串、解构赋值、展开运算符,简化日常操作。
- 箭头函数与默认参数,掌握函数的新写法。
- class 与面向对象,理解封装和继承。
- Promise 与 async/await,打破回调嵌套。
- import/export,完成模块化工程化。
- Proxy、Reflect、Map、Set、Symbol,作为进阶扩展。
这个顺序的好处是每一步都建立在前一步的基础上。如果直接跳到 class 和模块化,往往会因为 this 和模块边界不清晰而卡住。
11.2 工程化建议
- 默认用 const,需要改值才用 let,不用 var。
- 配置 ESLint,开启 no-var 和 prefer-const 规则。
- 给 package.json 加 test 脚本,把测试从“手动看输出”升级为“断言自动判断”。
- 目录上把 src、test 分开,示例代码和测试代码不要混放。
- 批量任务和接口处理优先考虑 Promise.allSettled,避免单个请求失败中断整个批次。
如果团队代码风格不统一,建议接 Prettier 统一格式化。格式问题虽然不影响运行,但在 review 时占用的注意力比功能逻辑还多,这个成本完全可以提前消除。
11.3 下一步方向
ES6 只是现代 JavaScript 的一个起点。把这套内容跑通之后,下一步可以按兴趣选三个方向:
一是服务端方向,学 Node.js 的 Express 或 Koa,把模块化和异步编程放到真实后端项目里用起来。二是前端框架方向,去学习 Vue 3 或 React,组件、路由、状态管理全都建立在 ES6 之上。三是类型方向,直接接触 TypeScript,把 ES6 的变量、函数、类进一步加上类型约束,提前发现很多隐蔽的运行时错误。
学习 ES6 最容易踩的坑就是“背了不用”。语法看懂了和能写出来之间有很长的距离。建议把文章里的示例复制到本地,每个文件都跑一遍,再根据自己的需求改一改,比如给列表做批量格式化、给接口数据做解构提取、给定时任务写 async 流程。改过的代码才是真正属于你的代码。