ES6 新特性详细讲解:常见使用方式、场景与坑
ES6(ES2015)是 JavaScript 历史上的一个巨大飞跃,引入了大量现代开发中不可或缺的语法与 API。下面按类别逐一剖析核心特性,重点说明日常怎么用、何时用、以及有哪些容易踩的坑。
1. let 和 const
基本讲解
- 块级作用域 :变量只在当前
{}内有效,不再像var那样函数级作用域。 let:声明可变变量。const:声明常量引用,对于对象/数组,内部属性可以改变,只是不能重新赋值。
常见使用方式与场景
js
// 循环中使用 let 获得每次迭代独立的绑定
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 0, 1, 2
}
// 若使用 var 会全部输出 3
// 用 const 声明不会被重新赋值的引用,增强代码可读性
const config = { api: '/v1' };
config.api = '/v2'; // OK,属性可以改
// config = {}; // 报错:Assignment to constant variable
场景 :全面替代 var,优先使用 const,只在明确需要重新赋值时用 let。
⚠️ 常见坑
-
暂时性死区(TDZ) :声明前访问变量会报
ReferenceError,而不是undefined。jsconsole.log(a); // ReferenceError let a = 1; -
const声明的对象仍可被修改 ,要完全冻结需用Object.freeze()。 -
重复声明 :同一作用域内
let/const不允许重复声明同名变量(var可以)。
2. 箭头函数
基本讲解
箭头函数 ()=>{} 没有自己的 this、arguments、super 或 new.target,它会捕获外层词法作用域的 this。
常见使用方式与场景
js
// 简短回调
[1, 2, 3].map(x => x * 2);
// 保持 this 指向
class Fetcher {
constructor() { this.data = null; }
fetch() {
fetch('/api')
.then(res => res.json())
.then(json => { this.data = json; }); // this 指向 Fetcher 实例
}
}
// 立即返回对象字面量要加小括号
const fn = name => ({ name });
场景 :几乎所有的短回调、需要绑定外层 this 的异步处理、数组方法回调。
⚠️ 常见坑
-
没有自己的
this:在对象方法中直接使用箭头函数会导致this指向外层(可能为window或undefined)。jsconst obj = { name: 'test', say: () => console.log(this.name) // this 指向全局,无法获取 obj.name }; obj.say(); // undefined -
不能用作构造函数 ,用
new会抛出TypeError。 -
没有
arguments对象 ,需用剩余参数...args替代。 -
不适用于需要动态
this的场景 ,如 Vue 组件的methods或事件处理函数绑定。
3. 模板字符串
基本讲解
使用反引号 ````` 包裹,支持多行字符串和 ${} 插值,还支持标签模板。
常见使用方式与场景
js
const name = 'World';
const str = `Hello ${name}!`;
// 多行字符串
const html = `
<div>
<p>多行文字</p>
</div>`;
// 标签模板(如 styled-components, GraphQL)
function highlight(strings, ...values) {
return strings.reduce((acc, str, i) =>
`${acc}${str}<mark>${values[i] || ''}</mark>`, '');
}
highlight`Hello ${name}, welcome!`; // Hello <mark>World</mark>, welcome!
场景:拼接 HTML、URL、SQL 查询、国际化文本、任何需要插入变量或多行文本的地方。
⚠️ 常见坑
- 模板字符串内的空白和换行都会被保留,可能导致意料之外的空格。
- 标签模板的
values数量与strings差 1,需要小心处理边界。 - 在
${}里放复杂表达式,注意可读性。
4. 解构赋值
基本讲解
从数组或对象中按模式提取值并赋值给变量。
- 数组解构 :
let [a, b] = [1, 2]; - 对象解构 :
let { x, y } = { x: 1, y: 2 };支持重命名{ x: newX }和默认值{ x = 10 }。
常见使用方式与场景
js
// 函数参数解构,直观获取配置
function ajax({ url, method = 'GET', data = {} }) { /*...*/ }
// 交换变量
[a, b] = [b, a];
// 提取深层属性
const { user: { profile: { email } } } = response;
场景 :提取接口返回数据、处理函数多个返回值、导入模块特定成员(import { readFile } from 'fs')。
⚠️ 常见坑
-
解构
null或undefined会报错 :jsconst { a } = null; // TypeError: Cannot destructure property 'a' of 'null'建议对可能为空的数据设置默认空对象
const { a } = obj || {};。 -
默认值只在值为
undefined时生效 ,null、false、0不会触发默认值。jsconst { x = 10 } = { x: null }; // x 是 null,不是 10 -
重命名与默认值同时使用时,注意顺序:
const { a: newA = 5 } = obj;。
5. 展开运算符(Spread)与剩余参数(Rest)
基本讲解
- 展开 :
...iterable将数组/字符串等可迭代对象展开为元素序列。 - 剩余参数 :
...args将多个实参收集成一个数组,代替arguments。
常见使用方式与场景
js
// 数组合并、浅拷贝
const arr = [1, 2, 3];
const newArr = [0, ...arr, 4]; // [0,1,2,3,4]
const copy = [...arr];
// 函数调用传参
Math.max(...arr);
// 剩余参数,收集多余实参
function sum(...nums) { return nums.reduce((a,b) => a+b, 0); }
// 浅拷贝对象(ES2018,此处作为扩展)
const objCopy = { ...originalObj };
场景:不可变数据操作、合并配置、函数参数灵活处理。
⚠️ 常见坑
-
展开只做浅拷贝 ,嵌套对象/数组仍然是引用共享,修改会互相影响。
jsconst orig = { a: 1, b: { c: 2 } }; const copy = { ...orig }; copy.b.c = 3; console.log(orig.b.c); // 3,被修改了! -
剩余参数必须是最后一个形参:
function(a, ...rest, b){}会报错。 -
...用于函数调用时,若展开一个超大数据量数组可能导致参数过多(引擎有参数数量限制)。
6. 函数默认参数
基本讲解
形参可以直接赋予默认值,且默认表达式会在调用时惰性求值。
常见使用方式与场景
js
function fetchData(url, method = 'GET', timeout = 5000) {
// ...
}
// 结合解构
function createElement({ type = 'div', content = '' } = {}) {
// 防止传入 undefined 导致解构报错
}
场景 :提供函数可选参数的默认行为,取代手动 options = options || {} 那种假值覆盖问题。
⚠️ 常见坑
-
默认值只在参数为
undefined时激活 ,null、false、0不会触发。 -
默认表达式作用域 :可以引用前面的参数,但遵循 TDZ。
jsfunction f(x = y, y = 1) {} // 调用无参时 y 尚未初始化,报 ReferenceError -
注意参数默认值会让函数产生一个独立的
arguments行为:在严格模式下,arguments不会反映默认值参数的变化,非严格模式有些差异,建议直接用剩余参数。
7. 类(Class)
基本讲解
语法糖,基于原型继承实现,提供了更清晰的构造函数、方法和 extends、super。
常见使用方式与场景
js
class Animal {
constructor(name) { this.name = name; }
speak() { console.log(`${this.name} makes a noise.`); }
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 必须在使用 this 前调用
this.breed = breed;
}
speak() { console.log(`${this.name} barks.`); }
}
场景:面向对象编程、React 类组件、封装复杂逻辑。
⚠️ 常见坑
- 必须使用
new调用 ,否则抛出TypeError。 - 子类构造函数里
super()必须且在使用this之前调用。 - 类方法不可枚举 (
enumerable: false),与对象字面量方法不同。 - 没有真正的私有属性 (ES6 标准中),后续通过
#语法或 TypeScriptprivate实现。ES6 中一般用_约定或Symbol/WeakMap模拟。 - 箭头函数在类字段中(类字段提案)能绑定
this,但那是后续提案,不是 ES6 的 class 本身。
8. 模块(ES Modules)
基本讲解
通过 import 和 export 实现静态模块系统,支持命名导出、默认导出以及重命名。
常见使用方式与场景
js
// math.js
export const PI = 3.14;
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
// app.js
import multiply, { PI, add as sum } from './math.js';
场景:现代前端工程化、Node.js (type: module)、Tree-shaking 依赖分析。
⚠️ 常见坑
- 静态结构 :
import和export只能在顶层作用域,无法在条件语句中使用(不能动态导入,需用import()函数,属 ES2020)。 - 只读绑定:导入的变量是只读的,尝试修改会报错(但对象属性可修改)。
- 循环依赖 :如果 a 导入 b,b 导入 a,可能会导致拿到的导出是未初始化的,需要重新设计模块或善用动态
import()。 - 路径必须完整 :浏览器环境下要加扩展名
.js,Node 中根据配置。
9. Promise
基本讲解
处理异步操作的状态机,有三种状态:pending、fulfilled、rejected。通过 .then() / .catch() 链式调用。
常见使用方式与场景
js
function fetchUser(id) {
return fetch(`/users/${id}`).then(res => res.json());
}
fetchUser(1)
.then(user => console.log(user))
.catch(err => console.error('失败', err))
.finally(() => console.log('请求结束')); // finally ES2018
// Promise 静态方法
Promise.all([p1, p2]) // 全部成功
Promise.race([p1, p2]) // 谁先完成
Promise.resolve(value)
Promise.reject(reason)
场景:网络请求、定时器、文件读取等所有异步操作,且是 async/await 的基础。
⚠️ 常见坑
-
忘记
return导致链断裂 :jsfetchUser(1).then(user => { updateUI(user); // 没有 return 下一个 then 会收到 undefined }).then(data => ...); -
未处理拒绝 :未加
.catch()的 Promise 一旦 reject,可能产生UnhandledPromiseRejection错误,需要全局兜底或必须 catch。 -
then 的回调返回一个 Promise 会自动展开,但同样要 return。
-
Promise.all一个失败全部失败,若需部分容错可用Promise.allSettled(ES2020)或用.catch包裹单个请求。
10. Set 与 Map
基本讲解
- Set :值唯一的集合,支持
add,has,delete,clear,可迭代。 - Map :键值对集合,键可以是任意类型,而对象键只能是字符串或 Symbol。支持
set,get,has,delete,可迭代。
常见使用方式与场景
js
// 数组去重
const arr = [1, 2, 2, 3];
const unique = [...new Set(arr)];
// Map 存储元数据,用对象做键
const cache = new Map();
const objKey = { id: 1 };
cache.set(objKey, 'data');
cache.get(objKey); // 'data'
场景:去重、缓存对象映射、记录访问状态、需要非字符串键的场景。
⚠️ 常见坑
-
Set 的比较是 SameValueZero :
NaN视为等于自身,+0和-0相等。jsnew Set([NaN, NaN]).size; // 1 -
Map 的键基于引用比较,对象作为键时,即使内容相同但引用不同也会被视为不同键。若想用内容匹配,需自行处理。
-
WeakSet/WeakMap 只能存放对象,且对键是弱引用,不可迭代,常用于 DOM 节点关联数据避免内存泄漏,不能用于需要遍历的场景。
11. Symbol
基本讲解
创建唯一且不可变的值,常用作对象属性的标识符,防止属性名冲突。Symbol() 每次返回全新的值。
常见使用方式与场景
js
const LOG_LEVEL = Symbol('logLevel');
const obj = {
[LOG_LEVEL]: 'debug',
normalProp: 'hi'
};
// for...in, Object.keys() 都不会枚举 Symbol 属性
console.log(obj[LOG_LEVEL]); // 'debug'
场景 :定义内部状态、自定义迭代器(Symbol.iterator)、框架中避免属性冲突。
⚠️ 常见坑
-
Symbol 不能隐式转换成字符串 :
jsconst s = Symbol('desc'); '' + s; // TypeError: Cannot convert a Symbol value to a string需使用
String(s)或s.description。 -
JSON.stringify 会忽略 Symbol 键和 Symbol 值。
-
全局 Symbol 注册用
Symbol.for('key'),但可能造成全局污染。
12. 迭代器(Iterator)与生成器(Generator)
基本讲解
- 迭代器协议 :对象实现
next()方法返回{ value, done }。 - 可迭代协议 :实现
Symbol.iterator方法,用于for...of、...展开等。 - 生成器 :
function*函数,可通过yield暂停与恢复,返回迭代器。
常见使用方式与场景
js
// 自定义迭代器
const range = {
from: 1, to: 5,
[Symbol.iterator]() {
let current = this.from;
return { next: () => current <= this.to ? { value: current++ } : { done: true } };
}
};
for (let n of range) { console.log(n); }
// 生成器做 ID 生成
function* idMaker() {
let id = 0;
while (true) yield id++;
}
const gen = idMaker();
gen.next().value; // 0
场景:处理惰性求值序列、实现异步流程控制(redux-saga)、自定义数据结构遍历。
⚠️ 常见坑
- 生成器返回的迭代器只能遍历一次。
- 注意
yield的返回值是下一个next(arg)传入的参数,容易在双向通信时混淆。 - 在生成器中未捕获的错误会使生成器终止。
- for...of 不能直接遍历普通对象(除非实现迭代器),必须迭代
Object.keys/values/entries。
13. Proxy 和 Reflect
基本讲解
- Proxy:创建对象的代理,拦截并自定义对象的基本操作(get、set、has、deleteProperty 等 13 种)。
- Reflect:提供操作对象的默认方法(与 Proxy 陷阱一一对应),使行为更可预测。
常见使用方式与场景
js
const handler = {
get(target, key, receiver) {
console.log(`Getting ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
if (typeof value !== 'number') throw new Error('必须为数字');
return Reflect.set(target, key, value, receiver);
}
};
const obj = new Proxy({}, handler);
obj.a = 10; // 成功
obj.b = 'x'; // 抛出错误
场景:数据验证、日志记录、响应式系统(如 Vue 3 的 reactivity)、属性访问控制、实现数组负索引等。
⚠️ 常见坑
- 代理会带来性能开销,在热点代码中需谨慎。
- this 陷阱 :代理中的方法若依赖
this,可能指向代理而非原始对象,需要绑定或使用receiver。 - 无法拦截某些操作:比如严格模式下某些操作、原型链上的 property 查找。
- 应使用
Reflect执行默认行为,否则可能破坏内部原理。
14. 新增数组与字符串方法
常见新增
- Array :
find,findIndex,fill,copyWithin,Array.from,Array.of,includes(ES7 但常与 ES6 共提)。 - String :
startsWith,endsWith,includes,repeat,padStart/padEnd(后俩 ES2017)。
使用方式与场景
js
// 查找第一个符合条件的元素
const users = [{id:1}, {id:2}];
const user = users.find(u => u.id === 2);
// 填充数组
new Array(3).fill(0); // [0,0,0]
// 将类数组/迭代器转为真数组
Array.from(document.querySelectorAll('li'));
// 字符串检测
'/api/users'.startsWith('/api');
场景 :替代 indexOf !== -1 的写法,填充默认数据,操作类数组。
⚠️ 常见坑
-
find返回第一个元素,未找到返回undefined;findIndex未找到返回-1。 -
fill使用同一个引用填充对象数组,导致所有元素共享同一个对象,修改一个会影响全部:jsconst arr = new Array(3).fill({}); arr[0].a = 1; console.log(arr[1].a); // 1,全指向同一个对象 -
Array.from对包含Symbol.iterator的对象转换更可靠,普通有 length 属性的也能转,但会缺失真正迭代。
15. for...of 循环
基本讲解
遍历可迭代对象(数组、字符串、Set、Map、arguments、NodeList 等),获取的是值 (for...in 获取键)。
使用与场景
js
// 遍历 Map
const map = new Map([['a', 1], ['b', 2]]);
for (let [key, value] of map) { /* ... */ }
// 遍历字符串
for (let char of 'hello') {}
场景 :无需索引的数组遍历,支持 break/continue,比 forEach 灵活。
⚠️ 常见坑
- 不能直接遍历纯对象,因为普通对象没有实现迭代器协议,会报错。可通过
Object.entries(obj)等处理。 - 在遍历过程中修改遍历集合会导致行为不一致,建议先复制一份。
总结与最佳实践
- 变量声明 :默认
const,需要改则let,忘记var。 - 函数 :回调用箭头函数,但对象方法和动态
this场景用普通函数。 - 异步 :Promise 链式调用记得
return和catch,更推荐async/await。 - 解构/展开:善用但注意浅拷贝问题,深层修改用深拷贝库或手动解构。
- 类 :组织复杂逻辑,别忘
super规则,私有性可结合 TypeScript 或#语法。 - 模块:静态导入为主,动态导入用于按需加载;留意循环依赖。
- 新数据结构:用 Map/Set 处理键非字符串、去重等;注意引用比较。
掌握这些特性及其边界情况,能大大提升代码简洁性、可读性和健壮性,同时避开常见的开发陷阱。