2025年|剑指前端offer
手写promise
基础架子
1 |
|
test.js
1 | const Promise = require('./promise.js') |
terminal
1 | $ npx promises-aplus-tests test.js |
前端路由
问题关键点
路由,hash路由,History路由,无刷新,SPA
路由是什么
- 路由(Routing)是指根据不同的URL地址(路径)来显示不同的内容或页面的机制。
- 服务端路由(Server Routing):URL发送到服务器,由服务器决定返回哪个页面或数据。
- 前端路由(Client-side Routing):在浏览器端,根据URL的变化动态切换页面内容,而无需重新加载整个页面。
路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。对于Web开发来说,路由的实质是URL到对应的处理程序的映射。Web路由既可以由服务端,也可以由前端实现。其中前端路由根据实现方式的不同,可以分为Hash路由和History路由。
前端路由对于服务端路由来说,最显著的特点是页面可以在无刷新的情况下进行页面的切换。基于前端路由的这一特点,诞生了一种无刷新的单页应用开发模式SPA。SPA通过前端路由避免了页面的切换打断用户体验,让Web应用的体验更接近一个桌面应用程序。
前端路由的实现方式和实现原理
- 前端路由主要通过监听URL的变化 来控制页面内容的渲染,常见实现方式有两种:
方法一:Hash模式 - URL中带#(例如:https://music.163.com/?from=infinity#/song?id=5308028)
- 浏览器不会向服务器发送#后面的内容
- 通过监听window.onhashchange来捕捉URL变化fragment 本质是用来标识次级资源,fragment有以下特点:
1
2
3
4
5
6
7foo://example.com:8042/over/there?name=ferret#nose
\_/ \______________/\_________/ \_________/ \__/
| | | | |
scheme authority path query fragment
| _____________________|__
/ \ / \
urn:example:animal:ferret:nose - 修改fragment的内容不会触发网页重载。
- 修改fragment的内容会改变浏览器的历史记录。
- 修改fragment的内容会触发浏览器的onhashchange事件。
- 基于fragment的以上特点,可实现基于Hash的前端路由。
原理: - 用户点击链接 → 改变hash
- hashchange事件触发
- 根据hash渲染对应的页面内容
- 优点:兼容性好,不会刷新页面, 无需服务端配置
- 缺点:URL不够美观,无法完全利用浏览器历史API,服务端无法获取
hash部分内容,可能和锚点功能冲突,SEO不友好。
M1M21
2
3
4window.addEventListener('hashchange', () => {
const route = location.hash.slice(1); // 去掉 #
render(route);
});方法二:History 模式(HTML5 History 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
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
51
52
53
54/**
* 解析 hash
* @param hash
* @returns
*/
function parseHash(hash) {
// 去除 # 号
hash = hash.replace(/^#/, "");
// 简单解析示例
const parsed = hash.split("?");
//如果 hash 中没有 ?,search 会是 undefined。
// 返回 hash 的 path 和 query
return {
pathname: parsed[0],
search: parsed[1],
};
//如果URL hash是#page?foo=1&bar=2
//const parsedHashRes = parseHash("#page?foo=1&bar=2");
//console.log(parsedHashRes);
// { pathname: "page", search: "foo=1&bar=2" }
//const params = new URLSearchParams(parsedHashRes.search);
//console.log(params.get("foo")) // "1"
//console.log(params.get("bar")) // "2"
/**
for (const [key, value] of params) {
console.log(key, value);
}
**/
//foo 1
//bar 2
}
/**
* 监听 hash 变化
* @returns
*/
function onHashChange() {
// 解析 hash
const { pathname, search } = parseHash(location.hash);
// 切换页面内容
switch (pathname) {
case "/home":
document.body.innerHTML = `Hello ${search}`;
return;
default:
return;
}
}
window.addEventListener("hashchange", onHashChange); - URL 看起来像正常路径(例如:http://example.com/home)
- 利用 history.pushState() 和 history.replaceState() 修改浏览器历史记录
- history.pushState:将给定的 Data 添加到当前标签页的历史记录栈中。
- history.replaceState:将给定的 Data 更新到历史记录栈中最新的一条记录中。
- 监听 popstate 事件来检测浏览器前进/后退
原理: - 用户点击链接 → 调用 pushState 改变 URL
- 渲染对应页面内容
- 用户按浏览器回退 → 触发 popstate 事件 → 渲染对应页面
- 优点:前端监控友好,SEO相对Hash路由友好,服务端可获取完整的链接和参数。
- 缺点:兼容性稍弱。,刷新页面会请求服务器,服务器需要配置重定向到入口页(各 path 均指向同一个 HTML如 index.html)
M1M21
2
3
4
5
6
7
8window.addEventListener('popstate', () => {
render(location.pathname);
});
function navigate(path) {
history.pushState({}, '', path);
render(path);
}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
26function onHistoryChange() {
const {pathname, search} = location;
switch(){
case "/home":
document.body.innnerHTML = `Hello ${search.replace(/^\?/,"")}`
return;
default:
document.body.innerHTML = "hello world";
return;
}
}
function pushState(target) {
history.pushState(null, "", target);
onHistoryChange()
}
setTimeout(() => {
pushState("/home?name=HZSDD")
},3000)
setTimeout(() => {
history.back()
},6000)
window.addEventListener("popstate", onHistoryChange)前端路由和服务端路由的区别
特性 前端路由 服务端路由 页面刷新 页面不刷新,只渲染局部 每次请求 URL 都刷新页面 URL 变化 浏览器地址栏变化,但请求不发送到服务器 URL 变化时,浏览器向服务器发请求 响应速度 快,局部渲染 慢,需要重新加载整个页面 SEO 不太友好(除非使用 SSR) 好,搜索引擎可直接抓取 技术实现 JS + hash/history API 服务器端路由框架(Express、Django、Rails 等) ### 前端路由的优势 - 体验更流畅:页面不刷新,用户操作即时响应
- 减少服务器压力:不需要每次请求都重新渲染整个页面
- 灵活控制页面渲染:可以按需加载组件或模块,提高性能
- 便于构建单页应用(SPA):结合 Vue、React、Angular 等框架使用
前端路由的不足
- 首次加载慢:单页应用需要一次性加载 JS 包
- SEO不友好:搜索引擎抓取困难,需要 SSR 或 prerender 解决
- 浏览器刷新问题:History 模式下刷新会向服务器发请求,服务器需配置重定向
- JS 依赖:如果用户禁用 JS,页面可能无法正常访问
参考资料:
- 剑指前端 Offer-前端路由实现
- chatgpt 4
- Hash嵌套路由解析 -> *Prac.
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88function parseHash(hash) {
hash = hash.replace(/^#/, "");
const [pathname, search] = hash.split("?");
const query = {};
if (search) {
const params = new URLSearchParams(search);
for (const [key, value] of params) {
const keys = key.split(/[\[\]]+/).filter(Boolean);
let parsedValue;
// 改进的类型转换逻辑
if (value === "") {
parsedValue = "";
} else if (!isNaN(value) && value.trim() !== "" && value.trim() !== "NaN") {
// 避免将 "NaN" 转换为数字
const num = Number(value);
parsedValue = String(num) === value.trim() ? num : value;
} else if (value.toLowerCase() === "true") {
parsedValue = true;
} else if (value.toLowerCase() === "false") {
parsedValue = false;
} else if (value === "null") {
parsedValue = null;
} else if (value === "undefined") {
parsedValue = undefined;
} else {
parsedValue = value;
}
let current = query;
for (let i = 0; i < keys.length; i++) {
const k = keys[i];
// 处理数组语法:tags[] 或 tags[0]
const isArrayKey = k === "" || (!isNaN(k) && k !== "");
if (i === keys.length - 1) {
// 最终赋值
if (isArrayKey) {
// 如果是数组语法,确保父级是数组
const parentKey = keys[i - 1];
if (i > 0 && current[parentKey] && !Array.isArray(current[parentKey])) {
current[parentKey] = [current[parentKey]];
}
if (Array.isArray(current)) {
current.push(parsedValue);
} else {
current[k] = parsedValue;
}
} else if (current[k] !== undefined) {
// 处理重复键名
if (!Array.isArray(current[k])) {
current[k] = [current[k]];
}
current[k].push(parsedValue);
} else {
current[k] = parsedValue;
}
} else {
// 遍历嵌套对象
if (!current[k] || typeof current[k] !== "object") {
current[k] = isArrayKey ? [] : {};
}
current = current[k];
}
}
}
}
return { pathname, query };
}
// 增强的测试用例
const testCases = [
"#page?foo=1&bar=2&bar=3&tags[]=a&tags[]=b&empty&flag=true&count=0&user[name]=Alice&user[age]=20",
"#home?ids[0]=1&ids[1]=2&filter[name]=John&filter[active]=true",
"#test?single=hello&number=42&bool=false&nullval=null",
"#empty",
"#path?arr=1&arr=2&obj[a]=1&obj[b]=2"
];
testCases.forEach(test => {
console.log(test);
console.log(parseHash(test));
console.log("---");
});webpack
问题1:请解释webpack的核心概念及其作用
1 | // 期待回答包含: |
问题2:Webpack 5 相比 Webpack 4 有哪些重大改进?请举例说明
1 | // 期待回答: |
问题3:请解释loader的执行顺序和编写自定义loader
1 | // 问题:以下loader的执行顺序是什么? |
问题4:请解释webpack插件机制并编写一个简单插件
1 | // 插件编写示例 |
问题5:如何优化webpack构建性能?请结合具体npm包说明
1 | const SpeedMeasurePlugin = require("speed-measure-webpack-plugin"); |
问题6:请解释Tree Shaking原理及如何确保其有效性
1 | // package.json 配置 |
=
问题7:请解释以下概念并说明其应用场景
1 | // Module Federation: 微前端架构,多个独立构建的应用共享代码 |
判断是否是平衡二叉树
前序遍历
1 |
|
后序遍历
1 | let isBalanced = function(root){ |
new操作符的模拟实现
1 |
|
防抖和节流
防抖:搜索/窗口大小变化
1 |
|
节流:滚动事件/按钮防止重复点击/鼠标移动和拖拽
1 |
|
反转链表
1 | // 迭代法 |
将列表还原为树状结构
递归
1 |
|
二叉搜索树的第k大节点
递归
1 | const kthLargest = () => { |
找的数组中重复的数字
哈希表
1 | const findRepeatNumber = function(nums){ |
交换
1 |
|
前端面试:BFC(块级格式化上下文)全面解析
1. 核心概念:什么是BFC?
标准回答:
BFC(Block Formatting Context)是 CSS 渲染过程中的一个独立区域。它决定了块级盒如何布局,并且与这个区域的外部毫不相干。简单来说,一个创建了 BFC 的元素,就像一个被隔离出来的独立容器,里面的子元素不会在布局上影响到外面的元素。
通俗解释:
“可以把它理解成 Web 页面里的一个’结界’。在这个’结界’内部,元素按照自己的规则进行排列,不受外部影响,同时内部的元素也不会跑出去影响外面的布局。”
2. 如何触发/创建BFC?
一个元素在满足以下任一条件时即可创建 BFC:
- 根元素(
<html>) - 整个页面就是一个最大的BFC - 浮动元素 -
float的值不是none - 绝对定位元素 -
position的值是absolute或fixed display为特定值 -inline-block、table-cell、table-caption、flex、inline-flex、grid、inline-gridoverflow值不是visible- 如overflow: hidden、scroll、auto(最常用且副作用较小的方式之一)contain值为layout、content、paint的元素display: flow-root(现代推荐方式,专门用于创建BFC,无副作用)面试技巧: 说完以上列表后,可以补充:”在实际开发中,我最常用的是
overflow: hidden和display: flow-root,因为flow-root是专门为创建BFC而设计的,没有任何副作用。”3. BFC的布局规则/特性
一个BFC区域内的布局遵循以下规则:
- 垂直排列 - 内部的块级盒会在垂直方向上一个接一个地放置
- 外边距重叠 - 垂直方向上的距离由
margin决定。属于同一个BFC的两个相邻块级盒的margin会发生重叠 - 接触包含块边界 - 每个元素的左外边距与包含块的左边界相接触(对于从左往右的格式化),即使存在浮动也是如此
- 不与浮动重叠 - BFC的区域不会与浮动元素重叠
- 独立容器 - BFC是一个独立的容器,容器里面的子元素不会影响到外面的元素,反之亦然
- 包含浮动元素 - 计算BFC的高度时,浮动元素也参与计算
4. BFC的常见应用场景
场景一:解决外边距重叠(Margin Collapse)
问题代码:
1 | <style> |
两个 .box 之间的垂直距离是 50px,而不是 100px,因为它们的 margin 发生了重叠。
BFC解决方案:
1 | <style> |
现在两个 .box 之间的垂直距离就是 100px,因为它们的 margin 处于不同的BFC中,不会重叠。
场景二:清除浮动,防止父元素高度塌陷
问题代码:
1 | <style> |
由于 .child 浮动脱离了文档流,.parent 的高度会塌陷为0。
BFC解决方案:
1 | <style> |
根据BFC规则:计算BFC的高度时,浮动元素也参与计算。所以父元素 .parent 的高度被撑开了。
场景三:阻止元素被浮动元素覆盖(实现两栏自适应布局)
问题代码:
1 | <style> |
主内容区 .main 会被浮动元素 .aside 覆盖一部分。
BFC解决方案:
1 | <style> |
根据BFC规则:BFC的区域不会与浮动元素重叠。所以 .main 会紧贴着 .aside 的右边框,形成两栏自适应布局。
5. 面试官可能追问的问题
Q: overflow: hidden 创建 BFC 有什么缺点?
A: 可能会意外地剪裁掉定位在容器之外的内容(如工具提示)。所以 display: flow-root 是更安全的选择。
Q: BFC 和 IFC(内联格式化上下文)有什么区别?
A: BFC 是针对块级元素的布局环境,规则是垂直排列;IFC 是针对内联元素的,规则是水平排列。
Q: BFC 和 Flex/Grid 布局有什么关系?
A: Flex容器和Grid容器本身会为它们的子项创建新的格式化上下文(FFC/GFC),这些上下文同样具有隔离性,可以解决类似BFC的问题(如margin重叠,清除浮动)。
6. 总结回答思路
当被问到BFC时,建议按照以下逻辑链回答:
- 下定义 - BFC是一个独立的渲染区域,内外互不影响
- 讲创建 - 列举多种触发BFC的方式,指出常用和推荐的
- 说规则 - 阐述BFC的核心布局规则
- 举例子 - 结合代码说明BFC如何解决实际开发中的三大经典问题
跨域
跨域问题,主要是因为浏览器同源策略,同源策略限制了脚本从 another-domain.com 访问 another-domain.com/api/data。
解决方案:JSONP,CORS,反向代理,页面通信postMessage。
跨域与监控
前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在 <script> 标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈。