不包含代码阅读题,常见的代码阅读题在系列的面经里面有。 简单高频,面试官放过你系列的手撕题
前端手撕
1.防抖
-
定义:每次触发都会重新计时,隔n秒后执行 (进化:react的防抖hook、搜索框带防抖)
//普通函数 function debounce(fn,delay){ let timer = null; return function(...args){ clearTimeout(timer); timer = setTimeout(()=>{ fn.apply(this,args); },delay); } } //React hook version const useDebounce(fn,delay){ const timer = useRef(null); const fnRef = useRef(fn); useEffect(()=>{ fnRef.current = fn; },[fn]); // 为什么需要[fn] const debounce = useMemo(()=>{ return function(..args){ clearTimeout(timer.current); timer.current = setTimeout(()=>{ fnRef.current(...args); },delay); }; },[delay]); useEffect(()=>{ return ()=>clearTimeout(timer.current); },[]); return debounce; }
2.数组flat
(不使用flat)
function flatten(arr){
let res = [];
for(let item of arr){
if(Array.isArray(item)){
const res1 = flatten(item);
res.push(...res1);
}else{
res.push(item);
}
}
return res;
}
function flatten(arr){
return arr.reduce((acc,cur)=>{
if(Array.isArray(cur)){
acc.push(...flatten(cur))
}else{
acc.push(cur)
}
return acc;
},[]);
}
3.Promise系列
手写PromiseAll
function myPromiseAll(Promises){
return new Promise((resolve,reject) =>{
const res =[];
let count = 0;
if(Promises.length===0){
resolve([]);
return; //结束
}
Promises.forEach((p,i) =>{ //把每个promise的then/catch注册好(同步),但是还没有执行完成,所以count在forEach执行完成后仍然是0)
Promise.resolve(p).then((value)=>{
count+=1;
res[i] = value;
if(count === Promises.length){
resolve(res);
}
}).catch((err)=>{
reject(err);
})
}
}
}
并发池 (限制并发请求的数量,同时最多3个请求);
function pool(tasks,limit){
let curFinished = 0;
let curRuns = 0;
let idx = 0;
const res = [];
return new Promise((resolve,reject)=>{
if(tasks.length === 0 ){
resolve(res);
return;
}
const runs = ()=>{
while(curRuns <limit && idx<tasks.length){
const cur = idx ++;
curRuns ++;
Promise.resolve(tasks[cur]())
.then(v=>{res[cur] = v;})
.catch(reject)
.finally(()=>{
curRuns--;
curFinished ++;
if(curFinished === tasks.length){
resolve(res);
} else runs();
}
}
runs();
}
}
固定数量的图片上传
function chunk(arr,batchSize){
const out = [];
for(let i = 0;i<arr.length;i+=batchSize){
out.push(arr.slice(i,i+batchSize);
}
return out;
}
实现Scheduler
class Scheduler {
constructor(limit = 2) {
this.limit = limit;
this.running = 0;
this.queue = [];
}
add(taskFn) {
// taskFn: () => Promise<any>
return new Promise((resolve, reject) => {
const run = () => {
this.running++;
Promise.resolve()
.then(taskFn)
.then(resolve, reject)
.finally(() => {
this.running--;
this._next();
});
};
this.queue.push(run);
this._next();
});
}
_next() {
while (this.running < this.limit && this.queue.length > 0) {
const job = this.queue.shift();
job();
}
}
}
4. CSS手写元素居中
水平垂直居中
//flex
.parent{
display:flex;
justify-content:center;
align-items:center;
.child{
position:absolute;
top:50%;
left:50%;
transform:translate(-50%,50%);
}
//绝对定位 + transform
.parent { position: relative; }
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
//
.parent {
display: grid;
place-items: center; /* 等价于 align-items + justify-items */
}
5. CSS手写两栏布局
//flex 常见 等高列
.container { display: flex; }
.left { width: 200px; }
.right { flex: 1; min-width: 0; }
//float + margin 兼容性好,需要清除浮动
.left {
float: left;
width: 200px;
}
.right {
margin-left: 200px;
}
.container::after {
content: "";
display: block;
clear: both;
}
//grid
.container {
display: grid;
grid-template-columns: 200px 1fr;
}
6. CSS三列布局
//flex
.wrap {
display: flex;
}
.left { width: 200px; }
.right { width: 240px; }
.mid { flex: 1; min-width: 0; }
//grid
.wrap {
display: grid;
grid-template-columns: 200px 1fr 240px;
}
.left { grid-column: 1; }
.mid { grid-column: 2; }
.right{ grid-column: 3; }
//圣杯 Float+负margin
<div class="wrap holy">
<div class="mid">Middle auto</div>
<div class="left">Left 200</div>
<div class="right">Right 240</div>
</div>
.holy{
padding: 0 240px 0 200px; /* 给左右留出位置 */
}
.holy .mid{
float:left;
width:100%;
}
.holy .left{
float:left;
width:200px;
margin-left:-100%;
position:relative;
left:-200px;
}
.holy .right{
float:left;
width:240px;
margin-left:-240px;
position:relative;
right:-240px;
}
.holy::after{
content:"";
display:block;
clear:both;
}
7.loadsh.get:如何读取深层嵌套的对象的属性
function get(obj, path, defaultValue) {
if (obj == null) return defaultValue;
const keys = Array.isArray(path) ? path : parsePath(path);
let cur = obj;
for (const key of keys) {
if (cur == null) return defaultValue;
cur = cur[key];
}
return cur === undefined ? defaultValue : cur;
}
function parsePath(path) {
if (typeof path !== "string" || path.trim() === "") return [];
// 把 a[0] / a["b"] / a['b'] 转成 a.0 / a.b / a.b
const normalized = path
.replace(/\[(\d+)\]/g, ".$1")
.replace(/\["([^"]+)"\]/g, ".$1")
.replace(/\['([^']+)'\]/g, ".$1")
.replace(/^\./, "");
return normalized.split(".").filter(Boolean);
}
// quick test
// const o = { a: [{ b: { c: 1 } }], x: { "x-y": 2 } };
// console.log(get(o, "a[0].b.c", 0)); // 1
// console.log(get(o, 'x["x-y"]', 0)); // 2
// console.log(get(o, "a[1].b.c", "NA")); // "NA"
8.观察者模式
on(event,fn) 注册
once(event, fn) 只执行一
off(evemt,fn)
trigger(event,...args) 触发
class Emitter {
constructor() {
this._events = new Map(); // event -> Set<listener>
}
on(event, fn) {
if (!this._events.has(event)) this._events.set(event, new Set());
this._events.get(event).add(fn);
// 返回取消订阅函数(实用加分)
return () => this.off(event, fn);
}
once(event, fn) {
const wrapper = (...args) => {
this.off(event, wrapper);
fn(...args);
};
wrapper._origin = fn; // 便于 off 传原 fn 也能移除 once 包装
return this.on(event, wrapper);
}
off(event, fn) {
// off() 清空全部
if (event === undefined) {
this._events.clear();
return;
}
const set = this._events.get(event);
if (!set) return;
// off(event) 清空该事件
if (fn === undefined) {
set.clear();
this._events.delete(event);
return;
}
// off(event, fn) 移除指定回调(含 once 包装)
for (const listener of set) {
if (listener === fn || listener._origin === fn) {
set.delete(listener);
}
}
if (set.size === 0) this._events.delete(event);
}
trigger(event, ...args) {
const set = this._events.get(event);
if (!set) return;
// 拷贝一份,避免回调里 off/once 修改集合影响遍历
[...set].forEach((fn) => fn(...args));
}
}
// quick test
// const bus = new Emitter();
// const off = bus.on("a", (x) => console.log("on", x));
// bus.once("a", (x) => console.log("once", x));
// bus.trigger("a", 1); // on 1, once 1
// bus.trigger("a", 2); // on 2
// off();
// bus.trigger("a", 3); // no output
算法手撕
1.快排
function quickSort(arr){
if(arr.length <=1) return arr;
const p = arr[0];
const left = [];
const right = [];
for(let i =0;i<arr.length;i++){
if(arr[i]<p) left.push(arr[i]);
else right.push(arr[i]);
}
return quickSort(left).concat(p,quickSort(right));
冒泡、插入、归并都是稳定的; 快排平均nlogn;最坏是n^2;
2.括号匹配
function isValid(s){
const stack = [];
const map = {
')','(',
']','[',
')','('
}
for(let ch of s){
if(map[ch]){
if(stack.pop()!==map[ch]) return false;
}else{
stack.push(ch);
}
}
return stack.length === 0;
}
3.链表反转
function reverse(head){
let prev = null;
let cur = head;
while(cur){
const next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
return prev;
}
4.翻转数组
function reverse(arr) {
let l = 0, r = arr.length - 1
while (l < r) {
[arr[l], arr[r]] = [arr[r], arr[l]]
l++
r--
}
return arr
}
5.数组转树
input:
const list = [
{ id: 1, pid: 0, name: "A" },
{ id: 2, pid: 1, name: "B" },
{ id: 3, pid: 1, name: "C" },
{ id: 4, pid: 2, name: "D" },
];
Code:
function arrayToTree(list, { idKey="id", pidKey="pid", childrenKey="children", rootPid=0 } = {}) {
const map = new Map();
const roots = [];
// 1) 先把所有节点放进 map,并初始化 children
for (const item of list) {
const node = { ...item, [childrenKey]: [] };
map.set(node[idKey], node);
}
// 2) 再做父子挂载
for (const item of list) {
const id = item[idKey];
const pid = item[pidKey];
const node = map.get(id);
if (pid === rootPid || pid == null) {
roots.push(node);
} else {
const parent = map.get(pid);
if (parent) parent[childrenKey].push(node);
else roots.push(node); // 父节点缺失:兜底当根
}
}
return roots;
}
6.树转数组
DFS先序:
function treeToArray(roots, { idKey="id", pidKey="pid", childrenKey="children" } = {}) {
const res = [];
function dfs(node, parentId) {
const { [childrenKey]: children, ...rest } = node;
// 还原/补充 pid
res.push({ ...rest, [pidKey]: parentId });
if (Array.isArray(children)) {
for (const child of children) {
dfs(child, node[idKey]);
}
}
}
for (const root of roots) dfs(root, 0);
return res;
}
BFS层序
function treeToArrayBFS(roots, { idKey="id", pidKey="pid", childrenKey="children" } = {}) {
const res = [];
const queue = roots.map(r => ({ node: r, parentId: 0 }));
while (queue.length) {
const { node, parentId } = queue.shift();
const { [childrenKey]: children, ...rest } = node;
res.push({ ...rest, [pidKey]: parentId });
if (Array.isArray(children)) {
for (const child of children) {
queue.push({ node: child, parentId: node[idKey] });
}
}
}
return res;
}
7.链表有环
function hasCycle(head){
let slow = head;
let fast = head;
while(fast && fast.next){
slow = slow.next;
fast = fast.next.next;
if(slow === fast){
let p = head;
while(p!==slow){
p = p.next;
slow = slow.next;
}
return p;
}
}
return false;
}
8.和为k的子数组个数
function sub(nums,k){
const cnt =new Map();
cnt.set(0,1);
let pre = 0;
let ans = 0;
for(const x of num){
pre+=x;
ans+=(cnt.get(pre-k) || 0);
cnt.set(pre,(cnt.get(pre) || 0) +1);
}
return ans;
}
9.695-最大岛屿
BFS
function maxAreaOfIsland(grid) {
const m = grid.length;
if (m === 0) return 0;
const n = grid[0].length;
let ans = 0;
const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (grid[i][j] !== 1) continue;
let area = 0;
const q = [[i, j]];
let head = 0;
grid[i][j] = 0;
while (head < q.length) {
const [r, c] = q[head++];
area++;
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] === 1) {
grid[nr][nc] = 0;
q.push([nr, nc]);
}
}
}
ans = Math.max(ans, area);
}
}
return ans;
}
分割等和子集的最大数量(最大k),分割成k个不相交的子集和,每个子集和相等,求最大的k
function maxEqualSumSubsets(nums) {
const n = nums.length;
const sum = nums.reduce((a, b) => a + b, 0);
// 排序:大数先放,剪枝更强
nums = [...nums].sort((a, b) => b - a);
// 尝试从最大 k 开始
for (let k = n; k >= 1; k--) {
if (sum % k !== 0) continue;
const target = sum / k;
if (nums[0] > target) continue;
if (canPartitionK(nums, k, target)) return k;
}
return 1;
function canPartitionK(nums, k, target) {
const buckets = new Array(k).fill(0);
function dfs(idx) {
if (idx === nums.length) return true;
const num = nums[idx];
const seen = new Set(); // 剪枝:同层相同 bucket 和不重复尝试
for (let i = 0; i < k; i++) {
if (buckets[i] + num > target) continue;
if (seen.has(buckets[i])) continue;
seen.add(buckets[i]);
buckets[i] += num;
if (dfs(idx + 1)) return true;
buckets[i] -= num;
// 剪枝:如果当前 num 放进空桶都失败,后面空桶也一样失败
if (buckets[i] === 0) break;
}
return false;
}
return dfs(0);
}
}
// quick test
// console.log(maxEqualSumSubsets([1,1,1,1])); // 4
// console.log(maxEqualSumSubsets([4,3,2,3,5,2,1])); // 4 (经典例子:每桶和=5)