ES6+ 核心语法速通

为什么要先学 ES6+

ES6 (ES2015) 是 JavaScript 的现代化起点。在那之前的 JS 写起来像玩具语言,之后才真正具备了写大型应用的能力。Vue 3 的源码、TypeScript、以及几乎所有现代前端库,都大量使用 ES6+ 语法。

Java 类比:ES6 之于 JS,大概相当于 Java 8 的 Lambda + Stream + Optional 那一波更新——都是让语言从"能用"变成"好用"的分水岭。


1. 变量声明:let / const 取代 var

// 旧写法(少碰)
var name = 'foo'

// 新写法
let age = 18        // 可变
const MAX = 100     // 不可变(但对象里的属性还是能改)

Java 对照

Java JS
int age = 18; let age = 18
final int MAX = 100; const MAX = 100

坑点


2. 箭头函数:轻量级 Lambda

// 普通函数
function add(a, b) {
  return a + b
}

// 箭头函数
const add = (a, b) => a + b

Java 对照

// Java 8 Lambda
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

关键差异:Java 的 Lambda 里 this 指向外部类;JS 的箭头函数里 this 也指向外层作用域。但普通 function 里的 this 是个坑——谁调用它就是谁,和 Java 完全不一样。

const obj = {
  count: 0,
  // ❌ 这里不要用箭头函数,this 会指向全局而非 obj
  bad: () => { this.count++ },
  // ✅ 方法简写,this 正确指向 obj
  good() { this.count++ },
}

建议:回调函数用箭头函数,对象方法用简写。


3. 解构赋值:一次取多个值

const user = { name: 'foo', age: 18, email: 'f@x.com' }

// 对象解构
const { name, age } = user
// 等价于 const name = user.name; const age = user.age;

// 数组解构
const [first, second] = [10, 20]

// 重命名 + 默认值
const { name: userName, role = 'guest' } = user

Java 对照:Java 没有解构。在 Java 里你得写:

String name = user.getName();
int age = user.getAge();

常见用途:函数参数直接解构,等价于 Java 的命名参数。

function createUser({ name, age, role = 'user' }) {
  // ...
}
createUser({ name: 'foo', age: 18 })

4. 模板字符串:告别字符串拼接

const name = 'foo'
const greeting = `Hello, ${name}! Today is ${new Date().toLocaleDateString()}.`

// 多行字符串也很自然
const html = `
  <div>
    <h1>${title}</h1>
  </div>
`

Java 对照:类似 Java 15 的文本块 """...""",加上 String.format() 或 Java 21 的 STR."Hello \{name}" 模板。


5. 扩展运算符 (...)

// 数组
const arr1 = [1, 2, 3]
const arr2 = [0, ...arr1, 4]   // [0, 1, 2, 3, 4]

// 对象合并
const base = { a: 1, b: 2 }
const extended = { ...base, c: 3 }   // { a: 1, b: 2, c: 3 }

// 函数参数收集
function sum(...nums) {
  return nums.reduce((a, b) => a + b, 0)
}
sum(1, 2, 3, 4)   // 10

Java 对照

注意:对象扩展是浅拷贝。嵌套对象还是引用。


6. Promise 与 async/await:告别回调地狱

这是 ES6+ 最重要的更新之一。前端几乎所有异步操作(网络请求、定时器、文件读写)都用它。

// 用 Promise
fetch('/api/user')
  .then(res => res.json())
  .then(user => console.log(user))
  .catch(err => console.error(err))

// 用 async/await(推荐)
async function loadUser() {
  try {
    const res = await fetch('/api/user')
    const user = await res.json()
    console.log(user)
  } catch (err) {
    console.error(err)
  }
}

Java 对照

Java JS
CompletableFuture<T> Promise<T>
.thenApply(fn) .then(fn)
.exceptionally(fn) .catch(fn)
Java 19+ 虚拟线程让异步代码看起来同步 async/await

关键理解

常见坑:忘记 await 导致拿到的是 Promise 对象而不是结果值。

6.1 原理:Promise 是个状态机

一个 Promise 内部就三个字段:

state:   'pending' | 'fulfilled' | 'rejected'   // 状态,一旦改变不可逆
value:   resolve 时传入的值 / reject 时的错误
callbacks: 一组 then/catch 注册的回调

状态转移规则(单向、不可逆):

              resolve(v)
   pending ─────────────► fulfilled(v)
      │
      │  reject(e)
      └─────────────► rejected(e)
const p = new Promise((resolve, reject) => {
  resolve(1)        // state: pending → fulfilled, value: 1
  resolve(2)        // 已经定型,这次调用被忽略
  reject('boom')    // 同样被忽略
})

Java 对照:和 CompletableFuture 几乎一致——complete() / completeExceptionally() 也只对未完成的 future 生效。

6.2 then 不是回调,是"返回新 Promise"

每个 .then() 都返回一个新的 Promise,链式调用本质是 Promise 的接力:

Promise.resolve(1)
  .then(v => v + 1)        // 返回新 Promise<2>
  .then(v => v * 10)       // 返回新 Promise<20>
  .then(v => Promise.resolve(v + 5))  // 返回 Promise<25>,自动解套
  .then(v => console.log(v))          // 25

关键规则

这就是为什么 .then().then().catch() 能工作——异常像水一样往下流。

6.3 微任务(microtask):await 后面的代码什么时候跑

JS 的事件循环里有两个队列:

┌──────────────┐     ┌──────────────┐
│ 宏任务队列    │     │ 微任务队列    │
│ (macrotask)  │     │ (microtask)  │
├──────────────┤     ├──────────────┤
│ setTimeout   │     │ Promise.then │
│ setInterval  │     │ queueMicrotask│
│ I/O          │     │ MutationObserver│
│ requestAnimationFrame│ │ async 函数 await 后续 │
└──────────────┘     └──────────────┘

执行规则(背下来)

  1. 跑同步代码到底
  2. 清空整个微任务队列(不止跑一个,跑到队列空为止)
  3. 渲染(如果需要)
  4. 一个宏任务
  5. 回到 2

经典面试题:

console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 输出顺序:1 → 4 → 3 → 2

为什么 32 前面?因为同步代码跑完后先清空整个微任务队列,再去碰宏任务。

await 的本质await x 等价于"把后续代码包进 Promise.resolve(x).then(...)"——所以 await 后的代码总是在微任务里跑。

async function foo() {
  console.log('A')
  await 1
  console.log('B')   // 这行实际是微任务
}
console.log('start')
foo()
console.log('end')
// start → A → end → B

Java 对照:JVM 没有微任务/宏任务的双队列设计,但行为上类似 CompletableFuturethenApplyAsync 在公共 ForkJoinPool 里跑——只是 JS 的微任务是固定优先于下一轮 I/O。

6.4 await 在循环里的陷阱

// ❌ 顺序执行,10 个请求串行,慢
for (const id of ids) {
  await fetch(`/api/${id}`)
}

// ✅ 并行发起,等所有完成
await Promise.all(ids.map(id => fetch(`/api/${id}`)))

// ✅ 并行发起,部分失败也继续
const results = await Promise.allSettled(ids.map(id => fetch(`/api/${id}`)))

Promise.all 任一 reject 就整体 reject;Promise.allSettled 等所有完成(不管成败),返回每项结果数组。对应 Java 的 CompletableFuture.allOf()


7. 模块化:import / export

// user.js
export function getUser() { /* ... */ }
export const USER_ROLE = 'admin'
export default class UserService { /* ... */ }

// main.js
import UserService, { getUser, USER_ROLE } from './user.js'

Java 对照

import * as UserModule from './user.js'   // 全部导入成命名空间
import { getUser as fetchUser } from './user.js'  // 重命名

8. 可选链 (?.) 与空值合并 (??)

这是 ES2020 加进来的,写 JS 和 TS 时非常常用。

// 可选链:避免 TypeError
const city = user?.address?.city   // 任何一层是 null/undefined 就返回 undefined

// 空值合并:只在 null/undefined 时用默认值
const name = user.name ?? '匿名'
// 注意:|| 会把 '' 和 0 也当"假值"处理,?? 只看 null/undefined

Java 对照


9. 数组高阶方法:函数式风格

const nums = [1, 2, 3, 4, 5]

nums.map(n => n * 2)           // [2, 4, 6, 8, 10]
nums.filter(n => n % 2 === 0)  // [2, 4]
nums.reduce((sum, n) => sum + n, 0)  // 15
nums.find(n => n > 3)          // 4
nums.some(n => n > 4)          // true
nums.every(n => n > 0)         // true

Java 对照:几乎一一对应 Java 8 Stream API。

nums.stream().map(n -> n * 2).toList();
nums.stream().filter(n -> n % 2 == 0).toList();
nums.stream().reduce(0, (a, b) -> a + b);

差异


10. 原理深挖:this 到底指谁

JS 里 this调用时绑定,不是定义时绑定,这和 Java 完全相反。Java 里 this 永远指当前实例,编译期就定了;JS 里同一个函数在不同地方调用,this 可能完全不同。

四条规则按优先级从低到高:

// 规则 1:默认绑定(独立调用 → this 是 undefined / 全局)
function show() { console.log(this) }
show()                  // 严格模式 undefined,非严格模式 window

// 规则 2:隐式绑定(作为方法调用 → this 是点前面的对象)
const obj = { name: 'A', show }
obj.show()              // this 是 obj

// 规则 3:显式绑定(call/apply/bind 强行指定)
show.call({ name: 'B' })   // this 是 { name: 'B' }

// 规则 4:new 绑定(new 调用 → this 是新创建的实例,最高优先级)
new show()              // this 是一个新对象

箭头函数是例外:它没有自己的 this,沿词法作用域链找最近的 this:

const obj = {
  name: 'A',
  show() {
    setTimeout(function () { console.log(this.name) }, 0)   // undefined(独立调用)
    setTimeout(() => console.log(this.name), 0)             // 'A'(继承外层 obj.show 的 this)
  }
}
obj.show()

Vue 里的体现:Composition API 之所以舒服,就是把"this 指谁"这种心智完全抹掉了——<script setup> 里没有 this,所有变量都是词法作用域的普通变量。Options API 才需要纠结 this.count 是哪个 this。


11. 原理深挖:闭包 = 函数 + 它捕获的作用域

function makeCounter() {
  let count = 0                    // 这个变量本该在 makeCounter 返回后销毁
  return {
    inc: () => ++count,
    get: () => count,
  }
}

const c = makeCounter()
c.inc(); c.inc()
console.log(c.get())   // 2

count 没被销毁,是因为返回的两个函数还引用着它。函数对象内部隐藏一个 [[Environment]] 槽,记着定义时所处的词法环境(变量表)。这个"函数 + 被捕获的环境"组合就是闭包

Java 对照:Java 的 Lambda 也是闭包,但它只能捕获 final 或事实 final 的局部变量——通过把变量值复制进 Lambda 实例的字段实现,被捕变量不可改。JS 的闭包可以读写外层变量,因为是真正的引用。

为什么这点对学 Vue 关键

// Vue 3 响应式的 effect 系统就靠闭包工作
function effect(fn) {
  const wrapped = () => {
    activeEffect = wrapped     // 闭包捕获了外层的 wrapped
    fn()
    activeEffect = null
  }
  wrapped()
}

后面学响应式时会反复看到这个套路:闭包让"哪个函数正在运行"这个上下文能在异步/回调里持续传递

常见坑——循环里用 var

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)   // 输出 3 3 3(三个回调共享同一个 i)
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0)   // 输出 0 1 2(每轮一个新的 i)
}

var 函数作用域,let 块作用域——let 在循环每一轮新建一个 i,每个回调闭包捕获的是不同的变量。这也是 ES6 改用 let 的核心动机之一。


12. 原理深挖:原型链 ≠ Java 类继承

JS 的"继承"不是类继承,是原型委托:每个对象都有一个隐藏指针 [[Prototype]](可通过 Object.getPrototypeOf(obj) 拿到),指向它的"原型对象"。访问属性找不到时,沿这个指针向上找。

class Animal {
  constructor(name) { this.name = name }
  speak() { console.log(this.name + ' speaks') }
}
class Dog extends Animal {
  bark() { console.log('woof') }
}

const d = new Dog('旺财')
d.bark()       // 自身找不到 → 沿原型链向上 → 找到 Dog.prototype.bark
d.speak()      // Dog.prototype 没有 → 再上一层 → Animal.prototype.speak
d.toString()   // 再往上 → Object.prototype.toString
d.foo          // 找到 null 还没有 → undefined

实际查找路径:

d ──[[Prototype]]──► Dog.prototype ──[[Prototype]]──► Animal.prototype ──[[Prototype]]──► Object.prototype ──► null

class 是语法糖——等价于:

function Animal(name) { this.name = name }
Animal.prototype.speak = function () { console.log(this.name + ' speaks') }

ES6 的 class 没有引入新机制,只是把这套原型操作写得像 Java。

Java 对照

Java 类继承 JS 原型链
编译期固定 运行时可改(Object.setPrototypeOf
字段在每个实例上 方法在原型上共享,字段在实例上
instanceof 看类层级 instanceof 看原型链上是否有对应的 prototype

面试考点Object.create(null) 创建一个没有原型的对象,连 toString / hasOwnProperty 都没有。常用作纯字典 Map,避免和原型上的方法名冲突。Vue 内部某些缓存就这么用。


13. 原理深挖:事件循环全景图

把第 6 节的微任务/宏任务放进完整图景:

┌─────────────────────────────────────────────────────┐
│                   Call Stack                        │
│  (同步代码、当前正在跑的函数)                     │
└─────────────┬───────────────────────────────────────┘
              │ 跑空了
              ▼
┌─────────────────────────────────────────────────────┐
│           Microtask Queue(清空到为空)             │
│  Promise.then / queueMicrotask / await 后续         │
└─────────────┬───────────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────────┐
│              Render(如果到了渲染时机)             │
│  样式计算 → 布局 → 绘制 → 合成                     │
└─────────────┬───────────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────────┐
│           Macrotask Queue(取一个跑)              │
│  setTimeout / I/O / UI 事件 / postMessage          │
└─────────────┬───────────────────────────────────────┘
              │
              └──── 回到顶部循环

几个推论

  1. setTimeout(fn, 0) 不是立刻执行——至少要等当前同步代码跑完 + 当前微任务队列清空 + 一次渲染机会
  2. 大量 Promise 链可以"饿死"宏任务——微任务队列必须清空,理论上一直 push 微任务能阻止下次渲染。所以别在 .then 里写无限递归
  3. Vue 的 nextTick 用的是微任务Promise.resolve().then)——所以"改完数据,下一个微任务里 DOM 已更新"
  4. requestAnimationFrame 在 render 之前触发——动画用它而不是 setTimeout,能保证刚好赶上下一帧

Java 对照:Node.js 的事件循环和浏览器类似但分阶段(timers / pending / poll / check / close),不过都是单线程 + 队列的设计。和 Java 的多线程并行模型完全不同。


14. 原理深挖:值类型 vs 引用类型 + 浅拷贝陷阱

JS 的内存模型只有两类:

let a = { x: 1 }
let b = a                  // b 和 a 指向同一个对象
b.x = 2
console.log(a.x)           // 2,被改了

// 扩展运算符是浅拷贝
let c = { ...a }           // c 是新对象,但里面的嵌套对象还是共享引用
let nested = { inner: { v: 1 } }
let d = { ...nested }
d.inner.v = 99
console.log(nested.inner.v)  // 99!inner 是共享的

深拷贝方案

// 浏览器原生(推荐,处理循环引用、Date、Map、Set)
const deep = structuredClone(obj)

// 老办法(不处理函数、循环引用、Date 会变字符串)
const deep2 = JSON.parse(JSON.stringify(obj))

Java 对照:和 Java 的对象引用语义一致,区别是 Java 有原生 Cloneable 和深拷贝库(Apache Commons),JS 长期没有标准答案,2022 才有了 structuredClone

Vue 场景:响应式系统对对象用 Proxy 包一层,对基本类型ref 包一层(因为基本类型没法挂 Proxy)——这个分裂正是源于 JS 的双内存模型。


小结:速查表

特性 一句话
let / const 取代 var,块级作用域
箭头函数 => 短函数 + 绑定外层 this(无独立 this)
解构 { a, b } = obj 批量取值
模板字符串 ` ` 拼接字符串 + 多行
扩展运算符 ... 展开数组/对象、收集参数(拷贝)
async / await 写起来像同步的异步,await 后续在微任务里跑
import / export 模块化
?. / ?? 空值安全
数组高阶方法 函数式风格,类似 Stream
Promise 状态机 pending → fulfilled/rejected,不可逆
事件循环 同步 → 清空微任务 → 渲染 → 一个宏任务
this 四规则 默认 < 隐式 < 显式 < new;箭头函数走词法
闭包 函数 + 它捕获的词法环境,Vue 响应式的基石
原型链 找属性时沿 [[Prototype]] 向上找,class 是语法糖
浅拷贝 vs 深拷贝 ... / Object.assign 浅;structuredClone

延伸阅读


下一篇:TypeScript 基础(从 Java 视角看 TS 的类型系统)