ES6+ 必用新特性:箭头函数、解构、模板字符串、扩展运算符

18 阅读6分钟

写在前面

ES6(即 ES2015)是 JavaScript 历史上规模最大的一次升级,之后每年的新版本(ES2016~)统称 ES6+。它对现代 JS 的影响是全方位的:let/const 取代 var、类与模块、Promise 与 async/await……但真正让日常代码"脱胎换骨"的,是四个使用频率最高、组合起来威力巨大的语法特性:

  1. 箭头函数 —— 更短的函数写法 + 词法 this;
  2. 解构赋值 —— 从数组/对象中"拆"出值;
  3. 模板字符串 —— 字符串插值与多行;
  4. 扩展运算符 / 剩余参数 —— 数组与对象的合并、复制、收集。

本文从语法讲到 this 陷阱、浅拷贝边界,再给 5 个实战,帮你把代码量砍掉三成的同时避开最常见的坑。


一、箭头函数

1.1 基本语法

箭头函数用 => 取代 function,写法更短:

// 普通函数
const add1 = function (a, b) { return a + b; };

// 箭头函数
const add2 = (a, b) => a + b;        // 单个表达式:自动 return

// 只有一个参数:省略括号
const double = x => x * 2;

// 没有参数:括号不能省
const hi = () => "hello";

// 函数体多条语句:必须写 return
const calc = (a, b) => {
  const sum = a + b;
  return sum * 2;
};

规则速记:

情况写法
一个参数x => x * 2(可省括号)
多个 / 零个参数(a, b) => ...、() => ...
单表达式函数体省略 {} 和 return
多语句函数体必须写 {} 和 return

1.2 核心区别:this 是词法绑定的

普通函数的 this 由"调用方式"决定(谁调用指向谁);箭头函数的 this 由"定义位置"决定(继承外层作用域),定义后不可改变。

const obj = {
  name: "Tom",
  sayLater: function () {
    // 普通函数:this 是 window,找不到 name
    setTimeout(function () {
      console.log(this.name);      // undefined
    }, 100);

    // 箭头函数:this 继承自 sayLater 的外层(obj)
    setTimeout(() => {
      console.log(this.name);      // "Tom"
    }, 100);
  },
};

所以箭头函数天生适合回调(定时器、事件、数组方法),不需要再写 var self = this 或 .bind(this)。

1.3 箭头函数的三个"不能"

// 1. 不能作为构造函数
const A = () => {};
new A();                 // TypeError: A is not a constructor

// 2. 没有自己的 arguments
const f = () => console.log(arguments);   // 报错/不是本函数参数
const g = (...args) => console.log(args); // 用 rest 代替

// 3. 没有 prototype
console.log(A.prototype);                 // undefined

因此需要动态 this 的地方(如对象方法、addEventListener 里要拿 this 指向元素)不适合用箭头函数。


二、解构赋值

解构让我们从数组或对象中按位置/按属性名取出值并赋给变量,是"赋值语法糖"。

2.1 数组解构

const arr = [1, 2, 3];

const [a, b, c] = arr;      // a=1, b=2, c=3
const [first] = arr;        // first=1
const [, , last] = arr;     // last=3(用逗号跳过)

// 默认值:只在 undefined 时生效
const [x = 10, y = 20] = [undefined, 5];   // x=10, y=5

// 交换变量:经典应用
let m = 1, n = 2;
[m, n] = [n, m];            // m=2, n=1,无需临时变量

// 剩余收集
const [head, ...rest] = [1, 2, 3, 4];   // head=1, rest=[2,3,4]

2.2 对象解构

const user = { id: 1, name: "Tom", age: 18 };

const { name, age } = user;        // 按属性名取值
const { id: userId } = user;       // 重命名:id → userId
const { email = "无" } = user;     // 默认值:无该属性时生效

// 嵌套解构
const data = { info: { city: "哈尔滨" } };
const { info: { city } } = data;   // city = "哈尔滨"

注意:对象解构按属性名匹配,数组解构按位置匹配;重命名写法是 原属性名: 新变量名。

2.3 函数参数解构

把参数直接"拆开用",配合默认值非常优雅:

function showUser({ name, age = 18 }) {
  console.log(`${name},${age}岁`);
}
showUser({ name: "Tom" });          // "Tom,18岁"

// 数组参数
function sum([a, b]) { return a + b; }
sum([3, 4]);                        // 7

这在 React 的 props、组件 options 参数里是标配写法。


三、模板字符串

用反引号 ` 包裹,支持插值与多行。

3.1 插值 ${}

const name = "Tom";
const age = 18;

// 传统:字符串拼接,易错
const old = "我叫 " + name + ",今年 " + age + " 岁";

// 模板字符串
const s = `我叫 ${name},今年 ${age} 岁`;

${} 里可以是任意表达式,甚至嵌套模板:

const total = 10;
const msg = `总计:${total * 2} 元`;          // 表达式
const list = `${[1, 2, 3].map(n => n * 2).join(", ")}`;   // 方法调用

3.2 多行字符串

// 传统:要写 \n 和续行
const html1 = "<div>\n  <p>你好</p>\n</div>";

// 模板字符串:直接换行
const html2 = `
  <div>
    <p>你好</p>
  </div>
`;

配合解构渲染 HTML 列表是最高频组合:

const users = [{ name: "Tom" }, { name: "Jerry" }];
const html = `
  <ul>
    ${users.map(u => `<li>${u.name}</li>`).join("")}
  </ul>
`;

3.3 标签模板(了解)

在模板字符串前加函数名,可对插值结果做预处理(常用于国际化、安全转义):

function tag(strings, ...values) {
  return strings[0] + values.map(v => `[${v}]`).join("");
}
tag`你好 ${"Tom"}`;     // "你好 [Tom]"

四、扩展运算符与剩余参数

... 同一个符号、两种用法:展开(拆开)与收集(聚合)。

4.1 展开数组

// 合并数组
const a = [1, 2], b = [3, 4];
const merged = [...a, ...b];          // [1,2,3,4]

// 复制数组(浅拷贝)
const copy = [...a];                  // 等价于 a.slice()

// 函数调用传参
const nums = [3, 5, 1];
Math.max(...nums);                    // 5(传统写法 Math.max.apply(null, nums))

// 字符串 → 数组
const chars = [..."abc"];             // ["a","b","c"]

4.2 展开对象

const base = { x: 1, y: 2 };
const copy = { ...base };            // 浅拷贝

// 合并对象(后面的覆盖前面的同名键)
const cfg = { ...base, y: 9, z: 3 }; // {x:1, y:9, z:3}

函数式更新状态的核心写法(React setState / Vue 不可变更新):

const state = { count: 0, name: "Tom" };
const next = { ...state, count: state.count + 1 };
// next 是全新对象,原 state 不变

4.3 剩余参数 rest(函数内收集)

function sum(...nums) {
  return nums.reduce((s, n) => s + n, 0);
}
sum(1, 2, 3);          // 6

// 与解构配合:首参固定,其余收集
const [head, ...rest] = [1, 2, 3];   // rest=[2,3]

箭头函数没有 arguments,用 (...args) 收集参数是最佳替代。

4.4 重要边界:展开是浅拷贝

... 只复制一层。嵌套的数组/对象仍是引用共享:

const original = { list: [1, 2, 3] };
const copy = { ...original };
copy.list.push(4);
console.log(original.list);   // [1,2,3,4]  ← 被改了!

// 深拷贝(一层的常用手段)
const deepCopy = { ...original, list: [...original.list] };

五、实战案例

实战 1:解构 + 模板字符串渲染列表

const users = [
  { id: 1, name: "Tom", city: "哈尔滨" },
  { id: 2, name: "Jerry", city: "北京" },
];

function render() {
  const html = `
    <ul>
      ${users.map(({ name, city }) => `
        <li>${name} — ${city}</li>
      `).join("")}
    </ul>
  `;
  document.querySelector("#list").innerHTML = html;
}
render();

实战 2:对象不可变更新(函数式)

let cart = { items: 1, total: 20 };

// 加一件:返回新对象,不改原对象
cart = { ...cart, items: cart.items + 1, total: cart.total + 20 };

// 数组形式:增删都用展开
let list = [1, 2, 3];
list = [...list, 4];             // 尾部追加
list = [0, ...list];             // 头部插入
list = list.filter(x => x !== 2); // 删除

实战 3:rest + 默认值 + 解构封装请求

function request(url, { method = "GET", timeout = 5000 } = {}, ...rest) {
  console.log(`请求:${method} ${url},超时 ${timeout}ms`);
  console.log("其余参数:", rest);
  // ...真正的 fetch 逻辑
}
request("/api/user", { method: "POST" }, "extra1");
// 请求:POST /api/user,超时 5000ms
// 其余参数:["extra1"]

实战 4:数组操作组合拳

// 数组去重 + 合并 + 排序
const a = [3, 1, 4], b = [1, 5, 9];
const merged = [...new Set([...a, ...b])].sort((x, y) => x - y);
// [1,3,4,5,9]

// 键值对数组 → 对象(解构 + reduce)
const pairs = [["name", "Tom"], ["age", 18]];
const obj = Object.fromEntries(pairs);      // {name:"Tom", age:18}

实战 5:购物车合并去重(实战优先)

const cart = [
  { id: 1, name: "苹果", price: 5, qty: 2 },
  { id: 2, name: "香蕉", price: 3, qty: 1 },
];

function addItem(newItem) {
  const idx = cart.findIndex(i => i.id === newItem.id);
  if (idx === -1) {
    return [...cart, { ...newItem, qty: 1 }];   // 新增
  }
  // 数量累加:不可变更新
  return cart.map((item, i) =>
    i === idx ? { ...item, qty: item.qty + 1 } : item
  );
}

六、高频误区清单

误区正解
箭头函数就是短一点this 是词法绑定,且不能当构造函数、无 arguments
对象方法用箭头函数安全需要动态 this(如事件里取元素)时不能用箭头函数
解构默认值对所有"假值"生效只在值为 undefined 时生效,null 不触发默认值
... 是深拷贝只浅拷贝一层,嵌套引用仍共享
对象展开顺序无所谓后面的同名属性会覆盖前面的
箭头函数可以 new不能作为构造函数,会抛 TypeError
数组解构按属性名数组按位置,对象才按属性名
arguments 在箭头函数里可用箭头函数没有 arguments,用 ...args

七、总结

一句话记住每个特性:

特性一句话
箭头函数写法更短 + this 继承外层,回调首选
解构数组按位置、对象按名字,{} 拆出即用
模板字符串反引号 + ${},告别 + 拼接
扩展运算符... 合并/复制数组与对象,rest 收集参数

四个特性组合起来能解决绝大多数"数据转换 + 渲染"的场景:解构取数据 → 模板字符串渲染 → 展开做不可变更新 → rest 收参数。