这一版不讲「是什么」,重点讲「为什么」:每个知识点都从引擎视角拆解原理——执行上下文怎么创建、变量到底存在哪、this 是怎么绑定的、事件循环每一步在干什么、Promise 状态机如何流转。所有手写代码都可直接运行验证。学透本章,框架源码只是「JS 特性的组合应用」。
不搞懂这一节,后面所有概念(提升、闭包、this、事件循环)都是在背结论。
很多人以为 JS 是「一行一行往下执行」的。不准确。每进入一个可执行环境(script / 函数 / eval),引擎都会先做一次「预扫描」,再逐行执行:
| 阶段 | 引擎做什么 | 对应的现象 |
|---|---|---|
| ① 创建阶段 (编译/预扫描) | 1. 创建执行上下文对象 2. 确定作用域链(外层词法环境的引用) 3. 确定 this 指向4. 扫描全部声明:函数声明整体提升;var 声明登记进变量环境并初始化为 undefined;let/const 登记进词法环境但不初始化 | 「变量提升」「函数可以先调用后声明」「TDZ 报错」 |
| ② 执行阶段 | 逐行执行,变量在环境中查找、赋值,函数调用时压入新的执行上下文 | 正常逻辑、闭包引用、调用栈溢出 |
console.log(a); // undefined(不是报错!创建阶段 a 已被登记并初始化为 undefined)
console.log(fn); // [Function: fn](函数声明整体提升,连函数体一起)
var a = 1;
function fn() {}
console.log(x); // ❌ ReferenceError(let 在词法环境里但未初始化 → 落入暂时性死区)
let x = 2;
作用域 = 变量的可访问范围。JS 用的是「词法作用域」(Lexical Scope):由你书写代码的位置决定,编译期就确定了,跟函数在哪调用无关。
let x = "global";
function outer() {
let y = "outer";
function inner() {
let z = "inner";
console.log(x, y, z); // 查找链:inner 环境 → outer 环境 → 全局环境
}
inner();
}
outer();
let v = 1;
function foo() { console.log(v); } // 定义在全局,作用域链永远指向全局
function bar() {
let v = 2;
foo(); // 输出 1,不是 2!foo 的作用域链由「写在哪」决定,不是「在哪被调用」
}
bar();
规则表格大家都会背,这里给「能解释清楚」的版本:
// 陷阱原理:var i 是函数级作用域,整个循环共享同一个 i
// setTimeout 的回调进宏任务队列时,循环早已跑完(i 已经是 3)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i)); // 3 3 3
}
// let j:每次循环迭代都会创建一个「新的词法环境」并把上一轮的 j 拷贝进去
// 相当于引擎在每轮循环体外包了一层作用域(for 循环的 let 有专门规范处理)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j)); // 0 1 2
}
// var 想修:用 IIFE 把当时的 i 拷贝捕获进函数作用域(ES5 时代的解法,现在用 let)
for (var k = 0; k < 3; k++) {
(function (saved) {
setTimeout(() => console.log(saved)); // 0 1 2
})(k);
}
const arr=[]; arr.push(1) 合法,arr=[] 报错。
八种类型(7 原始 + 1 引用),但真正决定你代码行为的是它们「存在哪」。
let o = { name: "张三", tags: ["a"] };
let p = o; // p 和 o 持有同一个地址
p.name = "李四"; // o.name 也是 "李四"(同一个对象)
// 浅拷贝:只拷贝第一层,嵌套的 tags 数组还是同一个地址
let s = { ...o };
s.name = "王五"; // ✅ o.name 不受影响(第一层是真拷贝)
s.tags.push("b"); // ❌ o.tags 也变成 ["a","b"](第二层是共享的)
let s="ab"; s[0]="c" 不会报错但也不生效(严格模式下赋值静默失败)——字符串的每个字符无法原地修改,所有字符串方法(slice/replace/toUpperCase)都返回新字符串。这跟数组方法要分清:sort/splice/push 是原地修改,slice/map/filter/concat 返回新数组。React 里强调「不可变数据」的理论根基就在这。
== 比较前,若两边类型不同,按规范走 ToPrimitive 抽象操作:
valueOf(),若还是对象再调 toString(),得到原始值[] == false // [] → "" → 0;false → 0 → true(两大坑王合体)
[] == ![] // ![] 是 false([] 是真值)→ 变成 [] == false → true(面试名题)
"0" == false // "0"→0, false→0 → true
null == 0 // false!null 只和 undefined 相等
NaN == NaN // false!NaN 与任何值都不等(包括自己)→ 判断用 Number.isNaN
// ToPrimitive 实战:自己控制转换行为
const price = {
valueOf() { return 100; }, // 优先被调用
toString() { return "100元"; }
};
price * 2 // 200(算术场景走 valueOf)
`${price}` // "100元"(字符串场景走 toString)
===。唯一可接受 == 的地方:if (x == null) 同时判断 null 和 undefined。
function deepClone(target) {
if (typeof target !== "object" || target === null) return target; // 原始值直接返回
const result = Array.isArray(target) ? [] : {};
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
result[key] = deepClone(target[key]); // 递归拷贝每一层
}
}
return result;
}
function deepClone(target, map = new WeakMap()) {
// 1. 原始类型 & 函数直接返回(函数一般共享,没必要克隆)
if (target === null || typeof target !== "object") return target;
// 2. Date / RegExp / 原始包装对象,用各自的构造器重建
if (target instanceof Date) return new Date(target);
if (target instanceof RegExp) return new RegExp(target.source, target.flags);
// 3. 循环引用:先登记「旧对象→新对象」,递归回来时直接复用,避免无限递归
if (map.has(target)) return map.get(target);
// 4. Map / Set 递归克隆内部成员
if (target instanceof Map) {
const m = new Map();
map.set(target, m);
target.forEach((v, k) => m.set(deepClone(k, map), deepClone(v, map)));
return m;
}
if (target instanceof Set) {
const s = new Set();
map.set(target, s);
target.forEach(v => s.add(deepClone(v, map)));
return s;
}
// 5. 普通对象/数组:保留原型链,用 Reflect.ownKeys 连 Symbol 键也拷到
const result = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
map.set(target, result); // 注意:递归前就要登记!
Reflect.ownKeys(target).forEach(key => {
result[key] = deepClone(target[key], map);
});
return result;
}
// 验证循环引用
const a = { name: "x" };
a.self = a;
const b = deepClone(a);
b.self === b // true,且没有栈溢出
structuredClone()(浏览器原生 API)已能处理循环引用和大部分类型,但不能克隆函数和 DOM——面试要能说出「原生 API + 它的局限」。
this 不是「定义时」决定的,是「调用时」决定的——这一句能解开 90% 的 this 疑惑。
| 优先级 | 规则 | 触发场景 | this 指向 |
|---|---|---|---|
| 1(最低) | 默认绑定 | 独立函数调用 fn() | 非严格模式 window;严格模式 undefined |
| 2 | 隐式绑定 | 对象方法 obj.fn() | 点号前面的那个对象(链式取最后一层) |
| 3 | 显式绑定 | fn.call(obj) / apply / bind | 指定的第一个参数 |
| 4(最高) | new 绑定 | new fn() | 新创建的那个实例对象 |
| 例外 | 箭头函数 | 任何场景 | 没有自己的 this,用「定义时外层作用域」的 this,call/bind 都改不动 |
// 规则2 隐式绑定 + 最经典的「丢失」陷阱
const obj = {
name: "obj",
say() { console.log(this.name); }
};
obj.say(); // "obj"(this = obj)
const f = obj.say; // 只是取出函数本身,跟 obj 的联系被切断
f(); // undefined(规则降级为默认绑定,this = window)
// 回调场景同理:setTimeout(obj.say, 0) 内部等价于拿到函数后 fn() 调用 → this 丢失
// 修复三连:setTimeout(obj.say.bind(obj), 0) / 箭头函数包裹 / 类字段 say = () => {}
// 规则4 new 的过程(下一节手写):new 会创建全新对象,this 指向它
// 规则3 显式绑定优先级低于 new:bind 过的函数依然可以被 new,this 以 new 为准
// 箭头函数:this 在「定义时」就锁定了外层作用域的 this
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++; // 箭头函数没有 this,往上找到 start 的 this(即 timer)
}, 1000);
}
};
Function.prototype.myCall = function (thisArg, ...args) {
// 1. 谁调用 myCall,this 就是哪个函数
const fn = this;
// 2. null/undefined 归一化为全局对象;原始值装箱成对象
thisArg = thisArg === null || thisArg === undefined ? globalThis : Object(thisArg);
// 3. 把函数临时挂成 thisArg 的属性 → 调用时就变成「对象.方法()」= 隐式绑定
const key = Symbol("fn");
thisArg[key] = fn;
const result = thisArg[key](...args);
// 4. 清理临时属性,避免污染
delete thisArg[key];
return result;
};
// apply 唯一区别:接收 args 数组 → thisArg[key](...argsArray)
Function.prototype.myBind = function (thisArg, ...preArgs) {
const fn = this;
// bind 的核心:返回一个「包了一层」的函数,调用时才执行,且支持继续传参(偏函数)
const bound = function (...restArgs) {
// new 优先级更高:如果被 new 调用,this 用新实例,忽略 thisArg
if (this instanceof bound) {
return fn.apply(this, [...preArgs, ...restArgs]);
}
return fn.apply(thisArg, [...preArgs, ...restArgs]);
};
// 维护原型链,让 new bound() 的实例 instanceof 原函数成立
bound.prototype = Object.create(fn.prototype);
return bound;
};
定义(能说出口的版本):函数与其词法作用域的组合。函数哪怕在定义处之外被调用,依然能访问定义时的作用域变量。
正常流程:函数执行完 → 执行上下文出栈 → 局部变量等垃圾回收。但如果有内层函数引用了外层变量,引擎判定「这块词法环境还有人持有」,不会回收——这个被持有的环境就是闭包的实体。
function createCounter() {
let count = 0; // 本该随 createCounter 执行完而销毁
return function () {
count++; // 但被返回的内层函数持有引用 → 环境被保留 → 变量「活着」
return count;
};
}
const c1 = createCounter();
const c2 = createCounter();
c1(); c1(); c1(); // 1 2 3
c2(); // 1(两次调用生成两个独立的作用域环境,count 互不干扰)
#count 出现前的标准做法// ❌ 隐患代码:闭包持有了巨大的 data,而事件监听不解除 → data 永远释放不掉
function mount() {
const data = new Array(1e6).fill("大对象");
document.getElementById("btn").addEventListener("click", () => {
console.log(data.length);
});
}
// 修复:用完移除监听,或让回调不引用 data,或 data 置 null
一句话区分:防抖 = 只认最后一次(停止触发 n 秒后执行);节流 = 固定频率执行(n 秒内最多一次)。搜索框输入联想用防抖;滚动加载、按钮防连点用节流。
// 防抖:带「立即执行」和「取消」选项
function debounce(fn, delay = 300, { immediate = false } = {}) {
let timer = null; // 闭包持有,跨调用保持
function debounced(...args) {
if (timer) clearTimeout(timer);
if (immediate && !timer) fn.apply(this, args); // 第一次触发立即执行
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, delay);
}
debounced.cancel = () => { clearTimeout(timer); timer = null; }; // 组件卸载时调用
return debounced;
}
// 节流:时间戳版(首次立即执行)+ 定时器版(停止后再补一次)的混合
function throttle(fn, interval = 300) {
let last = 0, timer = null;
return function (...args) {
const now = Date.now();
const remaining = interval - (now - last);
if (remaining <= 0) { // 距上次执行已超过 interval → 直接执行
clearTimeout(timer); timer = null;
last = now;
fn.apply(this, args);
} else if (!timer) { // 没到时间但保证「停止触发后最后一下也能执行」
timer = setTimeout(() => {
last = Date.now(); timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
useMemo/useRef/useCallback 持久化同一个实例。
class 是 ES6 的语法糖,底下全是原型机制——读懂这节才能读懂 Vue/React 源码里大量的 prototype 操作。
function Person(name) { this.name = name; }
Person.prototype.say = function () { console.log(`我是 ${this.name}`); };
const p1 = new Person("张三");
const p2 = new Person("李四");
p1.say(); // 自己身上没有 say → 沿 __proto__ 找到 Person.prototype.say
p1.say === p2.say // true:方法只有一份,两个实例共享(这正是原型「省内存」的意义)
Object.getPrototypeOf(p1) === Person.prototype // true
Person.prototype.constructor === Person // true(三角闭合)
// 原型链的终点
Object.getPrototypeOf(Person.prototype) === Object.prototype // true
Object.getPrototypeOf(Object.prototype) === null // true!链到头了
// 检测:in 遍历含原型链;hasOwnProperty 只看自身
"name" in p1; // true(自身)
"say" in p1; // true(原型链上)
p1.hasOwnProperty("say");// false
Person.__proto__ === Function.prototype(函数也是对象,由 Function 构造);② Function.__proto__ === Function.prototype(Function 自己构造自己,语言设计上的自举环)。还有 Object.__proto__ === Function.prototype——能推导出来就说明真懂了。
执行 new Person("张三") 时引擎做了四件事:
__proto__ 指向 Person.prototypefunction myNew(Ctor, ...args) {
// 步骤1:创建对象并挂原型(两种等价写法)
const obj = Object.create(Ctor.prototype);
// 步骤2:绑定 this 执行
const result = Ctor.apply(obj, args);
// 步骤3:构造函数返回了对象就用它,否则用 obj
return (result !== null && typeof result === "object") || typeof result === "function"
? result : obj;
}
// 验证
const p = myNew(Person, "王五");
p instanceof Person; // true
p.say(); // 我是 王五
function myInstanceof(obj, Ctor) {
if (obj === null || typeof obj !== "object") return false; // 原始值直接 false
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === Ctor.prototype) return true; // 链上每一层比对
proto = Object.getPrototypeOf(proto); // 沿链上移
}
return false; // 走到 null 还没找到
}
| 方案 | 思路 | 致命缺陷 |
|---|---|---|
| ① 原型链继承 | Child.prototype = new Parent() | 引用类型的属性被所有实例共享(改一个全变);创建子实例时没法给父类传参 |
| ② 借用构造函数 | Child 里 Parent.call(this) | 只能继承实例属性,父类原型上的方法拿不到;方法只能定义在构造函数里无法复用 |
| ③ 组合继承(经典) | ①+② 结合 | 父类构造函数被调用两次(call 一次 + new Parent() 一次),实例上有一份冗余的父类属性 |
| ④ 寄生组合式(最优解) | call 继承实例属性 + Object.create(Parent.prototype) 继承原型 | ——(ES6 class 的底层等价物) |
// 寄生组合式:ES6 之前的最优继承
function inherit(Child, Parent) {
Child.prototype = Object.create(Parent.prototype); // 只继承原型,不调父构造函数
Child.prototype.constructor = Child; // 修正 constructor 指向
}
function Parent(name) { this.name = name; this.tags = []; }
Parent.prototype.say = function () { console.log(this.name); };
function Child(name, age) {
Parent.call(this, name); // 只继承实例属性,且能传参
this.age = age;
}
inherit(Child, Parent);
Child.prototype.showAge = function () { console.log(this.age); };
const c = new Child("张三", 18);
c.say(); // 张三(原型链继承到)
c.showAge(); // 18
// ES6 class 本质等价于上面这套 + 语法糖差异
class Parent2 { constructor(name) { this.name = name; } say() {} }
class Child2 extends Parent2 {
constructor(name, age) { super(name); this.age = age; } // super(name) ≈ Parent.call(this, name)
}
typeof Parent2; // "function"!class 就是函数,方法挂在 Parent2.prototype 上
JS 只有一个调用栈,但浏览器是多线程的——事件循环是两者之间的调度机制。
console.log("1"); // 同步
setTimeout(() => console.log("2"), 0); // 宏任务
Promise.resolve().then(() => console.log("3")); // 微任务
Promise.resolve().then(() => {
console.log("4");
setTimeout(() => console.log("5"), 0); // 微任务里产生宏任务 → 排到队尾
});
console.log("6");
// 输出:1 6 3 4 2 5
// 同步(1,6) → 清微任务(3,4) → 取宏任务(2) → 清微任务(无) → 取宏任务(5)
setTimeout(fn, 0) 不是 0ms 执行,浏览器最小延迟约 4ms(嵌套调用还会更长),它只保证「至少延迟指定时间」;② await 后面的代码 = .then 回调 = 微任务——await 会先让出主线程,本轮同步代码先跑完。
Promise 本质是一个状态机:pending →(resolve)→ fulfilled 或 pending →(reject)→ rejected。状态一旦变更不可逆,之后所有的 resolve/reject 调用都是无效操作(这就是「值只会被决定一次」)。
// 简版手写:只处理核心流程(面试能写出来已超过大多数人)
class MyPromise {
constructor(executor) {
this.state = "pending";
this.value = undefined;
this.callbacks = []; // pending 时先存起来
const resolve = (value) => {
if (this.state !== "pending") return; // 状态不可逆
this.state = "fulfilled";
this.value = value;
this.callbacks.forEach(cb => cb.onFulfilled(value)); // 异步执行(微任务)
};
const reject = (reason) => {
if (this.state !== "pending") return;
this.state = "rejected";
this.value = reason;
this.callbacks.forEach(cb => cb.onRejected(reason));
};
try { executor(resolve, reject); } // executor 同步立即执行!
catch (e) { reject(e); }
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => { // then 返回新 Promise → 链式调用的根基
const handle = (fn, fallback) => {
queueMicrotask(() => { // then 回调必须是异步的(微任务)
try {
const result = (fn || fallback)(this.value);
result instanceof MyPromise ? result.then(resolve, reject) : resolve(result);
} catch (e) { reject(e); } // 回调抛错 → 自动冒泡给下一个 catch
});
};
if (this.state === "pending") this.callbacks.push({
onFulfilled: () => handle(onFulfilled),
onRejected: () => handle(onRejected)
});
else if (this.state === "fulfilled") handle(onFulfilled);
else handle(onRejected);
});
}
}
function myAll(promises) {
return new Promise((resolve, reject) => {
const results = []; let count = 0;
if (promises.length === 0) return resolve([]);
promises.forEach((p, i) => {
Promise.resolve(p).then(v => { // Promise.resolve 包一层:兼容普通值
results[i] = v; // 用下标保证顺序,不是完成顺序
if (++count === promises.length) resolve(results);
}, reject); // 一败全败,直接 reject
});
});
}
// allSettled:全部落定,永不 reject,结果带 status 字段
// race:第一个落定的(无论成败)决定结果
// any:第一个成功的决定结果;全失败才 reject(AggregateError)
async/await 是「Generator + 自动执行器」的语法糖。核心认知:
// 等价转换:理解了这个,await 的执行顺序题全会
async function foo() {
console.log("a");
const v = await bar(); // bar() 同步执行;await 处让出主线程
console.log("c"); // ≈ .then(() => { console.log("c") }) → 微任务
}
// 近似等价于:
function foo() {
console.log("a");
return Promise.resolve(bar()).then(v => { console.log("c"); });
}
// 并发陷阱:串行 vs 并行(真实项目高频 bug)
// ❌ 串行:总耗时 = 1s + 2s + 3s = 6s
const a1 = await fetchA(); // 1s
const b1 = await fetchB(); // 2s
const c1 = await fetchC(); // 3s
// ✅ 并行:总耗时 = max(1,2,3) = 3s —— 先发请求再 await
const pa = fetchA(), pb = fetchB(), pc = fetchC();
const [a2, b2, c2] = await Promise.all([pa, pb, pc]);
await p.catch(e => fallback)(给默认值)。注意:async 函数内部的错误不会让 Node 进程崩溃,但会变成 rejected Promise——忘了 catch 就是 unhandledrejection。
Vue3 响应式用 Proxy、迭代器协议贯穿所有集合类型、解构默认值是 Hooks 参数的标配——这节每个特性都对应框架里的真实用法。
Proxy 在「对象外面套一层拦截器」,13 种陷阱(trap)可拦截几乎一切操作;Reflect 提供与陷阱一一对应的「默认行为」调用,两者是天作之合。
const raw = { price: 100, qty: 2 };
const reactive = new Proxy(raw, {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`); // 依赖收集的入口
const result = Reflect.get(target, key, receiver); // 用 Reflect 执行默认行为
return typeof result === "object" && result !== null ? reactive(result) : result;
// 递归代理嵌套对象 = Vue3 「惰性深层响应式」的雏形(Vue2 是初始化时递归 defineProperty,一竿子到底)
},
set(target, key, value, receiver) {
console.log(`写入 ${String(key)} = ${value}`); // 触发更新的入口
return Reflect.set(target, key, value, receiver); // 必须返回 true,否则严格模式下报错
}
});
reactive.price; // 读取 price → 100
reactive.qty = 5; // 写入 qty = 5
| 对比项 | Object.defineProperty(Vue2) | Proxy(Vue3) |
|---|---|---|
| 拦截能力 | 只有 get/set,新增/删除属性拦不住(所以 Vue2 要 $set/$delete) | 13 种陷阱:含 deleteProperty、has、ownKeys 等 |
| 数组支持 | 差(靠重写 7 个变异方法 hack) | 原生支持索引赋值和 length 修改 |
| 深层代理 | 初始化时递归全部转化(性能开销前置) | 访问到才代理(惰性,大对象友好) |
| 兼容性 | IE9+ | 无法 polyfill(Vue3 放弃 IE 的根本原因) |
// Map vs 普通对象:key 不限类型、顺序保证、size 直取、频繁增删性能更好
const m = new Map();
m.set({ id: 1 }, "用户对象做键"); // 对象做键:普通对象会 toString 成 "[object Object]" 冲突
m.set("count", 0);
[...m.keys()]; // 保持插入顺序
// Set:去重 + 交并差集(比 filter+includes 快得多,O(1) 查找)
const union = new Set([...a, ...b]); // 并集
const intersect = a.filter(x => bSet.has(x)); // 交集
// WeakMap/WeakSet:键只能是对象、键是弱引用、不可遍历、没有 size
// 典型用途:给对象挂「私有数据」,对象销毁数据自动跟着回收(深拷贝的缓存也用它)
const privateData = new WeakMap();
class User {
constructor(token) { privateData.set(this, { token }); } // token 外部拿不到
}
可迭代对象(有 Symbol.iterator 方法、返回带 next() 的迭代器)就能被 for...of / 展开运算符 / 解构 / Promise.all 消费——Array、Map、Set、String、NodeList 都是;普通对象不是(所以不能 for...of)。
// 手写可迭代对象:让 for...of 遍历普通对象
const range = {
from: 1, to: 5,
[Symbol.iterator]() {
let cur = this.from, last = this.to;
return {
next() {
return cur <= last ? { value: cur++, done: false } : { value: undefined, done: true };
}
};
}
};
[...range]; // [1,2,3,4,5]
// Generator:function* + yield,函数可暂停可恢复
function* gen() {
const x = yield 1; // 执行到 yield 暂停,把 1 交出去;next(v) 时 v 作为 yield 的返回值传回来
const y = yield x * 2;
return x + y;
}
const g = gen();
g.next(); // { value: 1, done: false }
g.next(10); // { value: 20, done: false }(x = 10)
g.next(5); // { value: 15, done: true }(y = 5)
// ① ?. 可选链 vs &&:?. 只跳过 null/undefined,0 和 "" 照常返回
const val = obj?.a?.b ?? "默认"; // ?? 只对 null/undefined 兜底
const val2 = obj && obj.a || "默认"; // ❌ 旧写法:a 为 0 或 "" 时会误兜底
// ② 解构默认值生效条件:值严格等于 undefined 才生效
function toast({ duration = 3000, title } = {}) {} // 参数默认 {} 防止不传参报错
toast({ duration: 0 }); // duration = 0(✅ 正确保留,用 ?? 会坑)
toast({ duration: null }); // duration = null(null 不触发默认值!)
// ③ 解构重命名 + 嵌套默认值(读组件库源码必备)
const { size = "middle", onCancel: handleClose } = props;
const { theme: { primaryColor = "#1890ff" } = {} } = options;
全部亲手写一遍并跑通,比看十遍文章有用。前面标 ✅ 的本章已给完整实现,其余按同样深度自己补全。
| 题目 | 核心考点 | 难度 | 章节 |
|---|---|---|---|
| ✅ 深拷贝(循环引用) | WeakMap、递归、类型分派 | ★★★ | 2.3 |
| ✅ call / apply / bind | 隐式绑定原理、Symbol 防冲突 | ★★★ | 3.2 |
| ✅ 防抖 / 节流 | 闭包持状态、this 透传、cancel | ★★★ | 3.4 |
| ✅ new / instanceof | 原型链、Object.create | ★★ | 4.2 |
| ✅ Promise 简版 / all | 状态机、微任务、then 链穿透 | ★★★★ | 5.2 |
| 柯里化 curry | 闭包、参数长度判断(fn.length) | ★★★ | 本章作业 |
| 发布订阅 EventEmitter | Map 存回调、once/off 边界 | ★★★ | 本章作业 |
| 数组去重 / 扁平化 flatten | Set、reduce 递归、深度参数 | ★★ | 本章作业 |
| 寄生组合式继承 | 原型链 + 借用构造 | ★★★ | 4.3 ✅ |
| 函数记忆 memoize | 闭包 + Map 缓存、参数序列化做键 | ★★ | 本章作业 |
| sleep / 轮询 withTimeout | Promise + setTimeout 组合 | ★★ | 本章作业 |
1. 为什么 let x = x 报错而 var x = x 是 undefined?(TDZ 与提升差异)
2. [] == ![] 的完整推导过程?
3. 闭包变量为什么在函数返回后依然存活?什么情况下变成内存泄漏?
4. new 做了四件事,构造函数 return 对象 / return 原始值分别返回什么?
5. 微任务队列和宏任务队列的清空策略差异?await 后面的代码属于哪个?
6. Promise.then 链返回 Promise 时的「展开」规则是什么?
7. Proxy 相比 defineProperty 的三大优势?Vue3 为什么放弃 IE?
console.log 一样自然——这是所有前端面试的地基。