0%

前端面试题汇总1

72、大文件上传

1. 分片上传

将大文件分割成多个小块(chunks),分别上传到服务器,最后在服务器端合并。

实现步骤:

  1. 前端分片
    • 使用 File 对象的 slice 方法将文件分割成多个小块。
    • 每个分片通过 **FormData**上传到服务器。
    • 记录分片的顺序,方便服务器合并。
  2. 后端合并
    • 服务器接收到分片后,按顺序存储。
    • 所有分片上传完成后,服务器将分片合并成完整文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 分片大小(例如 1MB)
const CHUNK_SIZE = 1024 * 1024;

// 上传文件
async function uploadFile(file) {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const fileId = Date.now(); // 生成唯一文件 ID

for (let i = 0; i < totalChunks; i++) {
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);

const formData = new FormData();
formData.append('file', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);

// 上传分片
await uploadChunk(formData);
console.log(`Uploaded chunk ${i + 1}/${totalChunks}`);
}

console.log('All chunks uploaded');
}

// 上传单个分片
async function uploadChunk(formData) {
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Upload failed');
}
} catch (error) {
console.error('Error uploading chunk:', error);
}
}

// 选择文件并触发上传
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (file) {
uploadFile(file);
}
});
2. 断点续传

在上传过程中,如果网络中断或用户暂停上传,可以从上次中断的地方继续上传,而不是重新开始。

实现步骤:

  1. 记录上传进度
    • 前端记录已上传的分片索引。
    • 后端保存已接收的分片。
  2. 续传逻辑
    • 上传前,前端向后端查询已上传的分片。
    • 只上传未完成的分片。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
async function uploadFile(file) {
const CHUNK_SIZE = 1024 * 1024;
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
const fileId = Date.now();

// 查询已上传的分片
const uploadedChunks = await getUploadedChunks(fileId);

for (let i = 0; i < totalChunks; i++) {
if (uploadedChunks.includes(i)) {
console.log(`Chunk ${i} already uploaded`);
continue;
}

const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, file.size);
const chunk = file.slice(start, end);

const formData = new FormData();
formData.append('file', chunk);
formData.append('fileId', fileId);
formData.append('chunkIndex', i);
formData.append('totalChunks', totalChunks);

await uploadChunk(formData);
console.log(`Uploaded chunk ${i + 1}/${totalChunks}`);
}

console.log('All chunks uploaded');
}

// 查询已上传的分片
async function getUploadedChunks(fileId) {
try {
const response = await fetch(`/uploaded-chunks?fileId=${fileId}`);
const data = await response.json();
return data.uploadedChunks || [];
} catch (error) {
console.error('Error fetching uploaded chunks:', error);
return [];
}
}
3. 进度显示

通过 XMLHttpRequestfetchProgressEvent 监听上传进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function uploadChunk(formData) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/upload', true);

xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percent = (event.loaded / event.total) * 100;
console.log(`Upload progress: ${percent.toFixed(2)}%`);
}
};

xhr.onload = () => {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error('Upload failed'));
}
};

xhr.onerror = () => {
reject(new Error('Upload failed'));
};

xhr.send(formData);
});
}
5. 优化建议
  • 压缩文件:在上传前对文件进行压缩,减少传输体积。
  • 并发上传:使用多个并发请求上传分片,提高上传速度。
  • 文件校验:在上传完成后,通过 MD5 或 SHA 校验文件完整性。

73、前端高并发场景解决方案

1. 请求合并与批量处理

将多个小请求合并为一个批量请求,减少请求次数,降低服务器压力。

示例场景:

  • 上传多个文件时,将文件打包成一个请求发送。
  • 提交表单时,将多个字段合并为一个请求。
2. 请求队列与限流

通过队列控制请求的发送速率,避免短时间内发送过多请求。

实现方式:

  • 使用队列(如 Promise 队列)控制请求顺序。
  • 设置并发数限制,避免同时发送过多请求。
3. 缓存与本地存储

利用浏览器缓存或本地存储(如 localStorageIndexedDB)减少重复请求。

示例场景:

  • 缓存 API 响应数据,避免重复请求。
  • 将用户输入的数据暂存到本地,减少实时请求。
4. Web Workers

将计算密集型任务放到 Web Workers 中执行,避免阻塞主线程。

示例场景:

  • 大数据处理、图像处理等耗时操作。
5. 懒加载与分页

对于大量数据的展示,采用懒加载或分页的方式,减少一次性加载的数据量。

示例场景:

  • 长列表滚动加载。
  • 分页加载表格数据。
6. 服务端渲染(SSR)与静态化

对于高并发页面,可以采用服务端渲染(SSR)或静态化技术,减少前端渲染压力。

示例场景:

  • 新闻网站、电商首页等静态内容较多的页面。
7. CDN 加速

将静态资源(如图片、CSS、JS 文件)托管到 CDN,加速资源加载。

示例场景:

  • 图片、视频等大文件的分发。
8. 降级与兜底策略

在高并发场景下,确保系统在极端情况下仍能提供基本服务。

示例场景:

  • 当服务器压力过大时,返回简化页面或静态内容。
  • 使用本地缓存数据作为兜底。
9. 优化网络请求
  • HTTP/2:使用 HTTP/2 协议,支持多路复用,减少连接开销。
  • 压缩数据:使用 Gzip 或 Brotli 压缩请求和响应数据。
  • 减少 Cookie 大小:避免在请求中携带不必要的 Cookie。
10. 监控与报警

通过监控工具(如 Sentry、Prometheus)实时监控前端性能,及时发现并解决问题。

示例场景:

  • 监控页面加载时间、API 响应时间。
  • 设置报警规则,当性能指标异常时通知开发人员。

73、vue动态路由

1. 定义路由表

首先,定义所有可能的路由,包括需要权限的路由和公共路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// router.js
import Vue from 'vue';
import Router from 'vue-router';
import PublicPage from './components/PublicPage.vue';
import Login from './components/Login.vue';

Vue.use(Router);

// 公共路由(无需权限)
const publicRoutes = [
{
path: '/',
component: PublicPage,
},
{
path: '/login',
component: Login,
},
];

// 需要权限的路由
const adminRoutes = [
{
path: '/admin',
component: () => import('./components/Admin.vue'),
meta: { requiresAuth: true, role: 'admin' }, // 元信息,标识需要权限
},
];

const userRoutes = [
{
path: '/dashboard',
component: () => import('./components/Dashboard.vue'),
meta: { requiresAuth: true, role: 'user' },
},
];

const router = new Router({
mode: 'history',
routes: publicRoutes, // 初始只加载公共路由
});

export default router;
2. 动态添加路由

根据用户权限,动态添加需要权限的路由。

1
2
3
4
5
6
7
8
9
10
// 在用户登录后动态添加路由
function addRoutesBasedOnRole(role) {
const routes = role === 'admin' ? adminRoutes : userRoutes;
router.addRoutes(routes); // 动态添加路由
}

// 示例:用户登录后调用
store.dispatch('login', { username: 'admin', role: 'admin' }).then(() => {
addRoutesBasedOnRole(store.getters.userRole);
});
3. 路由守卫

通过全局路由守卫检查用户权限,决定是否允许访问路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// router.js
router.beforeEach((to, from, next) => {
const isAuthenticated = store.getters.isAuthenticated; // 是否已登录
const userRole = store.getters.userRole; // 用户角色

if (to.meta.requiresAuth && !isAuthenticated) {
// 如果需要权限且未登录,跳转到登录页
next('/login');
} else if (to.meta.role && to.meta.role !== userRole) {
// 如果角色不匹配,跳转到首页或 403 页面
next('/');
} else {
// 允许访问
next();
}
});
4. Vuex 管理用户状态

使用 Vuex 管理用户登录状态和角色信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export default new Vuex.Store({
state: {
user: null,
},
mutations: {
setUser(state, user) {
state.user = user;
},
},
actions: {
login({ commit }, user) {
// 模拟登录
return new Promise((resolve) => {
setTimeout(() => {
commit('setUser', user);
resolve();
}, 1000);
});
},
logout({ commit }) {
commit('setUser', null);
},
},
getters: {
isAuthenticated: (state) => !!state.user,
userRole: (state) => state.user?.role,
},
});
5. 登录与退出

在登录和退出时更新用户状态,并动态调整路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Login.vue
<template>
<div>
<input v-model="username" placeholder="Username" />
<input v-model="password" type="password" placeholder="Password" />
<button @click="login">Login</button>
</div>
</template>

<script>
export default {
data() {
return {
username: '',
password: '',
};
},
methods: {
login() {
const role = this.username === 'admin' ? 'admin' : 'user';
this.$store.dispatch('login', { username: this.username, role }).then(() => {
this.$router.push('/');
});
},
},
};
</script>

// Logout.vue
<template>
<button @click="logout">Logout</button>
</template>

<script>
export default {
methods: {
logout() {
this.$store.dispatch('logout');
this.$router.push('/login');
},
},
};
</script>

74、 window 对象上挂载了大量的事件监听函数

1. 事件委托(Event Delegation)

将事件监听器绑定到父元素上,利用事件冒泡机制处理子元素的事件。这样可以减少事件监听器的数量。

1
2
3
4
5
6
7
8
9
10
11
// 不推荐:为每个按钮绑定事件
document.querySelectorAll('button').forEach((button) => {
button.addEventListener('click', handleClick);
});

// 推荐:使用事件委托
document.body.addEventListener('click', (event) => {
if (event.target.tagName === 'BUTTON') {
handleClick(event);
}
});
2. 按需绑定事件

只在需要的时候绑定事件,在不需要的时候移除事件监听器。

1
2
3
4
5
6
7
8
9
function handleScroll() {
console.log('Scrolling...');
}

// 绑定事件
window.addEventListener('scroll', handleScroll);

// 移除事件
window.removeEventListener('scroll', handleScroll);
3. 防抖(Debounce)与节流(Throttle)

对于频繁触发的事件(如 resizescroll),使用防抖或节流减少事件处理函数的调用频率。

4. 使用 Passive Event Listeners

对于不会调用 preventDefault() 的事件(如 touchstarttouchmove),可以将事件监听器标记为 passive,以提升滚动性能。

1
window.addEventListener('touchmove', handleTouchMove, { passive: true });
5. 避免内存泄漏

确保在组件销毁或页面卸载时移除事件监听器。

6. 合并事件处理逻辑

将多个事件的处理逻辑合并到一个函数中,减少事件监听器的数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function handleEvent(event) {
switch (event.type) {
case 'click':
console.log('Clicked');
break;
case 'scroll':
console.log('Scrolled');
break;
default:
break;
}
}

window.addEventListener('click', handleEvent);
window.addEventListener('scroll', handleEvent);
7. 使用 WeakMap 管理事件监听器

使用 WeakMap 存储事件监听器,避免手动管理监听器的添加和移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const eventListeners = new WeakMap();

function addEventListener(target, event, handler) {
const wrappedHandler = (e) => handler(e);
eventListeners.set(handler, wrappedHandler);
target.addEventListener(event, wrappedHandler);
}

function removeEventListener(target, event, handler) {
const wrappedHandler = eventListeners.get(handler);
if (wrappedHandler) {
target.removeEventListener(event, wrappedHandler);
eventListeners.delete(handler);
}
}

// 使用
const handler = () => console.log('Clicked');
addEventListener(window, 'click', handler);
removeEventListener(window, 'click', handler);

75、webpack和vite的区别

WebpackVite 都是现代前端开发中常用的构建工具,但它们在设计理念、性能和使用场景上有显著区别。

1. 核心设计理念
Webpack
  • 基于打包:Webpack 是一个静态模块打包工具,它将所有资源(如 JavaScript、CSS、图片等)打包成一个或多个文件。
  • 支持多种模块化规范:CommonJS、AMD、ES Module 等。
  • 插件和加载器:通过丰富的插件(Plugins)和加载器(Loaders)支持各种功能扩展。
Vite
  • 基于原生 ES Module:Vite 利用现代浏览器对 ES Module 的原生支持,直接在浏览器中运行模块化的代码。
  • 按需加载:开发环境下不打包,而是按需加载模块,提升开发体验。
  • 构建工具:生产环境下使用 Rollup 进行打包。
2. 开发环境性能
Webpack
  • 打包时间长:在开发环境下,Webpack 需要将所有模块打包成一个或多个文件,项目越大,打包时间越长。
  • 热更新(HMR):支持热更新,但更新速度受项目规模影响。
Vite
  • 快速启动:开发环境下不打包,直接启动开发服务器,启动速度极快。
  • 按需加载:浏览器按需请求模块,减少初始加载时间。
  • 热更新(HMR):基于 ES Module 的热更新,更新速度更快。
3. 生产环境构建
Webpack
  • 成熟的打包机制:Webpack 提供了强大的打包功能,支持代码分割、Tree Shaking、懒加载等优化。
  • 插件生态:丰富的插件生态(如 html-webpack-pluginmini-css-extract-plugin 等)可以满足各种需求。
Vite
  • 基于 Rollup:生产环境下使用 Rollup 进行打包,生成更小、更高效的代码。
  • 开箱即用的优化:支持 Tree Shaking、代码分割等优化,但插件生态相对较少。
4. 配置复杂度
Webpack
  • 配置复杂:Webpack 的配置文件(webpack.config.js)通常比较复杂,尤其是需要自定义加载器和插件时。
  • 学习曲线陡峭:初学者可能需要花费较多时间学习 Webpack 的配置和原理。
Vite
  • 配置简单:Vite 的配置文件(vite.config.js)相对简单,大部分功能开箱即用。
  • 易于上手:对初学者更友好,学习曲线较低。
5. 生态系统
Webpack
  • 成熟的生态系统:Webpack 拥有庞大的插件和加载器生态系统,支持各种前端工具和框架。
  • 社区支持:由于历史悠久,社区支持和文档非常丰富。
Vite
  • 新兴生态系统:Vite 的插件生态相对较少,但正在快速发展。
  • 框架支持:Vite 对 Vue、React、Svelte 等现代框架有很好的支持。
6. 适用场景
Webpack
  • 大型项目:适合复杂的大型项目,尤其是需要高度自定义构建流程的场景。
  • 传统项目:适合需要兼容旧版浏览器的项目。
Vite
  • 现代项目:适合基于现代浏览器开发的项目,尤其是需要快速启动和高效开发体验的场景。
  • 小型到中型项目:适合中小型项目,或者对开发体验要求较高的项目。
7. 优缺点对比
Webpack

优点:

  • 功能强大,支持各种自定义配置。
  • 插件生态丰富,社区支持强大。
  • 适合大型复杂项目。

缺点:

  • 配置复杂,学习曲线陡峭。
  • 开发环境下打包速度较慢。
Vite

优点:

  • 开发环境下启动速度快,按需加载。
  • 配置简单,易于上手。
  • 适合现代前端项目。

缺点:

  • 插件生态相对较少。
  • 对旧版浏览器的支持有限。
总结
特性 Webpack Vite
核心设计 基于打包 基于原生 ES Module
开发性能 打包时间长,热更新较慢 快速启动,按需加载,热更新快
生产构建 成熟的打包机制 基于 Rollup,开箱即用的优化
配置复杂度 配置复杂 配置简单
生态系统 插件生态丰富 插件生态较少,但发展迅速
适用场景 大型复杂项目,传统项目 现代项目,中小型项目
  • 如果你需要高度自定义的构建流程,或者项目需要兼容旧版浏览器,Webpack 是更好的选择。
  • 如果你追求极致的开发体验,或者项目基于现代浏览器开发,Vite 是更优的选择。

76、从树结构数据中拿到想要的数据

1. 递归遍历

递归是处理树结构数据的常用方法。假设你有一个树结构的数据,每个节点可能有子节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const tree = {
id: 1,
name: 'Root',
children: [
{
id: 2,
name: 'Child 1',
children: [
{
id: 3,
name: 'Grandchild 1',
children: []
}
]
},
{
id: 4,
name: 'Child 2',
children: []
}
]
};

function findNodeById(node, id) {
if (node.id === id) {
return node;
}
for (let child of node.children) {
const result = findNodeById(child, id);
if (result) {
return result;
}
}
return null;
}

const node = findNodeById(tree, 3);
console.log(node); // { id: 3, name: 'Grandchild 1', children: [] }
2. 迭代遍历

如果你不想使用递归,可以使用栈或队列来实现迭代遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function findNodeByIdIterative(root, id) {
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
if (node.id === id) {
return node;
}
for (let child of node.children) {
stack.push(child);
}
}
return null;
}

const node = findNodeByIdIterative(tree, 4);
console.log(node); // { id: 4, name: 'Child 2', children: [] }
3. 使用广度优先搜索(BFS)

广度优先搜索通常使用队列来实现,适合在树中查找离根节点最近的节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function findNodeByIdBFS(root, id) {
const queue = [root]; // 初始化队列,放入根节点
while (queue.length > 0) { // 当队列不为空时循环
const node = queue.shift(); // 取出队列头部节点
if (node.id === id) { // 检查是否满足条件
return node; // 如果满足,返回该节点
}
// 将子节点按顺序加入队列
for (let child of node.children) {
queue.push(child);
}
}
return null; // 如果队列为空且未找到目标节点,返回 null
}

const node = findNodeByIdBFS(tree, 2);
console.log(node); // { id: 2, name: 'Child 1', children: [...] }
4. 使用深度优先搜索(DFS)

深度优先搜索可以使用递归或来实现,适合在树中查找特定节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function findNodeByIdDFS(root, id) {
const stack = [root]; // 初始化栈,放入根节点
while (stack.length > 0) { // 当栈不为空时循环
const node = stack.pop(); // 弹出栈顶节点
if (node.id === id) { // 检查是否满足条件
return node; // 如果满足,返回该节点
}
// 将子节点按逆序压入栈中(确保从左到右遍历)
for (let i = node.children.length - 1; i >= 0; i--) {
stack.push(node.children[i]);
}
}
return null; // 如果栈为空且未找到目标节点,返回 null
}

const node = findNodeByIdDFS(tree, 3);
console.log(node); // { id: 3, name: 'Grandchild 1', children: [] }
5. 使用 reducemap 进行数据处理

如果你需要对树中的每个节点进行处理,可以使用 reducemap 来遍历树并生成新的数据结构。

1
2
3
4
5
6
7
8
9
function mapTree(node, fn) {
return {
...fn(node),
children: node.children.map(child => mapTree(child, fn))
};
}

const newTree = mapTree(tree, node => ({ ...node, name: node.name.toUpperCase() }));
console.log(newTree);
总结
  • 递归:简单直观,适合深度优先遍历。
  • 迭代:避免递归栈溢出,适合深度优先或广度优先遍历。
  • 广度优先搜索(BFS):适合查找离根节点最近的节点。
  • 深度优先搜索(DFS):适合查找特定节点或遍历整个树。

77、async、await的设计和实现

  • **async**:标记函数为异步,自动返回 Promise
  • **await**:暂停执行,等待 Promise 完成。
  • 底层实现:依赖生成器函数和状态机,将异步代码转换为同步风格。
    • 生成器函数async/await 通过生成器函数和 Promise 实现。生成器函数可以暂停和恢复执行,await 利用这一特性等待 Promise 完成。
    • 状态机async 函数被编译为状态机,每个 await 对应一个状态,Promise 完成后恢复执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}

function _asyncToGenerator(fn) {
return function () {
var self = this, args = arguments;
return new Promise(function (resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}

78、深拷贝要注意哪些问题

1. 循环引用问题

如果对象内部存在循环引用(如 obj.a = obj),普通的递归深拷贝会导致栈溢出(Maximum call stack size exceeded)。

解决方案

使用 WeakMapMap 存储已拷贝的对象,遇到重复引用时直接返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepClone(obj, map = new WeakMap()) {
if (map.has(obj)) return map.get(obj); // 解决循环引用
if (typeof obj !== 'object' || obj === null) return obj;

const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone); // 存储已拷贝的对象

for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
2. 特殊对象类型

普通 JSON.parse(JSON.stringify(obj)) 或递归遍历无法正确处理以下情况:

  • Date 对象 → 会被转成字符串
  • RegExp 对象 → 会被转成空对象 {}
  • Map/Set → 会被丢弃或转成普通对象
  • Function → 会被丢弃
  • Symbol 属性 → 会被忽略
  • Blob/File/Buffer → 可能无法正确复制

解决方案

  • 手动处理特殊类型:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function deepClone(obj, map = new WeakMap()) {
if (map.has(obj)) return map.get(obj);
if (obj === null || typeof obj !== 'object') return obj;

// 处理 Date
if (obj instanceof Date) return new Date(obj);

// 处理 RegExp
if (obj instanceof RegExp) return new RegExp(obj);

// 处理 Map
if (obj instanceof Map) {
const clone = new Map();
map.set(obj, clone);
obj.forEach((value, key) => {
clone.set(key, deepClone(value, map));
});
return clone;
}

// 处理 Set
if (obj instanceof Set) {
const clone = new Set();
map.set(obj, clone);
obj.forEach(value => {
clone.add(deepClone(value, map));
});
return clone;
}

// 处理 Array 或普通 Object
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);

// 处理 Symbol 属性
const symKeys = Object.getOwnPropertySymbols(obj);
for (const key of symKeys) {
clone[key] = deepClone(obj[key], map);
}

// 处理普通属性
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}

return clone;
}
3. 性能问题
  • 递归深拷贝大对象可能导致 栈溢出性能瓶颈
  • 某些库(如 lodash.cloneDeep)优化了性能,但仍有开销。
4. 原型链问题
  • 普通深拷贝方法不会复制对象的原型链(__proto__Object.getPrototypeOf)。
  • 如果对象有自定义原型方法,拷贝后可能丢失。

解决方案

  • 手动设置原型:
1
2
3
4
5
6
7
function deepClone(obj, map = new WeakMap()) {
// ...(前面的逻辑)
const clone = Object.create(Object.getPrototypeOf(obj));
map.set(obj, clone);
// ...(拷贝属性)
return clone;
}
5. JSON.parse(JSON.stringify(obj)) 的局限性
  • 无法处理 undefinedfunctionSymbol → 会被忽略或报错。
  • 无法处理 Date → 会变成字符串。
  • 无法处理 RegExp → 会变成空对象 {}
  • 无法处理循环引用 → 会抛出错误。
总结
问题 解决方案
循环引用 使用 WeakMap 缓存已拷贝对象
特殊对象(Date/RegExp/Map/Set 手动处理
性能问题 使用 structuredClonelodash.cloneDeep
原型链丢失 手动设置 Object.create(proto)
JSON 方法的局限性 仅适用于纯数据

79、判断数组的方法

1. Array.isArray() (推荐)
1
2
Array.isArray([]); // true
Array.isArray({}); // false

这是 ES5 引入的最可靠的方法,也是目前推荐的方式。

2. instanceof 操作符
1
2
[] instanceof Array; // true
{} instanceof Array; // false

注意:这种方法在跨 frame 或跨 window 时会失效,因为不同全局环境有不同的 Array 构造函数。

3. constructor 属性
1
2
[].constructor === Array; // true
{}.constructor === Array; // false

同样存在跨 frame 问题,且 constructor 属性可以被修改。

4. Object.prototype.toString.call()
1
2
Object.prototype.toString.call([]) === '[object Array]'; // true
Object.prototype.toString.call({}) === '[object Array]'; // false
手写instanceof方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function myInstanceof(obj, constructor) {
// 基本类型直接返回 false
if (obj === null || typeof obj !== 'object') {
return false;
}

// 获取对象的原型
let proto = Object.getPrototypeOf(obj);

while (true) {
// 到达原型链顶端
if (proto === null) {
return false;
}

// 找到了匹配的构造函数原型
if (proto === constructor.prototype) {
return true;
}

// 继续向上查找原型链
proto = Object.getPrototypeOf(proto);
}
}

80、让中间页携带cookie的方法

1. 同源中间页(最简单情况)

如果中间页与主站同源,Cookie 会自动携带:

1
2
3
4
<!-- 同源跳转 -->
<a href="/middle-page">跳转到中间页</a>
<!-- 或 -->
window.location.href = "/middle-page";

运行 HTML

前端设置(需要后端配合)
1
2
3
4
// 使用 fetch 或 XMLHttpRequest 发送跨域请求时
fetch('https://other-domain.com/api', {
credentials: 'include' // 携带 cookies
});
后端必须设置:
1
2
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: 具体域名(不能是 *)
1
2
3
<iframe src="https://other-domain.com/middle-page" 
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals"
allow="cross-origin-isolated"></iframe>
4. 通过表单 POST 跳转携带 Cookie
1
2
3
<form method="post" action="https://other-domain.com/middle-page">
<input type="hidden" name="data" value="some-data">
</form>
5. 服务端代理方案

如果无法直接跨域,可以通过自己的服务端做代理:

81、Webpack工作流程

  1. 初始化:读取配置 → 创建Compiler → 加载插件
  2. 编译构建
    • 从入口开始递归解析依赖
    • 用Loader转换各模块生成 AST
    • 构建完整的模块依赖图
  3. 输出准备
    • 代码分割生成Chunk
    • 应用优化(如Tree Shaking)
    • 生成最终打包资源
  4. 输出结果:写入文件系统 → 完成构建

核心特点

  • 基于事件驱动的插件系统
  • 模块化处理(一切皆模块)
  • 增量构建(利用缓存优化)

一句话总结:Webpack通过解析依赖关系,将各种资源模块打包成静态文件,整个过程可被插件系统精细控制。

82、ES5实现继承的方式

  • 原型链继承(prototype)

  • 构造函数继承(借助 call)

  • 组合继承

  • 原型式继承(Object.create)

  • 寄生式继承

  • 寄生组合式继承

83、require 和 import 的区别

1. 来源不同
  • requireCommonJS 的模块导入语法(Node.js 默认使用)
  • importES6 (ES2015) 的模块导入语法
2. 加载时机
  • require运行时加载(动态加载)
  • import编译时加载(静态加载)
3. 主要区别
特性 require import
加载方式 同步/动态加载 异步/静态加载
模块解析 运行时解析 编译时解析
模块对象 可修改 只读引用
条件导入 支持 不支持(静态语法限制)
文件扩展名 通常需要显式指定 可省略(由打包工具处理)
默认导出 module.exports = ... export default ...
命名导出 exports.name = ... export const name = ...
4. 其他注意事项
  • 现代前端项目通常使用打包工具(如 Webpack、Rollup)将 import 转换为浏览器可识别的代码
  • import 支持 动态导入 (import()) 语法,这是异步的
5. 性能考虑

由于 import 是静态分析,打包工具可以进行更好的 tree-shaking(移除未使用的代码),而 require 的动态特性使得优化更困难。

84、js执行100万个任务,如何保证浏览器不卡顿

  1. 使用Web Workers

    Web Workers允许在后台线程中运行脚本,不阻塞主线程:

  2. 分片执行(使用setTimeout或requestIdleCallback)

    将大任务分解为小任务块:

  3. 使用requestAnimationFrame进行批处理

  4. 使用Generator函数和setTimeout

85、判断DOM元素是否在可视区域

方法一:使用 Element.getBoundingClientRect()
1
2
3
4
5
6
7
8
9
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
}

如果需要判断元素是否部分可见:

1
2
3
4
5
6
7
8
9
10
11
12
function isPartiallyInViewport(element) {
const rect = element.getBoundingClientRect();
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
const windowWidth = window.innerWidth || document.documentElement.clientWidth;

// 检查元素是否与视口垂直相交
const vertInView = (rect.top <= windowHeight) && (rect.top + rect.height >= 0);
// 检查元素是否与视口水平相交
const horInView = (rect.left <= windowWidth) && (rect.left + rect.width >= 0);

return (vertInView && horInView);
}
方法二:使用 Intersection Observer API(更高效,适合监听)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function observeElementVisibility(element, callback) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
callback(entry.isIntersecting);
});
});

observer.observe(element);
return observer; // 返回observer以便后续取消监听
}

// 使用示例
const target = document.querySelector('#myElement');
const observer = observeElementVisibility(target, (isVisible) => {
console.log('元素是否可见:', isVisible);
});

// 当不再需要时,可以取消观察
// observer.unobserve(target);
  1. getBoundingClientRect() 会触发回流(reflow),频繁使用可能影响性能
  2. Intersection Observer API 是现代浏览器推荐的方式,性能更好

86、图片懒加载的实现方式

1. 使用 loading="lazy" 属性(原生HTML5方案)
1
<img src="placeholder.jpg" data-src="actual-image.jpg" loading="lazy" alt="示例图片">
2. Intersection Observer API(推荐方案)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
document.addEventListener("DOMContentLoaded", function() {
const lazyImages = [].slice.call(document.querySelectorAll("img.lazy"));

if ("IntersectionObserver" in window) {
const lazyImageObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
const lazyImage = entry.target;
lazyImage.src = lazyImage.dataset.src;
lazyImage.classList.remove("lazy");
lazyImageObserver.unobserve(lazyImage);
}
});
});

lazyImages.forEach(function(lazyImage) {
lazyImageObserver.observe(lazyImage);
});
} else {
// 回退方案
lazyImages.forEach(function(lazyImage) {
lazyImage.src = lazyImage.dataset.src;
});
}
});
3. 传统滚动事件监听方案
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function lazyLoad() {
const images = document.querySelectorAll('img[data-src]');
const windowHeight = window.innerHeight;

images.forEach(img => {
const imgTop = img.getBoundingClientRect().top;

if (imgTop < windowHeight + 200) { // 提前200px加载
img.src = img.dataset.src;
img.removeAttribute('data-src');
}
});
}

// 防抖处理
const debounceLazyLoad = debounce(lazyLoad, 200);

// 初始加载
document.addEventListener('DOMContentLoaded', lazyLoad);
// 滚动时加载
window.addEventListener('scroll', debounceLazyLoad);
// 窗口大小变化时加载
window.addEventListener('resize', debounceLazyLoad);

function debounce(func, wait) {
let timeout;
return function() {
const context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
4. 第三方库实现
使用 lazysizes 库
1
2
3
4
5
6
7
8
9
10
11
12
<script src="https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.3.2/lazysizes.min.js"></script>

<!-- 基本用法 -->
<img data-src="image.jpg" class="lazyload" alt="示例图片">

<!-- 响应式图片 -->
<img
data-sizes="auto"
data-src="image.jpg"
data-srcset="image-400.jpg 400w, image-800.jpg 800w"
class="lazyload"
alt="示例图片">
使用 lozad.js
1
2
3
4
5
6
7
8
<script src="https://cdn.jsdelivr.net/npm/lozad/dist/lozad.min.js"></script>

<img data-src="image.jpg" class="lozad" alt="示例图片">

<script>
const observer = lozad('.lozad');
observer.observe();
</script>
-------------本文结束感谢您的阅读-------------