ES6 与 Node.js 群侠传:那些年我们追过的语法,和那些年把我们追杀的 API

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. 同步代码先执行(1、4)。
  2. 微任务(Promise.then)在当前阶段结束后立即执行(3)。
  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,我还是要愣一下。

相关推荐
XIAEE1 小时前
WebRTC 探险记:我是如何让两个浏览器"看对眼"的
java·spring
XIAEE1 小时前
UniApp 本地打包通关记:Android 是地狱难度,iOS 是隐藏 BOSS
java·spring
高晶1 小时前
Android 物联网组网实战:NFC 从零到落地
android·java
Dreamboat-L2 小时前
日志级别基础 + Spring Boot 日志实践指南
java·服务器·数据库·java-ee·springboot
啵啵啵鱼2 小时前
第二章 线性表
java·数据结构·算法
深入云栈2 小时前
Netty 4.2.x 源码深度解析 (十九):ChannelPool 连接池 —— 连接复用与资源管理
java·后端·架构
用户094248568032 小时前
第38章:从零构建JVM稳定性平台最小闭环
java·jvm
Zhuo1Joy2 小时前
压测全绿的假象:0% 错误率的系统与 17.2% 成功率的业务
java
silvamux_2 小时前
Spring Boot 调用大模型 api 最小可跑教程
java·spring boot·后端