前端面试常见题汇总(持续更新中...)
# 汇总
# 网络
UDP特点:无连接、传输速度快、不保证传输可靠性,适用于音视频场景。
怎么保证UDP的接收顺序:在包头加上序号。
TCP为什么要四次挥手:确保双方都已经准备好断开连接,比如服务端发出断开请求,然后客户端返回了ACK,但是这个ACK仅仅表示客户端收到了断开请求,而不是同意了断开,因为客户端可能还有数据在发送,所以还要客户端准备好了再向服务端发送断开请求,这时候已经3次挥手了,还需要服务端返回确认帧来告诉客户端它收到了断开请求。然后才能保证双方都做好了断开连接的准备。
HTTP状态码:
分类:
- 1xx:表示消息;
- 2xx:表示成功;
- 3xx:表示重定向;
- 4xx:表示请求错误;
- 5xx:表示服务器错误。
1xx:
表示请求被接收,需要继续处理,临时响应。
- 100:这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应。
- 101:服务器根据客户端的请求切换协议,主要用于websocket或http2升级。
2xx:
表示请求已成功被服务器接收、理解并接受。
- 200(成功):请求已成功,请求所希望的响应头或数据体将随之返回。
- 201(已创建):请求成功并且服务器创建了新的资源。
- 202(已创建):服务器已经接收请求,但尚未处理。
- 203(非授权消息):服务器已成功处理请求,但返回的信息可能来自另一来源。
- 204(无内容):服务器成功处理请求,但没有返回任何内容。
- 205(重置内容):服务器成功处理请求,但没有返回任何内容。
- 206(部分内容):服务器成功处理了部分请求。
# 安全
xss(跨站脚本攻击):允许攻击者将恶意代码植入到提供给其它用户使用的页面中。这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
分类:
存储型:
(1) 攻击者将恶意代码提交到目标网站的数据库中;
(2) 用户打开目标网站时,网站服务端将恶意代码从数据库取出,返回至浏览器;
(3) 浏览器接收到响应后解析执行,混在其中的恶意代码也被执行;
(4) 恶意代码窃取用户数据并发送到攻击者的服务器,或冒充用户行为,调用目标网站接口执行攻击者指定的操作。
反射型:常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
(1) 攻击者构造出特殊的URL,包含恶意代码;
(2) 用户打开带有恶意代码的URL时,网站服务端将恶意代码从URL取出,拼接在HTML中返回给浏览器;
(3) 用户浏览器接收到响应后解析执行,其中的恶意代码也被执行;
(4) 恶意代码窃取用户数据并发送到攻击者的服务器,或冒充用户行为,调用目标网站接口执行攻击者指定的操作。
==反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。==
DOM型:
(1) 攻击者构造出特殊的URL,包含恶意代码;
(2) 用户浏览器接收到响应后解析执行,其中的恶意代码也被执行;
(3) 恶意代码窃取用户数据并发送到攻击者的服务器,或冒充用户行为,调用目标网站接口执行攻击者指定的操作。
预防:
- 攻击者提交恶意代码:
- 用户输入过程中,前端过滤用户输入的恶意代码。但是如果攻击者绕开前端请求,直接构造请求就不能预防了。
- 在后端写入数据库前,对输入进行过滤,然后把内容给前端,但是这个内容在不同地方就会有不同显示。
- 浏览器执行恶意代码:
- 在使用
.innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用.textContent
、.setAttribute()
等。 - 如果用
Vue/React
技术栈,并且不使用v-html
/dangerouslySetInnerHTML
功能,就在前端render
阶段避免innerHTML
、outerHTML
的 XSS 隐患。 - DOM 中的内联事件监听器,如
location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的href
属性,JavaScript 的eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
- 在使用
csrf(跨站请求伪造):攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。
典型的CSRF攻击:
(1) 受害者登录a.com,并保留了登录凭证(Cookie);
(2) 攻击者引诱受害者访问了b.com;
(3) b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie;
(4) a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发 送的请求;
(5) a.com以受害者的名义执行了act=xx;
(6) 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操 作。
特点:
- 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生。
- 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据。
- 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用”。
- 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪。
预防:
- 阻止不明外域的访问
- 同源检测
- Samesite Cookie
- 提交时要求附加本域才能获取的信息
- CSRF Token
- 双重Cookie验证
sql注入:将恶意的
Sql
查询或添加语句插入到应用的输入参数中,再在后台Sql
服务器上解析执行进行的攻击。注入流程:
- 找出SQL漏洞的注入点;
- 判断数据库的类型以及版本;
- 猜解用户名和密码;
- 利用工具查找Web后台管理入口;
- 入侵和破坏。
预防:
- 严格检查输入变量的类型和格式;
- 过滤和转义特殊字符;
- 对访问数据库的Web应用程序采用Web应用防火墙。
# 工程化
webpack:
- 打包过程:识别入口文件、针对import关键字识别模块依赖、分析代码、转换代码、编译代码、输出代码。
- loader:文件加载器,比如css-loader,ts-loader等可以对文件进行相应的转译(如编译、压缩)。
- plugin:作为扩展使用,增强webpack的能力,webpack运行的生命周期会广播出许多时间,plugin通过监听这些事件,在适当的时机通过webpack提供的api改变输出结果。
# 浏览器
重绘:由于
节点几何属性
或样式
发生改变而不会影响布局
。如outline
、visibility
、color
、background-color
。回流:布局发生改变。(影响浏览器性能的因素,因为其变化涉及到部分页面或整个页面的布局更新)。
何时发生回流:
- 添加或删除可见DOM元素;
- 元素位置发生改变;
- 元素尺寸发生改变;
- 内容发生改变;
- 页面开始渲染;
- 浏览器窗口尺寸发生改变。
备注:回流一定发生重绘,重绘不一定发生回流。
浏览器渲染过程:
(1) 解析HTML,生成DOM树;同时解析样式表,生成CSS树;
(2) 合并DOM树和CSSOM树,生成渲染树;
(3) Layout(回流):根据生成的渲染树,进行回流,得到节点的几何信息(位置、大小);
(4) Painting(重绘):根据渲染树及回流得到的几何信息,得到节点的绝对像素;
(5) Display:将像素发送给GPU,展示在页面上(GPU将合成层合并为一个层,并展示在页面中)。
浏览器构建渲染树流程:
(1) 遍历DOM树的某个可见节点;
(2) 对于每个可见节点,从CSSOM树中找到对应的规则并应用;
(3) 根据每个可见节点及其对应样式,组合生成渲染树。
备注(不可见节点):
- script、link、meta等
- 通过css隐藏的节点。如
display: none
,visibility和opactity隐藏的节点仍会在渲染树中。
# HTML/CSS
# CSS3加速原理:
# BFC(Block Formatting Context):
# 触发BFC的CSS:
- overflow: hidden
- float: left/right
- display: inline-block
- position: absolute/fixed
- display: table-cell/flex/grid
# BFC规则:
BFC
就是一个块级元素,块级元素会在垂直方向一个接一个的排列;BFC
就是页面中的一个隔离的独立容器,容器里的标签不会影响到外部标签;垂直方向的距离由margin决定, 属于同一个
BFC
的两个相邻的标签外边距会发生重叠;计算
BFC
的高度时,浮动元素也参与计算。
# BFC解决问题:
- 使用float脱离文档流,高度塌陷;
- margin边距重叠;
- 两栏布局问题。
# JS
- ES6模块和CommonJs的区别:
# React
- virtual dom:react中render方法得到的并不是真实的dom节点,而是保存于内存中的js对象。
- 抽象了渲染过程,使得可以更好地实现跨平台。
- 初次渲染比较慢,因为中间需要计算虚拟节点树。
- 对于节点比较少的页面,频繁的更新节点使用虚拟dom效率反而不高。
- diff算法:
- 提升界面渲染的速度和性能,计算出真正改变的节点,不需要重新渲染整个页面
- 三大算法:
- tree diff:节点树之间的比较,只比较同层级的节点。对于跨层级的节点移动,在react中只会有创建和、删除的操作。比如A节点有B、C两个叶子节点,新的节点树变成了A节点的子节点只有B,而B节点在子节点是C。此时react有两个操作:
删除A节点下的C节点
和在B节点下创建C节点
。(一般不建议这么做) - component diff:如果是同一类组件,那么继续按照同层级的比较(用户可以通过
shouldComponentUpdate()
的方法来决定是否继续进行比较),如果不是同类型的组件,那么就替换整个组件下的所有子节点。 - element diff:react对于同层级的节点的更新提供了创建、删除、移动三种操作,通过节点的key和diff来判断新老节点。
- tree diff:节点树之间的比较,只比较同层级的节点。对于跨层级的节点移动,在react中只会有创建和、删除的操作。比如A节点有B、C两个叶子节点,新的节点树变成了A节点的子节点只有B,而B节点在子节点是C。此时react有两个操作:
- ref:操作原生js的一个桥梁/通道。
- ErrorBoundary:作用类似于try catch,主要是为了防止js执行错误时页面白屏,当js报错时可以跳转到预留的页面,当然,怎么处理报错可以工具自己需求调整。
# 手写函数
# Debounce
const debounce = (func, ms) => {
let timer = null;
return function (...rest) {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
func.apply(this, rest);
}, ms);
};
};
2
3
4
5
6
7
8
9
10
11
# Throttle
// 方式1
const throttle = (func, ms) => {
let timestamp = Date.now();
return function (...rest) {
if (Date.now() - timestamp < ms) {
return;
}
func.apply(this, rest);
timestamp = Date.now();
};
};
// 方式2
const throttle = (func, ms) => {
let flag = true;
return function (...args) {
if (!flag) return;
flag = false;
setTimeout(() => {
func.apply(this, args);
flag = true;
}, ms);
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 函数柯里化
const curry = func => {
return function curried(...args) {
// 当实参数量大于/等于func定义的形参
if (args.length >= func.length) {
return func.apply(this, args);
}
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
};
};
2
3
4
5
6
7
8
9
10
11
# 深拷贝
const deepClone = target => {
if (typeof target !== 'object') {
return target;
} else if (Array.isArray(target)) {
const arr = [];
target.forEach(el => {
if (typeof el !== 'object') {
arr.push(el);
} else {
arr.push(deepClone(el));
}
});
return arr;
} else if (target instanceof RegExp) {
return new RegExp(target.source, target.flags);
} else if (target instanceof Date) {
return new Date(target);
} else if (target instanceof Function) {
return function () {
return target.apply(this, arguments);
};
} else {
const obj = {};
Object.keys(target).forEach(key => {
if (typeof target[key] !== 'object') {
obj[key] = target[key];
} else {
obj[key] = deepClone(target[key]);
}
});
return obj;
}
};
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
# 数组扁平化
const arrayFlatten = arr => {
const newArr = [];
arr.forEach(el => {
if (Array.isArray(el)) {
newArr.push(...arrayFlatten(el));
} else {
newArr.push(el);
}
});
return newArr;
};
// [1, [2, [3]]] => [1, 2, 3]
2
3
4
5
6
7
8
9
10
11
12
13
# 对象扁平化
const objectFlatten = (obj, key = '', newObj = {}) => {
Object.keys(obj).forEach(k => {
if (typeof obj[k] !== 'object') {
newObj[`${key}${k}`] = obj[k];
} else if (Array.isArray(obj[k])) {
obj[k].forEach((el, i) => {
if (typeof el !== 'object') {
newObj[`${key}${k}[${i}]`] = obj[k][i];
} else {
objectFlatten(el, `${key}${k}.`, newObj);
}
});
} else {
objectFlatten(obj[k], `${key}${k}.`, newObj);
}
});
return newObj;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 对象反扁平化
const reverseObjectFlatten = (obj, newObj = {}) => {
Object.keys(obj).forEach(el => {
const keys = el.split('.');
let temp = newObj;
const cur = temp;
keys.forEach((key, i) => {
const match = key.match(/.+(?=(\[[0-9]+\])$)/);
if (match) {
if (!temp[match[0]]) {
temp[match[0]] = [];
}
const index = parseInt(match[1][1]);
if (i === keys.length - 1) {
temp[match[0]][index] = obj[el];
} else {
temp[match[0]][index] = {};
}
temp = temp[match[0]][index];
} else {
if (i === keys.length - 1) {
temp[key] = obj[el];
} else {
if (!temp[key]) {
temp[key] = {};
}
temp = temp[key];
}
}
});
Object.assign(newObj, cur);
});
return newObj;
};
// { a: 1, 'b.c': 3, 'b.arr[0].d': 1, 'b.arr[1]': 2 }
// => { a: 1, b: { c: 3, arr: [ {d: 1}, 2 ] } }
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
# 发布订阅
class EventEmitter {
constructor() {
this.callback = {};
this.curId = 0;
}
subscribe(name, cb, once = false) {
if (!this.callback[name]) {
this.callback[name] = [];
}
this.callback[name].push({ id: this.curId++, once, cb });
}
once(name, cb) {
this.subscribe(name, cb, true);
}
notify(name) {
if (this.callback[name]) {
this.callback[name].forEach(item => {
item.cb();
if (item.once) {
this.cancel(name, item.id);
}
});
}
}
cancel(name, id) {
if (this.callback[name]) {
this.callback[name] = this.callback[name].filter(item => item.id !== id);
}
}
}
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
# 图片懒加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载</title>
<style>
.image-container {
overflow: scroll;
width: 90vh;
height: 100vh;
margin-left: 50%;
transform: translateX(-50%);
background-color: azure;
}
.img {
width: 100%;
height: 25%;
}
</style>
</head>
<body>
<div class="image-container">
<img class="img" data-src="./images/1.jpg">
<img class="img" data-src="./images/2.jpg">
<img class="img" data-src="./images/3.jpg">
<img class="img" data-src="./images/4.jpg">
<img class="img" data-src="./images/5.jpg">
</div>
</body>
<script>
const lasyLoad = (entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
} else {
entry.target.src = '';
}
});
}
const observer = new IntersectionObserver(lasyLoad, {
threshold: 0, // 阈值,父子元素交集大小超过该值就会执行回调函数,默认为0
});
const imgs = document.getElementsByClassName('img')
for (let el of imgs) {
observer.observe(el)
}
</script>
</html>
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