上篇回顾 :2.1-02 把 CSS 布局从盒模型到 Flex/Grid 一次讲透,揭示「所有布局在解决空间分配矛盾」。本篇进入 JavaScript------前端的核心语言。对后端工程师来说,JS 最大的挑战不是语法(语法很简单),而是心智模型------原型链、闭包、this、事件循环,每一样都和 Java 截然不同。

一、开篇:一段让 Java 工程师困惑的 JS 代码
javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出:3 3 3(不是 0 1 2)
Java 工程师的第一反应:「为什么不是 0 1 2?循环不是顺序执行吗?」
答案是两个 JS 特性的叠加:var 的变量提升 + 事件循环把 setTimeout 推到循环结束后执行。此时 i 已经是 3。
改成 let 就好了:
javascript
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出:0 1 2
let 是块级作用域,每次循环创建新的 i。
这个例子说明:不理解 JS 的作用域和事件循环,代码行为会完全违背直觉。本篇把 JS 核心特性与 Java 对照讲透。
二、原型链:JS 的继承 vs Java 的继承
2.1 Java 的类继承
java
class Animal { void eat() {} }
class Dog extends Animal { void bark() {} }
Dog d = new Dog();
d.eat(); // 编译时确定,Animal 的方法
d.bark(); // Dog 的方法
Java 的继承是编译时确定的,基于类(class)。
2.2 JS 的原型继承
javascript
function Animal() {}
Animal.prototype.eat = function() { console.log('eat'); };
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.bark = function() { console.log('bark'); };
const d = new Dog();
d.eat(); // 沿原型链找到 Animal.prototype.eat
d.bark(); // Dog.prototype.bark
JS 的继承是运行时查找的,基于原型链(prototype chain)。
2.3 原型链查找过程
d → Dog.prototype → Animal.prototype → Object.prototype → null
访问 d.eat 时,JS 引擎沿这条链向上查找,找到第一个 eat 就用。
2.4 ES6 class 语法糖
javascript
class Animal {
eat() { console.log('eat'); }
}
class Dog extends Animal {
bark() { console.log('bark'); }
}
ES6 的 class 只是语法糖,底层仍是原型链。但写法更接近 Java,降低了心智负担。
关键区别:
| 维度 | Java | JS |
|---|---|---|
| 继承基础 | 类(class) | 原型(prototype) |
| 确定时机 | 编译时 | 运行时 |
| 多继承 | 不支持(接口可多实现) | 不支持(原型链单链) |
| 实例与类关系 | 实例属于类 | 实例有原型指向 |
三、闭包:函数 + 作用域的捆绑
3.1 什么是闭包
闭包 = 函数 + 定义该函数时的作用域。
javascript
function counter() {
let count = 0;
return function() {
return ++count;
};
}
const c = counter();
c(); // 1
c(); // 2
c(); // 3
count 是 counter 函数的局部变量,但被返回的内部函数「记住」了。即使 counter 执行完毕,count 也不会被回收------因为内部函数持有对它的引用。
3.2 闭包的真实用途
| 用途 | 示例 |
|---|---|
| 数据私有化 | 上例的 count 外部无法直接访问 |
| 回调中保持状态 | 事件监听器里记住上次点击时间 |
| 模块模式 | IIFE 封装私有变量 |
| 柯里化 | add(a)(b)(c) |
3.3 闭包的陷阱
javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
var 是函数作用域,整个循环共用一个 i。闭包记住的是同一个 i,循环结束后 i=3。
改成 let 后,每次循环创建新的块级作用域,闭包记住的是不同的 i。
四、this:JS 最混乱的概念
4.1 this 的四条规则
| 规则 | this 指向 | 示例 |
|---|---|---|
| 默认绑定 | 全局对象(严格模式 undefined) | foo() |
| 隐式绑定 | 调用对象 | obj.foo() |
| 显式绑定 | call/apply/bind 指定的对象 | foo.call(obj) |
| new 绑定 | 新创建的对象 | new Foo() |
优先级:new > 显式 > 隐式 > 默认。
4.2 箭头函数的 this
箭头函数没有自己的 this,继承外层作用域的 this。
javascript
const obj = {
name: 'Tom',
greet: function() {
setTimeout(() => {
console.log(this.name); // Tom(继承 greet 的 this)
}, 0);
setTimeout(function() {
console.log(this.name); // undefined(默认绑定全局)
}, 0);
}
};
4.3 与 Java this 的区别
Java 的 this 永远指向当前实例。JS 的 this 取决于调用方式,同一个函数在不同调用场景下 this 不同。
五、事件循环:单线程怎么处理并发
5.1 JS 的单线程模型
JS 是单线程的------同一时间只执行一个任务。为什么选择单线程?因为浏览器里 JS 操作 DOM,多线程同时操作 DOM 会冲突。
但单线程不代表不能处理 IO------用事件循环 + 异步回调。
5.2 事件循环模型
调用栈(Call Stack)
↑ 执行同步代码
↓ 遇到异步任务
Web APIs / Node APIs(浏览器/Node 提供的异步 API)
↓ 异步完成
任务队列(Task Queue)
← 宏任务:setTimeout、setInterval、IO
← 微任务:Promise.then、MutationObserver
↓ 调用栈空了
事件循环取一个微任务执行(清空所有微任务)
↓ 微任务清空后
取一个宏任务执行
↓ 循环
5.3 宏任务 vs 微任务的执行顺序
javascript
console.log('1'); // 同步
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步
// 输出:1 4 3 2
规则:同步代码 → 清空微任务 → 取一个宏任务 → 清空微任务 → 取下一个宏任务......
5.4 与 Java 多线程的对比
| 维度 | JS 事件循环 | Java 多线程 |
|---|---|---|
| 并发模型 | 单线程 + 异步回调 | 多线程 + 锁 |
| IO 处理 | 非阻塞 IO + 回调 | 阻塞 IO 或 NIO |
| 适用场景 | IO 密集(网络请求) | CPU 密集 + IO 密集 |
| 复杂度 | 回调地狱(async/await 缓解) | 锁竞争、死锁 |
各自哲学:JS 用单线程避免 DOM 冲突,用事件循环处理 IO;Java 用多线程榨干 CPU,用锁保证安全。
六、Promise → async/await
6.1 回调地狱
javascript
getUser(id, function(user) {
getOrders(user.id, function(orders) {
getOrderDetail(orders[0].id, function(detail) {
console.log(detail);
});
});
});
6.2 Promise 链式调用
javascript
getUser(id)
.then(user => getOrders(user.id))
.then(orders => getOrderDetail(orders[0].id))
.then(detail => console.log(detail))
.catch(err => console.error(err));
6.3 async/await:同步写法异步执行
javascript
async function getOrderDetail(id) {
try {
const user = await getUser(id);
const orders = await getOrders(user.id);
const detail = await getOrderDetail(orders[0].id);
console.log(detail);
} catch (err) {
console.error(err);
}
}
async/await 是 Promise 的语法糖,让异步代码看起来像同步。但底层仍是事件循环。
七、ES6+ 高频语法
7.1 解构
javascript
const { name, age } = user; // 对象解构
const [first, second] = arr; // 数组解构
function foo({ name, age = 18 }) {} // 默认值
7.2 展开
javascript
const newObj = { ...oldObj, extra: 1 }; // 对象展开
const newArr = [...oldArr, 1, 2]; // 数组展开
7.3 模块
javascript
// math.js
export function add(a, b) { return a + b; }
export default function multiply(a, b) { return a * b; }
// main.js
import multiply, { add } from './math.js';
ESM(ES Module)是 JS 官方模块标准,取代 CommonJS(require)。
八、动态类型 vs 静态类型
JS 是动态类型,变量类型运行时确定。灵活但容易出 bug。
javascript
function add(a, b) { return a + b; }
add(1, 2); // 3
add("1", 2); // "12"(字符串拼接,不是加法)
8.1 TypeScript:给 JS 加类型
typescript
function add(a: number, b: number): number {
return a + b;
}
add("1", 2); // 编译报错
TS 是 JS 的超集,编译时类型检查,运行时仍是 JS。大型项目推荐用 TS。
8.2 type hints + mypy 能把 Python 救回静态世界吗
能部分救------TS 和 Python 的 type hints 都是可选类型标注,不强制。但 TS 生态更成熟(主流框架都支持),Python 的 mypy 用得少。
九、小结表
| 概念 | JS | Java 对照 |
|---|---|---|
| 继承 | 原型链 | class extends |
| 闭包 | 函数+作用域 | lambda 捕获变量 |
| this | 取决于调用方式 | 永远是当前实例 |
| 并发 | 单线程+事件循环 | 多线程+锁 |
| 模块 | ESM | import |
| 类型 | 动态(TS 补静态) | 静态 |