外观
前端通识
HTML基础
HTML 页面的渲染
本文参考:html+CSS+js解析全过程。
主流程
从浏览器请求html文件,到屏幕上出现渲染的实际像素等,可以分为以下部分:
- 构建 DOM Tree
- 构建 CSSOM Tree
- 合成 Render Tree
- layout 回流:主要涉及到字体大小、元素长宽等 CSS 属性,计算元素的相对位置
- repaint 重绘:颜色等 CSS 属性,显示在屏幕上
解析HTML文件的细节
- 解析 DOM 元素
- 遇 script,DOM 解析暂停
- 包含 JS 代码
- 外联:加载 JS
- 执行 JS(该 JS 代码在 CSS 前,则不受 CSSOM 的阻塞)
- 遇 link CSS
- 加载 CSS 文件,不阻塞 DOM 解析
- 遇到 script:JS 加载,等待 CSSOM Tree 构建完成后再运行
- 防止 JS 代码执行时 获取旧的 CSS 属性
- 遇到 script:JS 加载,等待 CSSOM Tree 构建完成后再运行
- 构建 CSSOM Tree,DOM 继续解析
- 若有 JS,则运行
- 构建 DOM Tree
- 渲染
DOMContentLoaded 与 load 事件
- DOMContentLoaded 事件:在完成 DOM 解析完成,JS 执行完毕后触发。大多数浏览器也会等到 CSS 文件加载并解析完成。
- load 事件:所有外部资源与文件下载完毕后触发。
关于阻塞/非阻塞
- 内联 JS 会阻塞 DOM 的解析。
- 内联 CSS 会阻塞 DOM 解析。
- 外联 CSS 加载不阻塞 DOM 解析,阻塞 DOM 渲染。
- 外联 CSS 加载阻塞后续 script 内的 JS 代码执行(不阻塞前面的)
- JS 文件的普通加载与执行(非 async defer)会阻塞 DOM 的解析
- 因此实际上,后面有 script 的外联 CSS 会阻塞DOM的解析
<script defer>不会阻塞 DOM 的解析<script async>加载不阻塞,执行阻塞- iframe 内的 image 等资源不阻塞 DOM 解析
DOM 树与 CSSOM 树的构建
- DOM Tree 增量构建,而 CSSOM Tree 非渐进。
常用 meta 标签
常用 meta 标签
html
<!-- 设定页面使用的字符集 -->
<meta charset="utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<!-- 优先使用 IE 最新版本和 Chrome -->
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<!-- 国产360浏览器默认采用高速模式渲染页面 -->
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="
width=device-width,
initial-scale=1,
user-scalable=no
"
/>
<!-- 禁止设备检测手机号和邮箱 -->
<meta name="format-detection" content="telephone=no,email=no" />
<!-- QQ强制全屏 -->
<meta name="x5-fullscreen" content="true" />
<!-- UC强制全屏 -->
<meta name="full-screen" content="yes" />
<!-- uc强制竖屏 -->
<meta name="screen-orientation" content="portrait" />
<!-- QQ强制竖屏 -->
<meta name="x5-orientation" content="portrait" />
<!-- UC应用模式 -->
<meta name="browsermode" content="application" />
<!-- QQ应用模式 -->
<meta name="x5-page-mode" content="app" />
<!-- windows phone 点击无高光 -->
<meta name="msapplication-tap-highlight" content="no" />
<!--
针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,
比如黑莓
-->
<meta name="HandheldFriendly" content="true" />
<!-- 设置 web 应用在 iOS 设备中是否启用全屏模式 -->
<meta name="apple-mobile-web-app-capable" content="yes" />
<!--
设置 iOS 设备中 web 应用的状态栏样式。
只在用 `apple-apple-mobile-web-app-capable`
+ 开启全局模式后生效
-->
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!-- 启用/禁用 iOS Safari 浏览器中自动检测手机号的功能 -->
<meta name="format-detection" content="telephone=no" />
<!--
用于设定禁止浏览器从本机的缓存中读取页面内容。
设定后一旦离开网页就无法从缓冲中再读取
-->
<meta http-equiv="pragma" content="no-cache" />
<!-- 禁用缓存(再次访问需重新下载页面) -->
<meta http-equiv="cache-control" content="no-cache" />
<!--
可以用于设定网页的到期时间。
一旦网页过期,必须到服务器上重新传输
-->
<meta http-equiv="expires" content="0" />
<!-- 停留 2 秒钟后自动刷新到 URL 网址 -->
<meta http-equiv="Refresh" content="2;URL=http://www.example.com/" />
<!-- 用于 SEO,其中 description 的内容应不超过 150 个字符 -->
<meta http-equiv="keywords" content="keyword1,keyword2,keyword3" />
<meta http-equiv="description" content="This is my page" />
<!--
强制页面在当前窗口以独立页面显示,
用来防止别人在 iframe 里调用自己的页面
-->
<meta http-equiv="Window-target" content="_top" />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
88
89
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
88
89
<script /> 标签
本文参考资料如下:
script 标签用于嵌入可执行脚本或数据,一般用于嵌入一段 JavaScript 脚本,或者指向一个 JavaScript 文件。script 标签也可用于其他语言,比如 WebGL 的 GLSL shader 编程语言脚本,或者 JSON 数据等。
用 script 标签载入数据
注意,如果用 script 标签来载入数据(而非脚本):
- 这些数据必须是以内联的方式嵌入的,并且需要通过 type 属性指定数据的格式。
- 同时需要禁止使用以下属性:
src、async、nomodule、defer、crossorigin、integrity、referrerpolicy、fetchpriority。
比如这样是可以的:
html
<script id="data" type="application/json">
{ "a": "123" }
</script>
<script>
const jsonData = JSON.stringify(document.querySelector("#data").textContent);
</script>1
2
3
4
5
6
2
3
4
5
6
但是下面这样是不可以的:
html
<!-- 这样直接通过 src 属性去指向一个数据文件的方式是不可以的 -->
<script
id="data"
type="application/json"
src="https://bla.bla.com/blabla.json"
></script>1
2
3
4
5
6
2
3
4
5
6
模板语言
script 标签有个特点是其中的内容不会直接展现在页面上,所以有很多前端模板语言会使用 script 标签来存放 html 模板。我们可以自己写个简单的,像下面这样:
html
<div class="userInfo"></div>
<script id="template" type="text/template">
<div>
<div class="name">{{ userName }}</div>
<div class="address">{{ userAddress }}</div>
</div>
</script>
<script type="application/javascript">
const templateContent = document.querySelector("#template").textContent;
const templateData = {
userName: "name",
address: "address",
};
// 一般模版语言的渲染函数实际干的内容类似下面这样
document.querySelector(".userInfo").innerHTML = templateContent
.replace(/\{\{ userName \}\}/m, templateData.userName)
.replate(/\{\{ address \}\}/m, templateData.address);
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script defer>、<script async> 脚本的下载、解析动作与 HTML 解析的时序关系
从下图可以看书,除了 <script> 会暂停 HTML 的解析,其他比如 <script defer>、<script async>、<script type="module"> 脚本的下载与 HTML 的解析都是并行不会暂停 HTML 的解析的。
<script async>:
- 只对外部脚本有效。
- 请求该脚本的网络请求是异步的,不会阻塞浏览器解析 HTML。
- 多个 async script 之间的执行顺序是不确定的,取决于谁先被下载完毕。
- 多个 async script 之间的下载开始时间与书写顺序一致,他们是可以并行下载的,下载结束的时间顺序是不确定的。
- 下载完毕后会被直接解析执行,如果此时 HTML 还未被解析完,浏览器会暂停解析 HTML,先执行 JS 脚本。
- async script 一定会在页面的 load 事件之前执行,但与 DOMContentLoaded 事件则没有确定的先后关系。
<script defer>:
- 只对外部脚本有效。
- 请求该脚本的网络请求是异步的,不会阻塞浏览器解析 HTML。
- 多个 defer script 之间的执行书序和书写顺序一致。
- 多个 async script 之间的下载开始时间与书写顺序一致,他们是可以并行下载的,下载结束的时间顺序是不确定的。
- 下载完成后 JS 脚本不会立即执行,会等到浏览器解析完 HTML 后再执行 JS 脚本。
- 赋予 defer 属性的 script 脚本会在 HTML 文档解析完成后被执行,在此之后才会触发 DOMContentLoaded 事件。
CSS基础
三栏布局
要求
分为左、中、右三部分,高度均为屏幕高度,左边部分宽度为200px,另外两部分等分剩下的页面宽度。
实现
HTML 片段:
html
<html>
<head></head>
<body>
<div class="container">
<aside class="left">Left</aside>
<div class="wrapper">
<article class="middle">Middle</article>
<article class="right">Right</article>
</div>
</div>
</body>
</html>1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
LESS 样式代码:
less
.clearfix() {
&:after {
content: "";
clear: both;
display: block;
height: 0;
opacity: 0;
visibility: hidden;
}
}
html,
body,
div,
aside,
article {
margin: 0;
padding: 0;
}
html,
body,
.container,
.left,
.wrapper,
.middle,
.right {
height: 100%;
}
.container {
padding-left: 200px;
.clearfix();
.left {
float: left;
width: 200px;
margin-left: -200px;
background-color: skyblue;
}
.wrapper {
float: left;
width: 100%;
.middle,
.right {
float: left;
width: 50%;
}
.middle {
background-color: gray;
}
.right {
background-color: yellow;
}
}
}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
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
input 框加入 disabled 属性后字体颜色变淡
解决方案
css
input[disabled] {
opacity: 1;
}1
2
3
2
3
z-index
z-index 管理方案
建议使用CSS预处理器语言的情况下,对所有涉及z-index的属性的值放在一个文件中统一进行管理。这个主意是从饿了么前端团队代码风格指南中看到的。另外补充一下,应该将同一条直系链里同一层级的元素的z-index分类到一起进行管理。因为不同层级或者非直系链里的同一层级的元素是无法直接根据z-index来判断元素前后排列顺序的。
图片在父元素中水平、垂直居中
方案 1:(flex 布局)
less
.parent {
display: flex;
align-items: center;
justify-content: center;
}1
2
3
4
5
2
3
4
5
方案 2(使用 absolute 绝对定位)
less
.parent {
position: relative;
display: block;
.img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
方案 3(使用 table-cell)
less
.parent {
display: table-cell;
// width要写得大一点,以撑满容器之外部容器的宽度
width: 3000px;
text-align: center;
vertical-align: middle;
.img {
display: inline-block;
vertical-align: middle;
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
方案 4(如果父元素的高度为已知的定值,使用 line-height 实现)
less
.parent {
display: block;
text-align: center;
height: 300px;
line-height: 300px;
.img {
display: inline-block;
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
方案 5(写死间距)
less
.parent {
display: block;
.img {
display: block;
height: 100px;
margin: 150px auto 0;
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
方案 6(写死定位)
less
.parent {
position: relative;
display: block;
width: 600px;
height: 400px;
.img {
position: absolute;
width: 100px;
height: 300px;
top: 50px;
left: 250px;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
方案 7(撑开外部容器)
less
.parent {
// 包围内部元素
display: inline-block;
.img {
// 用来撑开父元素
padding: 30px 20px;
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
方案 8(作为背景图)
less
.parent {
display: block;
height: 300px;
background: transparent url("./attachments/logo.svg") scroll no-repeat center
center;
background-size: 100px 200px;
}1
2
3
4
5
6
7
2
3
4
5
6
7
弹性盒(Flexible Box)模型
justify-content
flex-start:默认值,伸缩项目向一行的起始位置靠齐;flex-end:伸缩项目向一行的结束位置靠齐;center:项伸缩项目向一行的中间位置靠齐;space-between:伸缩项目会平均地分布在行里。第一个伸缩项目一行中的最开始位置,最后一个伸缩项目在一行中最终点位置;space-around:伸缩项目会平均地分布在行里,两端保留一半的空间;initial:设置该属性为它的默认值;inherit:从父元素继承该属性。
align-items
stretch:默认值,项目被拉伸以适应容器;center:项目位于容器的中心;flex-start:项目位于容器的开头;flex-end:项目位于容器的结尾;baseline:项目位于容器的基线上;initial:设置该属性为它的默认值;inherit:从父元素继承该属性。
弹性盒实现竖向九宫格
要求
使用 flexbox 布局将 9 个格子排列成 3*3 的九宫格,且第一列排完才排第二列。
html
<html>
<head></head>
<body>
<section class="boxes-wrapper">
<div class="box">1</div>
<div class="box">2</div>
<div class="box">3</div>
<div class="box">4</div>
<div class="box">5</div>
<div class="box">6</div>
<div class="box">7</div>
<div class="box">8</div>
<div class="box">9</div>
</section>
</body>
</html>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
实现
less
body {
margin: 0;
}
.boxes-wrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
flex-wrap: wrap;
gap: 10px;
width: 320px;
height: 320px;
.box {
background-color: aqua;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
清除浮动的原理
如何清除浮动
清除浮动使用 clear: left/right/both。业界常用的 .clearfix 也是这么做的,只不过是把该样式写进了父元素的 :after 伪元素中,并加了 opacity: 0; display: block; height: 0; visibility: hidden; 等使伪元素不可见。
不清除浮动但包围浮动元素的方法
为浮动元素的父元素添加 overflow: hidden、或将父元素也浮动起来等使父元素形成 BFC(Block Formatting Context) 的方式,但这些方式在应用上没有 .clearfix 这种方式理想。
简述 position 属性各个值的区别
position 属性各个值的区别
fixed:类似absolute,但是是相对浏览器窗口而非网页页面进行定位。absolute:相对最近的position值非static的外层元素进行定位。relative:相对自身在文档流中的原始位置进行定位。static:position默认值,即元素本身在文档流中的默认位置(忽略top、bottom、left、right和z-index声明)。inherit:继承父元素position属性的值。
边距塌陷及其修复
边距塌陷的计算
竖直方向上相接触的 margin-top、margin-bottom 会塌陷:
- 若二者均为正值,或均为负值,取其绝对值大者;
- 若二者中一正一负,则取二者之和。
边距塌陷的修复方案
方案 1:父容器开启 BFC(最常用,修复父子塌陷)
BFC(块级格式化上下文)元素内部不会和外部 margin 合并,给父级加任意一种即可:
css
/* 任选其一 */
.parent {
/* 1. 浮动(副作用:脱离文档流) */
float: left;
/* 2. 绝对定位(脱离文档流) */
position: absolute;
/* 3. overflow 非 visible(推荐,无副作用) */
overflow: hidden;
/* 4. display 新布局模式(强力推荐) */
display: inline-block;
display: flow-root; /* 专门创建BFC,无任何副作用,最优解 */
/* 5. 弹性/网格布局,子元素完全消除塌陷 */
display: flex;
display: grid;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
最优写法:display: flow-root; 专门用于生成 BFC,不会改变布局、无副作用。
方案 2:父容器添加隔断(简单粗暴,适合简单页面)
在父子之间用 border / padding 隔断 margin,阻断穿透:
css
.parent {
/* 加透明边框,不影响视觉 */
border: 1px solid transparent;
/* 或添加极小内边距 */
padding-top: 0.1px;
}1
2
3
4
5
6
2
3
4
5
6
方案 3:改用 padding 替代子元素 margin(规避塌陷)
不使用子元素 margin-top,给父容器设置 padding-top,从根源避免塌陷:
css
/* 塌陷写法 */
.parent {
}
.child {
margin-top: 20px;
}
/* 修复写法,无塌陷 */
.parent {
padding-top: 20px;
}
.child {
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
方案 4:兄弟元素塌陷专用修复
兄弟塌陷无法用 BFC 解决,两种思路:
给中间加隔断元素(空标签 / 伪元素)
css
/* 兄弟之间插入伪元素隔断margin */
.brother1::after {
content: "";
display: block;
height: 0.1px;
}1
2
3
4
5
6
2
3
4
5
6
布局切换 flex/grid(一劳永逸,无塌陷)
css
.box-wrap {
display: flex;
flex-direction: column;
gap: 20px; /* 直接用gap代替margin,完美避开塌陷 */
}1
2
3
4
5
2
3
4
5
flex/grid 的 gap 间距完全不会出现 margin 合并问题,现代布局首选。
方案 5:空元素自身塌陷修复
给空块设置最小高度、边框、padding 任意一种:
css
.empty-box {
min-height: 1px;
/* 或 border/padding */
}1
2
3
4
2
3
4
高性能动画
CSS 动画会比 JS 动画的性能更好,JS 动画的优势主要在于:
- 更具定制性(毕竟 JS 比 CSS 更可编程);
- 更易实现对低端浏览器的兼容。
当然,大部分业务中,主要还是使用 CSS 动画的,对低端浏览器进行降级就可以了(保证页面可读可操作就可以了,不建议增加老旧设备的性能负担)。
几个高性能动画注意点
- 利用
transform: translate3d(x, y, z);可借助 3D 变形开启 GPU 加速(这会消耗更多内存与功耗,确有性能问题时再考虑)。 - 若动画开始时有闪烁,可尝试:
backface-visibility: hidden; perspective: 1000;。 - 尽可能少用
box-shadows和gradients这两页面性能杀手。 - CSS 动画属性可能会触发整个页面的重排(reflow/relayout)、重绘(repaint)和重组(recomposite)。其中 paint 通常是最花费性能的,尽可能避免使用触发 paint 的 CSS 动画属性。所以要尽可能通过修改
translate代替修改top/left/bottom/right来实现动画效果,可以减少页面重绘(repaint),前者只触发页面的重组,而后者会额外触发页面的重排和重绘。 - 尽量让动画元素脱离文档流(document flow)中,以减少重排(reflow)。
- 操作 DOM 的 js 语句能连着写尽量连着写,这样可借助浏览器的优化策略,将可触发重排的操作放于一个队列中,然后一次性进行一次重排;如果操作 DOM 的语句中间被其他诸如赋值语句之类的间断了,页面可能就会发生多次重排了。
JavaScript基础
JavaScript数据类型
- 基本数据类型(primitive data type):共 7 个,分别是:
Undefined、Null、Boolean、Number、String、Symbol、BigInt。 - 对象数据类型
BigInt
bigint 是基础数据类型。通过在整数末尾添加 n,或者通过调用 BigInt(传入整数或字符串),可以创建一个 bigint 类型的值。
javascript
const previouslyMaxSafeInteger = 9007199254740991n;
// 十进制
const alsoHuge = BigInt(9007199254740991);
// 9007199254740991n
// 十进制
const hugeString = BigInt("9007199254740991");
// 9007199254740991n
// 十六进制
const hugeHex = BigInt("0x1fffffffffffff");
// 9007199254740991n
// 八进制
const hugeOctal = BigInt("0o377777777777777777");
// 9007199254740991n
// 二进制
const hugeBin = BigInt(
"0b11111111111111111111111111111111111111111111111111111",
);
// 9007199254740991n1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
注意:
- 因为类型不同,
10n === 10的值为false(10n == 10的值为true)。 bigint值不支持用Math对象的原生方法进行处理。typeof 1n === "bigint";为true。typeof Object(1n) === "object";为true。
call、apply 和 bind
call:func.call(obj, param1, param2)
将 func 函数应用于 obj 对象上,此时 func 函数内部的 this 指向 obj 对象。
apply: func.apply(obj, [arg1, arg2])
与 call 类似,只是所有要传入的数据都是以数组的形式放到第二个参数里的,如 func.apply(obj, [arg1, arg2])。
一个经典用法是来求数组中的最大数:Math.max.apply(null, [1, 3, 5])。
另一个经典用法是用数组方法去处理非数组对象:[].slice.call(arguments, 1)。
bind: 不会立即执行,生成新函数
与 call 类似,但不会立即执行,而是生成了一个新函数,新函数的this指向的是我们传入的obj。
关于第一个参数
call、apply、bind的第一个参数,如果传了 null 或者 undefined 会被替换为全局对象(浏览器环境下的话就是 window 对象),如果传的是其他基础类型(比如1、'a'、false 等)则会被转换成基础类型对应的对象。
简单自测
javascript
function test(...args) {
console.log(this);
console.log(args);
}
console.log("call");
test.call("a", 1, true, "example", [], { a: 1 });
console.log("apply");
test.apply(false, [1, true, "example", [], { a: 1 }]);
console.log("bind");
/**
* 注意这里我们直接对 `bind` 生成的函数进行调用
* 并且在这个调用中也传入了几个新参数
*/
test.bind(0, 1, true, "example", [], { a: 1 })("hello", "world");1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
执行结果见下图:

实现 bind
core-js 的实现
core-js 库中 bind 的实现见:https://github.com/zloirock/core-js/blob/master/packages/core-js/internals/function-bind.js
我们自己实现一个,注意:
- 调用
bind后生成的是一个新函数(新函数名为newFn)。 - 调用
bind时传的第二个及后续参数会和调用新函数newFn时传入的参数合并作为入参。 - 调用
bind时会指定newFn中的this指向。
bind 的代码实现
javascript
Function.prototype.myBind = function () {
// 这里的 `this` 就是被 `bind` 处理前的函数
const func = this;
let obj = arguments[0];
// 如果 `obj` 是 `undefined` 或 `null`,则替换成全局对象
if (typeof obj === "undefined" || obj === null) {
obj = globalThis;
}
// 如果不是对象类型,比如是 `false`、`1` 等,则包装成对象
if (typeof obj !== "object") {
obj = new Object(obj);
}
let args = [].slice.call(arguments, 1);
return function () {
args = args.concat(Array.from(arguments));
const uniqueKey = Symbol("避免污染对象上的其他属性");
/**
* 使用 `symbol`作为 key,
* 可以避免影响其他人添加的可能同名的 key
*/
obj[uniqueKey] = func;
const result = obj[uniqueKey](...args);
// 删除临时添加的属性,避免污染对象
delete obj[uniqueKey];
return result;
};
};
function test(...args) {
console.log(this);
console.log(args);
console.log(arguments);
}
test.bind(0, 1, true, "example", [], { a: 1 })("hello", "world");
test.myBind(0, 1, true, "example", [], { a: 1 })("hello", "world");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
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
实现结果如下:

forEach、for-of、for-in 循环
forEach 无法中断
javascript
// `forEach` 循环无法通过 `break` 或 `return` 语句进行中断
arr.forEach(function (elem) {
console.log(elem);
});1
2
3
4
2
3
4
for-in 遍历对象的可枚举(enumerable)属性
javascript
/**
* for-in循环实际上是为循环对象的可枚举(enumerable)属性而设计的,
* 也能循环数组,不过不建议,因为key变成了数字
*/
const obj = { a: 1, b: 2, c: 3 };
for (const p in obj) {
console.log(`obj.${p} = ${obj[p]}`);
}
// 上面的代码依次输出内容如下:
// obj.a = 1
// obj.b = 2
// obj.c = 31
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
for-of 用于遍历可迭代的(iterable)的东西,比如字符串、数组、map、set、DOM collection,但不能遍历对象,因为对象不是 iterable 可迭代的
/**
* for-of能循环很多东西,
* 包括字符串、数组、map、set、DOM collection等等
* (但是不能遍历对象,因为对象不是iterable可迭代的)
*/
const iterable = [1, 2, 3];
for (const value of iterable) {
console.log(value);
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
for-in 和 for-of
基本上 for in 用于大部分常见的由 key-value 对构成的对象上以遍历对象内容。但是 for in 在遍历数组对象时并不方便,这时候用 for of 会很方便。
IIFE 与分号
如果习惯写完一条语句后不加分号的写法,碰到需要写 IIFE(Immediately-Invoked Function Expression,自执行函数)的时候容易踩到下面的坑:
javascript-ignore
const a = 1
(function () {})();1
2
2
上述代码会报错,因为第一行的 1 会和第二行一起被程序解析成 const a = 1(function () {})(),然后报错:Uncaught TypeError: 1 is not a function。
这时候可以这样写
javascript-ignore
const a = 1;
void (function () {})();
// 或
const a = 1;
void function () {}();
// 或者下面这种方式,但据说会多一次逻辑运算
const a = 1;
!(function () {})();1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Object.create(proto, [, propertiesObject])
JS 中除了使用 new 关键字,还可以通过 Object.create(proto, [, propertiesObject]) 创建对象。详见:Object.create()。
Object.create() 静态方法以一个现有对象作为原型,创建一个新对象。
javascript
const person = {
isHuman: false,
printIntroduction: function () {
console.log(`I am ${this.name}. Am I human? ${this.isHuman}`);
},
};
const me = Object.create(person);
// `name` 是 `me` 的属性,不是 `person` 的属性
me.name = "Matthew";
// 继承过来的属性值可以被重写
me.isHuman = true;
// 打印内容: "I am Matthew. Am I human? true"
me.printIntroduction();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用 Object.create() 实现类式继承
javascript
// Shape —— 父类
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类方法
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info("Shape moved.");
};
// Rectangle —— 子类
function Rectangle() {
Shape.call(this); // 调用父类构造函数。
}
// 子类继承父类
Rectangle.prototype = Object.create(Shape.prototype, {
/**
* 如果不将 `Rectangle.prototype.constructor`
* 设置为 `Rectangle`,
* 它将采用 `Shape`(父类)的 `prototype.constructor`。
* 为避免这种情况,
* 我们将 `prototype.constructor` 设置为 `Rectangle`(子类)。
*/
constructor: {
value: Rectangle,
enumerable: false,
writable: true,
configurable: true,
},
});
const rect = new Rectangle();
// true
console.log("rect 是 Rectangle 类的实例吗?", rect instanceof Rectangle);
// true
console.log("rect 是 Shape 类的实例吗?", rect instanceof Shape);
// 打印 'Shape moved.'
rect.move(1, 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
44
45
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
使用 Object.create() 的 propertyObject 参数
Object.create() 方法允许对对象创建过程进行精细的控制。实际上,字面量初始化对象语法可视为 Object.create() 的一种语法糖。使用 Object.create(),我们可以创建具有指定原型和某些属性的对象。请注意,第二个参数将键映射到属性描述符,这意味着你还可以控制每个属性的可枚举性、可配置性等,而这在 {} 字面量初始化对象语法中是做不到的。
javascript
o = {};
// 等价于:
o = Object.create(Object.prototype);
o = Object.create(Object.prototype, {
// foo 是一个常规数据属性
foo: {
writable: true,
configurable: true,
value: "hello",
},
// bar 是一个访问器属性
bar: {
configurable: false,
get() {
return 10;
},
set(value) {
console.log("Setting `o.bar` to", value);
},
},
});
/**
* 创建一个新对象
* 它的原型是一个新的空对象
* 并添加一个名为 'p',值为 42 的属性。
*/
o = Object.create({}, { p: { value: 42 } });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
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
使用 Object.create(),我们可以创建一个原型为 null 的对象。在字面量初始化对象语法中,相当于将 __proto__ 键赋值为 null。
javascript
o = Object.create(null);
// 等价于:
o = { __proto__: null };1
2
3
2
3
你可以使用 Object.create() 来模仿 new 运算符的行为。
javascript
function Constructor() {}
o = new Constructor();
// 等价于:
o = Object.create(Constructor.prototype);1
2
3
4
2
3
4
当然,如果 Constructor 函数中有实际的初始化代码,那么 Object.create() 方法就无法模仿它。
生成器函数与 yield 语句
代码示例
javascript
function* hello(name) {
yield `hello ${name}!`;
yield "I am glad to meet you!";
if (0.6 > 0.5) {
yield `It is a good day!`;
}
yield "See you later!";
}
// Generator函数执行后会返回一个迭代器,通过调用next方法依次yield相应的值
const iterator = hello("Yakima");
iterator.next(); // 返回{value: "hello Yakima!", done: false}
iterator.next(); // 返回{value: "I am glad to meet you!", done: false}
iterator.next(); // 返回{value: "It is a good day!", done: false}
iterator.next(); // 返回{value: "See you later!", done: false}
iterator.next(); // 返回{value: undefined, done: true}
iterator.next(); // 返回{value: undefined, done: true}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
生成器函数(Generator)与常见的函数的差异:
- 通常的函数以
function开始,而生成器函数以function*开始; - 在生成器函数内部,
yield是一个关键字,和return有点像。不同点在于,所有函数(包括生成器函数)都只能return一次,而在生成器函数中可以yield任意次。yield表达式暂停了生成器函数的执行,然后可以从暂停的地方恢复执行。
常见的函数不能暂停执行,而生成器函数可以,这是两者最大的区别。
扩展运算符进行对象拷贝时是浅拷贝
在 Babeljs.io Try it out 上转义的结果是:
javascript
function _typeof(o) {
"@babel/helpers - typeof";
return (
(_typeof =
"function" == typeof Symbol && "symbol" == typeof Symbol.iterator
? function (o) {
return typeof o;
}
: function (o) {
return o &&
"function" == typeof Symbol &&
o.constructor === Symbol &&
o !== Symbol.prototype
? "symbol"
: typeof o;
}),
_typeof(o)
);
}
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r &&
(o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})),
t.push.apply(t, o);
}
return t;
}
function _objectSpread(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2
? ownKeys(Object(t), !0).forEach(function (r) {
_defineProperty(e, r, t[r]);
})
: Object.getOwnPropertyDescriptors
? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t))
: ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _defineProperty(obj, key, value) {
key = _toPropertyKey(key);
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true,
});
} else {
obj[key] = value;
}
return obj;
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == _typeof(i) ? i : String(i);
}
function _toPrimitive(t, r) {
if ("object" != _typeof(t) || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r || "default");
if ("object" != _typeof(i)) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
var a = {
a: 1,
b: {
c: 3,
},
c: 5,
};
var d = _objectSpread({}, a);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
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
常用的异步处理方法
回调函数(最原始)
- 逻辑:把完成后要执行的函数当作参数传给异步方法,操作结束自动调用回调。
- 优点:最简单,无额外语法
- 缺点:回调地狱(多层嵌套)、错误处理分散、难以复用
js
// 回调嵌套地狱
readFile("a.txt", (err, data1) => {
readFile(data1, (err, data2) => {
readFile(data2, () => {});
});
});1
2
3
4
5
6
2
3
4
5
6
事件监听 EventEmitter / 事件循环
- 逻辑:异步操作触发事件,提前注册事件回调,完成时发射事件执行逻辑。
- 适用:IO 流、网络请求、GUI 界面、消息队列
- 代表:Node.js EventEmitter、浏览器 DOM 事件、Java Swing 事件
- 优点:解耦、支持多监听者
- 缺点:流程分散,难以追踪执行顺序
Promise(回调的标准化封装)
- 逻辑:封装异步操作状态(
pending/fulfilled/rejected),链式调用替代嵌套,统一错误捕获。 - 解决回调地狱核心方案,所有现代语言均有同类实现。
- 优点:链式调用、统一异常、可并行
Promise.all - 缺点:仍有链式冗余,无法直观同步写法
js
fetch("/api")
.then((res) => res.json())
.catch((err) => console.error(err));1
2
3
2
3
async /await(Promise 语法糖,当前主流)
- 基于 Promise 的语法糖,用同步书写逻辑写异步代码,可读性最强。
- 几乎所有现代语言标配:JS/TS、Python、C#、Rust、Go(近似方案)
- 优点:代码平铺、异常用 try/catch、逻辑清晰
- 缺点:必须包裹在 async 函数内
js
async function getData() {
try {
const res = await fetch("/api");
return res.json();
} catch (e) {
console.error(e);
}
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
生成器 Generator + Promise(JS 过渡方案)
利用函数暂停 / 恢复特性,手动封装异步流程,现在基本被 async/await 淘汰。
工业级异步架构
响应式编程 Reactive
- 核心:数据流驱动异步,数据源源不断推送,自动处理异步事件、异常、超时、重试。
- 代表库:RxJS(前端)、RxJava、Reactor
- 适用:实时推送、WebSocket、海量接口并发
消息队列异步(分布式后端)
- 跨服务异步解耦,业务不等待结果,发送消息后直接返回。
- 中间件:RabbitMQ、Kafka、RocketMQ
- 场景:订单创建后发消息异步发短信、日志异步存储
协程 Coroutine(轻量级异步,现代语言标配)
- 操作系统线程重量高,协程由用户态调度,占用资源极低,百万并发无压力。
- Go:Goroutine
- Java21:虚拟线程
- Python:asyncio 协程
- JS:基于事件循环的微任务协程模型
多线程 / 多进程异步(CPU 密集场景)
- 协程适合 IO 密集(网络、文件读写)。
- CPU 计算阻塞时,使用多线程 / 进程异步并行计算。
其他异步方式
- Web Worker(浏览器多线程异步计算)
- 定时器 setTimeout /setInterval(宏任务异步)
require, import
本文参考了以下文章:
一段时间以来,CommonJS 模块化方案一直是 Node.js 生态中的默认模块化方案。从 Node.js v8.5.0 开始,引入了 ES(ECMAScript) 模块化方案。这两种方案在执行时有一些差异。
- ES 模块化方案是 ECMAScript 语言的官方正式模块化方案,也是大多数浏览器原生支持的方案。使用
import和export来导入、导出模块。 - Node.js 默认采用 CommonJS 模块化方案。使用
require和module.exports/exports.<keyName>来导入和导出模块。 require()函数可以在程序的任何地方被调用,import则在文件头部被调用。- 一般用
require()引入的文件名使用 .js 作为文件名后缀,用import引入的文件名使用 .mjs 作为文件名后缀。(不绝对) require()得到的内容可以视作一个对象,里面有我们需要的属性或者方法。require()得到的是原始内容的一个拷贝(如果是对象的话就是浅拷贝),也就是重新又自己声明了一份变量,比如在b.js文件中声明了const { a } = require(./a.js)后在a.js文件中修改a的值也不会影响b.js中的a的值,两个文件中的a是不一样的。import则不会重新声明变量,在上面所述的场景中,a.js和b.js文件中的a一直都是同一个变量,值也始终相同。
require 引入 外部模块
require 除了支持通过传入一个本地文件路径来引用本地模块,也支持通过传入一个 web 地址来引入外部模块,比如这样:
javascript
const myVar = require("http://web-module.location");1
前端 Cache
HTTP 缓存见 HTTP 相关章节,本节不再赘述。
Disk Cache 和 Memory Cache
Disk Cache
当你访问一个网站的时候,一些资源(图片、CSS样式文件、JS脚本文件等)可能会被储存到你的硬盘中。这种就就是硬盘缓存(Disk Cache)。
硬盘缓存的优点有:
- 可长期保存(数周甚至数月之久)。
- 通常来说,一台机器中硬盘空间相对内存空间来说要多得多,所以硬盘缓存可以存储大量的内容。
硬盘缓存的缺点:
- 访问硬盘中的资源比直接访问内容中的资源要慢一些。
Memory Cache
Memory Cache 与 Disk Cache 不同的地方在于,命中 Memory Cache 的资源是被储存在设备的 RAM 中的,这种缓存资源访问起来速度非常快。但是只要你关掉浏览器,这些缓存就会被丢弃,也就是说下一次你重新打开浏览器尝试去访问这些资源时,就已经没有缓存了。
安全
CSRF:Cross-site request forgery,跨站请求伪装
攻击者会在用户不知情的情况下通过用户浏览器向网站后端发起请求。攻击者可以通过XSS攻击的方式触发CSRF攻击。
比如,在一个未做好安全防范的聊天室或者论坛上,攻击者发送了一个img标签:
html
<!-- 注意:这个img标签的src属性值并非一个真正的图片地址,而是一个请求银行网站的链接地址 -->
<img
src="https://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory"
/>1
2
3
4
2
3
4
如果一个用户在访问这个渲染好的html片段之前正好访问过这个银行网站, 并且cookie信息未过期,且银行方也没有做除了cookie之外的其他校验,那么这个用户的钱就有可能被直接转走了!
通过img标签触发的都是get请求,那是不是转账这种敏感请求都用post不用get就没这个问题了呢?也不是的,因为攻击者也可以通过构造form表单或者直接注入js脚本来实现非get请求。
html
<form action="https://bank.example.com/withdraw" method="POST">
<input type="hidden" name="account" value="bob" />
<input type="hidden" name="amount" value="1000000" />
<input type="hidden" name="for" value="mallory" />
</form>
<script>
window.addEventListener("DOMContentLoaded", () => {
document.querySelector("form").submit();
});
</script>1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
上面这段代码,只要是放在一个看不叫的iframe标签内部的话,触发时就不会导致页面跳转,也就不会被用户感知到。
预防措施:
- GET接口应该是幂等的。用别的比如POST接口去处理会产生变化且不以查询数据为目的的请求。POST接口应避免同时支持解析GET请求和查询字符串参数。
- 对所有可能会执行“改动操作”的非GET请求,应在请求体中携带一个可以区分不同用户的CSRF token。服务端在接收到请求后,校验token是否有效,如无效则忽视该请求。
- 这种预防措施生效的前提是假定攻击者无法获取到程序下发给用户的CSRF token。
- 每次登录都需要重新生成新的token。
- 用于敏感操作的cookie的有效期应该设置得短一点,同时响应头里的SameSite属性值应设置为Strict。在支持SameSite响应头的浏览器中,这个设置能保证跨域请求不会携带该敏感cookie,进而服务端校验请求时可以直接拦截。
- 应同时采用CSRF token和SameSite cookie。这样即便在SameSite cookie无效的情况下(比如从子域名发起的攻击请求。
XSS:Cross-site scripting,跨站脚本攻击
跨站脚本攻击(XSS)是攻击者通过向网站的客户端注入恶意代码来实现的。 受害者执行这段代码之后,攻击者就可以绕过访问权限的拦截,模拟用户行为。
如果Web应用没有采用足够的校验和编码措施来进行预防的话,这些攻击就可能会得逞。 用户的浏览器无法判断这些恶意脚本是“恶意的”,所以会允许它们访问cookie、token或者其他网站敏感信息,也会允许这些代码去修改HTML内容。
当动态内容,或者来自不可信来源(通常是网络请求)的数据,在未经校验是否有恶意内容的情况下被发送给用户时,就容易出现跨站脚本攻击。
这些恶意内容通常包括JavaScript代码,有时候也包括HTML、Flash,或者其他浏览器可以执行的代码。XSS攻击有很多类型,但通常包括:
- 将cookie或者session信息等私有数据传输给攻击者;
- 将用户重定向到一个由攻击者控制的网页上;
- 伪装成站点身份在用户的机器上执行其他恶意的操作。
我们可以将跨站脚本攻击分成3类:
- 存储型跨站脚本攻击。攻击者注入的脚本会被永久存储在目标服务器上,当受害者通过浏览器向服务端请求数据时,恶意脚本被发送给了受害者。
- 反射型跨站脚本攻击。当用户被诱导去点击恶意链接、提交恶意表单、或者访问恶意网站时,恶意内容被注入到目标网站,目标网站通过错误提示、搜索结果或者其他响应形式将注入代码返回到用户浏览器。此时,用户浏览器认为这些恶意代码来自可信任的来源,就正常去执行它们了。
- DOM型跨站脚本攻击。注入脚本将原先正常客户端脚本所需的DOM环境进行了恶意修改。导致客户端代码的运行结果不在预期内。
前端加密的意义
前端加密的意义不是为了防止中间人,而是提供一种隐私保护服务。
这样即使因为使用的是http协议导致通信过程被攻击者拦截, 攻击者直接拿到的也不是用户的原始密码,而是加密字符串,这个加密字符串可以直接被用于当前网站。 但是当攻击者拿这个加密字符串去其他网站尝试使用时,只要其他网站使用的不是同一套加密逻辑,就没有用。 就是说用户在其他网站使用这个密码还是相对安全的。
攻击者如果能拿到密码明文的话,还是很危险的,前端加密一定程度上可以增加这个难度(增加了攻击者从加密字符串破解出明文密码的过程)。
如何安全传输密码?
一般做法是使用https协议,并且对密码采用非对称加密算法(如RSA)处理后再进行传输。
使用https协议
使用https协议,可以避免用户密码在网络上裸奔。http协议是明文传输的,有3大风险:
- 窃听/嗅探风险:第三方可以截获通信数据。
- 数据篡改风险:第三方获取到通信数据后,会进行恶意修改。
- 身份伪造风险:第三方可以冒充他人身份参与通信。
https原理是什么呢?为什么它能解决http的三大风险呢?
https = http + SSL/TLS, SSL/TLS 是传输层加密协议, 它提供内容加密、身份认证、数据完整性校验, 以解决数据传输的安全性问题。
一次完整的https请求流程如下:

- 客户端发起https请求
- 服务器必须要有一套数字证书,可以自己制作,也可以向权威机构申请。这套证书其实就是一对公私钥。
- 服务器将自己的数字证书(含有公钥、证书的颁发机构等)发送给客户端。
- 客户端收到服务器端的数字证书之后,会对其进行验证,主要验证公钥是否有效,比如颁发机构,过期时间等等。如果不通过,则弹出警告框。如果证书没问题,则生成一个密钥(对称加密算法的密钥,其实是一个随机值),并且用证书的公钥对这个随机值加密。
- 客户端会发起https中的第二个请求,将加密之后的客户端密钥(随机值)发送给服务器。
- 服务器接收到客户端发来的密钥之后,会用自己的私钥对其进行非对称解密,解密之后得到客户端密钥,然后用客户端密钥对返回数据进行对称加密,这样数据就变成了密文。
- 服务器将加密后的密文返回给客户端。
- 客户端收到服务器发返回的密文,用自己的密钥(客户端密钥)对其进行对称解密,得到服务器返回的数据。
https的数据传输过程,数据都是密文的。 但是,即时使用了https协议传输密码信息,也不一定就安全了。 比如,https完全就是建立在证书可信的基础上的。 如果遇到中间人伪造证书,一旦客户端通过验证,安全性就没了。 通过伪造证书,https也是可能被抓包的。
加密算法
如上所述,即使用了https协议传输用户密码,只要用户信任了伪造证书,也还是会有安全隐患的。所以,对于密码,传输前还是需要先进行加密的。
加密算法有对称加密和非对称加密两种。
对称加密算法
加密和解密使用“相同密钥”的加密算法。 使用对称加密算法时,需要考虑如何将密钥给到客户端的问题,如果还是通过网络传输的方式,如果密钥在传输过程中被中间人拿到的话,还是有风险的。

常见的对称加密算法有:
- DES:基于使用56位密钥的对称算法。
- 3DES:每个数据块应用三次数据加密标准(DES)算法。
- AES:密码学中的高级加密标准。
- RC5:一种因简洁著称的对称分组加密算法。
- RC6:基于RC5设计的,以更好地符合AES的要求。
非对称加密算法
非对称加密算法需要2个密钥(公钥和私钥)。公钥和私钥是成对存在的,用公钥对数据进行加密,用对应的私钥才能解密。 使用费对称加密算法时,也需要考虑如何将密钥、公钥给到客户端的问题, 如果公钥在网络传输过程中被中间人拿到的话,中间人可以伪造公钥,把伪造的公钥给客户端,然后用自己的私钥解密从客户端过来的加密数据。

常见的非对称加密算法有:
- RSA:基于因子分解。
- Elgamal:基于离散对数。
- DSA:Digital Signature Algorithm,数字签名算法。
- D-H:(Diffie-Hellman)密钥交换算法。
- ECC:(Elliptical Curve Cryptography)——椭圆曲线加密。
如何安全存储密码?
密码安全送达服务端后,一定不能明文存储密码到数据库。可以先用哈希摘要算法加密密码,然后再保存到数据库。
哈希摘要算法:只能从明文生成一个对应的哈希值,不能反过来根据哈希值得到对应的明文。
MD5加密
MD5是一种非常经典的哈希摘要算法,被广泛应用于数据完整性校验、数据摘要、数据加密等。
直接MD5加密
对原始密码直接进行MD5加密的话是很不安全的。因为攻击者用彩虹表可以很容易破解出密码。 如果把所有20位以内的数字和字母组合的密码全部计算其MD5哈希值,并把密码和对应哈希值存到一个超大数据库里,就是一个彩虹表了。
提醒:网络上已经有很多MD5免费破解网站了,可以自己随便试。
优化方案:密码加盐后再进行MD5加密
先对字段进行加盐处理,再进行MD5加密,即MD5(password + salt)。只要salt够长,是没有办法通过彩虹表反查的。
在密码学中,通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这个过程称为“加盐”。
加盐有几个注意事项
- 不能再代码中写死盐,且盐需要有一定的长度(盐写死太简单的话,攻击者可能注册几个账号反推出来)。
- 每个密码都有独立的盐,并且盐要长一点,比如超过20位。(如果盐和原始密码都太短的话,容易被破解)。
- 盐最好是随机的值,并且是全球唯一的,这样就不可能有现成的彩虹表可以用于破解。
BCrypt
即使MD5加密前加了盐,密码仍有可能被暴力破解。可以采取更慢一点的算法,增加攻击者破解密码所需的成本,迫使攻击者放弃攻击。
为了应对暴力破解,我们需要非常耗时而非高效的哈希算法。 BCrypt算法的特点是可以通过参数设置重复计算的次数,重复计算的次数越多耗时越长。 如果计算一个哈希值需要耗时1秒以上,破解一个6位纯数字密码就需要耗时11.5天以上,更不要说高安全级别的密码了。暴力破解密码的可能性就很低了。
实际上,Spring Security 已经废弃了MessageDigestPasswordEncoder,推荐使用BCryptPasswordEncoder,也就是BCrypt来进行密码哈希。
如何应对暴力破解
感知到暴力破解危害时,应开启短信验证、图形验证码、账号暂时锁定等防御机制来进行抵御。
如何明确是暴力破解的话,可以采取封IP等措施。
refresh token 和 access token
本文参考了以下文章:
access token 是用来临时授权用户访问受保护的资源或执行特定操作用的,通常有效期较短(分钟级),以减少 token 被泄露带来的风险(攻击者可用于 access token 获取保密资源的时间越少越好)。refresh token 则是用于在 access token 过期后获取新的 access token 用的,从而减少用户因为 access token 过期而总是需要重复登录的问题。

refresh token 的一些最佳实践
1、轮转 refresh token (RTR,Refresh token rotation)
RTR 通过降低 refresh token 的有效期来提高了安全性。Refresh token rotation 的意思是,每次使用 refresh token 去获取 access token 时,服务端就让旧的 refresh token 失效,并返回给前端一个新的 refresh token。基本上可以认为,在该方案下,每个 refresh token 只能使用一次。这样,当攻击者获取到 refresh token 后,该 refresh token 失效的可能性将大大增加。
2、监测 refresh token 的复用
除了轮转 refresh token,还应该监测 refresh token 的复用。如果授权服务器发现有请求尝试使用一个已经用过的、无效了的 refresh token 去获取新 access token 的行为的话,授权服务器应让相关的 token 全部失效,包括所有已经下发给用户的 access token 和最近下发的 refresh token。
3、安全地存储 refresh token
4、设置合理的过期时间
度量过期时间时,其起始时间除了用下发 token 的时间,也可以用 token 最后一次被使用的时间。
5、监控 refresh token 的异常使用情况
在一个应用中,服务端解析 access token 的场景要比解析 refresh token 的场景多得多,我们可以在 access token 中仅包好较少的用户信息,而在 refresh token 中包含较多的信息(比如用户 ip 地址),然后在 refresh token 刷 access token 的请求中,对用户 ip 地址是否有变动等进行检查。
从输入URL到整个页面显示在用户面前发生了什么
大体流程
- 浏览器从url中解析出服务器的主机名
- 浏览器将服务器的主机名转换成服务器的IP地址(DNS)
- 浏览器将端口号从url中解析出来
- 浏览器建立一条与web服务器的TCP连接
- 浏览器向服务器发送一条HTTP的请求报文
- 服务器向浏览器回送一条HTTP的响应报文
- 关闭连接,浏览器渲染
1. 浏览器查找域名对应的IP地址
IP地址:IP协议为互联网上的每一个网络和每一台主机都分配的一个逻辑地址。通过IP地址才能确定一台主机(服务器)的位置。
域名(DN,Domain Name):IP地址不便于用户记忆和使用,故用域名来代替纯数字的IP地址。
DNS(Domain Name System):每个域名都对应一个或多个提供相同服务的服务器的IP地址,只有知道服务器IP地址才能建立连接,所以需要通过DNS把域名解析成一个IP地址。
域名和IP的关系
域名和IP不是一一对应的关系,可以把多个提供相同服务的服务器IP设置为同一个域名,同一时刻一个域名可以解析出多个IP地址;同时,一个IP地址可以绑定多个域名,数量不限。
再强调一下,同一时刻一个域名是可以解析出多个IP地址的(多条A记录很常见)。只是每次域名解析请求会根据对应的负载均衡算法计算出一个IP地址返回给访客。
bash
vivi@vivi:~$ nslookup aliyun.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
Name: aliyun.com
Address: 140.205.60.46
Name: aliyun.com
Address: 106.11.172.9
Name: aliyun.com
Address: 140.205.135.3
Name: aliyun.com
Address: 106.11.253.83
Name: aliyun.com
Address: 106.11.249.99
Name: aliyun.com
Address: 106.11.248.146
Name: aliyun.com
Address: 2401:b180:1:60::6
Name: aliyun.com
Address: 2401:b180:1:60::5
vivi@vivi:~$ dig aliyun.com
; <<>> DiG 9.16.1-Ubuntu <<>> aliyun.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47126
;; flags: qr rd ra; QUERY: 1, ANSWER: 6, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 65494
;; QUESTION SECTION:
;aliyun.com. IN A
;; ANSWER SECTION:
aliyun.com. 300 IN A 106.11.248.146
aliyun.com. 300 IN A 106.11.249.99
aliyun.com. 300 IN A 106.11.253.83
aliyun.com. 300 IN A 140.205.135.3
aliyun.com. 300 IN A 106.11.172.9
aliyun.com. 300 IN A 140.205.60.46
;; Query time: 8 msec
;; SERVER: 127.0.0.53#53(127.0.0.53)
;; WHEN: 三 2月 21 16:12:48 CST 2024
;; MSG SIZE rcvd: 1351
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
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
DNS重定向
这里的知识点,对前端而言只要知道使用CDN存放静态资源这种优化策略的原理是DNS负载均衡: 如果一个大型网站的所有请求都由同一个服务器进行处理,是不现实的; 并且对用户而言,用户并不关注具体是哪台机器处理了他的请求。 因此,DNS可以根据多个服务器中每个服务器的负载量、该机器离用户的地理位置的距离等信息 返回其中某个适合的主机的IP地址,这个过程就是DNS负载均衡,又叫做DNS重定向。 CDN(Content Delivery Network)就是利用DNS的重定向技术,DNS服务器会返回一个跟用户最接近的点的IP地址。
要实现一个域名对应多个IP地址的效果,首先需要了解DNS(域名系统)的工作原理。
DNS(Domain Name System)是因特网的一项服务,它作为域名和IP地址相互映射的一个分布式数据库,能够使人们更方便地访问互联网。我们平时访问网站更多的是通过域名而非IP地址去触达,但域名并不能被计算机直接识别,所以需要通过DNS将域名“翻译”成可由计算机直接识别的IP地址。具体的操作方式,是在DNS解析操作平台,添加一条解析记录(A记录或AAAA记录),将网站的域名指向服务器的IP地址。一般情况下,一个域名对应一个IP地址,也就只需添加一条解析记录即可。如果想要实现一个域名对应多个IP地址,就需要添加多条解析记录,这也是通过DNS实现负载均衡的简单原理。
如我们想要将http://www.example.com这个域名分别指向1.1.1.1(北京电信)、2.2.2.2(上海移动)、3.3.3.3(深圳联通)三个IP。那么我们就可以在DNS服务器中配置三个A记录,分别为
- http://www.example.com IN A 114.100.20.201;
- http://www.example.com IN A 114.100.20.202;
- http://www.example.com IN A 114.100.20.203;
这样,每次域名解析请求都会根据对应的负载均衡算法计算出一个不同的IP地址返回给访客,这样就构成了一个服务器集群,并实现负载均衡的效果。在实际场景中,当北京用户访问http://www.example.com域名时,DNS会根据负载均衡算法和A记录得出一个就近IP地址1.1.1.1返回给客户端,当上海用户访问http://www.example.com域名时,DNS就会返回给2.2.2.2的服务器地址,深圳用户返回3.3.3.3。
不同用户就近访问不同的服务器IP地址,访问速度大大提升,同时也减轻了单个服务器的访问压力。
实现负载均衡的方式有很多种,其中DNS是一种十分简单和有效的技术手段,它主要有以下几点优势:
- 将负载均衡工作交给DNS,省去了网站管理维护负载均衡服务器的麻烦;
- 技术实现比较灵活,操作简单,成本低,适用于大多数TCP/IP应用;
- 对于部署在服务器上的应用来说,不需要修改任何代码就能实现不同机器上的应用访问;
- 很多DNS系统还支持基于地理位置的域名解析,可以将域名解析成距离用户地理位置最近的服务器地址,加快用户访问速度。
但基于DNS的负载均衡同样也存在一些弊端:
- 目前的DNS系统是需要经过递归服务器、顶级服务器、权威服务器以及众多缓存等多级解析的,在每一个环节都可能存在解析记录缓存。如果服务器IP发生变动,即使修改了A记录,也需要各级缓存失效后才能生效。而在解析生效前的这段时间,用户可能就会根据缓存记录访问到已经被更换过的服务器上,从而导致访问失败。
- DNS负载均衡采用的是简单的轮询算法,不能区分不同服务器之间的性能和负载差异,不能反映服务器当前的运行状态,所以负载均衡效果并不太好。
- 为了本地DNS服务器能够及时同步权威服务器上的最新记录,所以一般将DNS缓存刷新时间设置得比较小,这就会导致DNS频繁发起解析请求,从而造成额外的网络问题。
所以一些大型网站总是使用DNS域名解析作为第一级负载均衡手段,然后在通过提供负载均衡服务的内部服务器再进行负载均衡,将最终请求发到真实的服务器上,从而完成最终请求。
2. 浏览器根据IP地址与服务器建立socket连接
建立连接——三次握手
知道了服务器的IP地址,便可开始与服务器建立连接了,通信连接的建立需要经历以下三个过程:
- 客户端首先发送一个带有SYN标志的数据包给服务端(您好,我想认识你);
- 服务端接受SYN数据包之后,回传一个SYN/ACK标志的数据包以示传达确认连接信息(好的,很高兴认识你);
- 客户端收到SYN/ACK的确认数据包之后,再回传一个ACK标志的数据包给服务端,表示‘握手’结束(我也很高兴认识你),至此,客户端便与服务器建立了连接。
说明:
- TCP协议:三次握手的过程采用TCP协议,其可以保证信息传输的可靠性,三次握手过程中,若一方收不到确认信息,协议会要求重新发送信号。
TCP的作用是啥?
- 提供无差错的数据传输
- 按序传输(数据总是会按照发送顺序到达)
- 未分段的数据流(可以在任意时刻以任意尺寸将数据发送出去)
3. 浏览器与服务器通信:浏览器发出请求、服务器处理请求、渲染
当服务器与客户端建立了连接之后,客户端便开始与服务器进行通信。 网页请求是一个客户端向服务器请求数据==>服务器返回相应数据的单向的请求过程。
- 浏览器根据URL生成HTTP请求,请求中包含请求文件的位置、请求文件的方式等信息;
- 服务器接到请求后,会根据HTTP请求中的信息来决定如何获取相应的HTML文件;
- 服务器将得到的HTML文件发送给浏览器;
- 在浏览器还没有完全接受完HTML文件时便开始渲染、显示页面;
- 在执行HTML中代码时,根据需要,浏览器会继续请求图片、CSS、Javascript、视频、音频等文件,过程类似。
针对浏览器渲染、显示页面的过程,说明如下:
- 浏览器端是一个边解析边渲染的过程。
- HTML Parser将HTML内容解析为DOM Tree,CSS Parser将CSS内容解析为样式规则(Style Rules);
- 根据样式规则和DOM Tree来渲染树(Render Tree),在这个渲染树的过程中会发生回流(layout/reflow/relayout),回流就是浏览器计算各个盒模型的位置、大小等属性的过程;
- 等浏览器确定了盒模型的位置、尺寸等数据后开始绘制页面,这个过程称为重绘(Painting/repaint)。
4. 浏览器与服务器断开连接
断开连接——四次挥手:
- 客户端向服务端先发送一个带有FIN标志的数据包(我想关闭);
- 服务端接受FIN数据包之后,回传一个ACK的数据包给客户端以示传达确认关闭信息(知道了:只是表示确认我知道你想要关闭了,但是我可能还有事还在处理,不一定现在就关)
- 服务端向客户端发送一个FIN标志的数据包,请求关闭连接(我处理好了,可以关闭了)
- 客户端收到FIN的数据包之后,回传一个ACK的数据包给服务端,以表示确认关闭(好的,那我关了),服务器收到确认信号后断开连接。
说明:
- 为什么服务器在接到断开请求时不立即同意断开:当服务器收到断开连接的请求时,可能仍然有数据未发送完毕,所以服务器先发送确认信号,等所有数据发送完毕后再同意断开;
- 第四次挥手后,主机发送确认信号后并没有立即断开连接,而是等待了2个报文传送周期,原因是:如果第四次挥手的确认信息丢失,服务器将会重新发送第三次挥手的断开连接的信号,而服务器发觉丢包与重新发送断开连接到达主机的时间正好为2个报文传输周期。
Performance 性能数据统计
简书性能背景
发现简书控制台会输出性能相关的一些统计数据,类似下面这样:
json
{
"link": "https://www.jianshu.com/",
"times": {
"alltime": 31772,
"details": {
"redirect": 0,
"dns": 0,
"ttfb": 751,
"static": 741,
"render": 31018,
"onload": 1624684987502
},
"lifecycle": {
"_1": {
"key": "redirect",
"desc": "网页重定向的耗时",
"value": 0
},
"_2": {
"key": "cache",
"desc": "检查本地缓存的耗时",
"value": 0
},
"_3": {
"key": "dns",
"desc": "DNS查询的耗时",
"value": 0
},
"_4": {
"key": "tcp",
"desc": "TCP连接的耗时",
"value": 0
},
"_5": {
"key": "request",
"desc": "客户端发起请求的耗时",
"value": 738
},
"_6": {
"key": "response",
"desc": "服务端响应的耗时",
"value": 3
},
"_7": {
"key": "render",
"desc": "渲染页面的耗时",
"value": 31018
},
"__": 31759
}
},
"ua": "Mozilla/5.0 (Macintosh; ... 4 Safari/537.36"
}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
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
去把他们编译产物拿出来格式化处理后,我们来一个个看这些值都是怎么取到的。
link和ua
link的取值为window.location.href.split("?")[0]。
ua的取值为:navigator.userAgent。
times
编译产物代码里性能数据的核心代码如下:
javascript
window.addEventListener("load", function () {
setTimeout(function () {
var e = window.performance;
if (e) {
var t = e.timing,
n = {
_1: {
key: "redirect",
desc: "网页重定向的耗时",
value: t.redirectEnd - t.redirectStart,
},
_2: {
key: "cache",
desc: "检查本地缓存的耗时",
value: t.domainLookupStart - t.fetchStart,
},
_3: {
key: "dns",
desc: "DNS查询的耗时",
value: t.domainLookupEnd - t.domainLookupStart,
},
_4: {
key: "tcp",
desc: "TCP连接的耗时",
value: t.connectEnd - t.connectStart,
},
_5: {
key: "request",
desc: "客户端发起请求的耗时",
value: t.responseStart - t.requestStart,
},
_6: {
key: "response",
desc: "服务端响应的耗时",
value: t.responseEnd - t.responseStart,
},
_7: {
key: "render",
desc: "渲染页面的耗时",
value: t.domComplete - t.responseEnd,
},
},
o = 0;
(0, a.default)(n).forEach(function (e) {
n[e] && n[e].value > 0 && (o += n[e].value);
}),
(n.__ = o);
var r = {
redirect: t.redirectEnd - t.redirectStart,
dns: t.domainLookupEnd - t.domainLookupStart,
ttfb: t.responseStart - t.navigationStart,
static: t.responseEnd - t.requestStart,
render: t.domComplete - t.responseEnd,
onload: t.responseEnd - t.redirectStart,
},
i = {
alltime: t.domComplete - t.navigationStart,
details: r,
lifecycle: n,
},
l = Date.now() % 5 == 0;
if (
((0, a.default)(r).forEach(function (e) {
(r[e] > 15e3 || r[e] < 0) && (l = !1);
}),
l)
) {
var u =
"/" === window.location.pathname
? "/index"
: window.location.pathname;
try {
d.default
.post(
"https://tr.jianshu.com/fe/1/mon/atf",
(0, s.default)(
{},
{
url: window.location.href.split("?")[0],
ua: navigator.userAgent,
path: u,
total: t.domComplete - t.navigationStart,
app: "maleskine",
tags: [
"undefined" != typeof ParadigmSDKv3
? "with-Paradigm"
: "without-Paradigm",
],
},
r,
),
)
.then(function () {})
.catch(function () {});
} catch (e) {}
}
console &&
console.log({
link: window.location.href.split("?")[0],
times: i,
ua: navigator.userAgent,
});
}
}, 0);
});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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
可以看出,主要是利用了performance这个性能api,当有该API时,才会记录相关的数据。
javascript
const timing = performance.timing;
// 网页重定向的耗时
const redirect = timing.redirectEnd - timing.redirectStart;
// 检查本地缓存的耗时
const cache = timing.domainLookupStart - timing.fetchStart;
// DNS查询的耗时
const dns = timing.domainLookupEnd - timing.domainLookupStart;
// TCP连接的耗时
const tcp = timing.connectEnd - timing.connectStart;
// 客服端发起请求的耗时
const request = timing.responseStart - timing.requestStart;
// 服务端响应的耗时
const response = timing.responseEnd - timing.responseStart;
// 渲染页面的耗时
const render = timing.domComplete - timing.responseEnd;
//
const ttfb = timing.responseStart - timing.navigationStart;
// 发起请求到响应结束的耗时
const statics = timing.responseEnd - timing.requestStart;
//
const onload = timing.responseEnd - timing.redirectStart;
//
const alltime = timing.domComplete - timing.navigationStart;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
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
performance.timing弃用
这个API其实已经被弃用,你可以通过在控制台执行performance.getEntries()来查看相关的一些性能数据。
这里面的API和属性太多就不介绍了。
Resource loading phases

页面加载、响应、渲染耗时
我们不看简书上的逻辑,根据MDN上的例子,可知:
页面加载时间
javascript
const perfData = window.performance.timing;
const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart;1
2
2
请求响应时间
javascript
const connectTime = perfData.responseEnd - perfData.requestStart;1
页面渲染时间
javascript
const renderTime = perfData.domComplete - perfData.domLoading;1
前端优化
常见的前端优化技巧
- 大体
- 减少服务器请求数:
- 将多个JS/CSS文件进行合并;
- 图片不需要经常改动时,可使用CSS sprite;
- 如果仅单个页面使用某JS/CSS文件,可以直接将文件内容放于html页面中(若多个页面公用相同的JS/CSS)文件,则不该这么做,应该利用好浏览器缓存功能。
- 加快资源访问速度:
- CDN。
- 减小文件大小:
- 将图片适当压缩;
- 压缩JS/CSS文件。
- 提高代码执行效率。
- 减少服务器请求数:
- JS
- 需要多次使用的值(比如需要遍历的数组对象的length),应先将其存为一个变量,然后调用该变量以减少JS查询的时间;
- 于页面底部引入脚本,先将页面内容呈现给用户;
- 提高代码复用率,减少代码冗余。
- CSS
- 于页面头部引入样式,避免用户看到布局错乱的内容;
- 不要使用CSS表达式。
- HTML
- 主要是SEO方面的优化,添加name为keyword和description的meta标签,减少外链,外链上加上rel="nofollow",标签尽量符合语义,减少不必要的嵌套标签。
- 其他
- 预解析
- 比如首页添加≶link rel="prerender" href="/about.html" />
- 利用缓存
- 比如可以使用百度静态资源公共库(cdn.code.baidu.com),若用户以前访问过其他引用了相同资源地址的文件的话,缓存的优势就出来了。
- 预解析
- 总结:像压缩图片这种方法对提高网页加载速度的效果是很明显的,但是有些优化方法对于访问量小的小型网站而言并没有啥好呢么必要,比如:如果某JS文件本来就只用100来行,压缩后减少的文件大小对页面访问速度的提高等于没有,对服务器压力的减少也没啥意义。
高频率触发的事件的性能优化
一些事件,比如touchmove可能会被高频率地触发,如果该事件对应的handler函数中需要处理的逻辑较多,可能会导致FPS下降影响程序流畅度,在这种情况下,可以考虑将handler中的执行体放于setTimeout(function () { //执行的代码 }, 0)中,程序会变流畅。
debounce 防抖
当事件触发时,函数不立即执行,而是延迟一段时间后再执行。并且这期间只要事件再次被触发,就重新计算延迟时间。所以如果事件被不停触发的话,函数就一直不会被执行。
javascript
/**
* 将函数进行防抖处理,生成一个新的防抖函数
* @param {function|Promise} fn - 需要进行防抖处理的原始函数(可以是async函数)
* @param {number} [delay = 1000] - 防抖延迟时间,单位 ms
* @param {boolean} [immediate = false] 不在延迟时间内的每次第一次触发时是否立即执行
* @param {function} [callback] - 回调函数,用于获取每次最终执行后的结果
* @return {function} 生成的防抖函数
*/
function debounce(fn, delay = 1000, immediate = false, callback) {
let timer = 0;
let isFirstTime = false;
let self = this;
function returnFunc(...args) {
if (timer) {
clearTimeout(timer);
timer = 0;
}
const mainTask = () => {
let errorObj = null;
// apply 第二个入参为数组,call 和 bind 的第二个入参是0到多个arguments
let result = fn.apply(self, args);
const doCallback = () => {
if (typeof callback === "function") {
// 第一个参数表示error,第二个参数表示实际结果
callback(errorObj, result);
}
};
if (result instanceof Promise) {
result
.then((res) => {
result = res;
doCallback();
})
.catch((err) => {
errorObj = err;
doCallback();
});
} else {
doCallback();
}
};
if (immediate && isFirstTime) {
mainTask();
isFirstTime = false;
}
timer = setTimeout(() => {
mainTask();
isFirstTime = true;
}, delay);
}
return returnFunc;
}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
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
throttle 节流
节流的话,当事件被频繁触发时,函数也只会按指定的间隔频率被触发,函数被触发的时间间隔只会大于等于指定时间间隔。节流与防抖的最大区别在于防抖的核心是延时等待,节流的核心在于保证最小时间间隔。
javascript
function throttle(fn, delay) {
let lastTime = 0;
return function () {
const currentTime = Date.now();
if (lastTime > 0 && currentTime - lastTime < delay) {
return;
}
lastTime = currentTime;
fn.apply(this, arguments);
};
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
移动端开发
响应式页面设计的原理
响应式设计
响应式页面设计的原理是让页面根据浏览器屏幕宽度/视口宽度自适应,较理想地呈现出页面内容。
较常见的做法是使用 CSS media query, 而且通常会在meta标签中对viewport的宽度等进行设定(比如设定 width: device-width)。 但即便不用这种方法,只要页面能根据屏幕宽度做出自适应的调整,那就是响应式设计。
rem布局原理
rem布局原理
rem布局的原理是通过设置根元素的字体大小,来控制页面元素的尺寸。
javascript
function fit() {
// 750是设计稿的宽度
const scale = document.documentElement.clientWidth / 750;
// 开发时,以100px对应1rem进行计算
document.querySelector("html").style.fontSize = 100 * scale + "px";
}
document.addEventListener("DOMContentLoaded", () => {
fit();
window.addEventListener("resize", fit);
});1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
移动端 click 事件延时
在移动端使用 click 事件会产生 300ms 的延迟。
问题的产生
移动端存在双击放大的问题,所以在移动端点击事件发生时,为了判断用户的行为(到底是要双击还是要点击),浏览器通常会等待 300ms, 如果 300ms 之内,用户没有再次点击,则判定为点击事件,否则判定为双击缩放。
现代 web 对性能的极致追求,对用户体验的高标准,让这 300ms 的卡顿变得难以接受。
如何解决
user-scalable:no禁止缩放——没有缩放就不存在双击,也就没有点击延迟。指针事件:CSS:
-ms-touch-action:none点击后浏览器不会启用缩放操作,也就不存在延迟。然而这种方法兼容性很不好。FastClick 库:针对这个问题所开发的轻量级库。FastClick 在检测到 touchend 事件后,会立即触发一个模拟的 click 事件,并把 300ms 后真正的 click 事件阻止掉。
FastClick 的用法
javascript
window.addEventListener("load", function () {
// 虽然可以绑定到更具体的元素,但绑定到body上能使整个应用都受益
FastClick.attach(document.body);
});1
2
3
4
2
3
4
当 FastClick 检测到页面中使用了 user-scalable:no 或者 touch-action:none 方案时,会静默退出。
伪类 :active 失效
只需给 document 绑定 touchstart 或 touchend 事件即可, 如 document.addEventListener('touchstart', function () {}, false)。
更简单的方法是直接在 html 中 body 标签上添加属性 ontouchstart=""。
格式检测
不让安卓手机识别邮箱:
html
<meta content="email=no" name="format-detection" />1
禁止IOS识别长串数字为电话:
html
<meta content="telephone=no" name="format-detection" />1
交互限制
禁止 iOS 弹出各种操作窗口:-webkit-touch-callout: none;
禁止用户选中文字:-webkit-user-select: none;
input[type="date"] 不支持 placeholder
html
<input
placeholder="占位符"
type="text"
onfocus="(this.type='date')"
onblur="(this.type='text')"
/>1
2
3
4
5
6
2
3
4
5
6
iOS 部分版本 Date 构造函数不支持 YYYY-MM-DD 格式入参
iOS 部分版本的 Date 构造函数不支持规范标准中定义的 YYYY-MM-DD 格式,如 new Date('2013-11-11') 是 Invalid Date,但支持 YYYY/MM/DD 格式,可用 new Date('2013/11/11')。最通用的还是直接年月日逐个参数直接传入的方式,即 new Date(year, month, date)。
类似的,对于 yyyy-mm-dd hh:mm:ss 格式的日期,可以通过类似下面的方法将其转换为 Date 对象实例(适用于所有设备):
兼容 iOS 的日期对象构造方法
javascript
// 将形如"yyyy-mm-dd hh:mm:ss"的日期字符串转换为日期对象(兼容IOS设备)
function longStringToDate(dateString) {
if (dateString && dateString.length === 19) {
// Attention: there is a space between regular expression
const tempArr = dateString.split(/[- :]/);
return new Date(
tempArr[0],
tempArr[1] - 1,
tempArr[2],
tempArr[3],
tempArr[4],
tempArr[5],
);
}
return "Invalid Date";
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
微前端
微前端方案利弊
“微前端”这个概念刚出来的时候火了一阵子,现在继续提“微前端”的人已经很少了。从招聘软件的职位描述里可以很明显的感觉到几乎没人提“微前端”了,这是用脚投票实打实得投出来的结果——因为大部分项目都不需要使用微前端。
微前端解决的是什么问题?大部分人会这么说:
- 将大项目拆分成小项目,避免巨无霸项目的编译耗时问题。
- 允许每个小项目使用不同的技术栈,方便逐步升级。
- 每个小项目可以独立发布,风险隔离。
- 多个前端项目在用户侧交互起来像是一个前端项目。
如果再和“不使用微前端,直接使用多个独立前端项目,彼此之间通过 URL 跳转”这种朴素方案对比的话,其实就只有最后一个优点是成立的了。
那这个优点是我们的业务痛点吗?好像是的。但是让我们来看一个场景:你随便打开一个你觉得最复杂的手机 APP 或者PC端的 Web app,你点进去之后你平时经常浏览的页面有几个?根本就没几个页面!而且事实上就算把不常访问的页面都算上,一般也没几个页面。实际上,用户对于低频访问的页面,是不介意多一次明显的页面跳转的,用户只对高频访问的页面的交互体验敏感。比如,对于一个互联网金融项目而言,用户做风险测评和做账户开户这两个操作都是低频操作。大部分项目,完全可以这样设计:
- 高频访问的页面放到一个独立项目里。
- 低频访问的页面按相关性拆分成多个独立项目。
这样就可以避免微前端方案带来的一系列弊端了。
前端面试
实现对象深拷贝
实现方式
javascript
function deepClone(obj) {
// if not object
if (typeof obj !== "object") {
return obj;
}
// if null
if (obj === null) {
return null;
}
// if array
if (Array.isArray(obj)) {
return obj.map((elem) => deepClone(elem));
}
// if obj
const tempObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
tempObj[key] = deepClone(obj[key]);
}
}
return tempObj;
}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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
数组扁平化
问题描述
- 将数组中的所有数组元素扁平化成顶层元素,返回新数组,不修改原数组
- 增加去重功能,重复的为基本数据类型的元素不显示(只是不显示重复的部分,即如果有两个2,第二个重复的2不显示,第一个2还是要显示的)
解决方案
javascript
function flattenArray(arr) {
if (!Array.isArray(arr)) {
throw new TypeError("You should pass in an Array parameter");
}
const tempArr = [];
const tempObj = {};
void (function recurison(item) {
if (Array.isArray(item)) {
item.forEach(recurison);
} else {
if (typeof item === "object") {
tempArr.push(item);
} else if (!tempObj[item]) {
tempArr.push(item);
tempObj[item] = true;
}
}
})(arr);
return tempArr;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
数字数组奇偶排序
问题描述
将数组中奇数放在右边,偶数放在左边,不允许使用额外空间。
说明:从一个数组中间删除元素splice的运行代价是比较大的。
解决方案
javascript
const arr = [1, 4, 5, 2, 3, 7, 8];
arr.sort(function (a, b) {
return a % 2 !== 0;
});1
2
3
4
2
3
4
求数组中第二大的数
问题描述
要求:
- 不能对这个数组进行整体排序;
- 若要用循环,只能一重循环;
- 不使用额外空间。
解决方案
思路:把最大的数字标记为null,然后再求此时的最大数字。
javascript
const arr = [1, 3, 5, 2, 7, 6];
arr[arr.indexOf(Math.max.apply(null, arr))] = null;
Math.max.apply(null, arr);1
2
3
2
3