跳到主要内容

ES6+ 现代语法精讲

引入段落:在长达十余年的时间里,JavaScript 的演进非常缓慢,开发者们深受 var 变量提升、没有模块化机制、回调地狱等历史遗留问题的折磨。直到 2015 年,ECMAScript 第 6 版(简称 ES6,官方称作 ES2015)横空出世,这堪称是 JavaScript 历史上最为惊天动地的一次核裂变级升级。它引入了数量庞大的新语法和新特性,彻底将 JS 推向了现代化、工程化的高级语言行列。

自此之后,TC39 委员会改变了发布策略,改为每年发布一个较小的迭代版本(ES2016, ES2017 直至今日)。业界习惯将 ES2015 及之后的所有新标准统称为 ES6+。作为现代前端开发者,无论是使用 React/Vue,还是编写 Node.js,熟练运用这些语法早已不是加分项,而是必须项。

一、变量声明革命:告别 var 的混乱时代

在 ES6 之前,var 是声明变量的唯一途径,但它的设计充满了早期的随意性。

1.1 var 的原罪:变量提升与全局污染

var 声明的变量会被“提升(Hoisting)”到当前函数作用域的顶部,即使你在代码最底部声明它,也会导致意外的结果。更糟糕的是,如果它不在任何函数里,它会直接挂载到 window 对象上,污染全局空间。此外,它允许同一作用域内被重复声明。

1.2 新纪元:letconst

ES6 引入了两个全新的关键字,并带来了极其严谨的**块级作用域(Block Scope)**概念——变量的作用域被严格限制在最近的花括号 {} 内。

  • let:用于声明值可能会改变的变量。不允许同一作用域内重复声明。
  • const:用于声明常量,声明时必须立刻初始化。一旦声明,便不允许再重新赋值(改变内存地址指向)。

:::warning 面试高频考点:暂时性死区 (TDZ) 使用 letconst 声明的变量同样存在“提升”,但在代码执行到声明该变量的那一行之前,这个变量处于“暂时性死区”。如果在声明前就试图访问它,引擎会极其严厉地抛出 ReferenceError,而不是像 var 那样返回恶心的 undefined。这种机制强制我们养成先声明、后使用的良好习惯。:::

1.3 const 的一个常见误区

const 保证的不是变量的值永远不变,而是变量绑定的内存地址指针永远不变

const user = { name: 'Alice' };
// 这是合法的!对象的属性并不受 const 约束,因为 user 指针的内存地址没变
user.name = 'Bob';

// 这是非法的!试图让 user 指向一个新对象的内存地址,会报错 TypeError
user = { name: 'Charlie' };

// 如果你想彻底冻结对象,必须使用 Object.freeze(user)

二、箭头函数 (Arrow Functions):简洁与 This 革命

箭头函数不仅仅是少写几个 function 字母的语法糖,它从根本上解决了 JS 开发者几十年来最头痛的问题:动态漂移的 this 指向。

2.1 令人愉悦的简写语法

当结合数组的高阶函数(如 map, filter)时,箭头函数让代码优雅得像一首诗。

// 旧时代的写法
var squares = [1, 2, 3].map(function (x) {
return x * x;
});

// 箭头函数的终极形态:
// 1. 只有一个参数时,省略括号 ()
// 2. 函数体只有一行 return 时,省略花括号 {} 和 return 关键字
const squares = [1, 2, 3].map((x) => x * x);

2.2 静态的词法绑定 this

传统普通函数的 this 取决于谁调用了它(运行时绑定)箭头函数根本没有自己的 this!它会像普通变量查找一样,向外层作用域去寻找 this,并捕获定义时所在上下文的 this 作为自己的值(静态绑定)。

const Timer = {
seconds: 0,
start: function () {
// 过去:由于 setTimeout 的回调是交给全局执行的,里面的 this 会变成 window
// 你不得不写烦人的 var self = this; 或者 bind(this)

// 现在:箭头函数没有自己的 this,完美捕获并锁定了外层 start 方法的 this (即 Timer 对象)
setInterval(() => {
this.seconds++;
console.log(this.seconds);
}, 1000);
},
};
Timer.start();

注意:正是因为它没有自身的机制,箭头函数绝对不能被当作构造函数去 new,也不能用作 Vue 组件的 methods 或对象字面量本身的属性方法,否则 this 会指向错误的全局环境。

三、解构赋值 (Destructuring):优雅的数据提取

这是一种模式匹配语法,允许你直接从复杂的对象或数组中提取出你需要的值,并赋给独立的变量。它极大地减少了代码中频繁出现的 . 链式访问。

3.1 对象解构的华丽操作

const response = {
status: 200,
data: {
user: { id: 1, name: 'Alice', role: 'admin' },
},
};

// 1. 深度嵌套提取
// 2. 变量重命名(将 name 存入 userName 变量)
// 3. 设置默认值(如果不存在 avatar,使用默认字符串)
const {
data: {
user: { name: userName, role, avatar = 'default.png' },
},
} = response;

console.log(userName); // "Alice"
console.log(avatar); // "default.png"

它在函数参数中也极其常用(尤其是在 React 接收 props 时): function Avatar({ userId, size = "small" }) { ... }

3.2 数组解构

数组解构依赖的是位置顺序。非常适合交换变量的值,以及提取函数返回的多个结果。

let a = 1,
b = 2;
// 不需要借助令人讨厌的临时变量 temp,一行代码完成交换
[a, b] = [b, a];

// 跳过不需要的元素
const rgb = [255, 128, 0];
const [, g] = rgb; // g = 128

四、模板字符串 (Template Literals)

告别丑陋的单引号、双引号拼接(+)和恶心的反斜杠换行(\n)。使用反引号 ` 包裹字符串,内部使用 ${} 动态注入变量或任意 JS 表达式。

const productName = 'MacBook Pro';
const price = 12999;

// 多行书写自然清晰,表达式甚至可以执行函数
const html = `
<div class="card">
<h2>产品名:${productName}</h2>
<p class="price">折后价:¥${price * 0.9}</p>
</div>
`;

五、全能的三个点:扩展运算符与剩余参数 (...)

根据上下文的不同,... 扮演着两个动作相反但同样强悍的角色。

5.1 扩展运算符 (Spread Operator):打碎与铺开

用于将一个可迭代对象(如数组)“炸开”成逗号分隔的散落值序列。它彻底改变了数组的合并与复制方式。后来(ES2018)这一特性扩展到了对象上。

// 数组的浅拷贝与合并
const arr1 = [1, 2];
const arr2 = [...arr1, 3, 4]; // 结果: [1, 2, 3, 4]

// 对象的浅拷贝与合并(替代了繁琐的 Object.assign)
const defaults = { theme: 'light', lang: 'zh' };
const userPrefs = { theme: 'dark', fontSize: 16 };
// 后面展开的属性如果出现同名键,会覆盖前面的
const mergedConfig = { ...defaults, ...userPrefs };
// 结果: { theme: 'dark', lang: 'zh', fontSize: 16 }

5.2 剩余参数 (Rest Parameters):收集与聚拢

出现在函数定义的形参列表的最后一位,用于将传入的任意数量的零散参数,自动“打包”收集进一个真正的数组中。它完美取代了过去那个容易出错且不是真数组的 arguments 伪对象。

function sum(firstNumber, ...restNumbers) {
// restNumbers 是一个标准数组,可以直接使用 reduce 等高阶方法
return restNumbers.reduce((acc, val) => acc + val, firstNumber);
}
sum(10, 1, 2, 3); // 16

六、模块化大一统:ES Modules (ESM)

在很长一段黑暗时期,JS 没有模块系统,导致全局变量灾难冲突。社区拼凑出了 CommonJS (用于 Node.js) 和 AMD/CMD。最终,ES6 原生定义了统治一切的模块系统标准:importexport

  • 命名导出/导入 (Named Exports):一个文件可以导出无数个变量或函数。导入时必须使用大括号 {} 且名称严格一致(除非使用 as 重命名)。
  • 默认导出/导入 (Default Exports):一个文件只能有一个代表该文件核心的主导出。导入时不需要大括号,你可以随意给它起名。
// --- utils.js ---
export const API_URL = 'https://api.com'; // 命名导出
export function formatDate() {} // 命名导出
export default class RequestClient {} // 默认导出 (通常在文件末尾)

// --- main.js ---
// 将默认导出命名为 Client,同时按需提取命名导出的内容
import Client, { API_URL, formatDate as format } from './utils.js';

ES2020 更是加入了极其重要的动态导入机制 import('./module.js')。它不阻塞代码执行,返回一个 Promise,是现代前端框架实现路由级代码分割(Code Splitting)和懒加载的底层核心基石。

七、防御性编程的福音:可选链与空值合并

在深层嵌套的 JSON 数据结构中,为了防止代码因为中间某个属性为 null 而崩溃报错(著名的 TypeError: Cannot read properties of undefined),我们过去不得不写出恶心至极的串联防守代码: const zip = user && user.address && user.address.zipCode;

7.1 可选链操作符 ?. (ES2020)

只要在访问链条中加入 ?.,一旦引擎检测到该级属性是 nullundefined,就会立刻停止向深处查找,并极其优雅地返回 undefined

// 这一行代码替代了上面冗长的防守!
const zip = user?.address?.zipCode;

// 甚至支持对可选方法的调用:如果回调函数不存在,什么都不发生,不会报错
props.onSuccess?.(responseData);

7.2 空值合并操作符 ?? (ES2020)

当我们需要给变量提供一个回退默认值时,过去常使用逻辑或 ||。但 || 的缺陷在于它是宽泛的“假值判断”,遇到 0, "" (空字符串), false 也会将其视作不存在而采用默认值,这经常引发严重逻辑 Bug。

?? 是严谨的空值判断:只有当左侧的值严格为 null 或是 undefined 时,才会采用右侧的后备默认值!

const userPoints = 0; // 用户的真实积分为 0

const pointsA = userPoints || 100; // 错误:由于 0 是假值,导致积分变成了默认的 100
const pointsB = userPoints ?? 100; // 正确:因为 0 并不是 null/undefined,保留真实积分 0

总结

上面列举的仅仅是 ES6+ 庞大更新中的冰山一角。还有强大的 Promise(异步编程的核心,下一章详述)、Symbol 数据类型、Map/Set 高效数据结构、Proxy 拦截器(Vue3 响应式的灵魂),甚至最新的顶级 Await 等等。

拥抱 ES6+,不仅是用语法糖偷懒,更是一场编程思维的进化。它带来了不变性约束(const)、避免了副作用污染、提升了代码的声明式程度。作为现代前端开发者,保持对 ECMAScript 年度新规范的持续关注,是你职业生涯的必修课。