运行结果题
考察作用域
题目一
代码:
var foo = 1;
function fn() {
foo = 3;
return;
function foo() {
// todo
}
}
fn();
console.log(foo);
分析:
因为 fn 函数内部对 foo 的赋值操作不会影响全局作用域中的 foo 变量。
结果:
// 输出 1
题目二
代码:
var a = 10;
function test() {
console.log(a);
var a = 20;
console.log(a);
}
test();
分析:
- 函数作用域中
var a会变量提升,但不会提升赋值。 - 因此第一次打印时
a已声明未赋值 →undefined; - 第二次打印时
a已被赋值为20。
结果:
// undefined
// 20
考察事件循环
题目一
代码:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
结果:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
题目二
代码:
async function test() {
console.log(1);
await new Promise((resolve) => {
console.log(2);
resolve();
}).then(() => {
console.log(3);
});
console.log(4);
}
test();
console.log(5);
分析:
1、2同步执行;await后暂停,主线程继续执行 → 输出5;.then()回调进入微任务队列 → 输出3;await继续执行 → 输出4。
结果:
1
2
5
3
4
题目三
代码:
setTimeout(function () {
console.log('1');
}, 0);
async function async1() {
console.log('2');
const data = await async2();
console.log('3');
return data;
}
async function async2() {
return new Promise((resolve) => {
console.log('4');
resolve('async2 的结果');
}).then((data) => {
console.log('5');
return data;
});
}
async1().then((data) => {
console.log('6');
console.log(data);
});
new Promise(function (resolve) {
console.log('7');
// resolve()
}).then(function () {
console.log('8');
});
分析:
主线程同步执行:
setTimeout(...)// 宏任务,延迟执行async1()// 调用 async1console.log("2")→ 输出 2await async2()
async2()// 调用 async2console.log("4")→ 输出 4resolve→.then(...)放入微任务
new Promise(...)// 执行同步函数console.log("7")→ 输出 7then不触发(未 resolve)
微任务队列执行:
async2().then(...)→console.log("5")→ 输出 5await async2()后续 →console.log("3")→ 输出 3async1().then(...)→console.log("6")→ 输出 6console.log(data)→ 输出async2的结果
宏任务队列执行:
setTimeout→console.log("1")→ 输出 1
结果:
2
4
7
5
3
6
async2 的结果
1
题目四
代码:
async function testAwait() {
console.log('1. async 函数开始');
// 遇到 await,先执行 promise 同步代码,再暂停函数
const result = await new Promise((resolve) => {
console.log('2. promise 内部同步代码');
// 模拟异步操作(如接口请求)
setTimeout(() => {
console.log('5. promise 异步操作完成');
resolve('数据');
}, 1000);
});
// 这部分代码会被包装成微任务,等 promise 完成后执行
console.log('6. await 恢复,拿到结果:', result);
}
// 执行 async 函数
testAwait();
// 3. await 暂停时,主线程执行后面的同步代码
console.log('3. 主线程处理同步代码');
// 4. 主线程处理宏任务(setTimeout)
setTimeout(() => {
console.log('4. 主线程处理其他宏任务');
}, 500);
结果:
1. async 函数开始
2. promise 内部同步代码
3. 主线程处理同步代码
4. 主线程处理其他宏任务
5. promise 异步操作完成
6. await 恢复,拿到结果: 数据
考察 Promise(高)
题目一
代码:
const p = new Promise((resolve, reject) => {
console.log(0);
reject();
console.log(1);
resolve();
console.log(2);
});
p.then((res) => {
console.log(3);
})
.then((res) => {
console.log(4);
})
.catch((res) => {
console.log(5);
})
.then((res) => {
console.log(6);
})
.catch((res) => {
console.log(7);
})
.then((res) => {
console.log(8);
});
分析:
reject和resolve不会影响 promise 函数体内的代码执行所以输出0 1 2。- 由于先有
reject所以前两个then链式调用不会有输出,直接到第一个catch输出5。 - 由于上一段代码没有错误,所以正常
then输出6和8。
结果:
0
1
2
5
6
8
题目二
代码:
class Scheduler {
constructor(limit) {
this.limit = limit;
this.queue = [];
this.running = 0;
}
add(task) {
return new Promise((resolve) => {
const run = async () => {
this.running++;
const res = await task();
resolve(res);
this.running--;
if (this.queue.length) {
this.queue.shift()();
}
};
if (this.running < this.limit) {
run();
} else {
this.queue.push(run);
}
});
}
}
const timeout = (time) => new Promise((r) => setTimeout(r, time));
const scheduler = new Scheduler(2);
const addTask = (time, name) =>
scheduler.add(() => timeout(time).then(() => console.log(name)));
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
分析:
- 同时最多运行 2 个任务。
- 起初执行
1,2;2先完成,触发3; - 接着
1完成,触发4。 - 最终执行顺序:
2→3→1→4。
结果:
2
3
1
4
题目三
代码:
Promise.resolve()
.then(() => {
console.log(1);
return Promise.resolve(2);
})
.then((res) => {
console.log(res);
return 3;
})
.then(console.log);
Promise.resolve()
.then(() => {
console.log('A');
return Promise.reject('B');
})
.catch((err) => {
console.log('C', err);
})
.then(() => {
console.log('D');
});
分析:
第一个链:
- 输出
1;返回Promise.resolve(2); - 等待完成 → 输出
2; - 下一次
then→ 输出3。
第二个链:
- 输出
A;返回reject('B')→ 进入catch; - 输出
C B; catch返回resolved状态 →then执行 → 输出D。
结果:
1
A
2
C B
3
D
字符串处理
题目一:字符串连续重复字符输出
代码:
function compressString(str) {
let res = '';
let count = 1;
for (let i = 0; i < str.length; i++) {
if (str[i] === str[i + 1]) {
count++;
} else {
res += str[i] + count + ' ';
count = 1;
}
}
return res.trim();
}
console.log(compressString('aaabbcdddde'));
分析:
- 初始化计数
count = 1;遍历字符串逐个对比相邻字符。 - 如果当前字符与下一个相同 → 计数累加。
- 否则说明一段连续字符结束,将该字符和次数拼接到结果字符串中,并重置
count = 1。 - 末尾返回完整拼接的字符串。
结果:
a3 b2 c1 d4 e1
题目二:输出字符、开始结束位置
代码:
function getCharRanges(str) {
const res = [];
let start = 0;
for (let i = 1; i <= str.length; i++) {
if (str[i] !== str[i - 1]) {
res.push({
char: str[start],
start,
end: i - 1
});
start = i;
}
}
return res;
}
console.log(getCharRanges('aaabbcdddde'));
分析:
- 用
start记录当前连续字符的起点。 - 从第二个字符开始遍历,如果当前字符与前一个不同,说明一段结束。
- 将当前段的
char、start、end推入结果数组。 start移动到下一个段的起始位置。
结果:
[
{ "char": "a", "start": 0, "end": 2 },
{ "char": "b", "start": 3, "end": 4 },
{ "char": "c", "start": 5, "end": 5 },
{ "char": "d", "start": 6, "end": 9 },
{ "char": "e", "start": 10, "end": 10 }
]
async / await 执行顺序
题目一
代码:
async function async1() {
console.log('A');
await async2();
console.log('B');
}
async function async2() {
console.log('C');
}
console.log('D');
async1();
console.log('E');
分析:
console.log('D')→ 同步执行。- 调用
async1()输出A。 - 执行
await async2(),进入async2,输出C,await后面的语句(B)被放入微任务队列。 - 同步任务结束后执行
console.log('E')。 - 最后微任务队列中执行
console.log('B')。
结果:
D
A
C
E
B
考察 this 指向
题目一
代码:
var name = 'window';
const obj = {
name: 'obj',
getName() {
return function () {
console.log(this.name);
};
}
};
obj.getName()();
分析:
getName()返回的是一个普通函数,而不是箭头函数;- 当执行
obj.getName()()时,第二次调用独立执行,this指向全局对象; - 因此输出
window。 - 若
getName的return改成箭头函数则输出obj。
结果:
window
函数组合 / 柯里化执行结果
题目一
代码:
function add(a) {
return function (b) {
return function (c) {
console.log(a, b, c);
return a + b + c;
};
};
}
console.log(add(1)(2)(3));
分析:
- 每次调用返回下一级函数;
- 最后一级函数执行时,闭包保留前面参数;
- 执行
console.log(a,b,c)输出1 2 3; - 返回相加结果
6。
结果:
1 2 3
6
防抖 / 节流执行顺序
题目一
代码:
function debounce(fn, delay) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
}
const log = debounce(() => console.log('run'), 200);
log();
log();
log();
setTimeout(log, 300);
分析:
- 前三次快速连续触发,前两次被清除,只有最后一次触发定时器执行;
- 第
300ms再次触发时,前一次已经执行完,因此又执行一次; - 所以共执行两次
"run"。
结果:
run
run
sleep / get 执行顺序
题目一
代码:
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function run() {
console.log('A');
await sleep(100);
console.log('B');
await sleep(0);
console.log('C');
}
run();
console.log('D');
分析:
A:同步执行;D:await会让出执行权,主线程继续;B:sleep(100)结束后异步执行;C:sleep(0)结束后微任务执行;
结果:
A
D
B
C
数组方法执行结果
题目一
代码:
const arr = [1, 2, [3, 4, [5]]];
const result = arr.flat(2).reduce((acc, cur) => acc + cur, 0);
console.log(result);
分析:
flat(2)→ 深度拍平 2 层:[1, 2, 3, 4, 5];reduce从 0 开始累计相加;- 得到结果
15。
结果:
15
题目二
代码:
function group(arr, fn) {
return arr.reduce((acc, cur) => {
const key = fn(cur);
if (!acc[key]) acc[key] = [];
acc[key].push(cur);
return acc;
}, {});
}
const data = [6.1, 4.2, 6.3];
console.log(group(data, Math.floor));
分析:
reduce遍历数组;- 通过
fn(cur)计算分组键; - 同键的元素放入同一数组;
- 典型函数式思维实现分组逻辑。
结果:
{
"4": [4.2],
"6": [6.1, 6.3]
}
题目三
代码:
const arr = [1, [2, [3, 4]]];
const res = arr.flat().reduce((a, b) => a.concat(b), []);
console.log(res);
分析:
flat()默认深度为 1,只展开一层;- 因此数组变为
[1, 2, [3, 4]]; reduce用concat合并元素,但[3,4]仍为数组;- 没有完全拍平。
结果:
[1, 2, [3, 4]]
手撕代码题
手写 Call / Apply / Bind
手写 Call
call()方法允许调用具有 this 值和参数的函数。
thisArg:要将函数内的this关键字指向的对象。arg1, arg2, ...:要传递给函数的参数。
Function.prototype.myCall = function (thisArg, ...args) {
thisArg = thisArg || window;
thisArg.fn = this;
const result = thisArg.fn(...args);
delete thisArg.fn;
return result;
};
手写 Apply
apply()方法与 call()方法类似,但接受一个参数数组而不是一系列单独的参数。
thisArg:要将函数内的this关键字指向的对象。[arg1, arg2, ...]:一个数组,包含要传递给函数的参数。
Function.prototype.myApply = function (thisArg, argsArray) {
thisArg = thisArg || window;
thisArg.fn = this;
const result = thisArg.fn(...(argsArray || []));
delete thisArg.fn;
return result;
};
手写 Bind
bind()方法创建了一个新的函数,其中 this 关键字被绑定到传递给 bind()方法的参数中的值。
thisArg:要将函数内的this关键字指向的对象。arg1, arg2, ...:要在调用新函数时传递的参数。
Function.prototype.myBind = function (thisArg, ...args) {
let self = this;
return function (...newArgs) {
// 在 new 场景下,this 指向实例
const isNew = this instanceof self;
const context = isNew ? this : Object(thisArg);
// 为了避免污染,使用唯一 key
const fnKey = Symbol('fn');
context[fnKey] = self;
const result = context[fnKey](...args, ...newArgs);
delete context[fnKey];
return result;
};
};
手写 New
// 模拟 new 关键字的函数
function myNew(constructor, ...args) {
// 1. 创建一个新对象,并将新对象的原型指向构造函数的原型
const obj = Object.create(constructor.prototype);
// 2. 执行构造函数,将 this 绑定到新对象
const result = constructor.apply(obj, args);
// 3. 如果构造函数返回了对象,则返回该对象,否则返回新创建的对象
return typeof result === 'object' && result !== null ? result : obj;
}
手写防抖
多次触发合并为最后一次执行(如搜索输入联想)。
// 防抖函数
function debounce(fn, delay) {
let timer = null; // 用于保存定时器
return function (...args) {
// 如果已有定时器,清除它(重新计时)
if (timer) clearTimeout(timer);
// 设置新的定时器,延迟执行原函数
timer = setTimeout(() => {
fn.apply(this, args); // 确保this指向正确
timer = null; // 执行后清空定时器
}, delay);
};
}
手写节流
控制执行频率,固定间隔执行一次(如滚动加载、高频点击)。
// 节流函数(定时器版)
function throttleTimer(fn, interval) {
let timer = null;
return function (...args) {
// 如果没有定时器,则设置一个
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null; // 执行后清空定时器
}, interval);
}
};
}
用 setTimeout 模拟实现 setInterval
function mySetInterval(fn, time = 1000) {
let timer = null;
function interval() {
fn();
timer = setTimeout(interval, time);
}
timer = setTimeout(interval, time);
// 返回一个函数用于清除定时器
return () => {
clearTimeout(timer);
};
}
// 使用示例:
// let cancel = mySetInterval(() => {
// console.log(111);
// }, 1000);
//
// setTimeout(() => {
// cancel(); // 停止 interval
// }, 5000);
将虚拟 Dom 转化为真实 Dom
虚拟 DOM 结构:
{
"tag": "DIV",
"attrs": {
"id": "app"
},
"children": [
{
"tag": "SPAN",
"children": [{ "tag": "A", "children": [] }]
},
{
"tag": "SPAN",
"children": [
{ "tag": "A", "children": [] },
{ "tag": "A", "children": [] }
]
}
]
}
目标真实 DOM:
<div id="app">
<span>
<a></a>
</span>
<span>
<a></a>
<a></a>
</span>
</div>
实现:
// 真正的渲染函数
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === 'number') {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === 'string') {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作 这一步是关键
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
手写 Promise
- 状态管理:
pending→fulfilled/rejected,状态不可逆。 - 微任务异步回调:
queueMicrotask保证then异步执行。 - 链式调用:每个
then返回新 Promise,可继续链式调用。 - 异常捕获:
executor或回调抛错会进入reject。
class MyPromise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (val) => {
if (this.status !== 'pending') return;
this.status = 'fulfilled';
this.value = val;
this.onFulfilledCallbacks.forEach((fn) => fn());
};
const reject = (err) => {
if (this.status !== 'pending') return;
this.status = 'rejected';
this.reason = err;
this.onRejectedCallbacks.forEach((fn) => fn());
};
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v;
onRejected =
typeof onRejected === 'function'
? onRejected
: (e) => {
throw e;
};
return new MyPromise((resolve, reject) => {
const handle = (callback, value) => {
queueMicrotask(() => {
try {
const result = callback(value);
// 处理 then 返回 Promise 的情况
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (err) {
reject(err);
}
});
};
if (this.status === 'fulfilled') {
handle(onFulfilled, this.value);
} else if (this.status === 'rejected') {
handle(onRejected, this.reason);
} else {
this.onFulfilledCallbacks.push(() => handle(onFulfilled, this.value));
this.onRejectedCallbacks.push(() => handle(onRejected, this.reason));
}
});
}
}
// 测试
new MyPromise((resolve) => setTimeout(() => resolve(42), 500)).then((res) =>
console.log('成功:', res)
);
手写深拷贝
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj; // 如果是基础类型或null,直接返回
}
if (hash.has(obj)) {
return hash.get(obj); // 解决循环引用
}
let clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone); // 存储当前对象的拷贝
// 考虑 Symbol 类型的 key
const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
for (let key of keys) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
clone[key] = deepClone(obj[key], hash); // 递归拷贝
}
}
return clone;
}
手写发布订阅
class PubSub {
constructor() {
// 存储事件和对应的订阅者
this.events = {};
}
/**
* 订阅事件
* @param {string} event 事件名称
* @param {Function} callback 回调函数
* @returns {Function} 用于取消订阅的函数
*/
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
// 返回取消订阅的函数
return () => {
this.unsubscribe(event, callback);
};
}
/**
* 取消订阅
* @param {string} event 事件名称
* @param {Function} callback 要取消的回调
*/
unsubscribe(event, callback) {
if (!this.events[event]) {
return;
}
this.events[event] = this.events[event].filter((cb) => cb !== callback);
}
/**
* 发布事件
* @param {string} event 事件名称
* @param {...any} args 传递给订阅者的参数
*/
publish(event, ...args) {
if (!this.events[event]) {
return;
}
this.events[event].forEach((callback) => {
callback(...args);
});
}
/**
* 清除某个事件的所有订阅
* @param {string} event 事件名称
*/
clear(event) {
if (this.events[event]) {
delete this.events[event];
}
}
/**
* 获取某个事件的订阅者数量
* @param {string} event 事件名称
* @returns {number} 订阅者数量
*/
getSubscriberCount(event) {
return this.events[event] ? this.events[event].length : 0;
}
}
手写 Ajax / 封装 Axios
手写 Ajax
- 使用
XMLHttpRequest创建请求。 - 使用
Promise包装异步操作,支持链式调用。 - 支持
GET/POST(可拓展PUT/DELETE等)。 - 简单处理响应状态码和异常。
function ajax({ url, method = 'GET', data = null }) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.statusText));
}
}
};
xhr.onerror = () => {
reject(new Error('Network Error'));
};
if (method.toUpperCase() === 'POST' && data) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
} else {
xhr.send();
}
});
}
// 使用示例
// ajax({ url: '/api/test' })
// .then(res => console.log(res))
// .catch(err => console.error(err));
封装 Axios
- 类封装请求方法,统一接口调用。
get/post方法快捷封装,减少每次写config。- 支持 JSON 请求 / 响应处理,可扩展
headers、拦截器等。 Promise链式调用,符合现代前端习惯。
class Axios {
request(config) {
const { url, method = 'GET', data = null } = config;
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
resolve(xhr.responseText);
}
} else {
reject(new Error(xhr.statusText));
}
}
};
xhr.onerror = () => reject(new Error('Network Error'));
if (method.toUpperCase() === 'POST' && data) {
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify(data));
} else {
xhr.send();
}
});
}
get(url, config = {}) {
return this.request({ ...config, url, method: 'GET' });
}
post(url, data, config = {}) {
return this.request({ ...config, url, method: 'POST', data });
}
}
// 使用示例
// const axios = new Axios();
// axios.get('/api/test').then(res => console.log(res));
// axios.post('/api/test', { name: 'V' }).then(res => console.log(res));
列表转树数据处理
假设从后端获取了大量的扁平化列表数据,请写一个函数将其转换为 ECharts tree 图所需要的嵌套结构。
输入数据:
const list = [
{ id: 1, name: '部门 A', parentId: 0 },
{ id: 2, name: '部门 B', parentId: 1 },
{ id: 3, name: '部门 C', parentId: 1 },
{ id: 4, name: '部门 D', parentId: 2 }
];
输出数据:
[{
"id": 1,
"name": "部门 A",
"children": [
{ "id": 2, "name": "部门 B", "children": [...] },
{ "id": 3, "name": "部门 C", "children": [] }
]
}]
代码:
function buildTree(list) {
const map = new Map();
const result = [];
// 1. 先把每个节点放入 map 中,并初始化 children
list.forEach((item) => {
map.set(item.id, { ...item, children: [] });
});
// 2. 构建父子关系
list.forEach((item) => {
if (item.parentId === 0) {
// 根节点
result.push(map.get(item.id));
} else {
const parent = map.get(item.parentId);
if (parent) {
parent.children.push(map.get(item.id));
}
}
});
return result;
}
函数柯里化实现
fn.length获取函数形参个数,用于判断参数是否足够执行。- 每次调用返回新函数,把已有参数与新参数合并,递归调用。
- 当参数足够时,调用原函数返回结果。
- 支持多种组合形式调用:单参数、多参数、混合。
// 普通函数
function sum(a, b, c) {
return a + b + c;
}
// 柯里化实现
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args); // 参数够了就执行
} else {
return (...rest) => curried(...args, ...rest); // 参数不够,返回新的函数
}
};
}
// 使用
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2, 3)); // 6
sum 函数柯里化实现
设计一个 sum 函数,使其满足以下要求:
sum(1, 2).sumOf()// 返回 3sum(1, 2)(3).sumOf()// 返回 6sum(1)(2, 3, 4).sumOf()// 返回 10sum(1, 2)(3, 4)(5).sumOf()// 返回 15
function sum(...args) {
let total = args.reduce((acc, curr) => acc + curr, 0);
function innerSum(...innerArgs) {
total += innerArgs.reduce((acc, curr) => acc + curr, 0);
return innerSum;
}
innerSum.sumOf = () => total;
return innerSum;
}
实现 get 方法
var obj = { a: { b: { c: 2 } } };
console.log(get(obj, 'a.b.c')); // 输出 2
function get(obj, path) {
// 使用 ?. 和 reduce 简化
return path.split('.').reduce((acc, key) => acc?.[key], obj);
}
lastPromise 实现
单次请求控制:每次请求都会取消上一次的请求,只有最后一次请求会被执行。
function lastPromise(promiseFn) {
let lastPromise = null;
return function (...args) {
const currentPromise = promiseFn(...args);
lastPromise = currentPromise;
return new Promise((resolve, reject) => {
currentPromise
.then((result) => {
// 只有当当前 promise 是最后一个时,才 resolve
if (currentPromise === lastPromise) {
resolve(result);
}
})
.catch((err) => {
// 同样,只有最后一个 promise 的错误才会被抛出
if (currentPromise === lastPromise) {
reject(err);
}
});
});
};
}
// 使用
// const fetchData = lastPromise(async (url) => {
// const res = await fetch(url);
// return res.json();
// });
// fetchData('api/1');
// fetchData('api/2'); // 只有 'api/2' 的请求会被处理
promise 最大并发请求 / 调度器
- 并发调度:控制最大并发数量,任务排队等候执行,保证不超过最大并发数量。
- 链式调用:每个
add()返回Promise,保证异步任务执行顺序。
class Scheduler {
constructor(limit) {
this.limit = limit;
this.queue = [];
this.active = 0;
}
add(promiseFn) {
return new Promise((resolve) => {
const task = async () => {
this.active++;
try {
const result = await promiseFn();
resolve(result); // 将任务结果 resolve 出去
} catch (e) {
// 即使任务失败,也需要执行下一个
resolve(); // 或者 reject(e),取决于业务需求
} finally {
this.active--;
if (this.queue.length) {
this.queue.shift()();
}
}
};
if (this.active < this.limit) {
task();
} else {
this.queue.push(task);
}
});
}
}
手写 promise.all 和 promise.race
Promise.all
收集每个 Promise 的结果,全部完成才 resolve,否则 reject。
function myAll(promises) {
return new Promise((resolve, reject) => {
if (!promises || promises.length === 0) {
return resolve([]);
}
let results = [],
count = 0;
promises.forEach((p, i) => {
Promise.resolve(p)
.then((val) => {
results[i] = val;
count++;
if (count === promises.length) {
resolve(results);
}
})
.catch(reject);
});
});
}
Promise.race
谁先完成(resolve/reject)就返回结果。
function myRace(promises) {
return new Promise((resolve, reject) => {
if (!promises || promises.length === 0) {
return; // 或者 resolve(undefined)
}
promises.forEach((p) => {
Promise.resolve(p).then(resolve).catch(reject);
});
});
}
数组算法
数组去重
function unique(arr) {
return [...new Set(arr)];
}
console.log(unique([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]
手写 flat 方法
Array.prototype.myFlat = function (depth = 1) {
if (!Array.isArray(this)) {
throw new TypeError('myFlat must be called on an array');
}
let result = [];
for (const item of this) {
if (Array.isArray(item) && depth > 0) {
result.push(...item.myFlat(depth - 1));
} else {
result.push(item);
}
}
return result;
};
数组扁平化
// 使用 reduce
function flatten(arr) {
return arr.reduce(
(acc, item) =>
Array.isArray(item) ? acc.concat(flatten(item)) : acc.concat(item),
[]
);
}
console.log(flatten([1, [2, [3, [4]]]])); // [1, 2, 3, 4]
// ES6 flat 方法
const arr = [1, [2, [3, [4, 5]]], 6];
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6]
排序算法
Array.prototype.sort():该方法可以对数组进行原地排序,即直接修改原数组,不会返回新的数组。默认情况下,它会将数组元素转换为字符串,然后按照 Unicode 码点排序。如果需要按照其他方式排序,可以传入一个比较函数作为参数。Array.prototype.reverse():该方法可以将数组中的元素按照相反的顺序重新排列,并返回新的数组。- 冒泡排序(Bubble Sort):这是一种简单的排序算法,它重复地遍历要排序的数组,比较相邻的元素并交换位置,直到整个数组都已经排序。
- 快速排序(Quick Sort):这是一种快速的排序算法,它的基本思想是选择一个基准元素,然后将数组中的元素分为小于基准元素和大于基准元素的两部分,再对这两部分分别进行排序。
- 插入排序(Insertion Sort):这是一种简单的排序算法,它将数组分为已排序和未排序两部分,然后将未排序部分的第一个元素插入到已排序部分的正确位置上。
- 选择排序(Selection Sort):这是一种简单的排序算法,它将数组分为已排序和未排序两部分,然后从未排序部分选择最小的元素并放到已排序部分的末尾。
- 归并排序(Merge Sort):这是一种分治的排序算法,它将数组分成两个子数组,分别对这两个子数组进行排序,然后将排序后的子数组合并成一个有序的数组。
冒泡排序
function bubbleSort(arr) {
for (let i = 0; i < arr.length - 1; i++) {
let swapped = false;
for (let j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]; // 交换
swapped = true;
}
}
if (!swapped) break; // 如果一轮没有交换,说明已经有序
}
return arr;
}
console.log(bubbleSort([5, 2, 8, 3, 1])); // [1, 2, 3, 5, 8]
快速排序
- 时间复杂度:O(n log n)
- 如何对快排进行优化?
- 优化基准:取中间值,三数取中或随机基准,降低最坏概率。
- 小数组优化:当分区后子数组长度较小时,改用插入排序。
- 尾递归优化:始终递归较小分区。
- 三路划分:分为
< pivot、= pivot、> pivot三段,避免重复比较。 - 原地排序:尽量在原数组上交换,节省额外空间。
- 快排最坏情况:每次选取的基准都是当前子数组的最大或最小元素,时间复杂度为 O(n^2)。
function quickSort(arr) {
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = [];
const right = [];
const equal = [];
for (let x of arr) {
if (x < pivot) {
left.push(x);
} else if (x > pivot) {
right.push(x);
} else {
equal.push(x);
}
}
return [...quickSort(left), ...equal, ...quickSort(right)];
}
console.log(quickSort([5, 2, 8, 3, 1])); // [1, 2, 3, 5, 8]
归并排序
function mergeSort(arr) {
// 递归终止条件
if (arr.length <= 1) return arr;
// 拆分数组
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
// 递归排序并合并
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
const result = [];
let i = 0,
j = 0;
// 比较两个子数组元素,按升序合并
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
// 拼接剩余元素
return result.concat(left.slice(i)).concat(right.slice(j));
}
// 示例
console.log(mergeSort([5, 2, 9, 1, 3])); // [1, 2, 3, 5, 9]
数组操作函数
数组平分
fn([1,2,3,4,5], 2) //结果为 [[1,2],[3,4],[5]]
function splitArray(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
result.push(arr.slice(i, i + size));
}
return result;
}
console.log(splitArray([1, 2, 3, 4, 5], 2)); // [[1, 2], [3, 4], [5]]
group 函数分类
function group(arr, fn) {
return arr.reduce((acc, item) => {
const key = fn(item);
if (!acc[key]) acc[key] = [];
acc[key].push(item);
return acc;
}, {});
}
const data = [6.1, 4.2, 6.3];
console.log(group(data, Math.floor)); // { 4: [4.2], 6: [6.1, 6.3] }
CSS 布局实现
CSS 三角形
.triangle {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid red;
}
可滚动列表前三个样式
.scrollable-list {
max-height: 200px;
overflow-y: auto;
}
.scrollable-list li:nth-child(-n + 3) {
background-color: lightgreen;
}
响应式矩形布局
.rectangle {
width: calc(100% - 100px); /* 距离左右各 50px */
padding-bottom: 75%; /* 高度是宽度的 3/4,即 4:3 */
background-color: lightblue;
position: relative;
margin: 0 auto; /* 水平居中 */
}
- 【前端面试】HTML篇
- 【前端面试】CSS篇
- 【前端面试】JS篇
- 【前端面试】Vue篇
- 【前端面试】Git篇
- 【前端面试】前端工程化篇
- 【前端面试】手写题篇
- 【前端面试】前端性能优化篇
- 【前端面试】浏览器&网络篇
整理不易,有回答错误之处欢迎在评论区指正交流~