ES6 与 Node.js 群侠传:那些年我们追过的语法,和那些年把我们追杀的 API
如果说 JavaScript 是一个江湖,那 ES6 就是一次"武林大洗牌",而 Node.js 就是那个把江湖搬到服务器上的神秘门派。
序章:从 var 的混乱说起
很久很久以前,JavaScript 的世界只有 var。
js
javascript
var name = '张三'
if (true) {
var name = '李四'
}
console.log(name) // 李四
张三在 if 里走了一遭,出来就变成李四了。
这不是 bug,这是 var 的"函数作用域"特性------它根本不在乎 if 的 {},它只认函数。
js
css
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 3 3 3
三个 setTimeout,打印了三个 3。 因为 var 只有一个 i,循环结束时 i 已经是 3 了。
这就是 ES6 之前的世界:变量像脱缰的野马,跑得到处都是。
直到 ES6 带着 let 和 const 降临,江湖才终于有了规矩。
第一章:ES6 三大护法------let、const、箭头函数
let:有边界的变量
js
javascript
let name = '张三'
if (true) {
let name = '李四'
}
console.log(name) // 张三
let 是块级作用域,{} 就是它的边界。 出了这个门,它就不认识你了。
同样的 for 循环:
js
javascript
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
// 0 1 2
每次循环的 i 都是独立的。 世界终于正常了。
const:一把锁
js
ini
const PI = 3.14159
PI = 3.14 // ❌ 报错:Assignment to constant variable
const 声明的变量,不能重新赋值。
但注意------const 锁的是"绑定",不是"值":
js
ini
const obj = { name: '张三' }
obj.name = '李四' // ✅ 可以
obj = { name: '王五' } // ❌ 报错
const arr = [1, 2, 3]
arr.push(4) // ✅ 可以
arr = [5, 6] // ❌ 报错
这就是为什么大家都说:"优先用 const,需要改再用 let,永远别用 var。"
箭头函数:函数界的"瘦身达人"
js
css
// 传统函数
function add(a, b) {
return a + b
}
// 箭头函数
const add = (a, b) => a + b
箭头函数不仅短,还顺手解决了 this 的世纪难题。
js
javascript
const obj = {
name: '张三',
say() {
setTimeout(function() {
console.log(this.name) // undefined
}, 100)
}
}
传统函数的 this 是"谁调用我,我就指向谁"。 在 setTimeout 里,this 变成了 window(或 undefined)。
js
javascript
const obj = {
name: '张三',
say() {
setTimeout(() => {
console.log(this.name) // 张三
}, 100)
}
}
箭头函数没有自己的 this,它继承外层的 this。
这就是箭头函数最核心的规则:它不制造 this,它只是 this 的搬运工。
第二章:ES6 四大神器------解构、模板字符串、展开、模块
解构:拆包裹的艺术
js
javascript
// 对象解构
const user = { name: '张三', age: 18 }
const { name, age } = user
// 数组解构
const [first, second] = [1, 2, 3]
// 函数参数解构
function greet({ name, age }) {
console.log(`${name} 今年 ${age} 岁`)
}
以前取个属性要写 user.name、user.age,现在一行搞定。
最爽的是交换变量:
js
css
let a = 1, b = 2
;[a, b] = [b, a]
// a = 2, b = 1
不用再引入第三个变量了。
模板字符串:字符串拼接的救星
js
javascript
// 以前
var msg = '你好,' + name + ',你今年 ' + age + ' 岁'
// 现在
const msg = `你好,${name},你今年 ${age} 岁`
反引号 + ${},就是模板字符串的全部。
它还支持换行:
js
css
const html = `
<div>
<h1>${title}</h1>
<p>${content}</p>
</div>
`
再也不用手写 \n 了。
展开运算符:三个点的魔法
js
javascript
// 展开数组
const arr = [1, 2, 3]
const newArr = [...arr, 4, 5] // [1, 2, 3, 4, 5]
// 展开对象
const user = { name: '张三' }
const newUser = { ...user, age: 18 } // { name: '张三', age: 18 }
// 函数传参
Math.max(...arr) // 3
... 的核心作用是"把东西摊开"。
它最常用的场景是"浅拷贝"和"合并":
js
ini
const state = { count: 0, name: '张三' }
const newState = { ...state, count: 1 } // { count: 1, name: '张三' }
React 和 Vue 里的不可变更新,全靠它。
模块:从 require 到 import
js
javascript
// CommonJS(Node.js 传统写法)
const fs = require('fs')
module.exports = { foo }
// ES Module(ES6 新写法)
import fs from 'fs'
export { foo }
export default foo
ES Module 是静态的,import 必须在顶层,不能放在 if 里。 这让打包工具能做"静态分析"和"tree-shaking"------没用的代码,打包时直接删掉。
而 CommonJS 是动态的,require 可以放在任何地方。
Node.js 从 v12 开始支持 ES Module,但需要 .mjs 后缀或 "type": "module"。
第三章:异步三兄弟------Promise、async、await
这是 ES6 之后最重要的变化,也是新手最容易晕的地方。
回调地狱
js
javascript
getUser(function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
getProduct(detail.productId, function(product) {
console.log(product)
})
})
})
})
这叫"回调地狱",也叫"厄运金字塔"。 四层嵌套已经算少的,真实项目里十层都有。
Promise:给异步一个"承诺"
js
ini
getUser()
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => getProduct(detail.productId))
.then(product => console.log(product))
.catch(err => console.error(err))
Promise 把嵌套变成了链式调用。 每个 .then 返回一个新的 Promise,像流水线一样。
Promise 有三种状态:
- pending:进行中
- fulfilled:成功
- rejected:失败
状态一旦改变,就不会再变。
async / await:让异步看起来像同步
js
javascript
async function getProductInfo() {
try {
const user = await getUser()
const orders = await getOrders(user.id)
const detail = await getOrderDetail(orders[0].id)
const product = await getProduct(detail.productId)
console.log(product)
} catch (err) {
console.error(err)
}
}
async 函数返回一个 Promise,await 等待 Promise 完成。
看起来像同步代码,写起来像同步代码,但它是异步的。
这就是异步的终极形态------用同步的写法,表达异步的逻辑。
第四章:Node.js------把 JavaScript 搬上服务器
ES6 是语法升级,Node.js 是战场升级。
Node.js 的本质是:把 Chrome 的 V8 引擎抽出来,加上文件系统、网络、进程等 API,让 JavaScript 能跑在服务器上。
事件循环:Node.js 的心脏
js
javascript
console.log('1')
setTimeout(() => console.log('2'), 0)
Promise.resolve().then(() => console.log('3'))
console.log('4')
// 输出:1 4 3 2
为什么 3 比 2 先输出?
因为 Node.js 的事件循环分阶段:
- 同步代码先执行(1、4)。
- 微任务(Promise.then)在当前阶段结束后立即执行(3)。
- 宏任务(setTimeout)在下一个阶段执行(2)。
记住一句话:微任务永远比宏任务先执行。
模块系统:require 的秘密
js
javascript
// math.js
module.exports = {
add: (a, b) => a + b
}
// index.js
const math = require('./math')
math.add(1, 2)
Node.js 的 require 是同步的,模块加载后会缓存。
第一次 require 会执行模块代码,第二次直接读缓存。
这就是为什么模块里的单例能生效:
js
arduino
// config.js
const config = { db: 'mysql' }
module.exports = config
// a.js 和 b.js 都 require 同一个 config,拿到的是同一个对象
Buffer 和 Stream:处理大数据的利器
js
javascript
const fs = require('fs')
// 一次性读取(适合小文件)
const data = fs.readFileSync('big.mp4')
// 流式读取(适合大文件)
const stream = fs.createReadStream('big.mp4')
stream.on('data', chunk => {
console.log('收到一块数据', chunk.length)
})
stream.on('end', () => {
console.log('读完了')
})
Buffer 是 Node.js 处理二进制数据的容器。
Stream 是"分块处理"的思想------不把整个文件读进内存,而是一块一块处理。
这就是为什么 Node.js 能处理几个 G 的日志文件而不爆内存。
http 模块:手写一个服务器
js
javascript
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ message: 'Hello, Node.js' }))
})
server.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000')
})
这就是一个最基础的 Node.js 服务器。
但真实项目里,我们通常用框架------比如 Express 或 Koa:
js
ini
// Koa
const Koa = require('koa')
const app = new Koa()
app.use(async ctx => {
ctx.body = { message: 'Hello, Koa' }
})
app.listen(3000)
Koa 的中间件是"洋葱模型":
text
markdown
请求 → 中间件1 → 中间件2 → 中间件3 → 响应
← ← ←
await next() 之前的代码在"请求阶段"执行,之后的代码在"响应阶段"执行。
第五章:那些年我们踩过的坑
坑一:this 指向不明
js
javascript
const obj = {
name: '张三',
say: () => {
console.log(this.name) // undefined
}
}
箭头函数的 this 继承外层,而外层是全局(或模块),不是 obj。
所以对象的方法,别用箭头函数。
坑二:Promise 忘记 catch
js
scss
fetchData().then(data => {
console.log(data)
})
// 如果出错,会抛出 UnhandledPromiseRejection
每个 Promise 链都应该有 .catch,或者用 async/await + try/catch。
坑三:forEach 里的 await 不生效
js
javascript
// ❌ 错误
async function process(items) {
items.forEach(async item => {
await doSomething(item) // 不会被等待
})
console.log('完成') // 会先打印
}
forEach 不认识 async,它只是同步地调用每个回调。
正确写法是用 for...of:
js
javascript
// ✅ 正确
async function process(items) {
for (const item of items) {
await doSomething(item)
}
console.log('完成')
}
坑四:require 的循环依赖
text
css
a.js require b.js
b.js require a.js
循环依赖时,Node.js 返回的是"未完成的模块"(部分导出)。
这也是为什么 ES Module 更受欢迎------它的静态分析能提前发现问题。
终章:ES6 和 Node.js 教会我的事
写完这一圈,我最大的感受是:
ES6 让 JavaScript 从"能跑"变成了"好写"。
- let/const 解决了变量污染。
- 箭头函数解决了 this 混乱。
- 解构和模板字符串解决了代码啰嗦。
- Promise 和 async/await 解决了回调地狱。
- 模块解决了代码组织。
Node.js 让 JavaScript 从"浏览器脚本"变成了"全栈语言"。
- 事件循环让它能高并发。
- Stream 让它能处理大数据。
- 模块系统让它能组织大型项目。
- npm 生态让它有无限可能。
但工具再好,也只是工具。
真正的功夫,还是在你解决问题的能力上。
P.S. 写完这篇文章后,我又在 forEach 里写了 await。看来,踩过的坑,还会再踩一遍。
P.P.S. ES6 是 2015 年发布的,到现在已经十年了。但每次写 var,我还是要愣一下。