最近在看新的机会,把之前整理的前端面试基础知识重新梳理了一遍。内容涵盖
CSS 布局、JavaScript 核心概念、前端手写题、高频算法、框架原理、
计算机网络与浏览器、TypeScript 高频考点。这份资料既可以作为答案索引,也提供了针对"看懂但现场写不出来"的训练方法。
标注
✨的内容为本次整理时补充的高频考点。2026-06-14 | 持续更新中
目录
- [一、CSS 布局与技巧](#一、CSS 布局与技巧)
- [二、JavaScript 核心概念](#二、JavaScript 核心概念)
- 三、前端手写题全集(38项)
- 四、算法高频题
- 五、框架原理
- 六、公司真题精选
- 七、计算机网络与浏览器
- [八、TypeScript 高频](#八、TypeScript 高频)
- 九、复习路线建议
一、CSS 布局与技巧
1.1 三栏布局
css
/* 方法1:float */
.left { float: left; width: 200px; background: red; }
.right { float: right; width: 200px; background: blue; }
.middle { margin: 0 200px; background: green; }
/* 方法2:flex */
.container { display: flex; }
.left { flex: 0 0 200px; }
.middle { flex: 1; }
.right { flex: 0 0 200px; }
关键点:float 方案需注意 DOM 顺序(left/right 写在 middle 前面);flex 方案更直观。
1.2 两栏布局
css
/* float */
.left { float: left; width: 200px; }
.right { margin-left: 200px; }
/* flex */
.container { display: flex; }
.left { flex: 0 0 200px; }
.right { flex: 1; }
1.3 水平居中
css
/* 1. text-align(行内元素/文本) */
.parent { text-align: center; }
/* 2. margin:auto(定宽块级) */
.child { width: 200px; margin: 0 auto; }
/* 3. flex */
.parent { display: flex; justify-content: center; }
/* 4. absolute + transform */
.child {
position: absolute; left: 50%;
transform: translateX(-50%);
}
1.4 垂直居中
css
/* 1. line-height(单行文本,height == line-height) */
/* 2. table-cell */
.parent { display: table-cell; vertical-align: middle; }
/* 3. flex */
.parent { display: flex; align-items: center; }
/* 4. absolute + transform */
.child {
position: absolute; top: 50%;
transform: translateY(-50%);
}
1.5 0.5px 的线
css
.line {
height: 1px;
transform: scaleY(0.5);
transform-origin: 0 0;
}
1.6 CSS 三角形
css
.triangle {
width: 0; height: 0;
border: 20px solid transparent;
border-bottom-color: red; /* 尖角朝上 */
}
原理:四个 border 交汇处各占一个三角区域,将其中三边设为透明即可。
1.7 五点骰子
html
<!-- 使用 flexbox + justify-content + align-items 实现骰子五点布局 -->
<div class="dice">
<div class="row"><span></span><span></span></div>
<div class="row"><span></span></div>
<div class="row"><span></span><span></span></div>
</div>
1.8 宽度为高度的一半
css
/* 方法1:vw */
.box { width: 50vw; height: 25vw; }
/* 方法2:CSS变量 + calc */
.box { --w: 200px; width: var(--w); height: calc(var(--w) / 2); }
/* 方法3:padding-bottom百分比(相对于父元素宽度) */
.box { width: 100%; padding-bottom: 50%; }
二、JavaScript 核心概念
2.1 变量提升与作用域
javascript
// 🔴 经典题1:函数内 var 提升
var a = 10;
function test() {
a = 100;
console.log(a); // 100
console.log(this.a); // 10(window.a)
var a; // 变量提升到函数顶部
console.log(a); // 100
}
test();
// 🔴 经典题2:var vs let 在 for 循环 + setTimeout
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出:4, 4, 4(var没有块级作用域)
for (let i = 1; i <= 3; i++) {
setTimeout(() => console.log(i), 0);
}
// 输出:1, 2, 3(let有块级作用域,每次迭代新绑定)
// 🔴 经典题3:函数提升高于变量提升
var fun = 123;
function fun() { console.log('function'); }
fun(); // TypeError: fun is not a function(fun被覆盖为123)
// 🔴 经典题4:参数与变量提升
function fun(n) {
console.log(n); // 123(形参n已有值,var n声明被忽略)
var n = 456;
console.log(n); // 456
}
var n = 123;
fun(n);
// 🔴 经典题5:没有var声明 => 全局变量
(function() {
var a = b = 3; // 等价于 b=3; var a=b;
})();
console.log(b); // 3(b是全局变量)
console.log(a); // ReferenceError: a is not defined
// 🔴 字节真题:函数提升遮蔽全局变量
var a = 1;
function b() {
a = 10;
return;
function a() {} // 函数声明提升,a变成局部函数
}
b();
console.log(a); // 1(函数作用域内的a是局部函数,不影响全局a)
2.2 闭包
javascript
// 🔴 闭包陷阱
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function() { console.log(i); };
}
data[0](); // 3(闭包捕获的都是同一个i)
data[1](); // 3
data[2](); // 3
// ✅ 解决方案1:let(块级作用域)
for (let i = 0; i < 3; i++) {
data[i] = function() { console.log(i); };
}
// ✅ 解决方案2:IIFE
for (var i = 0; i < 3; i++) {
(function(j) { data[j] = function() { console.log(j); }; })(i);
}
// 🔴 小米题:嵌套闭包
var n = 10;
function fn() {
var n = 20;
function f() { n++; console.log(n); }
f(); // 21
return f;
}
var x = fn(); // 21
x(); // 22(闭包保持对fn内n的引用)
x(); // 23
console.log(n); // 10(全局n未变)
2.3 this 指向
javascript
// 🔴 规则速记:谁调用指向谁(箭头函数除外)
var obj = {
a: 2,
f: function() { console.log(this.a); },
m: () => { console.log(this.a); } // 箭头函数this=定义时的外层this
};
var f2 = obj.f;
var a = 'hello';
f2(); // 'hello'(无调用者 => window/globalThis)
obj.f(); // 2(obj调用)
obj.m(); // 'hello'(箭头函数,this指向window)
// 🔴 setTimeout中的this
var id = 'Global';
function fun1() {
setTimeout(function() {
console.log(this.id); // 普通函数,this=window
}, 1000);
}
function fun2() {
setTimeout(() => {
console.log(this.id); // 箭头函数,this=fun2的this
}, 1000);
}
fun1.call({id: 'Obj'}); // 'Global'
fun2.call({id: 'Obj'}); // 'Obj'
// 🔴 arguments 中的 this
var length = 100;
function f1() { console.log(this.length); }
var obj = {
x: 100,
f2: function(f1) {
f1(); // 100(window.length)
arguments[0](); // 2(arguments.length=2,arguments调用了f1)
}
};
obj.f2(f1, 1);
2.4 事件循环(Event Loop)
javascript
// 🔴 经典题:宏任务 vs 微任务
setTimeout(() => {
console.log(1);
new Promise(resolve => resolve()).then(() => console.log(2));
});
setTimeout(() => { console.log(3); });
new Promise(resolve => {
console.log(4);
resolve();
console.log(5);
}).then(() => { console.log(6); });
console.log(7);
new Promise(resolve => resolve()).then(() => { console.log(8); });
// 输出:4, 5, 7, 6, 8, 1, 2, 3
// 解析:
// 同步:4,5,7
// 微任务队列:then6, then8 → 6,8
// 宏任务1:1, then2 → 1,2(微任务插队)
// 宏任务2:3
// 🔴 百度题
console.log(3);
Promise.resolve().then(() => {
console.log('Promise1');
setTimeout(() => console.log('setTimeout2'), 0);
});
setTimeout(() => {
console.log("setTimeout1");
Promise.resolve().then(() => console.log('Promise2'));
}, 0);
// 输出:3, Promise1, setTimeout1, Promise2, setTimeout2
核心规则:
- 同步代码 → 微任务队列(Promise.then) → 宏任务队列(setTimeout)
- 每个宏任务执行完后,会清空微任务队列再执行下一个宏任务
async/await:await后面的代码相当于.then()回调
javascript
// 🔴 async/await 执行顺序
async function async1() {
console.log('async1 start'); // 2
await async2(); // 立即执行async2
console.log('async1 end'); // 7(await 后续进入微任务队列)
}
async function async2() {
console.log('async2'); // 3
setTimeout(() => console.log('async2宏'), 0); // 9
new Promise(resolve => resolve())
.then(() => console.log('2内微')); // 6
console.log('2内同步2'); // 4
}
console.log('script start'); // 1
async1();
new Promise(resolve => resolve())
.then(() => console.log('resolve')); // 8
console.log('script end'); // 5
// 输出:1, 2, 3, 4, 5, 2内微, async1 end, resolve, async2宏
// 原因:async2 内部的 then 比 await async2 的续体更早进入微任务队列
2.5 原型链与继承
五种继承方式:
javascript
// 1. 原型链继承
Child.prototype = new Parent(); // 核心
// ❌ 弊端1:所有子实例共享父类引用属性
// ❌ 弊端2:子类原型上的方法必须在替换原型之后添加
// 2. 构造函数继承
function Child() { Parent.call(this, args); } // 核心
// ✅ 解决引用属性共享问题
// ❌ 弊端:无法继承父类原型上的方法
// 3. 组合继承 = 构造 + 原型链
function Child() { Parent.call(this, args); }
Child.prototype = new Parent();
// ✅ 前两种的结合,但调用了两次父类构造函数
// 4. 寄生组合继承(最优方案)
function Child() { Parent.call(this, args); }
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 5. ES6 class
class Child extends Parent {
constructor(...args) { super(...args); }
}
原型链关键结论:
Object.create(x)--- 浅拷贝,创建新对象,原型指向xdelete yideng.address删除不了原型上的属性,所以还是输出原型上的值
2.6 箭头函数
javascript
// 箭头函数特性:
// 1. 没有自己的this,this继承自外层作用域(定义时的this,不是调用时的)
// 2. 不能用作构造函数(没有prototype)
// 3. 没有arguments对象
var x = 11;
var obj = {
x: 22,
say: () => { console.log(this.x); } // this = window
};
obj.say(); // 11,不是22!
// 返回匿名函数的坑
var obj = {
x: 33,
getAge: function() {
return function() { console.log(this.x); }; // this = window
}
};
obj.getAge()(); // 11(全局的x)
2.7 其他陷阱题
javascript
// 🔴 parseInt 陷阱
[1,2,3].map(parseInt);
// 等于 [parseInt(1,0), parseInt(2,1), parseInt(3,2)]
// 结果:[1, NaN, NaN]
// parseInt第二个参数是进制(2-36),0视为10进制
// 🔴 浮点数精度
0.1 + 0.2 === 0.3 // false(二进制无限循环)
0.1 + 0.5 === 0.6 // true
// 解决:Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON
// 🔴 自执行函数变量泄露
(function() { var a = b = 3; })();
console.log(typeof a); // 'undefined'
console.log(b); // 3(b没var,泄漏为全局)
// 🔴 NaN === NaN // false
// 🔴 Symbol('a') === Symbol('a') // false
// 🔴 null == undefined // true
// 🔴 1 == 2 == 0 // true(解析为 (1==2)==0 → false==0 → true)
// 🔴 null instanceof Object // false(null是基本类型)
// 🔴 值传递 vs 引用传递
var a = 1, b = 2;
function swap(a, b) { var tmp = a; a = b; b = tmp; }
swap(a, b);
console.log(a, b); // 1, 2(基本类型值传递,不影响外部)
// 如果是对象属性则可以通过引用修改
var obj = {a:1, b:2};
function swapO(obj, k1, k2) { var t = obj[k1]; obj[k1] = obj[k2]; obj[k2] = t; }
swapO(obj, 'a', 'b');
console.log(obj); // {a:2, b:1}(对象引用传递)
三、前端手写题全集(38项)
标注
✨ 新增的为本次整理时补充的高频题目。
3.1 防抖(Debounce)
javascript
function debounce(fn, delay) {
let timer;
return function () {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, arguments), delay);
};
}
// ✨ 增强版:支持立即执行 + 取消
function debounce(fn, delay, immediate = false) {
let timer = null;
const debounced = function (...args) {
const callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) fn.apply(this, args);
}, delay);
if (callNow) fn.apply(this, args);
};
debounced.cancel = () => { clearTimeout(timer); timer = null; };
return debounced;
}
// 场景:搜索框输入、窗口resize
3.2 节流(Throttle)
javascript
// 方法1:时间戳版(第一次立即触发,最后一次不触发)
function throttle(fn, delay) {
let pre = Date.now();
return function () {
let now = Date.now();
if (now - pre > delay) {
fn.apply(this, arguments);
pre = Date.now();
}
};
}
// 方法2:定时器版(第一次延迟触发,最后一次会触发)
function throttle(fn, delay) {
let timer;
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
};
}
// ✨ 增强版:首尾都触发(时间戳+定时器结合)
function throttle(fn, delay) {
let timer = null, lastTime = 0;
return function (...args) {
const now = Date.now();
const remaining = delay - (now - lastTime);
if (remaining <= 0) {
if (timer) { clearTimeout(timer); timer = null; }
lastTime = now;
fn.apply(this, args);
} else if (!timer) {
timer = setTimeout(() => {
lastTime = Date.now();
timer = null;
fn.apply(this, args);
}, remaining);
}
};
}
// 场景:scroll事件、按钮点击
3.3 深拷贝
javascript
// ✅ 方法1:JSON(最简单,有局限性)
JSON.parse(JSON.stringify(obj));
// ❌ 无法拷贝:函数、undefined、Symbol、循环引用、Date/RegExp
// ✅ 方法2:递归(简化版,支持数组和对象)
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
let result = obj.constructor === Array ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key]);
}
}
return result;
}
// ✅ 方法3:面试增强版(支持循环引用、Date、RegExp、Map、Set、Symbol键)
function deepClone(obj, cache = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) return obj;
if (cache.has(obj)) return cache.get(obj); // 循环引用
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Map) {
const result = new Map();
cache.set(obj, result);
obj.forEach((value, key) => {
result.set(deepClone(key, cache), deepClone(value, cache));
});
return result;
}
if (obj instanceof Set) {
const result = new Set();
cache.set(obj, result);
obj.forEach(value => result.add(deepClone(value, cache)));
return result;
}
const newObj = Array.isArray(obj)
? []
: Object.create(Object.getPrototypeOf(obj));
cache.set(obj, newObj);
Reflect.ownKeys(obj).forEach(key => {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if ('value' in descriptor) {
descriptor.value = deepClone(descriptor.value, cache);
}
Object.defineProperty(newObj, key, descriptor);
});
return newObj;
}
// 面试时先声明支持范围;函数和 WeakMap/WeakSet 通常不做深复制
3.4 浅拷贝
javascript
function shallowClone(obj) {
var newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) newObj[key] = obj[key];
}
return newObj;
}
// 其他方法:Object.assign({}, obj)、{...obj}
3.5 Promise 系列
3.5.1 Promise 状态机教学版(不可链式)
javascript
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.value = null;
this.fulfilledCallbacks = [];
this.rejectedCallbacks = [];
const resolve = (value) => {
if (this.status === 'pending') {
this.status = 'resolved';
this.value = value;
this.fulfilledCallbacks.forEach(fn => fn(value));
}
};
const reject = (value) => {
if (this.status === 'pending') {
this.status = 'rejected';
this.value = value;
this.rejectedCallbacks.forEach(fn => fn(value));
}
};
try { executor(resolve, reject); }
catch (err) { reject(err); }
}
then(onFulfilled, onRejected) {
if (this.status === 'pending') {
this.fulfilledCallbacks.push(() => onFulfilled(this.value));
this.rejectedCallbacks.push(() => onRejected(this.value));
}
if (this.status === 'resolved') onFulfilled(this.value);
if (this.status === 'rejected') onRejected(this.value);
}
}
这段只用于理解状态、回调队列和状态不可逆,不支持链式调用、值穿透、
thenable 吸收和微任务调度,不能当作完整 Promise 实现。
3.5.2 Promise.all
javascript
function promiseAll(promises) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
return reject(new TypeError('arguments must be an array'));
}
let count = 0;
const n = promises.length;
const results = new Array(n);
if (n === 0) return resolve([]);
for (let i = 0; i < n; i++) {
Promise.resolve(promises[i]).then(
(val) => {
count++;
results[i] = val;
if (count === n) resolve(results);
},
(err) => reject(err) // 任何一个失败就整个失败
);
}
});
}
// 标准 Promise.all 接收 iterable;面试简化版若限制为数组,应主动说明
3.5.3 Promise.allSettled
javascript
Promise.myAllSettled = function (promises) {
return new Promise((resolve) => {
const results = [];
let count = 0;
if (promises.length === 0) { resolve(results); return; }
promises.forEach((promise, index) => {
Promise.resolve(promise).then(
value => {
results[index] = { status: 'fulfilled', value };
count++;
if (count === promises.length) resolve(results);
},
reason => {
results[index] = { status: 'rejected', reason };
count++;
if (count === promises.length) resolve(results);
}
);
});
});
};
// 关键:与all不同,allSettled不会因某个reject而中断,总是等全部完成
3.5.4 Promise.race
javascript
Promise.myRace = function (promises) {
return new Promise((resolve, reject) => {
for (const promise of promises) {
Promise.resolve(promise).then(resolve, reject);
}
});
};
// 关键:谁先完成(无论成功/失败)就用谁的结果
3.6 发布订阅(EventEmitter)
javascript
var shoeObj = {};
shoeObj.list = [];
shoeObj.listen = function (key, fn) {
if (!this.list[key]) this.list[key] = [];
this.list[key].push(fn);
};
shoeObj.trigger = function () {
var key = Array.prototype.shift.call(arguments);
var fns = this.list[key];
if (!fns || fns.length === 0) return;
for (let i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
class Event {
constructor() { this.eventMap = {}; }
register(key, fn) {
if (!this.eventMap[key]) this.eventMap[key] = [];
this.eventMap[key].push(fn);
}
run(key, ...args) {
if (this.eventMap[key]?.length) {
this.eventMap[key].forEach(fn => fn.apply(null, args));
}
}
}
// ✨ 完整版:支持 once、off
class EventEmitter {
constructor() { this.events = {}; }
on(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
}
once(event, callback) {
const wrapper = (...args) => {
callback.apply(this, args);
this.off(event, wrapper);
};
wrapper.originCallback = callback;
this.on(event, wrapper);
}
off(event, callback) {
if (!this.events[event]) return;
this.events[event] = this.events[event].filter(
cb => cb !== callback && cb.originCallback !== callback
);
}
emit(event, ...args) {
if (!this.events[event]) return;
this.events[event].forEach(cb => cb.apply(this, args));
}
}
3.7 call / apply / bind
javascript
// ✅ 手写 call
Function.prototype.myCall = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context == null ? globalThis : Object(context);
const key = Symbol('fn');
context[key] = this;
try {
return context[key](...args);
} finally {
delete context[key];
}
};
// ✅ 手写 apply
Function.prototype.myApply = function (context, args) {
if (typeof this !== 'function') throw new TypeError('not a function');
context = context == null ? globalThis : Object(context);
const key = Symbol('fn');
context[key] = this;
try {
return args == null ? context[key]() : context[key](...args);
} finally {
delete context[key];
}
};
// ✅ 手写 bind
Function.prototype.myBind = function (context, ...args) {
if (typeof this !== 'function') throw new TypeError('not a function');
const target = this;
function bound(...rest) {
// 使用 new 调用时忽略绑定的 context
const thisArg = this instanceof bound ? this : context;
return target.apply(thisArg, [...args, ...rest]);
}
if (target.prototype) {
bound.prototype = Object.create(target.prototype);
Object.defineProperty(bound.prototype, 'constructor', {
value: bound,
configurable: true
});
}
return bound;
};
3.8 手写 new
javascript
function _new(constructor, ...args) {
// 1. 创建新对象,原型指向构造函数的 prototype
let obj = Object.create(constructor.prototype);
// 2. 执行构造函数,绑定 this
let result = constructor.apply(obj, args);
// 3. 如果构造函数返回了对象,则返回该对象;否则返回新对象
return typeof result === 'object' && result !== null ? result : obj;
}
3.9 数组扁平化(Flatten)
javascript
function flatten(arr, res = []) {
for (const item of arr) {
if (Array.isArray(item)) flatten(item, res);
else res.push(item);
}
return res;
}
// flatten([[1,2,3],4,5,6,[[7]],[]]) → [1,2,3,4,5,6,7]
// 其他方法:
// arr.flat(Infinity)
// arr.toString().split(',').map(Number)
3.10 数组去重
javascript
function unique(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
arr.splice(j, 1);
j--;
}
}
}
return arr;
}
// 其他方法:
// [...new Set(arr)]
// arr.filter((item, index) => arr.indexOf(item) === index)
3.11 手写 Array.map
javascript
Array.prototype.map2 = function (callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
result.push(callback(this[i], i, this));
}
return result;
};
3.12 sleep 函数
javascript
// 方法1:Promise
function sleep(delay) {
return new Promise(resolve => setTimeout(resolve, delay));
}
sleep(2000).then(() => console.log('2秒后执行'));
// 方法2:async/await
async function test() {
console.log('hello');
await sleep(2000);
console.log('world');
}
3.13 once 函数
javascript
function once(fn) {
let flag = false, result;
return function (...args) {
if (flag) return result;
result = fn(...args);
flag = true;
return result;
};
}
const f = (x) => x;
const onceF = once(f);
onceF(3); // 3
onceF(4); // 3(缓存了第一次结果)
3.14 深合并(Deep Merge)
javascript
function deepMerge(target, ...sources) {
if (sources.length === 0) return target;
const source = sources.shift();
if (source == null) return deepMerge(target, ...sources);
for (const key of Reflect.ownKeys(source)) {
const sourceValue = source[key];
const targetValue = target[key];
const sourceIsObject = sourceValue !== null && typeof sourceValue === 'object';
const targetIsObject = targetValue !== null && typeof targetValue === 'object';
if (sourceIsObject && targetIsObject
&& !Array.isArray(sourceValue) && !Array.isArray(targetValue)) {
target[key] = deepMerge(targetValue, sourceValue);
} else {
target[key] = deepClone(sourceValue);
}
}
return deepMerge(target, ...sources);
}
// 此版本约定:普通对象递归合并,数组直接覆盖;实际面试先确认数组策略
3.15 并发请求控制
javascript
const limitRequest = (urls, maxNum) => {
return new Promise((resolve) => {
if (urls.length === 0) { resolve([]); return; }
if (!Number.isInteger(maxNum) || maxNum <= 0) {
throw new RangeError('maxNum must be a positive integer');
}
const results = [];
let index = 0, count = 0;
async function request() {
if (index === urls.length) return;
const i = index++;
const url = urls[i];
try {
results[i] = await fetch(url);
} catch (err) {
results[i] = err;
} finally {
count++;
if (count === urls.length) resolve(results);
request(); // 完成后启动下一个
}
}
const times = Math.min(maxNum, urls.length);
for (let i = 0; i < times; i++) request();
});
};
// 核心:用 index 指针分配任务,每个完成后再调用 request() 启动新的
3.16 异步串行执行
javascript
function queuePromise(promises) {
const results = [];
let p = Promise.resolve();
promises.forEach(item => {
p = p.then(() => item().then(val => results.push(val)));
});
return p.then(() => results);
}
// 用法:
// queuePromise([() => createPromise(1, 3000), () => createPromise(2, 1000)])
// 输出:3s后→1, 再1s后→2
3.17 数组转树 & 树转数组
javascript
function arraytoTree(arr) {
let root = arr[0]; arr.shift();
return {
id: root.id, val: root.val,
children: arr.length > 0 ? toTree(root.id, arr) : []
};
}
function toTree(parentId, arr) {
let children = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i].parentId === parentId) {
children.push({
id: arr[i].id, val: arr[i].val,
children: toTree(arr[i].id, arr)
});
}
}
return children;
}
// ✨ 优化版:O(n) Map法
function arrayToTree(items) {
const map = new Map();
const roots = [];
items.forEach(item => {
map.set(item.id, { ...item, children: [] });
});
items.forEach(item => {
const node = map.get(item.id);
if (item.parentId == null || !map.has(item.parentId)) {
roots.push(node);
} else {
map.get(item.parentId).children.push(node);
}
});
return roots;
}
// 树转数组
function treeToArray(tree) {
const result = [];
const flatten = (nodes, parentId = 0) => {
nodes.forEach(node => {
const { children, ...rest } = node;
result.push({ ...rest, parentId });
if (children?.length) flatten(children, rest.id);
});
};
flatten(tree);
return result;
}
3.18 LRU 缓存
javascript
// ES6 Map 简洁版(Map 的 key 顺序即插入顺序)
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return -1;
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value); // 移到末尾
return value;
}
put(key, value) {
if (this.cache.has(key)) this.cache.delete(key);
this.cache.set(key, value);
if (this.cache.size > this.capacity) {
// Map.keys().next() 返回最久未使用的 key
this.cache.delete(this.cache.keys().next().value);
}
}
}
// 双向链表版见算法部分 LeetCode 146
3.19 JSONP
javascript
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
// 使用:addScriptTag("http://api.example.com?callback=handleData")
// 原理:利用 script 标签不受同源策略限制
3.20 手写 AJAX
javascript
function ajax(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`${xhr.status} error`));
}
}
};
xhr.send(null);
});
}
// XMLHttpRequest readyState 状态:
// 0 - UNSENT, 1 - OPENED, 2 - HEADERS_RECEIVED, 3 - LOADING, 4 - DONE
3.21 ES5 实现 const
javascript
function _const(key, value) {
window[key] = value;
Object.defineProperty(window, key, {
enumerable: false,
configurable: false,
get: () => value,
set: (newValue) => {
if (newValue !== value) throw new TypeError('Assignment to constant variable');
}
});
}
// ⚠️ 局限:挂载在 window 上,不是真正的块级作用域
3.22 驼峰 ↔ 下划线
javascript
// 下划线转驼峰
function toHump(name) {
return name.replace(/\_(\w)/g, (all, letter) => letter.toUpperCase());
}
toHump('hello_js_go'); // 'helloJsGo'
// 驼峰转下划线
function toLine(name) {
return name.replace(/([A-Z])/g, '_$1').toLowerCase();
}
toLine('aBcdEfg'); // 'a_bcd_efg'
3.23 虚拟DOM Diff
javascript
// 1. 虚拟DOM → 真实DOM
function createElement(vnode) {
let { tag, attrs = {}, children = [] } = vnode;
if (!tag) return null;
let elem = document.createElement(tag);
for (let attrName in attrs) {
if (attrs.hasOwnProperty(attrName)) elem.setAttribute(attrName, attrs[attrName]);
}
children.forEach(childVnode => elem.appendChild(createElement(childVnode)));
return elem;
}
// 2. 仅用于解释"同层比较"的极简伪代码
function updateChildren(vnode, newVnode) {
let children = vnode.children || [];
let newChildren = newVnode.children || [];
children.forEach((childVnode, index) => {
let newChildVnode = newChildren[index];
if (childVnode.tag === newChildVnode.tag) {
updateChildren(childVnode, newChildVnode); // 同tag继续递归
} else {
replaceNode(childVnode, newChildVnode); // 不同直接替换
}
});
}
注意:以上不是可运行的完整 Diff。真正实现还需处理文本节点、属性更新、
节点增删、key、真实 DOM 映射和组件生命周期。面试中优先讲算法目标和框架策略,
不要把这段伪代码描述为完整实现。
3.24 链式调用
javascript
// 核心:每个方法 return this
class Person {
setAge(data) { this.age = data; return this; }
setSex(data) { this.sex = data; return this; }
}
const p = new Person();
p.setAge(12).setSex('male'); // 链式调用
3.25 拖拽
javascript
// 核心:mousedown 记录偏移 → mousemove 更新位置 → mouseup 停止
box.onmousedown = function (e) {
let x = e.clientX - box.offsetLeft;
let y = e.clientY - box.offsetTop;
let isDrop = true;
document.onmousemove = function (e) {
if (!isDrop) return;
box.style.left = Math.min(maxX, Math.max(0, e.clientX - x)) + 'px';
box.style.top = Math.min(maxY, Math.max(0, e.clientY - y)) + 'px';
};
document.onmouseup = function () { isDrop = false; };
};
3.26 Promise 调度器(Scheduler)✨ 新增
javascript
/**
* 实现一个带并发限制的异步调度器
* 保证同时最多运行 max 个异步任务
*/
class Scheduler {
constructor(max) {
this.max = max;
this.running = 0;
this.queue = [];
}
add(task) {
return new Promise((resolve, reject) => {
const run = async () => {
this.running++;
try {
resolve(await task());
} catch (error) {
reject(error);
} finally {
this.running--;
this._runNext();
}
};
if (this.running < this.max) run();
else this.queue.push(run);
});
}
_runNext() {
if (this.running < this.max && this.queue.length) {
const next = this.queue.shift();
next();
}
}
}
// 测试
// const scheduler = new Scheduler(2);
// const timeout = (delay, value) =>
// () => new Promise(resolve => setTimeout(() => resolve(value), delay));
// scheduler.add(timeout(1000, 1)).then(console.log);
// scheduler.add(timeout(500, 2)).then(console.log);
// scheduler.add(timeout(300, 3)).then(console.log);
// scheduler.add(timeout(400, 4)).then(console.log);
// 期望:最多同时2个在执行
3.27 异步串行 + 重试 ✨ 新增
javascript
async function serialWithRetry(tasks, retries = 2) {
const results = [];
for (const task of tasks) {
let lastError;
for (let attempt = 0; attempt <= retries; attempt++) {
try {
results.push(await task());
lastError = null;
break;
} catch (err) {
lastError = err;
}
}
if (lastError) results.push({ error: lastError });
}
return results;
}
// 不使用 async/await 的版本
function retryTask(task, retries) {
return task().catch(err => {
if (retries > 0) return retryTask(task, retries - 1);
throw err;
});
}
3.28 React Hook 实现思路 ✨ 新增
javascript
// 教学级伪实现:用数组和调用顺序解释"为什么 Hook 不能写在条件分支中"
const hooks = [];
let hookIndex = 0;
function useState(initialValue) {
const currentIndex = hookIndex++;
if (!(currentIndex in hooks)) {
hooks[currentIndex] =
typeof initialValue === 'function' ? initialValue() : initialValue;
}
const setState = nextValue => {
const previous = hooks[currentIndex];
hooks[currentIndex] =
typeof nextValue === 'function' ? nextValue(previous) : nextValue;
render(); // 教学占位:真实 React 会调度更新
};
return [hooks[currentIndex], setState];
}
function render() {
hookIndex = 0;
// Component();
}
面试重点:
- 以上只是解释调用顺序的伪实现,不是 React 源码
- 多个组件需要各自的 Fiber/Hook 链表,不能共享一个全局数组
- Hook 的闭包陷阱(stale closure):定时器中的 state 不会更新
- 为什么需要 deps 数组:决定何时重新执行
- useMemo vs useCallback:前者缓存值,后者缓存函数
3.29 函数柯里化(Curry)✨ 新增
javascript
// 基础版:收集够参数才执行
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
return curried.apply(this, [...args, ...nextArgs]);
};
};
}
// 用法示例
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
// 面试追问:柯里化的价值?
// 1. 参数复用(生成偏函数)
// 2. 函数组合(compose/pipe 需要单参数函数)
// 3. 延迟执行
3.30 手写 instanceof ✨ 新增
javascript
function myInstanceof(left, right) {
// 基本类型直接返回 false
if (typeof left !== 'object' || left === null) return false;
let proto = Object.getPrototypeOf(left);
const prototype = right.prototype;
while (proto) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
// 测试
myInstanceof([], Array); // true
myInstanceof([], Object); // true
myInstanceof(123, Number); // false(基本类型)
myInstanceof(null, Object); // false
// 核心:沿原型链向上查找 left.__proto__ === right.prototype
3.31 compose / pipe ✨ 新增
javascript
// ✅ compose(从右向左执行)
const compose = (...fns) => {
if (fns.length === 0) return (arg) => arg;
if (fns.length === 1) return fns[0];
return fns.reduceRight((prev, next) => (...args) => next(prev(...args)));
};
// ✅ pipe(从左向右执行,更符合阅读习惯)
const pipe = (...fns) => {
if (fns.length === 0) return (arg) => arg;
if (fns.length === 1) return fns[0];
return fns.reduce((prev, next) => (...args) => next(prev(...args)));
};
// 实战用法(结合柯里化)
const filter = curry((fn, arr) => arr.filter(fn));
const map = curry((fn, arr) => arr.map(fn));
const getAdminNames = pipe(
filter(u => u.role === 'admin'),
map(u => u.name)
);
getAdminNames(users);
// 面试追问:Redux 中间件的 applyMiddleware 内部就用了 compose
3.32 LazyMan(链式调用 + 任务队列)✨ 新增
javascript
class LazyMan {
constructor(name) {
this.tasks = [];
this.tasks.push(() => {
console.log(`Hi! This is ${name}`);
this.next();
});
// 异步启动任务队列
setTimeout(() => this.next(), 0);
}
next() {
const task = this.tasks.shift();
task && task();
}
eat(food) {
this.tasks.push(() => {
console.log(`Eat ${food}`);
this.next();
});
return this; // 链式调用
}
sleep(seconds) {
this.tasks.push(() => {
setTimeout(() => {
console.log(`Wake up after ${seconds} seconds`);
this.next();
}, seconds * 1000);
});
return this;
}
sleepFirst(seconds) {
this.tasks.unshift(() => { // unshift 到队头!
setTimeout(() => {
console.log(`Wake up after ${seconds} seconds`);
this.next();
}, seconds * 1000);
});
return this;
}
}
function createLazyMan(name) { return new LazyMan(name); }
// LazyMan('Tony').eat('lunch').sleep(2).eat('dinner').sleepFirst(1);
// 输出:Wake up after 1s → Hi! Tony → Eat lunch → Wake up after 2s → Eat dinner
// 核心考点:任务队列 + 链式调用 + 异步控制 + sleepFirst前置插入
3.33 Array.prototype.reduce ✨ 新增
javascript
Array.prototype.myReduce = function (callback, initialValue) {
if (typeof callback !== 'function') throw new TypeError('callback must be a function');
const hasInitialValue = arguments.length > 1;
let index = 0;
let accumulator;
if (hasInitialValue) {
accumulator = initialValue;
} else {
while (index < this.length && !(index in this)) index++;
if (index >= this.length) {
throw new TypeError('Reduce of empty array with no initial value');
}
accumulator = this[index++];
}
for (; index < this.length; index++) {
if (!(index in this)) continue;
accumulator = callback(accumulator, this[index], index, this);
}
return accumulator;
};
// 测试
[1, 2, 3, 4].myReduce((acc, cur) => acc + cur, 0); // 10
// 面试追问:没有 initialValue 时,用数组第一个元素作为初始值
3.34 版本号比较 ✨ 新增
javascript
function compareVersion(v1, v2) {
const arr1 = v1.split('.');
const arr2 = v2.split('.');
const len = Math.max(arr1.length, arr2.length);
for (let i = 0; i < len; i++) {
const n1 = parseInt(arr1[i] || 0);
const n2 = parseInt(arr2[i] || 0);
if (n1 > n2) return 1;
if (n1 < n2) return -1;
}
return 0;
}
compareVersion('1.2.3', '1.2.4'); // -1
compareVersion('2.0', '1.9.9'); // 1
// 常见扩展:比较"1.2.0-alpha.1"这种带预发布标签的版本号
3.35 Promise.prototype.finally + 静态方法 ✨ 新增
javascript
// ✅ Promise.prototype.finally
Promise.prototype.myFinally = function (callback) {
return this.then(
value => Promise.resolve(callback()).then(() => value),
reason => Promise.resolve(callback()).then(() => { throw reason; })
);
};
// ✅ Promise.resolve
Promise.myResolve = function (value) {
if (value instanceof Promise) return value;
return new Promise((resolve) => resolve(value));
};
// ✅ Promise.reject
Promise.myReject = function (reason) {
return new Promise((_, reject) => reject(reason));
};
// ✅ Promise.any(ES2021)
Promise.myAny = function (promises) {
return new Promise((resolve, reject) => {
const errors = [];
let count = 0;
if (promises.length === 0) {
return reject(new AggregateError([], 'All promises were rejected'));
}
promises.forEach((p, i) => {
Promise.resolve(p).then(resolve, (err) => {
errors[i] = err;
count++;
if (count === promises.length) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
});
});
};
// Promise.any 与 Promise.race 的区别:any 等第一个成功的,全失败才 reject
3.36 Promise/A+ 的 then 解析核心 ✨ 升级
javascript
// 重点展示 then 链式调用与 resolvePromise 解析过程。
// 这是核心片段,需与统一使用 resolved/rejected、value 字段的构造器配套。
class MyPromise {
// ... 构造函数同上
then(onFulfilled, onRejected) {
// 值穿透:非函数参数自动转为函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e; };
const promise2 = new MyPromise((resolve, reject) => {
const fulfilledMicrotask = () => {
queueMicrotask(() => {
try {
const x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
const rejectedMicrotask = () => {
queueMicrotask(() => {
try {
const x = onRejected(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (err) {
reject(err);
}
});
};
if (this.status === 'resolved') fulfilledMicrotask();
else if (this.status === 'rejected') rejectedMicrotask();
else if (this.status === 'pending') {
this.fulfilledCallbacks.push(fulfilledMicrotask);
this.rejectedCallbacks.push(rejectedMicrotask);
}
});
return promise2;
}
catch(onRejected) {
return this.then(null, onRejected);
}
}
// resolvePromise --- Promise/A+ 核心
function resolvePromise(promise2, x, resolve, reject) {
// 1. 不可自己等待自己
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected'));
}
// 2. 如果 x 是 Promise 实例,采用它的状态
if (x instanceof MyPromise) {
x.then(
value => resolvePromise(promise2, value, resolve, reject),
reject
);
} else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
// 3. 如果 x 是 thenable(对象/函数且有 then 方法)
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(x,
value => {
if (called) return;
called = true;
resolvePromise(promise2, value, resolve, reject);
},
reason => {
if (called) return;
called = true;
reject(reason);
}
);
} else {
resolve(x);
}
} catch (err) {
if (called) return;
called = true;
reject(err);
}
} else {
// 4. 普通值直接 resolve
resolve(x);
}
}
// 面试官评分点:
// ✅ 状态不可逆 ✅ 值穿透 ✅ 异步执行(queueMicrotask)
// ✅ then 返回新 Promise ✅ resolvePromise 处理 thenable ✅ 循环引用检测
面试通常优先考
Promise.all、并发控制和事件循环。除非岗位明确偏底层,不建议把完整 Promise/A+ 实现作为最高优先级背诵内容。
3.37 setTimeout 模拟 setInterval
javascript
function mySetInterval(delay) {
let timer = setTimeout(() => {
console.log(1);
clearTimeout(timer);
mySetInterval(delay); // 递归调用
}, delay);
}
// 为什么不用setInterval?setInterval不关心回调执行时间,可能堆积
3.38 对象判空
javascript
// 方法1:JSON.stringify(obj) === '{}'
// 方法2:for (let key in obj) return false; return true;
// 方法3:Object.getOwnPropertyNames(obj).length === 0
// 方法4:Object.keys(obj).length === 0
四、算法高频题
标注
✨ 新增的为本次整理时补充的 LeetCode 高频题。
📌 双指针 & 滑动窗口
4.1 两数之和(LC 1)⭐
javascript
function twoSum(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const complement = target - nums[i];
if (map.has(complement)) return [map.get(complement), i];
map.set(nums[i], i);
}
return [];
}
// 时间:O(n) 空间:O(n)
4.2 三数之和(LC 15)⭐⭐
javascript
function threeSum(nums) {
nums.sort((a, b) => a - b);
const res = [];
for (let i = 0; i < nums.length - 2; i++) {
if (nums[i] > 0) break; // 剪枝
if (i > 0 && nums[i] === nums[i - 1]) continue; // 去重
let l = i + 1, r = nums.length - 1;
while (l < r) {
const sum = nums[i] + nums[l] + nums[r];
if (sum === 0) {
res.push([nums[i], nums[l], nums[r]]);
while (l < r && nums[l] === nums[l + 1]) l++;
while (l < r && nums[r] === nums[r - 1]) r--;
l++; r--;
} else if (sum < 0) l++;
else r--;
}
}
return res;
}
// 时间:O(n²) 空间:O(1)
4.3 最长无重复子串(LC 3)⭐⭐
javascript
function lengthOfLongestSubstring(s) {
const map = new Map();
let left = 0, maxLen = 0;
for (let right = 0; right < s.length; right++) {
const char = s[right];
if (map.has(char) && map.get(char) >= left) {
left = map.get(char) + 1;
}
map.set(char, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
// 时间:O(n) 空间:O(min(n, 128))
4.4 接雨水(LC 42)⭐⭐⭐
javascript
function trap(height) {
if (height.length < 3) return 0;
let left = 0, right = height.length - 1;
let leftMax = 0, rightMax = 0, water = 0;
while (left < right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if (leftMax < rightMax) {
water += leftMax - height[left];
left++;
} else {
water += rightMax - height[right];
right--;
}
}
return water;
}
// 时间:O(n) 空间:O(1)
// 延伸:单调栈解法(面试常要求讲思路)
4.5 最长回文子串(LC 5)⭐⭐
javascript
function longestPalindrome(s) {
if (s.length < 2) return s;
let start = 0, maxLen = 1;
function expand(l, r) {
while (l >= 0 && r < s.length && s[l] === s[r]) { l--; r++; }
const len = r - l - 1;
if (len > maxLen) { maxLen = len; start = l + 1; }
}
for (let i = 0; i < s.length; i++) {
expand(i, i); // 奇数长度
expand(i, i + 1); // 偶数长度
}
return s.substring(start, start + maxLen);
}
// 时间:O(n²) 空间:O(1)
4.6 合并区间(LC 56)⭐⭐
javascript
function merge(intervals) {
if (intervals.length <= 1) return intervals;
intervals.sort((a, b) => a[0] - b[0]);
const res = [intervals[0]];
for (let i = 1; i < intervals.length; i++) {
const last = res[res.length - 1];
if (intervals[i][0] <= last[1]) {
last[1] = Math.max(last[1], intervals[i][1]);
} else {
res.push(intervals[i]);
}
}
return res;
}
// 时间:O(n log n) 空间:O(1)
📌 链表
4.7 反转链表(LC 206)⭐⭐
javascript
// 迭代法(推荐)
function reverseList(head) {
let prev = null, curr = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
// 递归法
function reverseList(head) {
if (!head || !head.next) return head;
const newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
// 时间:O(n) 空间:迭代O(1) / 递归O(n)
4.8 环形链表(LC 141)⭐
javascript
function hasCycle(head) {
if (!head || !head.next) return false;
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) return true;
}
return false;
}
// 找环的入口(LC 142)
function detectCycle(head) {
if (!head || !head.next) return null;
let slow = head, fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
slow = head;
while (slow !== fast) { slow = slow.next; fast = fast.next; }
return slow;
}
}
return null;
}
4.9 相交链表(LC 160)⭐
javascript
function getIntersectionNode(headA, headB) {
let pA = headA, pB = headB;
while (pA !== pB) {
pA = pA ? pA.next : headB;
pB = pB ? pB.next : headA;
}
return pA;
}
// 双指针走完自己走对方,最终会在交点相遇或同时为null
📌 二叉树
4.10 二叉树层序遍历(LC 102)⭐⭐
javascript
function levelOrder(root) {
if (!root) return [];
const res = [], queue = [root];
while (queue.length) {
const levelSize = queue.length, level = [];
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
res.push(level);
}
return res;
}
4.11 二叉树的最近公共祖先(LC 236)⭐⭐
javascript
function lowestCommonAncestor(root, p, q) {
if (!root || root === p || root === q) return root;
const left = lowestCommonAncestor(root.left, p, q);
const right = lowestCommonAncestor(root.right, p, q);
if (left && right) return root; // p,q在两侧
return left || right; // 在同一侧
}
// 时间:O(n) 空间:O(h)
4.12 二叉树的右视图(LC 199)⭐⭐
javascript
function rightSideView(root) {
if (!root) return [];
const res = [], queue = [root];
while (queue.length) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
if (i === levelSize - 1) res.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return res;
}
📌 回溯
4.13 全排列(LC 46)⭐⭐
javascript
function permute(nums) {
const res = [];
const used = new Array(nums.length).fill(false);
function backtrack(path) {
if (path.length === nums.length) { res.push([...path]); return; }
for (let i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.push(nums[i]);
backtrack(path);
path.pop();
used[i] = false;
}
}
backtrack([]);
return res;
}
4.14 子集(LC 78)⭐⭐
javascript
function subsets(nums) {
const res = [];
function backtrack(start, path) {
res.push([...path]);
for (let i = start; i < nums.length; i++) {
path.push(nums[i]);
backtrack(i + 1, path);
path.pop();
}
}
backtrack(0, []);
return res;
}
4.15 组合总和(LC 39)⭐⭐
javascript
function combinationSum(candidates, target) {
const res = [];
function backtrack(start, path, remain) {
if (remain < 0) return;
if (remain === 0) { res.push([...path]); return; }
for (let i = start; i < candidates.length; i++) {
path.push(candidates[i]);
backtrack(i, path, remain - candidates[i]); // 可重复,用i
path.pop();
}
}
backtrack(0, [], target);
return res;
}
4.16 括号生成(LC 22)⭐⭐
javascript
function generateParenthesis(n) {
const res = [];
function backtrack(left, right, str) {
if (str.length === n * 2) { res.push(str); return; }
if (left < n) backtrack(left + 1, right, str + '(');
if (right < left) backtrack(left, right + 1, str + ')');
}
backtrack(0, 0, '');
return res;
}
📌 动态规划
4.17 最长递增子序列(LC 300)⭐⭐
javascript
function lengthOfLIS(nums) {
const tails = []; // tails[i] = 长度为i+1的LIS的最小末尾值
for (const num of nums) {
let l = 0, r = tails.length;
while (l < r) {
const mid = (l + r) >> 1;
if (tails[mid] < num) l = mid + 1;
else r = mid;
}
if (l === tails.length) tails.push(num);
else tails[l] = num;
}
return tails.length;
}
// 时间:O(n log n) 空间:O(n)
// 延伸:输出具体序列需要 O(n²) DP
4.18 爬楼梯(LC 70)⭐
javascript
function climbStairs(n) {
if (n <= 2) return n;
let a = 1, b = 2;
for (let i = 3; i <= n; i++) { [a, b] = [b, a + b]; }
return b;
}
// 本质:斐波那契,dp[i] = dp[i-1] + dp[i-2]
4.19 打家劫舍(LC 198)⭐⭐
javascript
function rob(nums) {
if (nums.length === 0) return 0;
if (nums.length === 1) return nums[0];
let prev2 = nums[0], prev1 = Math.max(nums[0], nums[1]);
for (let i = 2; i < nums.length; i++) {
const cur = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1; prev1 = cur;
}
return prev1;
}
// dp[i] = max(dp[i-1], dp[i-2] + nums[i])
4.20 最大子数组和(LC 53)⭐
javascript
function maxSubArray(nums) {
let dp = nums[0], max = nums[0];
for (let i = 1; i < nums.length; i++) {
dp = Math.max(nums[i], dp + nums[i]);
max = Math.max(max, dp);
}
return max;
}
4.21 买卖股票的最佳时机(LC 121)⭐
javascript
function maxProfit(prices) {
let minPrice = Infinity, maxProfit = 0;
for (const price of prices) {
minPrice = Math.min(minPrice, price);
maxProfit = Math.max(maxProfit, price - minPrice);
}
return maxProfit;
}
📌 二分 & 排序
4.22 排序数组中查找第一个和最后一个位置(LC 34)⭐⭐
javascript
function searchRange(nums, target) {
function findBound(isLeft) {
let l = 0, r = nums.length - 1, res = -1;
while (l <= r) {
const mid = (l + r) >> 1;
if (nums[mid] === target) {
res = mid;
if (isLeft) r = mid - 1; // 继续向左
else l = mid + 1; // 继续向右
} else if (nums[mid] < target) l = mid + 1;
else r = mid - 1;
}
return res;
}
return [findBound(true), findBound(false)];
}
// 💡 二分模板:while(l <= r),找到后不立即返回,继续缩范围
4.23 数组中的第K个最大元素(LC 215)⭐⭐
javascript
function findKthLargest(nums, k) {
const target = nums.length - k; // 第k大 = 第(n-k)小
let left = 0, right = nums.length - 1;
while (left <= right) {
const pivotIndex = partition(nums, left, right);
if (pivotIndex === target) return nums[pivotIndex];
else if (pivotIndex < target) left = pivotIndex + 1;
else right = pivotIndex - 1;
}
return -1;
}
function partition(nums, left, right) {
const pivot = nums[right];
let i = left;
for (let j = left; j < right; j++) {
if (nums[j] < pivot) { [nums[i], nums[j]] = [nums[j], nums[i]]; i++; }
}
[nums[i], nums[right]] = [nums[right], nums[i]];
return i;
}
// 时间:O(n)期望 空间:O(1)
4.24 快速排序
javascript
function quickSort(nums, left, right) {
if (left >= right) return;
let i = left, j = right;
while (i < j) {
while (i < j && nums[left] <= nums[j]) j--;
while (i < j && nums[i] <= nums[left]) i++;
[nums[i], nums[j]] = [nums[j], nums[i]];
}
[nums[i], nums[left]] = [nums[left], nums[i]];
quickSort(nums, left, i - 1);
quickSort(nums, i + 1, right);
}
📌 图 & 搜索
4.25 岛屿数量(LC 200)⭐⭐
javascript
function numIslands(grid) {
if (!grid.length) return 0;
const m = grid.length, n = grid[0].length;
let count = 0;
function dfs(i, j) {
if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] === '0') return;
grid[i][j] = '0'; // 沉岛
dfs(i + 1, j); dfs(i - 1, j); dfs(i, j + 1); dfs(i, j - 1);
}
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] === '1') { count++; dfs(i, j); }
}
}
return count;
}
// 时间:O(m×n) 空间:O(m×n)
4.26 二进制矩阵最短路径(BFS)
javascript
// 类似 LC 1091,八方向可改为四方向
function minRoad(nums, n) {
const dir = [[1, 0], [-1, 0], [0, 1], [0, -1]];
if (nums[0][0] === 0 || nums[n - 1][n - 1] === 0) return -1;
if (n === 1) return 0;
let q = [[0, 0]];
nums[0][0] = 0; // 标记已访问
let step = 0;
while (q.length) {
let l = q.length;
while (l--) {
let [x, y] = q.shift();
if (x === n - 1 && y === n - 1) return step;
for (let i = 0; i < dir.length; i++) {
let xi = x + dir[i][0], yi = y + dir[i][1];
if (xi < 0 || xi >= n || yi < 0 || yi >= n || nums[xi][yi] === 0) continue;
q.push([xi, yi]);
nums[xi][yi] = 0;
}
}
step++;
}
return -1;
}
📌 杂项
4.27 LRU 缓存(LC 146)⭐⭐
javascript
class ListNode {
constructor(key, val) {
this.key = key; this.val = val;
this.prev = null; this.next = null;
}
}
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map();
this.dummyHead = new ListNode(-1, -1);
this.dummyTail = new ListNode(-1, -1);
this.dummyHead.next = this.dummyTail;
this.dummyTail.prev = this.dummyHead;
}
_removeNode(node) { node.prev.next = node.next; node.next.prev = node.prev; }
_addToHead(node) {
node.next = this.dummyHead.next; node.prev = this.dummyHead;
this.dummyHead.next.prev = node; this.dummyHead.next = node;
}
_moveToHead(node) { this._removeNode(node); this._addToHead(node); }
_removeTail() { const tail = this.dummyTail.prev; this._removeNode(tail); return tail.key; }
get(key) {
if (!this.map.has(key)) return -1;
const node = this.map.get(key);
this._moveToHead(node);
return node.val;
}
put(key, value) {
if (this.map.has(key)) {
const node = this.map.get(key);
node.val = value;
this._moveToHead(node);
} else {
const node = new ListNode(key, value);
this.map.set(key, node);
this._addToHead(node);
if (this.map.size > this.capacity) {
this.map.delete(this._removeTail());
}
}
}
}
// 时间:O(1) get/put
4.28 有效的括号(LC 20)⭐
javascript
function isValid(s) {
const stack = [];
const map = { ')': '(', ']': '[', '}': '{' };
for (const c of s) {
if (!map[c]) stack.push(c);
else if (stack.pop() !== map[c]) return false;
}
return stack.length === 0;
}
4.29 螺旋矩阵(LC 54)⭐⭐
javascript
function spiralOrder(matrix) {
if (!matrix.length || !matrix[0].length) return [];
const res = [];
let top = 0, bottom = matrix.length - 1;
let left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
for (let i = left; i <= right; i++) res.push(matrix[top][i]);
top++;
for (let i = top; i <= bottom; i++) res.push(matrix[i][right]);
right--;
if (top <= bottom) {
for (let i = right; i >= left; i--) res.push(matrix[bottom][i]); bottom--;
}
if (left <= right) {
for (let i = bottom; i >= top; i--) res.push(matrix[i][left]); left++;
}
}
return res;
}
4.30 汇总区间
javascript
// 输入:[0,1,2,4,5,7] 输出:["0->2","4->5","7"]
function solve(arr) {
const result = [];
const n = arr.length;
let i = 0;
while (i < n) {
let p = i; i++;
while (arr[i] === arr[i - 1] + 1) i++;
let q = i - 1;
result.push(p < q ? `${arr[p]}->${arr[q]}` : `${arr[p]}`);
}
return result;
}
补充:字符串相加(LC 415)⭐
javascript
function addStrings(num1, num2) {
let i = num1.length - 1, j = num2.length - 1, carry = 0, res = '';
while (i >= 0 || j >= 0 || carry) {
const a = i >= 0 ? +num1[i--] : 0;
const b = j >= 0 ? +num2[j--] : 0;
const sum = a + b + carry;
res = (sum % 10) + res;
carry = Math.floor(sum / 10);
}
return res;
}
补充:复原 IP 地址(LC 93)
javascript
function restoreIpAddresses(s) {
if (s.length < 4 || s.length > 12) return [];
const res = [];
function isValid(str) {
if (str.length > 1 && str.startsWith('0')) return false;
const num = parseInt(str);
return num >= 0 && num <= 255;
}
function backtrack(level, max, path, s) {
if (level === max) {
if (isValid(s)) res.push([...path, s].join('.'));
return;
}
for (let i = 1; i < 4; i++) {
const tmp = s.slice(0, i);
const restStr = s.slice(i);
if (isValid(tmp) && restStr.length / (max - level) >= 1) {
path.push(tmp);
backtrack(level + 1, max, path, s.slice(i));
path.pop();
}
}
}
backtrack(0, 3, [], s);
return res;
}
补充:盛最多水的容器 ✨ 新增(LC 11)⭐⭐
javascript
function maxArea(height) {
let left = 0, right = height.length - 1;
let maxWater = 0;
while (left < right) {
const h = Math.min(height[left], height[right]);
maxWater = Math.max(maxWater, h * (right - left));
// 移动较矮的一边(移动高的那一边只会让面积变小)
if (height[left] < height[right]) left++;
else right--;
}
return maxWater;
}
// 时间:O(n) 空间:O(1)
补充:合并两个有序链表 ✨ 新增(LC 21)⭐
javascript
function mergeTwoLists(l1, l2) {
const dummy = new ListNode(-1);
let cur = dummy;
while (l1 && l2) {
if (l1.val < l2.val) { cur.next = l1; l1 = l1.next; }
else { cur.next = l2; l2 = l2.next; }
cur = cur.next;
}
cur.next = l1 || l2;
return dummy.next;
}
补充:零钱兑换 ✨ 新增(LC 322)⭐⭐
javascript
function coinChange(coins, amount) {
// dp[i] = 凑出金额 i 的最少硬币数
const dp = new Array(amount + 1).fill(Infinity);
dp[0] = 0;
for (let i = 1; i <= amount; i++) {
for (const coin of coins) {
if (i >= coin) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] === Infinity ? -1 : dp[amount];
}
// 时间:O(amount × n) 空间:O(amount)
补充:验证二叉搜索树 ✨ 新增(LC 98)⭐⭐
javascript
function isValidBST(root, min = -Infinity, max = Infinity) {
if (!root) return true;
if (root.val <= min || root.val >= max) return false;
return isValidBST(root.left, min, root.val)
&& isValidBST(root.right, root.val, max);
}
// 核心:每个节点有一个合法的值范围 (min, max),递归传递约束
补充:二叉树中序遍历 ✨ 新增(LC 94)--- 迭代法 ⭐⭐
javascript
// 迭代法(不用递归),必掌握
function inorderTraversal(root) {
const res = [], stack = [];
let cur = root;
while (cur || stack.length) {
while (cur) {
stack.push(cur);
cur = cur.left; // 一路向左
}
cur = stack.pop();
res.push(cur.val); // 访问节点
cur = cur.right; // 转向右子树
}
return res;
}
补充:最长公共前缀 ✨ 新增(LC 14)⭐
javascript
function longestCommonPrefix(strs) {
if (!strs.length) return '';
let prefix = strs[0];
for (let i = 1; i < strs.length; i++) {
while (strs[i].indexOf(prefix) !== 0) {
prefix = prefix.slice(0, -1); // 不断缩短前缀
if (!prefix) return '';
}
}
return prefix;
}
补充:每日温度 ✨ 新增(LC 739)--- 单调栈 ⭐⭐
javascript
function dailyTemperatures(temperatures) {
const n = temperatures.length;
const answer = new Array(n).fill(0);
const stack = []; // 单调递减栈,存索引
for (let i = 0; i < n; i++) {
// 当前温度 > 栈顶温度 → 找到栈顶索引的答案
while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) {
const prevIndex = stack.pop();
answer[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return answer;
}
// 输入:[73,74,75,71,69,72,76,73] 输出:[1,1,4,2,1,1,0,0]
// 核心:单调栈用于"找下一个更大的元素"
📌 面试高优先补充题
这一组用于补齐基础模板。优先做到能闭卷写出,不要求继续扩充相似题。
合并两个有序数组(LC 88)⭐
javascript
function mergeSortedArray(nums1, m, nums2, n) {
let i = m - 1, j = n - 1, k = m + n - 1;
while (j >= 0) {
if (i >= 0 && nums1[i] > nums2[j]) nums1[k--] = nums1[i--];
else nums1[k--] = nums2[j--];
}
}
// 从后向前写,避免覆盖 nums1 中尚未处理的元素
反转链表 II(LC 92)⭐⭐
javascript
function reverseBetween(head, left, right) {
const dummy = { next: head };
let prev = dummy;
for (let i = 1; i < left; i++) prev = prev.next;
const current = prev.next;
for (let i = 0; i < right - left; i++) {
const moved = current.next;
current.next = moved.next;
moved.next = prev.next;
prev.next = moved;
}
return dummy.next;
}
// 头插法:每次把区间内下一个节点插到区间头部
重排链表(LC 143)⭐⭐
javascript
function reorderList(head) {
if (!head?.next) return;
let slow = head, fast = head;
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
}
let second = reverseList(slow.next);
slow.next = null;
let first = head;
while (second) {
const next1 = first.next, next2 = second.next;
first.next = second;
second.next = next1;
first = next1;
second = next2;
}
}
// 三步:找中点、反转后半段、交替合并
二叉树最大深度(LC 104)⭐
javascript
function maxDepth(root) {
if (!root) return 0;
return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
}
对称二叉树(LC 101)⭐
javascript
function isSymmetric(root) {
function mirror(left, right) {
if (!left || !right) return left === right;
return left.val === right.val
&& mirror(left.left, right.right)
&& mirror(left.right, right.left);
}
return mirror(root?.left, root?.right);
}
从前序与中序构造二叉树(LC 105)⭐⭐
javascript
function buildTree(preorder, inorder) {
const indexMap = new Map(inorder.map((value, index) => [value, index]));
let preorderIndex = 0;
function build(left, right) {
if (left > right) return null;
const value = preorder[preorderIndex++];
const root = new TreeNode(value);
const middle = indexMap.get(value);
root.left = build(left, middle - 1);
root.right = build(middle + 1, right);
return root;
}
return build(0, inorder.length - 1);
}
用栈实现队列(LC 232)⭐
javascript
class MyQueue {
constructor() {
this.input = [];
this.output = [];
}
push(value) {
this.input.push(value);
}
_move() {
if (!this.output.length) {
while (this.input.length) this.output.push(this.input.pop());
}
}
pop() {
this._move();
return this.output.pop();
}
peek() {
this._move();
return this.output[this.output.length - 1];
}
empty() {
return !this.input.length && !this.output.length;
}
}
// 每个元素最多进出两个栈各一次,均摊 O(1)
标准二分查找(LC 704)⭐
javascript
function search(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const middle = left + ((right - left) >> 1);
if (nums[middle] === target) return middle;
if (nums[middle] < target) left = middle + 1;
else right = middle - 1;
}
return -1;
}
搜索旋转排序数组(LC 33)⭐⭐
javascript
function searchRotated(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const middle = left + ((right - left) >> 1);
if (nums[middle] === target) return middle;
if (nums[left] <= nums[middle]) {
if (nums[left] <= target && target < nums[middle]) right = middle - 1;
else left = middle + 1;
} else {
if (nums[middle] < target && target <= nums[right]) left = middle + 1;
else right = middle - 1;
}
}
return -1;
}
// 每轮至少有一半区间有序,再判断 target 是否落在该区间
搜索二维矩阵(LC 74)⭐
javascript
function searchMatrix(matrix, target) {
if (!matrix.length || !matrix[0].length) return false;
const rows = matrix.length, cols = matrix[0].length;
let left = 0, right = rows * cols - 1;
while (left <= right) {
const middle = left + ((right - left) >> 1);
const value = matrix[Math.floor(middle / cols)][middle % cols];
if (value === target) return true;
if (value < target) left = middle + 1;
else right = middle - 1;
}
return false;
}
前 K 个高频元素(LC 347)⭐⭐
javascript
function topKFrequent(nums, k) {
const count = new Map();
nums.forEach(num => count.set(num, (count.get(num) || 0) + 1));
const buckets = Array.from({ length: nums.length + 1 }, () => []);
count.forEach((frequency, num) => buckets[frequency].push(num));
const result = [];
for (let frequency = buckets.length - 1; frequency >= 0; frequency--) {
for (const num of buckets[frequency]) {
result.push(num);
if (result.length === k) return result;
}
}
}
// 桶排序 O(n);追问时再补容量为 k 的最小堆方案
课程表(LC 207)⭐⭐
javascript
function canFinish(numCourses, prerequisites) {
const graph = Array.from({ length: numCourses }, () => []);
const indegree = new Array(numCourses).fill(0);
for (const [course, prerequisite] of prerequisites) {
graph[prerequisite].push(course);
indegree[course]++;
}
const queue = [];
for (let i = 0; i < numCourses; i++) {
if (indegree[i] === 0) queue.push(i);
}
let head = 0, learned = 0;
while (head < queue.length) {
const course = queue[head++];
learned++;
for (const next of graph[course]) {
if (--indegree[next] === 0) queue.push(next);
}
}
return learned === numCourses;
}
// 拓扑排序:最终无法处理完全部节点,说明图中存在环
第二优先级 DP
时间充足后再补以下三道,不挤占前面模板的复现时间:
- LC 64 最小路径和:二维 DP
- LC 139 单词拆分:完全背包/字符串 DP
- LC 1143 最长公共子序列:二维序列 DP
五、框架原理
5.1 Vue 2 响应式(Object.defineProperty)
javascript
var obj = {};
var val;
Object.defineProperty(obj, 'name', {
set: function (newVal) {
val = newVal;
input.value = newVal;
p.innerHTML = newVal;
},
get: function () { return val; }
});
input.addEventListener('input', function (e) {
obj.name = e.target.value; // 触发set → 更新DOM
});
// 局限:无法检测属性的添加/删除、数组索引变化
5.2 Vue 3 响应式(Proxy)
javascript
let obj = { text: 'vue3响应式' };
const list = new Set();
const newObj = new Proxy(obj, {
get(target, key) {
list.add(effect); // 依赖收集
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
list.forEach(fn => fn()); // 派发更新
return true;
}
});
function effect() { document.getElementById('test').innerText = newObj.text; }
effect();
newObj.text = 'vue3变了'; // 自动触发DOM更新
// 优势:可以检测属性的添加、删除、数组变化
5.3 Vue vs React Diff 算法
| 对比项 | Vue | React |
|---|---|---|
| 核心约束 | 同层比较,利用 key 识别节点 | 同层比较,利用 key 与元素类型复用 Fiber |
| Vue 2 | 双端指针比较 | --- |
| Vue 3 | 最长递增子序列减少 DOM 移动 | --- |
| React | --- | Reconciliation 标记插入、移动、删除等副作用 |
| 复杂度 | 常见列表场景约 O(n) | 常见列表场景约 O(n) |
不要简单表述为"Vue 就地复用、React 全部重建"。两者都会在满足条件时复用节点,
具体行为取决于节点类型、key、编译优化和协调过程。
5.4 浏览器渲染流程
- JS 加载 :会阻塞 DOM 解析 (
<script>标签默认同步执行) - CSS 加载 :不会阻塞 DOM 树解析,但会阻塞渲染(render tree) 和 JS 执行
- 关键渲染路径:DOM → CSSOM → Render Tree → Layout → Paint
扩展常考知识点:
defer:异步下载,DOMContentLoaded 前按顺序执行async:异步下载,下载完立即执行(乱序)- 回流(Reflow):改变尺寸/位置 → 重新 Layout
- 重绘(Repaint):改变颜色/背景 → 直接 Paint
六、公司真题精选
6.1 字节跳动/百度/拼多多------事件循环综合
javascript
// 字节
for (var i = 0; i < 5; i++) {
setTimeout(async () => { await console.log(i++); }, 1000);
} // 5 6 7 8 9(var没有块级作用域,1秒后i=5)
// B站
Promise.reject(0).catch(e => e).catch(e => console.log(e));
// Promise {<fulfilled>: 0} --- 第一个catch返回了0,第二个catch不触发
// 拼多多
Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => console.log('timer2'), 0);
});
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => console.log('promise2'));
}, 0);
console.log('start');
// start → promise1 → timer1 → promise2 → timer2
6.2 字节------箭头函数 this
javascript
const obj = { a: 1, func: () => { console.log(this.a); } };
obj.func(); // undefined(箭头函数this指向定义时的window)
// 综合 this 题
const objA = { value: 'a', fn: fn };
const objB = { value: 'b', fn: callArrowFn };
const objC = { value: 'c', fn: getArrowFn() }; // 箭头函数在全局定义,this=window
fn(); // undefined(全局调用)
objA.fn(); // 'a'
objB.fn(); // 'b'(callArrowFn普通函数,this=objB,内部箭头函数继承这个this)
objC.fn(); // undefined(getArrowFn在全局调用,返回的箭头函数this=window)
6.3 美团------变量提升 + 传参
javascript
// var提升
var a = 1;
function f() {
alert(a); // undefined(局部var a提升)
a = 2; alert(a); // 2
var a = 3; alert(a); // 3
}
f(); alert(a); // 1(全局a未变)
// 对象传参
function changeObj(o) {
o.url = 'meituan'; // 修改了传入对象的属性
o = new Object(); // o重新指向新对象,不影响外部
o.url = 'baidu';
}
let test = new Object();
changeObj(test);
console.log(test.url); // 'meituan',不是'baidu'
6.4 其他公司真题速记
| 题 | 来源 | 关键点 |
|---|---|---|
typeof Person |
百度 | typeof class === 'function' |
{}解构 |
百度 | Object.create({x:1}) --- x在原型上,...rest 只取自有属性 |
| 构造函数 return | 百度 | 构造函数返回非对象时忽略,返回this |
| 冒泡排序 | interview31 | 双重循环,每轮把最大元素移到最后 |
| 约瑟夫环变体 | pp.js | 循环删除,模4取余判断 |
| 水果最小成本 | 美团DP | dp[i] = min(dp[i], dp[j-1] + cost),区间DP |
七、计算机网络与浏览器 ✨ 新增
7.1 HTTP 缓存策略
| 类型 | 头字段 | 行为 |
|---|---|---|
| 强缓存 | Cache-Control: max-age=3600 |
期限内直接用缓存,不发请求 |
Expires(HTTP 1.0) |
绝对时间,有时钟偏差问题 | |
| 协商缓存 | ETag / If-None-Match |
服务端对比资源hash,304/200 |
Last-Modified / If-Modified-Since |
对比修改时间,精度秒级 |
浏览器缓存流程:
请求 → 有强缓存且未过期?→ 直接用缓存(200 from disk/memory)
→ 无强缓存或已过期 → 发请求带 If-None-Match / If-Modified-Since
→ 服务端返回 304 → 更新缓存时间,用本地缓存
→ 服务端返回 200 → 返回新资源,更新缓存
7.2 跨域(CORS)
http
// 简单请求:浏览器自动加 Origin 头
Origin: https://example.com
// 服务端返回:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true // 允许携带 Cookie
// 非简单请求(如 content-type: application/json)先发预检请求:
OPTIONS /api HTTP/1.1
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
// 服务端返回:
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400 // 预检缓存时间
其他跨域方案:JSONP(只支持GET)、代理服务器(Nginx反向代理、webpack devServer proxy)
7.3 XSS 与 CSRF
| 攻击 | 原理 | 防御 |
|---|---|---|
| XSS(跨站脚本) | 注入恶意脚本到页面 | 输入过滤/转义、CSP、HttpOnly Cookie |
| CSRF(跨站请求伪造) | 利用已登录态发伪造请求 | CSRF Token、SameSite Cookie、Referer校验 |
javascript
// XSS 防御:转义 HTML
function escapeHtml(str) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
return str.replace(/[&<>"']/g, char => map[char]);
}
7.4 浏览器存储
| 特性 | Cookie | localStorage | sessionStorage |
|---|---|---|---|
| 容量 | ~4KB | ~5MB | ~5MB |
| 有效期 | 可设过期时间 | 永久(手动删除) | 标签页关闭即清除 |
| 发送到服务端 | 每次请求自动带 | 不自动发送 | 不自动发送 |
| 作用域 | 同源+路径 | 同源 | 同源+同标签页 |
八、TypeScript 高频 ✨ 新增
8.1 常用工具类型
typescript
// 面试中常要求口述或手写这些类型的实现
// Partial<T> --- 所有属性变为可选
type MyPartial<T> = { [K in keyof T]?: T[K] };
// Required<T> --- 所有属性变为必填
type MyRequired<T> = { [K in keyof T]-?: T[K] };
// Readonly<T> --- 所有属性变为只读
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
// Pick<T, K> --- 从 T 中选出 K 属性
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
// Omit<T, K> --- 从 T 中排除 K 属性
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// Record<K, V> --- 构造 K→V 的对象类型
type MyRecord<K extends keyof any, V> = { [P in K]: V };
// Exclude<T, U> --- 从联合类型中排除
type MyExclude<T, U> = T extends U ? never : T;
// Extract<T, U> --- 从联合类型中提取
type MyExtract<T, U> = T extends U ? T : never;
// ReturnType<T> --- 获取函数返回值类型
type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : never;
8.2 泛型约束
typescript
// 基础泛型
function identity<T>(arg: T): T { return arg; }
// 泛型约束 --- 要求有 length 属性
function logLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
// 条件类型 --- 根据条件返回不同类型
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'
// keyof --- 获取对象所有 key 的联合类型
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
九、复习路线建议
9.1 使用原则
这份长文档是答案索引 ,不是每天从头阅读的教材。训练时先关掉答案,
在无代码补全的编辑器中完成;失败后再回到对应章节订正。
一道题达到"掌握"必须同时满足:
- 能先讲清输入、输出、边界和核心思路。
- 能在 25 分钟内独立写完,不依赖答案或代码补全。
- 能手动跑通至少 3 个测试,包括空值、单元素或重复元素。
- 能说出时间、空间复杂度以及一个替代方案。
- 在第 1、3、7 天再次随机抽中时仍能完成。
9.2 第一梯队:必须稳定写出
前端手写 15 项:
text
Promise.all / allSettled / race
并发请求控制 / Scheduler / 异步串行与重试
防抖 / 节流 / 深拷贝
EventEmitter / LRU
数组转树 / call-apply-bind / new
算法核心题型:
text
双指针与滑窗:两数之和、三数之和、最长无重复子串、接雨水
链表:反转链表、反转链表II、环入口、相交链表、重排链表
二叉树:层序、最大深度、验证BST、最近公共祖先、前中序构造
回溯与图:全排列、子集、括号生成、岛屿数量、课程表
动态规划:最长回文子串、最长递增子序列、最大子数组、零钱兑换
二分与排序:标准二分、左右边界、旋转数组、Top K、快速排序
其他:合并区间、LRU、螺旋矩阵、字符串相加、有效括号
第二梯队包括完整 Promise/A+、LazyMan、复杂类型体操和低频 CSS 手写。
第一梯队未稳定前,不增加第二梯队训练时间。
9.3 每日训练结构
每天算法时间约 3 小时,建议固定为:
text
20分钟:随机抽一道旧题,模拟开场并口述
40分钟:一道新题,补覆盖面
60分钟:复现三道旧题(第1、3、7天间隔)
30分钟:随机一道中等题,25分钟计时编码
30分钟:随机两道前端手写题
20分钟:记录失败原因,修正后再次闭卷实现
新题最多一道。当天没有完成复现时,不通过增加新题制造"进度感"。
9.4 现场编码流程
拿到题目后不要立即写代码,先在纸上或注释中写四行:
text
1. 状态/变量分别代表什么
2. 循环或递归过程始终保持什么条件
3. 空输入、单元素、重复值如何处理
4. 3至6行伪代码
编码时持续口述当前步骤。卡住超过 60 秒时,回到具体样例手动推演,
不要在脑中同时维护全部变量。
9.5 错题记录模板
markdown
| 日期 | 题目 | 首次结果 | 失败类型 | 修正动作 | D+1 | D+3 | D+7 |
|------|------|----------|----------|----------|-----|-----|-----|
| 6/14 | LC 5 | 超时 | 边界混乱 | 先写 expand 定义 | ✅ | ⬜ | ⬜ |
失败类型只使用以下标签,便于一周后统计:
无思路:没有识别题型或状态定义。代码组织:思路正确,但变量和流程无法落地。边界:空值、越界、重复值或终止条件错误。API/语法:语言细节记忆错误。压力:模拟时明显慌乱或无法持续表达。
9.6 每周验收
每周至少进行两次 45 分钟模拟面试:
- 前 5 分钟澄清问题并讲方案。
- 25 分钟无补全编码。
- 10 分钟测试、复杂度分析和追问。
- 5 分钟复盘表达与卡顿点。
通过标准不是"这道题刷过",而是从第一梯队随机抽取 10 道,
至少 8 道能一次写对;前端手写随机抽取 5 道,至少 4 道通过。
9.7 概念题复习
- 变量提升、闭包、this、事件循环:必须能逐行解释输出。
- Promise、浏览器渲染、缓存、跨域:必须能从机制讲到实际排障。
- Vue/React Diff:讲目标、约束、key 和各自策略,不背错误对比表。
- 公司真题:只记录真实遇到的新题和失败原因,不继续无边界收集。
本文会持续根据真实面试反馈更新。建议将长文作为答案索引,
训练时先闭卷完成,再回来订正。