
P1:使用 Promise 实现红绿灯交替重复
function trafficLight(times: number, order: string[] = ['red', 'yellow', 'green']) {
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
const lights: Record<string, number> = { red: 3000, yellow: 1000, green: 2000 };
return new Promise<void>(async resolve => {
for (let i = 0; i < times; i++) {
for (const color of order) {
console.log(color);
await delay(lights[color]);
}
}
resolve();
});
}
// 递归写法(更直观)
function loop(times: number) {
const tick = (count: number): Promise<void> => {
if (count >= times) return Promise.resolve();
return new Promise(res => setTimeout(res, 1000))
.then(() => { console.log('🔴'); return new Promise(r => setTimeout(r, 1000)); })
.then(() => { console.log('🟡'); return new Promise(r => setTimeout(r, 1000)); })
.then(() => { console.log('🟢'); })
.then(() => tick(count + 1));
};
return tick(0);
}关键点:setTimeout 包成 Promise,用 async/await 或 Promise 链按顺序触发。
P2:bind、call、apply 有什么区别?如何手写 bind?
区别:
| 方法 | 参数形式 | 调用时机 | 返回值 |
|---|---|---|---|
call(obj, arg1, arg2, ...) | 逐个参数 | 立即调用 | 函数返回值 |
apply(obj, [args]) | 数组 | 立即调用 | 函数返回值 |
bind(obj, arg1, ...) | 逐个参数 | 返回新函数 | 绑定 this 的新函数 |
手写 call:
Function.prototype.myCall = function(thisArg: any, ...args: any[]) {
const fn = Symbol('fn'); // 唯一 key 避免覆盖
thisArg = thisArg ?? globalThis;
thisArg[fn] = this;
const result = thisArg[fn](...args);
delete thisArg[fn];
return result;
};手写 apply:
Function.prototype.myApply = function(thisArg: any, args: any[] = []) {
const fn = Symbol('fn');
thisArg = thisArg ?? globalThis;
thisArg[fn] = this;
const result = thisArg[fn](...args);
delete thisArg[fn];
return result;
};手写 bind(考点,需考虑参数合并 + new 调用):
Function.prototype.myBind = function(thisArg: any, ...presetArgs: any[]) {
const self = this;
return function Fn(...laterArgs: any[]) {
// 用 new 调用时 this 应指向新对象
const isNew = this instanceof Fn;
return self.apply(isNew ? this : thisArg, [...presetArgs, ...laterArgs]);
};
};P3:字符串压缩(利用字符重复次数)
function compress(str: string): string {
if (!str) return '';
let result = '';
let count = 1;
for (let i = 0; i < str.length; i++) {
if (str[i] === str[i + 1]) {
count++;
} else {
result += str[i] + count;
count = 1;
}
}
return result.length < str.length ? result : str;
}示例:'aabcccccaaa' → 'a2b1c5a3'
P4:new 操作符具体干了什么?
四步执行:
- 创建空对象
obj = {}; - 设置原型:
obj.__proto__ = Constructor.prototype; - 执行构造函数(
this指向obj),为obj添加属性; 判断返回值:
- 显式返回对象 → 返回该对象;
- 其他情况 → 返回
obj。
手写实现:
function myNew(Constructor: Function, ...args: any[]) {
const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}P5:如何实现上拉加载、下拉刷新?
核心原理:
// 上拉加载:监听 scroll,判断距离底部
function onScroll() {
const { scrollTop, clientHeight, scrollHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 50) {
loadMore(); // 触发加载
}
}
// 下拉刷新:touch 事件计算下拉距离
let startY = 0, pulling = false;
el.addEventListener('touchstart', e => {
if (window.scrollY === 0) {
startY = e.touches[0].clientY;
pulling = true;
}
});
el.addEventListener('touchmove', e => {
if (!pulling) return;
const dy = e.touches[0].clientY - startY;
if (dy > 0) {
e.preventDefault(); // 阻止默认滚动
el.style.transform = `translateY(${dy}px)`;
}
});
el.addEventListener('touchend', e => {
if (pulling) {
pulling = false;
el.style.transform = '';
// 触发刷新
refresh().finally(() => {});
}
});推荐方案:直接用第三方库(如 better-scroll、vue-virtual-scroller、React 的 react-pull-to-refresh)。
P6:大文件怎么实现断点续传?
核心流程:
- 文件分片:使用
Blob.slice()将文件切成固定大小(5MB)的 chunks; - 计算唯一标识:
spark-md5计算文件 hash(避免全量上传); - 秒传校验:上传前查询服务端是否已有该 hash 的文件,有则秒传;
- 分片上传:每个 chunk 携带
index、hash、文件hash; - 断点续传:上传前查询服务端已有分片,跳过已上传部分;
- 合并请求:所有分片上传完成后通知服务端合并。
关键代码骨架:
async function upload(file: File) {
const CHUNK_SIZE = 5 * 1024 * 1024;
const chunks = [];
for (let i = 0; i < file.size; i += CHUNK_SIZE) {
chunks.push(file.slice(i, i + CHUNK_SIZE));
}
const fileHash = await calcHash(chunks);
const { uploaded = [] } = await api.checkFile(fileHash);
const tasks = chunks
.map((chunk, idx) => ({ idx, chunk }))
.filter(({ idx }) => !uploaded.includes(idx))
.map(({ idx, chunk }) =>
api.uploadChunk(chunk, fileHash, idx).retry(3)
);
// 限制并发 6
await pLimit(6, tasks);
await api.mergeChunks(fileHash);
}P7:防抖与节流的编码实现
防抖(debounce):触发后等待 wait ms,若再次触发则重置。
function debounce<T extends (...args: any[]) => any>(fn: T, wait: number) {
let timer: any = null;
return function (this: any, ...args: Parameters<T>) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), wait);
};
}节流(throttle):固定时间内只执行一次(首触发或尾触发)。
// 时间戳版(首触发立即执行)
function throttle<T extends (...args: any[]) => any>(fn: T, wait: number) {
let last = 0;
return function (this: any, ...args: Parameters<T>) {
const now = Date.now();
if (now - last >= wait) {
last = now;
fn.apply(this, args);
}
};
}
// 定时器版(尾触发)
function throttle2(fn: Function, wait: number) {
let timer: any = null;
return function (this: any, ...args: any[]) {
if (!timer) {
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, wait);
}
};
}使用场景:防抖 → 搜索输入、resize;节流 → 滚动、mousemove、点击。
P8:AJAX 的原理与实现
原理:通过 XMLHttpRequest 或 Fetch 异步与服务器交换数据,局部刷新页面。
手写 XHR:
function ajax({ url, method = 'GET', data, headers = {} }: any) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v as string));
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = () => reject(new Error('Network Error'));
xhr.send(data ? JSON.stringify(data) : null);
});
}Fetch 版本:
async function fetchJSON(url: string, options: RequestInit = {}) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json();
}P9:深拷贝浅拷贝区别与实现
区别:
- 浅拷贝:只复制对象的第一层属性,引用类型仍共享同一地址(
Object.assign、{...obj}、slice)。 - 深拷贝:递归复制所有层级,新对象与原对象完全隔离。
深拷贝实现:
function deepClone<T>(obj: T, map = new WeakMap()): T {
if (obj === null || typeof obj !== 'object') return obj;
if (obj instanceof Date) return new Date(obj) as any;
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags) as any;
if (map.has(obj as any)) return map.get(obj as any);
const cloneObj: any = Array.isArray(obj) ? [] : {};
map.set(obj as any, cloneObj);
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloneObj[key] = deepClone((obj as any)[key], map);
}
}
return cloneObj;
}生产推荐:structuredClone(obj)(原生)、lodash.cloneDeep(处理更多类型)。
注意:函数、Symbol、原型链、循环引用、特殊对象(Map/Set)需特别处理。
P10:JS 实现二叉树与基本操作
class TreeNode {
val: number;
left: TreeNode | null = null;
right: TreeNode | null = null;
constructor(val: number) { this.val = val; }
}
// 遍历
const preorder = (root: TreeNode | null): number[] => {
if (!root) return [];
return [root.val, ...preorder(root.left), ...preorder(root.right)];
};
const inorder = (root: TreeNode | null): number[] =>
!root ? [] : [...inorder(root.left), root.val, ...inorder(root.right)];
const postorder = (root: TreeNode | null): number[] =>
!root ? [] : [...postorder(root.left), ...postorder(root.right)];
// 层序(BFS)
const levelOrder = (root: TreeNode | null): number[][] => {
if (!root) return [];
const result: number[][] = [];
const queue: TreeNode[] = [root];
while (queue.length) {
const level: number[] = [];
const size = queue.length;
for (let i = 0; i < size; i++) {
const node = queue.shift()!;
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level);
}
return result;
};P11:实现轮播图组件
核心要点:
<!-- Vue 版 -->
<template>
<div class="carousel" @mouseenter="pause" @mouseleave="play">
<div class="track" :style="{ transform: `translateX(-${index * 100}%)` }">
<div v-for="(img, i) in images" :key="i" class="slide">
<img :src="img" />
</div>
</div>
<button @click="prev">‹</button>
<button @click="next">›</button>
<div class="dots">
<span v-for="(_, i) in images" :key="i"
:class="{ active: i === index }" @click="index = i" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps<{ images: string[]; interval?: number }>();
const index = ref(0);
let timer: any = null;
const next = () => index.value = (index.value + 1) % props.images.length;
const prev = () => index.value = (index.value - 1 + props.images.length) % props.images.length;
const play = () => { timer = setInterval(next, props.interval ?? 3000); };
const pause = () => clearInterval(timer);
onMounted(play);
onBeforeUnmount(pause);
</script>关键点:
transform+transition实现平滑滚动;- 自动播放 + 鼠标悬停暂停;
- 无限循环(首尾克隆 + 索引修正);
- 触摸滑动支持(移动端)。
P12:将数字转换为汉语输出
function trans(num: number): string {
if (num === 0) return '零';
const digits = '零一二三四五六七八九';
const units = ['', '十', '百', '千'];
const bigUnits = ['', '万', '亿', '万亿'];
const sectionToCN = (n: number): string => {
let result = '';
let zeroFlag = false;
let i = 0;
while (n > 0) {
const d = n % 10;
if (d === 0) {
if (!zeroFlag && result) zeroFlag = true;
} else {
if (zeroFlag) { result = '零' + result; zeroFlag = false; }
result = digits[d] + units[i] + result;
}
n = Math.floor(n / 10);
i++;
}
return result;
};
let result = '';
let bigIdx = 0;
let needZero = false;
while (num > 0) {
const section = num % 10000;
if (section === 0) {
if (result && !result.startsWith('零')) needZero = true;
} else {
const cn = sectionToCN(section);
if (needZero) result = '零' + result;
result = cn + bigUnits[bigIdx] + result;
needZero = false;
}
num = Math.floor(num / 10000);
bigIdx++;
}
return result;
}P13:编写 Vue 组件,使用插槽接收外部内容
<!-- Card.vue -->
<template>
<div class="card">
<header v-if="$slots.header">
<slot name="header" :title="title" />
</header>
<main>
<slot>默认内容</slot>
</main>
<footer v-if="$slots.footer">
<slot name="footer" />
</footer>
</div>
</template>
<script setup lang="ts">
defineProps<{ title?: string }>();
</script>
<!-- 使用 -->
<Card title="卡片">
<template #header="{ title }">
<h2>{{ title }}</h2>
</template>
<p>主体内容</p>
<template #footer>
<button>确定</button>
</template>
</Card>P14:去除字符串中出现次数最少的字符,不改变原顺序
function removeMinChars(str: string): string {
const counts = new Map<string, number>();
for (const ch of str) counts.set(ch, (counts.get(ch) || 0) + 1);
const minCount = Math.min(...counts.values());
return [...str].filter(ch => counts.get(ch)! > minCount).join('');
}P18:树转数组(扁平化)
function treeToArray(tree: any[]): any[] {
const result: any[] = [];
const dfs = (nodes: any[]) => {
nodes.forEach(node => {
result.push({ id: node.id, name: node.name, pid: node.pid });
if (node.children?.length) dfs(node.children);
});
};
dfs(tree);
return result;
}P19:数组转树
function arrayToTree(arr: Array<{ id: number; pid: number; name: string }>): any[] {
const map = new Map<number, any>();
const roots: any[] = [];
arr.forEach(item => map.set(item.id, { ...item, children: [] }));
arr.forEach(item => {
const node = map.get(item.id)!;
if (item.pid === 0) {
roots.push(node);
} else {
map.get(item.pid)?.children.push(node);
}
});
return roots;
}P20:删除链表的一个节点
已知要删除节点的指针(无头节点引用):
function deleteNode(node: ListNode): void {
// 把下一个节点的值复制到当前节点,然后删除下一个节点
node.val = node.next!.val;
node.next = node.next!.next;
}已知链表头 + 值:
function deleteNodeByVal(head: ListNode | null, val: number): ListNode | null {
if (!head) return null;
if (head.val === val) return head.next;
let cur = head;
while (cur.next && cur.next.val !== val) cur = cur.next;
if (cur.next) cur.next = cur.next.next;
return head;
}P21:实现请求并发控制(最多 N 个并发)
async function pLimit<T>(max: number, tasks: (() => Promise<T>)[]): Promise<T[]> {
const results: T[] = [];
let idx = 0;
const workers = Array.from({ length: max }, async () => {
while (idx < tasks.length) {
const cur = idx++;
results[cur] = await tasks[cur]();
}
});
await Promise.all(workers);
return results;
}进阶:失败重试 + 优先级 → 推荐使用 p-limit + p-retry npm 包。
P22:实现 fetchWithRetry(带重试机制)
async function fetchWithRetry(
url: string,
options: RequestInit = {},
retries = 3,
delay = 1000
): Promise<Response> {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res;
} catch (err) {
if (retries <= 0) throw err;
await new Promise(r => setTimeout(r, delay));
// 指数退避
return fetchWithRetry(url, options, retries - 1, delay * 2);
}
}P23:链表环的入口节点
快慢指针法:
function detectCycle(head: ListNode | null): ListNode | null {
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) break;
}
if (slow !== fast) return null;
// 相遇后,让 slow 从头出发,再次相遇即入口
slow = head;
while (slow !== fast) {
slow = slow.next!;
fast = fast.next!;
}
return slow;
}原理:快指针走 2k 步,慢指针走 k 步,相遇时让慢指针回到头,再次相遇即为环入口。
P24:多叉树指定层节点的个数
function countAtLevel(root: any, targetLevel: number, curLevel = 1): number {
if (!root) return 0;
if (curLevel === targetLevel) return 1;
let count = 0;
for (const child of (root.children || [])) {
count += countAtLevel(child, targetLevel, curLevel + 1);
}
return count;
}
// BFS 版
function countAtLevelBFS(root: any, targetLevel: number): number {
if (!root || targetLevel < 1) return 0;
let level = 1, queue = [root];
while (queue.length) {
if (level === targetLevel) return queue.length;
const next: any[] = [];
for (const node of queue) next.push(...(node.children || []));
queue = next;
level++;
}
return 0;
}P25:手写快速排序
function quickSort(arr: number[]): number[] {
if (arr.length <= 1) return arr;
const pivot = arr[0];
const left = arr.slice(1).filter(x => x <= pivot);
const right = arr.slice(1).filter(x => x > pivot);
return [...quickSort(left), pivot, ...quickSort(right)];
}
// 原地版(性能更好)
function quickSortInPlace(arr: number[], lo = 0, hi = arr.length - 1): number[] {
if (lo < hi) {
const pivotIdx = partition(arr, lo, hi);
quickSortInPlace(arr, lo, pivotIdx - 1);
quickSortInPlace(arr, pivotIdx + 1, hi);
}
return arr;
}
function partition(arr: number[], lo: number, hi: number): number {
const pivot = arr[hi];
let i = lo - 1;
for (let j = lo; j < hi; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[hi]] = [arr[hi], arr[i + 1]];
return i + 1;
}复杂度:平均 O(n log n),最坏 O(n²)。
P26:有序数组原地去重
function uniqueSorted(arr: number[]): number[] {
if (arr.length <= 1) return arr;
let slow = 0;
for (let fast = 1; fast < arr.length; fast++) {
if (arr[fast] !== arr[slow]) {
slow++;
arr[slow] = arr[fast];
}
}
return arr.slice(0, slow + 1);
}复杂度:O(n) 时间,O(1) 空间。
P27:计算数组中的平均时间
题目:给定形如 [{ start: '10:00', end: '12:00' }, ...] 的数组,计算所有时间段的平均时长(小时)。
function averageTime(intervals: Array<{ start: string; end: string }>): number {
const toMinutes = (t: string) => {
const [h, m] = t.split(':').map(Number);
return h * 60 + m;
};
const total = intervals.reduce((sum, { start, end }) => {
return sum + (toMinutes(end) - toMinutes(start));
}, 0);
return total / intervals.length / 60; // 转小时
}