前端编码规范与调试心得分享

前端编码规范与调试心得分享

大纲

  • 编码规范篇章
    • 探索:大厂相关规范的定义
    • 落地:企业项目中的实战工具
    • 深入:编码规范的细致探讨
    • 提升:设计模式和编程思想
  • 调试心得分享
    • 框架篇: 浏览器调试框架介绍
    • 原理篇:堆栈分析原理
    • 工具篇:调试工具的探索
    • 技巧篇:真实调试技巧举例
  • 总结

编码规范篇章

探索:大厂相关规范的定义

前端编码规范会带来什么好处

  1. 提高代码的可读性和可维护性:规范的代码更易于阅读和理解,也更容易被其他开发者所接受。遵循相同的编码规范有助于确保代码的一致性,提高团队协作效率。
  2. 减少错误和 bug:规范的代码更容易发现和修复错误,因为每个人都知道相同的代码结构、命名约定和最佳实践。
  3. 提高开发效率:遵循规范的代码使得开发者可以更快地编写和理解代码,减少在编码规范上的争论和内耗。
  4. 提高代码质量:规范的代码往往更加简洁和高效,因为每个人都遵循了最佳实践和性能优化方法。
  5. 方便代码审查和重构:规范的代码使得代码审查和重构更加高效,因为每个人都知道相同的代码结构和命名约定。

腾讯: TG Ideas

https://tgideas.qq.com/doc/index.html

包含内容如下

  • 通用规范
  • PC 端专属规范
  • 移动端专属规范
  • 特殊业务规范
  • 组件与工具
  • 技术分享

京东: 凹凸实验室

https://guide.aotu.io/index.html

包含内容如下:

  • HTML 规范
  • 图片规范
  • CSS 规范
  • 命名规范
  • Javascript 规范
  • React 规范

百度:EPEC

https://github.com/ecomfe/spec

包含内容如下:

  • JavaScript 规范
  • CSS 规范
  • HTML 规范
  • Less 编码规范
  • E-JSON 数据传输标准
  • 模块和加载器规范
  • 包结构规范
  • 项目目录结果规范
  • 图表库规范
  • react 编码规范

爱彼迎:Airbnb

https://github.com/airbnb/javascript

主要包含内容:

  • JavaScript 内部细节规范
  • 数组
  • 对象
  • 解构
  • 箭头函数
  • 类和构造函数
  • 等等

民间标准:standard

https://github.com/standard/standard

  • 各类标准的定义
  • 安装到代码中和 eslint 结合

Vue: 风格指南

2.x https://v2.cn.vuejs.org/v2/style-guide/index.html
3.x https://cn.vuejs.org/style-guide/rules-use-with-caution.html

主要内容包括:

  • Vue 使用过程中的规范写法
  • 推荐用法

React: 参考总览

官方:https://react.docschina.org/reference/react
个人整理: https://jdf2e.github.io/jdc_fe_guide/docs/react/code/

主要内容包括:

  • Hook
  • 组件
  • API
  • 指示符

TypeScript: 编码规范

官方:https://www.tslang.cn/docs/handbook/declaration-files/do-s-and-don-ts.html
个人整理: https://bosens-china.github.io/Typescript-manual/download/zh/wiki/coding_guidelines.html


主要内容包括:

  • 命名
  • 组件
  • 类型
  • 一般假设
  • 标记
  • 注释

民间标准:阮一峰 编程风格

https://es6.ruanyifeng.com/#docs/style

主要内容包括:

  • 如何将 ES6 的新语法,运用到编码实践之中,与传统的 JavaScript 语法结合在一起,写出合理的、易于阅读和维护的代码。

Bootstrap:编程规范

https://codeguide.bootcss.com/

  • 编写一致、灵活和可持续的 HTML 和 CSS 代码的规范。

落地:企业项目中的实战工具

EsLint: 编程规范

注:TsLint 已经停止维护,迁移到 EsLint

https://eslint.org/

  • 业界使用最多的编程规范

目前深度使用

Prettier:编程规范

https://www.prettier.cn/

  • 一个“有态度”的代码格式化工具
  • 支持大量编程语言
  • 已集成到大多数编辑器中

目前深度使用

EditorConfig:跨编辑器规范文件

https://editorconfig.org/

在多人协作的项目中,为了同一代码格式,可以使用 editorconfig 来定义,这样不仅可以帮助协作者快速了解当前项目要求的代码风格,也可以方便使用一些格式化工具保证代码风格按照预期格式化。

部分项目使用

GitHooks:提交钩子

https://git-scm.com/docs/githooks

规范的执行者,不符合规范的代码不允许提交。

部分项目使用

深入:编码规范的细致探讨

本章节参考京东的凹凸实验室为主要参考。

HTML 规范

DOCTYPE 声明

HTML 文件必须加上 DOCTYPE 声明,并统一使用 HTML5 的文档声明:

1
<!DOCTYPE html>
页面语言 LANG
1
<html lang="zh-CN"></html>
CHARSET

一般情况下统一使用 “UTF-8” 编码

1
<meta charset="UTF-8" />

由于历史原因,有些业务可能会使用 “GBK” 编码

1
<meta charset="GBK" />
元素及标签闭合

HTML 元素共有以下 5 种:

空元素:area、base、br、col、command、embed、hr、img、input、keygen、link、meta、param、source、track、wbr
原始文本元素:script、style
RCDATA 元素:textarea、title
外来元素:来自 MathML 命名空间和 SVG 命名空间的元素。
常规元素:其他 HTML 允许的元素都称为常规元素。

元素标签的闭合应遵循以下原则:

原始文本元素、RCDATA 元素以及常规元素都有一个开始标签来表示开始,一个结束标签来表示结束。
某些元素的开始和结束标签是可以省略的,如果规定标签不能被省略,那么就绝对不能省略它。
空元素只有一个开始标签,且不能为空元素设置结束标签。
外来元素可以有一个开始标签和配对的结束标签,或者只有一个自闭合的开始标签,且后者情况下该元素不能有结束标签。

为了能让浏览器更好的解析代码以及能让代码具有更好的可读性,有如下约定:

所有具有开始标签和结束标签的元素都要写上起止标签,某些允许省略开始标签或和束标签的元素亦都要写上。

书写风格

HTML 代码大小写
HTML 标签名、类名、标签属性和大部分属性值统一用小写

推荐:

1
<div class="demo"></div>

不推荐:

1
2
<div class="DEMO"></div>
<div class="DEMO"></div>
类型属性

不需要为 CSS、JS 指定类型属性,HTML5 中默认已包含

推荐:

1
2
<link rel="stylesheet" href="" />
<script src=""></script>

不推荐:

1
2
<link rel="stylesheet" type="text/css" href="" />
<script type="text/javascript" src=""></script>

元素属性

元素属性值使用双引号语法
元素属性值可以写上的都写上

推荐:

1
<input type="text" /> <input type="radio" name="name" checked="checked" />

不推荐:

1
2
3
4
<input type="text" />
<input type="text" />

<input type="radio" name="name" checked />

特殊字符引用

文本可以和字符引用混合出现。这种方法可以用来转义在文本中不能合法出现的字符。

在 HTML 中不能使用小于号 “<” 和大于号 “>”特殊字符,浏览器会将它们作为标签解析,若要正确显示,在 HTML 源代码中使用字符实体

推荐:

1
<a href="#">more&gt;&gt;</a>

不推荐:

1
<a href="#">more>></a>

代码缩进
统一使用四个空格进行代码缩进,使得各编辑器表现一致(各编辑器有相关配置)

1
2
3
<div class="jdc">
<a href="#"></a>
</div>

纯数字输入框
使用 type=”tel” 而不是 type=”number”

1
<input type="tel" />

代码嵌套
元素嵌套规范,每个块状元素独立一行,内联元素可选

推荐:

1
2
3
4
5
<div>
<h1></h1>
<p></p>
</div>
<p><span></span><span></span></p>

不推荐:

1
2
3
4
5
6
7
8
<div>
<h1></h1>
<p></p>
</div>
<p>
<span></span>
<span></span>
</p>

段落元素与标题元素只能嵌套内联元素

推荐:

1
2
<h1><span></span></h1>
<p><span></span><span></span></p>

不推荐:

1
2
<h1><div></div></h1>
<p><div></div><div></div></p>
单行注释

一般用于简单的描述,如某些状态描述、属性描述等

注释内容前后各一个空格字符,注释位于要注释代码的上面,单独占一行

推荐:

1
2
<!-- Comment Text -->
<div>...</div>

不推荐:

1
2
3
4
5
6
7
<div>...</div>
<!-- Comment Text -->

<div>
<!-- Comment Text -->
...
</div>
模块注释

一般用于描述模块的名称以及模块开始与结束的位置

1
注释内容前后各一个空格字符,<!-- S Comment Text --> 表示模块开始,<!-- E Comment Text --> 表示模块结束,模块与模块之间相隔一行

推荐写法:

1
2
3
4
5
6
7
<!-- S Comment Text A -->
<div class="mod_a">...</div>
<!-- E Comment Text A -->

<!-- S Comment Text B -->
<div class="mod_b">...</div>
<!-- E Comment Text B -->

不推荐写法:

1
2
3
4
5
6
<!-- S Comment Text A -->
<div class="mod_a">...</div>
<!-- E Comment Text A -->
<!-- S Comment Text B -->
<div class="mod_b">...</div>
<!-- E Comment Text B -->

嵌套模块注释
当模块注释内再出现模块注释的时候,为了突出主要模块,嵌套模块不再使用

图片格式

常见的图片格式有 GIF、PNG8、PNG24、JPEG、WEBP,根据图片格式的特性和场景需要选取适合的图片格式。

GIF
GIF 图象是基于颜色列表的(存储的数据是该点的颜色对应于颜色列表的索引值),最多只支持 8 位(256 色)。GIF 文件内部分成许多存储块,用来存储多幅图象或者是决定图象表现行为的控制块,用以实现动画和交互式应用。GIF 文件还通过 LZW 压缩算法压缩图象数据来减少图象尺寸

特性
优秀的压缩算法使其在一定程度上保证图像质量的同时将体积变得很小。
可插入多帧,从而实现动画效果。
可设置透明色以产生对象浮现于背景之上的效果。
由于采用了 8 位压缩,最多只能处理 256 种颜色,故不宜应用于真彩色图片。

PNG

PNG 是 20 世纪 90 年代中期开始开发的图像文件存储格式,其目的是企图替代 GIF 和 TIFF 文件格式,同时增加一些 GIF 文件格式所不具备的特性。流式网络图形格式(Portable Network Graphic Format,PNG)名称来源于非官方的“PNG’s Not GIF”,是一种位图文件(bitmap file)存储格式,读成“ping”。PNG 用来存储灰度图像时,灰度图像的深度可多到 16 位,存储彩色图像时,彩色图像的深度可多到 48 位,并且还可存储多到 16 位的 α 通道数据。PNG 使用从 LZ77 派生的无损数据压缩算法。

特性
支持 256 色调色板技术,文件体积小。
无损压缩
最高支持 48 位真彩色图像以及 16 位灰度图像。
支持 Alpha 通道的透明/半透明特性。
支持图像亮度的 Gamma 校准信息。
支持存储附加文本信息,以保留图像名称、作者、版权、创作时间、注释等信息。
渐近显示和流式读写,适合在网络传输中快速显示预览效果后再展示全貌。
使用 CRC 防止文件出错。
最新的 PNG 标准允许在一个文件内存储多幅图像。

JPEG
JPEG 是一种针对照片视频而广泛使用的一种有损压缩标准方法。这个名称代表 Joint Photographic Experts Group(联合图像专家小组)。此团队创立于公元 1986 年,1992 年发布了 JPEG 的标准而在 1994 年获得了 ISO 10918-1 的认定

特性
适用于储存 24 位元全采影像
采取的压缩方式通常为有损压缩
不支持透明或动画
压缩比越高影像耗损越大,失真越严重
压缩比在 10 左右肉眼无法辨出压缩图与原图的差别`

WEBP
WebP,是一种同时提供了有损压缩与无损压缩的图片文件格式,派生自视频编码格式 VP8,是由 Google 在购买 On2 Technologies 后发展出来。WebP 最初在 2010 年发布,2011 年 11 月 8 日,Google 开始让 WebP 支持无损压缩和透明色的功能,而在 2012 年 8 月 16 日的参考实做 libwebp 0.2.0 中正式支持

特性
同时提供有损压缩和无损压缩两种图片文件格式
文件体积小,无损压缩后,比 PNG 文件少了 45% 的文件大小;有损压缩后,比 JPEG 文件少了 25% - 34% 文件大小
浏览器兼容差,目前只支持客户端 Chrome 和 Opera 浏览器以及安卓原生浏览器(Andriod 4.0+),

团队约定

内容图

内容图多以商品图等照片类图片形式存在,颜色较为丰富,文件体积较大

优先考虑 JPEG 格式,条件允许的话优先考虑 WebP 格式
尽量不使用 PNG 格式,PNG8 色位太低,PNG24 压缩率低,文件体积大

背景图

背景图多为图标等颜色比较简单、文件体积不大、起修饰作用的图片

  • PNG 与 GIF 格式,优先考虑使用 PNG 格式,PNG 格式允许更多的颜色并提供更好的压缩率
  • 图像颜色比较简单的,如纯色块线条图标,优先考虑使用 PNG8 格式,避免不使用 JPEG 格式
  • 图像颜色丰富而且图片文件不太大的(40KB 以下)或有半透明效果的优先考虑 PNG24 格式
  • 图像颜色丰富而且文件比较大的(40KB - 200KB)优先考虑 JPEG 格式
  • 条件允许的,优先考虑 WebP 代替 PNG 和 JPEG 格式
图片质量

上线的图片都应该经过压缩处理,压缩后的图片不应该出现肉眼可感知的失真区域
60 质量的 JPEG 格式图片与质量大于 60 的相比,肉眼已看不出明显的区别,因此保存 JPEG 图的时候,质量一般控制在 60,若保真度要求高的图片可适量提高到 80,图片大小控制在 200KB 以内

CSS 规范

CSS Sprites VS Data URIs

CSS Sprites 特点

  • 减少请求数
  • 加速图片的显示
  • 维护更新成本大
  • 更多的内存消耗,特别是大体积或有过多空白的 Sprites 图
  • 图片渗漏,相邻的不需展示的图片有可能出现在展示元素中,特别是在高清设备移动设备上

Data URIs(base64 编码)

  • 减少请求数
  • 转换文件体积大,大约比原始的二进制大 33%
  • IE6 / IE7 不支持
  • 图片显示相对较慢,需要更多的 CPU 消耗

CSS Sprites 使用建议

  • 适合使用频率高更新频率低的小图标
  • 尽量不留太多的空白
  • 体积较大的图片不合并
  • 确保要合并的小图坐标数值和合并后的 Sprites 图尺寸均为偶数

Data URIs(base64 编码)使用建议

  • 适合更新频率高的小图片,如某些具备自定义功能的标题 icon 等
  • 转换成 Base64 编码的图片应小于 2KB
  • 移动端不使用 Base64 编码
  • 要兼容 IE6/IE7 的不使用
样式表编码

样式文件必须写上 @charset 规则,并且一定要在样式文件的第一行首个字符位置开始写,编码名用 “UTF-8”
字符 @charset “”; 都用小写字母,不能出现转义符,编码名允许大小混写

css 代码格式化

样式书写一般有两种:一种是紧凑格式 (Compact)

1
2
3
4
.jdc {
display: block;
width: 50px;
}

一种是展开格式(Expanded)

1
2
3
4
.jdc {
display: block;
width: 50px;
}

团队约定

统一使用展开格式书写样式

css 代码大小写

样式选择器,属性名,属性值关键字全部使用小写字母书写,属性字符串允许使用大小写。

1
2
3
4
/* 推荐 */
.jdc {
display: block;
}
1
2
3
4
/* 不推荐 */
.JDC {
display: BLOCK;
}

选择器

尽量少用通用选择器 *
不使用 ID 选择器
不使用无具体语义定义的标签选择器

1
2
3
4
5
6
7
/* 推荐 */
.jdc {
}
.jdc li {
}
.jdc li p {
}
1
2
3
4
5
6
7
/* 不推荐 */
* {
}
#jdc {
}
.jdc div {
}
重置样式

为了保证代码在不同浏览器上的一致性,需要在加载样式前进行样式重置。

移动端

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
* {
-webkit-tap-highlight-color: transparent;
outline: 0;
margin: 0;
padding: 0;
vertical-align: baseline;
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
legend,
button,
input,
textarea,
th,
td {
margin: 0;
padding: 0;
vertical-align: baseline;
}
img {
border: 0 none;
vertical-align: top;
}
i,
em {
font-style: normal;
}
ol,
ul {
list-style: none;
}
input,
select,
button,
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
font-family: inherit;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
a {
text-decoration: none;
color: #666;
}
body {
margin: 0 auto;
min-width: 320px;
max-width: 640px;
height: 100%;
font-size: 14px;
font-family: -apple-system, Helvetica, sans-serif;
line-height: 1.5;
color: #666;
-webkit-text-size-adjust: 100% !important;
text-size-adjust: 100% !important;
}
input[type="text"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}

PC 端

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
html,
body,
div,
h1,
h2,
h3,
h4,
h5,
h6,
p,
dl,
dt,
dd,
ol,
ul,
li,
fieldset,
form,
label,
input,
legend,
table,
caption,
tbody,
tfoot,
thead,
tr,
th,
td,
textarea,
article,
aside,
audio,
canvas,
figure,
footer,
header,
mark,
menu,
nav,
section,
time,
video {
margin: 0;
padding: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
font-weight: normal;
}
article,
aside,
dialog,
figure,
footer,
header,
hgroup,
nav,
section,
blockquote {
display: block;
}
ul,
ol {
list-style: none;
}
img {
border: 0 none;
vertical-align: top;
}
blockquote,
q {
quotes: none;
}
blockquote:before,
blockquote:after,
q:before,
q:after {
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
strong,
em,
i {
font-style: normal;
font-weight: normal;
}
ins {
text-decoration: underline;
}
del {
text-decoration: line-through;
}
mark {
background: none;
}
input::-ms-clear {
display: none !important;
}
body {
font: 12px/1.5 \5FAE\8F6F\96C5\9ED1, \5B8B\4F53, "Hiragino Sans GB", STHeiti, "WenQuanYi Micro Hei",
"Droid Sans Fallback", SimSun, sans-serif;
background: #fff;
}
a {
text-decoration: none;
color: #333;
}
a:hover {
text-decoration: underline;
}
媒体查询

媒体查询一般用于,非 REM 布局下的大小屏、桌面端移动端一套代码的适配、不同设备像素比下的图片适配、不同宽度设备下的元素重新排版等。

判断设备横竖屏

1
2
3
4
5
6
7
/* 横屏 */
@media all and (orientation: landscape) {
}

/* 竖屏 */
@media all and (orientation: portrait) {
}

判断设备宽高

1
2
3
/* 设备宽度大于 320px 小于 640px */
@media all and (min-width: 320px) and (max-width: 640px) {
}

判断设备像素比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 设备像素比为 1 */
@media only screen and (-webkit-min-device-pixel-ratio: 1),
only screen and (min-device-pixel-ratio: 1) {
}

/* 设备像素比为 1.5 */
@media only screen and (-webkit-min-device-pixel-ratio: 1.5),
only screen and (min-device-pixel-ratio: 1.5) {
}

/* 设备像素比为 2 */
@media only screen and (-webkit-min-device-pixel-ratio: 2),
only screen and (min-device-pixel-ratio: 2) {
}

命名规范

参考凹凸实验室的命名规范

目录命名
  • 项目文件夹:projectname
  • 样式文件夹:css
  • 脚本文件夹:js
  • 样式类图片文件夹:img

我们实用中的命名规范是基于 vue react 这种开源框架的规范做第一层,在页面和组件中根据业务逻辑做第二层,在工具函数、服务层根据模块抽离做第三层。

命名规范需要做到尽量合理。

HTML/CSS 文件命名

确保文件命名总是以字母开头而不是数字,且字母一律小写,以下划线连接且不带其他标点符号,如:

1
2
3
4
5
6
7
8
9
<!-- HTML -->
jdc.html
jdc_list.html
jdc_detail.html

<!-- SASS -->
jdc.scss
jdc_list.scss
jdc_detail.scss

为什么要小写?原因是因为,windows 大小写不敏感。如有两个文件 a.js A.js 在 windows 上会有读取问题,和 git 上传删除提交等问题。

这里要求小写是统一约束和规范,理论上来说同一个文件夹下只要不要有两个名称除了大小写不一致,文件名一致的文件即可。

遵循约束可以减少发生的问题。

ClassName 命名

ClassName 的命名应该尽量精短、明确,必须以字母开头命名,且全部字母为小写,单词之间统一使用下划线 “_” 连接

命名原则

基于姓氏命名法(继承 + 外来),如下图:

推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="modulename">
<div class="modulename_info">
<div class="modulename_son"></div>
<div class="modulename_son"></div>
...
</div>
</div>

<!-- 这个是全站公用模块,祖先模块允许直接出现下划线 -->
<div class="mod_info">
<div class="mod_info_son"></div>
<div class="mod_info_son"></div>
...
</div>

不推荐

1
2
3
4
5
<div class="modulename_info">
<div class="modulename_info_son"></div>
<div class="modulename_info_son"></div>
...
</div>

在子孙模块数量可预测的情况下,严格继承祖先模块的命名前缀

1
2
3
4
<div class="modulename">
<div class="modulename_cover"></div>
<div class="modulename_info"></div>
</div>

当子孙模块超过 4 级或以上的时候,可以考虑在祖先模块内具有识辨性的独立缩写作为新的子孙模块

推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div class="modulename">
<div class="modulename_cover"></div>
<div class="modulename_info">
<div class="modulename_info_user">
<div class="modulename_info_user_img">
<img src="" alt="" />
<!-- 这个时候 miui 为 modulename_info_user_img 首字母缩写-->
<div class="miui_tit"></div>
<div class="miui_txt"></div>
...
</div>
</div>
<div class="modulename_info_list"></div>
</div>
</div>

不推荐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div class="modulename">
<div class="modulename_cover"></div>
<div class="modulename_info">
<div class="modulename_info_user">
<div class="modulename_info_user_img">
<img src="" alt="" />
<div class="modulename_info_user_img_tit"></div>
<div class="modulename_info_user_img_txt"></div>
...
</div>
</div>
<div class="modulename_info_list"></div>
</div>
</div>

模块命名

全站公共模块:以 mod 开头

1
<div class="mod_yours"></div>

业务公共模块:以 业务名mod 开头

1
<div class="paipai_mod_yours"></div>

常用命名推荐

注意:ad、banner、gg、guanggao 等有机会和广告挂勾的字眠不建议直接用来做 ClassName,因为有些浏览器插件(Chrome 的广告拦截插件等)会直接过滤这些类名,因此

1
<div class="ad"></div>

这种广告的英文或拼音类名不应该出现

另外,敏感不和谐字眼也不应该出现,如:

1
2
3
4
5
6
<div class="fuck"></div>
<div class="jer"></div>
<div class="sm"></div>
<div class="gcd"></div>
<div class="ass"></div>
<div class="KMT"></div>

说到 css 的模块命名方法,这块内容在当前开发看来是上一代的做法。

现在的 css 都会经历预编译的过程,加入 scoped 则会增加当前页面的 hash,确保样式只对当前页生效。

下一代的原子化 css 则采用 jit 编译模式,使用约定的名称去彻底抛弃的命名规范。

此章节作为思想去学习,让大家知道为什么要这样去做。

JS 语言规范

类型

原始类型: 存取原始类型直接作用于值本身

布尔类型
Null 类型
Undefined 类型
数字类型
BigInt 类型
字符串类型
符号类型 Symbol

1
2
3
4
5
6
const foo = 1;
let bar = foo;

bar = 9;

console.log(foo, bar); // 1, 9

复杂类型: 访问复杂类型作用于值的引用

1
2
3
4
5
6
7
8
9
object
array
function
const foo = [1, 2, 3]
const bar = foo

bar[0] = 9

console.log(foo[0], bar[0]) // 9, 9
引用

请记得 const 和 let 都是块级作用域,var 是函数级作用域

1
2
3
4
5
6
7
// const and let only exist in the blocks they are defined in.
{
let a = 1;
const b = 1;
}
console.log(a); // ReferenceError
console.log(b); // ReferenceError

对所有引用都使用 const,不要使用 var,eslint: prefer-const, no-const-assign
原因:这样做可以确保你无法重新分配引用,以避免出现错误和难以理解的代码

1
2
3
4
5
6
7
// bad
var a = 1;
var b = 2;

// good
const a = 1;
const b = 2;

如果引用是可变动的,使用 let 代替 var,eslint: no-var
原因:let 是块级作用域的,而不像 var 属于函数级作用域

1
2
3
4
5
6
7
8
9
10
11
// bad
var count = 1;
if (count < 10) {
count += 1;
}

// good
let count = 1;
if (count < 10) {
count += 1;
}
对象

请使用字面量值创建对象,eslint: no-new-object

1
2
3
4
5
// bad
const a = new Object{}

// good
const a = {}

别使用保留字作为对象的键值,这样在 IE8 下不会运行

1
2
3
4
5
6
7
8
9
10
11
// bad
const a = {
default: {}, // default 是保留字
common: {},
};

// good
const a = {
defaults: {},
common: {},
};

当使用动态属性名创建对象时,请使用对象计算属性名来进行创建

原因:因为这样做就可以让你在一个地方定义所有的对象属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getKey(k) {
return `a key named ${k}`;
}

// bad
const obj = {
id: 5,
name: "San Francisco",
};
obj[getKey("enabled")] = true;

// good
const obj = {
id: 5,
name: "San Francisco",
[getKey("enabled")]: true,
};

请使用对象方法的简写方式,eslint: object-shorthand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bad
const item = {
value: 1,

addValue: function (val) {
return item.value + val;
},
};

// good
const item = {
value: 1,

addValue(val) {
return item.value + val;
},
};

请使用对象属性值的简写方式,eslint: object-shorthand

原因:这样更简短且描述更清楚

1
2
3
4
5
6
7
8
9
10
11
const job = "FrontEnd";

// bad
const item = {
job: job,
};

// good
const item = {
job,
};

将简写的对象属性分组后统一放到对象声明的开头

原因:这样更容易区分哪些属性用了简写的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const job = "FrontEnd";
const department = "JDC";

// bad
const item = {
sex: "male",
job,
age: 25,
department,
};

// good
const item = {
job,
department,
sex: "male",
age: 25,
};

只对非法标识符的属性使用引号,eslint: quote-props
原因:因为通常来说我们认为这样主观上会更容易阅读,这样会带来代码高亮上的提升,同时也更容易被主流 JS 引擎优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
const bad = {
foo: 3,
bar: 4,
"data-blah": 5,
};

// good
const good = {
foo: 3,
bar: 4,
"data-blah": 5,
};

不要直接使用 Object.prototype 的方法, 例如 hasOwnProperty, propertyIsEnumerable 和 isPrototypeOf 方法,eslint: no-prototype-builtins
原因:这些方法可能会被对象自身的同名属性覆盖 - 比如 { hasOwnProperty: false } 或者对象可能是一个 null 对象(Object.create(null))

1
2
3
4
5
6
7
8
9
10
11
12
// bad
console.log(object.hasOwnProperty(key));

// good
console.log(Object.prototype.hasOwnProperty.call(object, key));

// best
const has = Object.prototype.hasOwnProperty; // cache the lookup once, in module scope.
console.log(has.call(object, key));
/* or */
import has from "has"; // https://www.npmjs.com/package/has
console.log(has(object, key));

优先使用对象展开运算符 … 来做对象浅拷贝而不是使用 Object.assign,使用对象剩余操作符来获得一个包含确定的剩余属性的新对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// very bad
const original = { a: 1, b: 2 };
const copy = Object.assign(original, { c: 3 }); // this mutates `original` ಠ_ಠ
delete copy.a; // so does this

// bad
const original = { a: 1, b: 2 };
const copy = Object.assign({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 }

// good
const original = { a: 1, b: 2 };
const copy = { ...original, c: 3 }; // copy => { a: 1, b: 2, c: 3 }

const { a, ...noA } = copy; // noA => { b: 2, c: 3 }
数组

请使用字面量值创建数组,eslint: no-array-constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
const items = new Array()

// good
const items = []
向数组中添加元素时,请使用 push 方法

const items = []

// bad
items[items.length] = 'test'

// good
items.push('test')

使用展开运算符 … 复制数组

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
const items = [];
const itemsCopy = [];
const len = items.length;
let i;

// bad
for (i = 0; i < len; i++) {
itemsCopy[i] = items[i];
}

// good
itemsCopy = [...items];

把一个可迭代的对象转换为数组时,使用展开运算符 … 而不是 Array.from

1
2
3
4
5
6
7
const foo = document.querySelectorAll(".foo");

// good
const nodes = Array.from(foo);

// best
const nodes = [...foo];

使用 Array.from 来将一个类数组对象转换为数组

1
2
3
4
5
6
7
const arrLike = { 0: "foo", 1: "bar", 2: "baz", length: 3 };

// bad
const arr = Array.prototype.slice.call(arrLike);

// good
const arr = Array.from(arrLike);

遍历迭代器进行映射时使用 Array.from 代替扩展运算符 …, 因为这可以避免创建中间数组

1
2
3
4
5
// bad
const baz = [...foo].map(bar);

// good
const baz = Array.from(foo, bar);

使用数组的 map 等方法时,请使用 return 声明,如果是单一声明语句的情况,可省略 return

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
// good
[1, 2, 3]
.map((x) => {
const y = x + 1;
return x * y;
})

[
// good
(1, 2, 3)
].map((x) => x + 1);

// bad
const flat = {}[([0, 1], [2, 3], [4, 5])].reduce((memo, item, index) => {
const flatten = memo.concat(item);
flat[index] = flatten;
});

// good
const flat = {}[([0, 1], [2, 3], [4, 5])].reduce((memo, item, index) => {
const flatten = memo.concat(item);
flat[index] = flatten;
return flatten;
});

// bad
inbox.filter((msg) => {
const { subject, author } = msg;
if (subject === "Mockingbird") {
return author === "Harper Lee";
} else {
return false;
}
});

// good
inbox.filter((msg) => {
const { subject, author } = msg;
if (subject === "Mockingbird") {
return author === "Harper Lee";
}

return false;
});

如果一个数组有多行则要在数组的开括号后和闭括号前使用新行

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
// bad
const arr = [
[0, 1],
[2, 3],
[4, 5],
];

const objectInArray = [
{
id: 1,
},
{
id: 2,
},
];

const numberInArray = [1, 2];

// good
const arr = [
[0, 1],
[2, 3],
[4, 5],
];

const objectInArray = [
{
id: 1,
},
{
id: 2,
},
];

const numberInArray = [1, 2];
解构赋值

当需要使用对象的多个属性时,请使用解构赋值,eslint: prefer-destructuring
愿意:解构可以避免创建属性的临时引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bad
function getFullName(user) {
const firstName = user.firstName;
const lastName = user.lastName;

return `${firstName} ${lastName}`;
}

// good
function getFullName(user) {
const { firstName, lastName } = user;

return `${firstName} ${lastName}`;
}

// better
function getFullName({ firstName, lastName }) {
return `${firstName} ${lastName}`;
}

当需要使用数组的多个值时,请同样使用解构赋值,eslint: prefer-destructuring

1
2
3
4
5
6
7
8
const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;

函数需要回传多个值时,请使用对象的解构,而不是数组的解构

原因:可以非破坏性地随时增加或者改变属性顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
function doSomething() {
return [top, right, bottom, left];
}
// 如果是数组解构,那么在调用时就需要考虑数据的顺序
const [top, xx, xxx, left] = doSomething();

// good
function doSomething() {
return { top, right, bottom, left };
}

// 此时不需要考虑数据的顺序
const { top, left } = doSomething();
字符串

字符串统一使用单引号的形式 ‘’,eslint: quotes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// bad
const department = "JDC"

// good
const department = 'JDC'
字符串太长的时候,请不要使用字符串连接符换行 \,而是使用 +

const str = '凹凸实验室 凹凸实验室 凹凸实验室' +
'凹凸实验室 凹凸实验室 凹凸实验室' +
'凹凸实验室 凹凸实验室'
程序化生成字符串时,请使用模板字符串,eslint: prefer-template template-curly-spacing

const test = 'test'

// bad
const str = ['a', 'b', test].join()

// bad
const str = 'a' + 'b' + test

// good
const str = `ab${test}`

不要对字符串使用 eval(),会导致太多漏洞, eslint: no-eval

不要在字符串中使用不必要的转义字符, eslint: no-useless-escape

1
2
3
4
5
6
// bad
const foo = "'this' is \"quoted\"";

// good
const foo = "'this' is \"quoted\"";
const foo = `my name is '${name}'`;
函数

不要使用 Function 构造函数创建函数, eslint: no-new-func
原因:此方式创建函数和对字符串使用 eval() 一样会产生漏洞

1
2
3
4
5
// bad
const add = new Function("a", "b", "return a + b");

// still bad
const subtract = Function("a", "b", "return a - b");

在函数签名中使用空格,eslint: space-before-function-paren space-before-blocks

1
2
3
4
5
6
7
const f = function () {};
const g = function () {};
const h = function () {};

// good
const x = function b() {};
const y = function a() {};

使用具名函数表达式而非函数声明,eslint: func-style
原因:这样做会导致函数声明被提升,这意味着很容易在文件中定义此函数之前引用它,不利于可读性和可维护性。如果你发现函数定义既庞大又复杂以至于不能理解文件的其他部分,或许你应该将它拆分成模块!别忘记要显式命名表达式,而不用管名字是否是从包含的变量(通常出现在现代浏览器中或者使用 Babel 编译器的时候)中推断的。这样会消除错误调用堆栈中的任何假设。 (讨论)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// bad
function foo() {
// ...
}

// bad
const foo = function () {
// ...
};

// good
// lexical name distinguished from the variable-referenced invocation(s)
const short = function longUniqueMoreDescriptiveLexicalFoo() {
// ...
};

用圆括号包裹自执行匿名函数,eslint:wrap-iife
原因:一个立即执行匿名函数表达式是一个单一的单元,将其及其调用括号包装在括号中,能够清楚地表达这一点。注意,在到处都是模块的世界中几乎不需要 IIFE。

1
2
3
4
// immediately-invoked function expression (IIFE)
(function () {
console.log("Welcome to the Internet. Please follow me.");
})();

不要在非函数代码块(if , while 等)中声明函数,eslint:no-loop-func

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
if (isUse) {
function test() {
// do something
}
}

// good
let test;
if (isUse) {
test = () => {
// do something
};
}

不要将参数命名为 arguments,会导致该参数的优先级高于每个函数作用域内原先存在的 arguments 对象

1
2
3
4
5
6
7
8
9
// bad
function foo(name, options, arguments) {
// ...
}

// good
function foo(name, options, args) {
// ...
}

不要使用 arguments,使用 剩余运算符 …

arguments 只是一个类数组,而 … 是一个真正的数组

1
2
3
4
5
6
7
8
9
10
// bad
function test() {
const args = Array.prototype.slice.call(arguments);
return args.join("");
}

// good
function test(...args) {
return args.join("");
}

使用参数默认值语法而不是修改函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// really bad
function handleThings(opts) {
// No! We shouldn't mutate function arguments.
// Double bad: if opts is falsy it'll be set to an object which may
// be what you want but it can introduce subtle bugs.
opts = opts || {};
// ...
}

// still bad
function handleThings(opts) {
if (opts === void 0) {
opts = {};
}
// ...
}

// good
function handleThings(opts = {}) {
// ...
}

避免参数默认值的副作用

1
2
3
4
5
6
7
8
9
let b = 1;
// bad
function count(a = b++) {
console.log(a);
}
count(); // 1
count(); // 2
count(3); // 3
count(); // 3

将参数默认值放在最后

1
2
3
4
5
6
7
8
9
// bad
function handleThings(opts = {}, name) {
// ...
}

// good
function handleThings(name, opts = {}) {
// ...
}

不要更改参数,eslint: no-param-reassign
原因:操作作为参数传入的对象可能在原始调用中造成意想不到的变量副作用

1
2
3
4
5
6
7
8
9
// bad
function f1(obj) {
obj.key = 1;
}

// good
function f2(obj) {
const key = Object.prototype.hasOwnProperty.call(obj, "key") ? obj.key : 1;
}

不要给参数重新赋值,eslint: no-param-reassign
原因:参数重新赋值可能会导致无法预期的行为,尤其是当操作 arguments 对象时,也可能导致优化问题,尤其是在 V8 引擎中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bad
function f1(a) {
a = 1;
}

function f2(a) {
if (!a) {
a = 1;
}
}

// good
function f3(a) {
const b = a || 1;
}

function f4(a = 1) {}

调用可变参数函数时建议使用展开运算符 …., eslint: prefer-spread
原因:显然你无需使用上下文,很难结合 new 和 apply

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
const x = [1, 2, 3, 4, 5];
console.log.apply(console, x);

// good
const x = [1, 2, 3, 4, 5];
console.log(...x);

// bad
new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5]))();

// good
new Date(...[2016, 8, 5]);
箭头函数

当你必须使用函数表达式(传递匿名函数)时,使用箭头函数标记. eslint: prefer-arrow-callback, arrow-spacing
原因:它将创建在 this 上下文中执行的函数版本,通常是您想要的,并且语法更简洁

如果您有一个相当复杂的函数,则可以将该逻辑移到其自己的命名函数表达式中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// bad
[1, 2, 3]
.map(function (x) {
const y = x + 1;
return x * y;
})

[
// good
(1, 2, 3)
].map((x) => {
const y = x + 1;
return x * y;
});

如果函数体只包含一条没有副作用的返回表达式的语句,可以省略花括号并使用隐式的 return, 否则保留花括号并使用 return 语句,eslint: arrow-parens, arrow-body-style

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
// bad
[1, 2, 3]
.map((number) => {
const nextNumber = number + 1`A string containing the ${nextNumber}.`;
})

[
// good
(1, 2, 3)
].map((number) => `A string containing the ${number}.`)

[
// good
(1, 2, 3)
].map((number) => {
const nextNumber = number + 1;
return `A string containing the ${nextNumber}.`;
})

[
// good
(1, 2, 3)
].map((number, index) => ({
index: number,
}));

// No implicit return with side effects
function foo(callback) {
const val = callback();
if (val === true) {
// Do something if callback returns true
}
}

let bool = false;

// bad
foo(() => (bool = true));

// good
foo(() => {
bool = true;
});

一旦表达式跨多行,使用圆括号包裹以便更好阅读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// bad
["get", "post", "put"]
.map((httpMethod) =>
Object.prototype.hasOwnProperty.call(
httpMagicObjectWithAVeryLongName,
httpMethod
)
)

[
// good
("get", "post", "put")
].map((httpMethod) =>
Object.prototype.hasOwnProperty.call(
httpMagicObjectWithAVeryLongName,
httpMethod
)
);

函数如果只接收一个参数并且没使用用花括号,则省略圆括号,否则为了清晰明确则使用圆括号包裹参数,注意:总是使用圆括号也是可以接受的,eslint 中的 “always” 选项,eslint: arrow-parens

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
// bad
[1, 2, 3]
.map((x) => x * x)

[
// good
(1, 2, 3)
].map((x) => x * x)

[
// good
(1, 2, 3)
].map(
(number) =>
`A long string with the ${number}. It’s so long that we’ve broken it ` +
"over multiple lines!"
)

[
// bad
(1, 2, 3)
].map((x) => {
const y = x + 1;
return x * y;
})

[
// good
(1, 2, 3)
].map((x) => {
const y = x + 1;
return x * y;
});
类&构造函数

使用 class,避免直接操作 prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// bad
function Queue (contents = []) {
this._queue = [..contents]
}
Queue.prototype.pop = function () {
const value = this._queue[0]
this._queue.splice(0, 1)
return value
}

// good
class Queue {
constructor (contents = []) {
this._queue = [...contents]
}

pop () {
const value = this._queue[0]
this._queue.splice(0, 1)
return value
}
}

使用 extends 来实现继承

原因:这是一个不会破坏 instanceof 的内建实现原型式继承的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
const inherits = require("inherits");
function PeekableQueue(contents) {
Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function () {
return this.queue[0];
};

// good
class PeekableQueue extends Queue {
peek() {
return this.queue[0];
}
}

如果未声明构造函数,则类会有一个默认的构造函数,没必要用空的构造函数或者将其委托给父类,eslint: no-useless-constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// bad
class Jedi {
constructor() {}

getName() {
return this.name;
}
}

// bad
class Rey extends Jedi {
constructor(...args) {
super(...args);
}
}

// good
class Rey extends Jedi {
constructor(...args) {
super(...args);
this.name = "Rey";
}
}

避免类成员重复,eslint: no-dupe-class-members
原因:重复的类成员声明会默认使用最后声明的,通常会导致 bug

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// bad
class Foo {
bar() {
return 1;
}
bar() {
return 2;
}
}

// good
class Foo {
bar() {
return 1;
}
}

// good
class Foo {
bar() {
return 2;
}
}

模块
使用标准的 ES6 模块语法 import 和 export
原因:模块是未来,让我们现在开始使用未来的特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// bad
const util = require('./util')
module.exports = util

// good
import Util from './util'
export default Util

// better
import { Util } from './util'
export default Util
不要使用 import 的通配符 *,这样可以确保你只有一个默认的 export

// bad
import * as Util from './util'

// good
import Util from './util'
同个文件每个模块只允许 import 一次,有多个 import 请书写在一起,eslint: no-duplicate-imports

原因:这样可以让代码更易于维护

1
2
3
4
5
6
7
8
9
10
// bad
import foo from "foo";
// … some other imports … //
import { named1, named2 } from "foo";

// good
import foo, { named1, named2 } from "foo";

// good
import foo, { named1, named2 } from "foo";

将所有 import 语句放在文件最前方,eslint: import/imports-first

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// bad
import foo from "foo";
foo.init();

import bar from "bar";

// good
import foo from "foo";
import bar from "bar";

foo.init();
多行导入应该像多行数组和对象文字一样缩进;
// bad
import { longNameA, longNameB, longNameC, longNameD, longNameE } from "path";

// good
import { longNameA, longNameB, longNameC, longNameD, longNameE } from "path";

在模块 import 声明中禁止使用 Webpack 的 loader 语法,eslint: import/

1
2
3
4
5
6
7
8
no - webpack - loader - syntax;
// bad
import fooSass from "css!sass!foo.scss";
import barCss from "style!css!bar.css";

// good
import fooSass from "foo.scss";
import barCss from "bar.css";
迭代器

不要使用 iterators,建议使用 JS 更高优先级的函数代替 for-in 或 for-of 循环,除非迫不得已,eslint: no-iterator no-restricted-syntax
const numbers = [1, 2, 3, 4, 5]

1
2
3
4
5
6
7
8
9
10
11
12
// bad
let sum = 0;
for (let num of numbers) {
sum += num;
}

// good
let sum = 0;
numbers.forEach((num) => (sum += num));

// better
const sum = numbers.reduce((total, num) => total + num, 0);
生成器

现阶段请不要使用生成器 generator
原因:因为不能很好地翻译成 ES5 代码

对象属性
使用 . 来访问对象属性

1
2
3
4
5
6
7
8
9
10
const joke = {
name: "haha",
age: 28,
};

// bad
const name = joke["name"];

// good
const name = joke.name;

当访问的属性是变量时使用 []

1
2
3
4
5
6
7
8
9
10
const luke = {
jedi: true,
age: 28,
};

function getProp(prop) {
return luke[prop];
}

const isJedi = getProp("jedi");
变量声明

声明变量时,请使用 const、let 关键字,如果没有写关键字,变量就会暴露在全局上下文中,这样很可能会和现有变量冲突,另外,也很难明确该变量的作用域是什么。这里推荐使用 const 来声明变量,我们需要避免全局命名空间的污染。eslint: no-undef prefer-const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// bad
demo = new Demo()

// good
const demo = new Demo()
将所有的 constlet 分组

// bad
let a
const b
let c
const d
let e

// good
const b
const d
let a
let c
let e

变量不要进行链式赋值
原因:变量链式赋值会创建隐藏的全局变量

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
// bad
(function example() {
// JavaScript interprets this as
// let a = ( b = ( c = 1 ) );
// The let keyword only applies to variable a; variables b and c become
// global variables.
let a = (b = c = 1);
})();

console.log(a); // throws ReferenceError
console.log(b); // 1
console.log(c)(
// 1

// good
(function example() {
let a = 1;
let b = a;
let c = a;
})()
);

console.log(a); // throws ReferenceError
console.log(b); // throws ReferenceError
console.log(c); // throws ReferenceError

// the same applies for `const`

不允许出现未被使用的变量,eslint: no-unused-vars
原因:声明但未被使用的变量通常是不完全重构犯下的错误.这种变量在代码里浪费空间并会给读者造成困扰

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
// bad

var some_unused_var = 42;

// Write-only variables are not considered as used.
var y = 10;
y = 5;

// A read for a modification of itself is not considered as used.
var z = 0;
z = z + 1;

// Unused function arguments.
function getX(x, y) {
return x;
}

// good

function getXPlusY(x, y) {
return x + y;
}

const x = 1;
const y = a + 2;

alert(getXPlusY(x, y));

// 'type' is ignored even if unused because it has a rest property sibling.
// This is a form of extracting an object that omits the specified keys.
const { type, ...coords } = data;
// 'coords' is now the 'data' object without its 'type' property.
Hoisting;

var 存在变量提升的情况,即 var 声明会被提升至该作用域的顶部,但是他们的赋值并不会。而 const 和 let 并不存在这种情况,他们被赋予了 Temporal Dead Zones, TDZ, 了解 typeof 不再安全很重要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function example() {
console.log(notDefined); // => throws a ReferenceError
}

function example() {
console.log(declareButNotAssigned); // => undefined
var declaredButNotAssigned = true;
}

function example() {
let declaredButNotAssigned;
console.log(declaredButNotAssigned); // => undefined
declaredButNotAssigned = true;
}

function example() {
console.log(declaredButNotAssigned); // => throws a ReferenceError
console.log(typeof declaredButNotAssigned); // => throws a ReferenceError
const declaredButNotAssigned = true;
}

匿名函数的变量名会提升,但函数内容不会

1
2
3
4
5
6
7
8
9
function example() {
console.log(anonymous); // => undefined

anonymous();

var anonymous = function () {
console.log("test");
};
}

命名的函数表达式的变量名会被提升,但函数名和函数函数内容并不会

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function example() {
console.log(named); // => undefined

named(); // => TypeError named is not a function

superPower(); // => ReferenceError superPower is not defined

var named = function superPower() {
console.log("Flying");
};
}

function example() {
console.log(named); // => undefined

named(); // => TypeError named is not a function

var named = function named() {
console.log("named");
};
}

比较运算符&相等
使用 === 和 !== 而非 == 和 !=,eslint: eqeqeq

条件声明例如 if 会用 ToBoolean 这个抽象方法将表达式转成布尔值并遵循如下规则

Objects 等于 true
Undefined 等于 false
Null 等于 false
Booleans 等于 布尔值
Numbers 在 +0, -0, 或者 NaN 的情况下等于 false, 其他情况是 true
Strings 为 ‘’ 时等于 false, 否则是 true

1
2
3
4
if ([0] && []) {
// true
// 数组(即使是空数组)也是对象,对象等于true
}
分号

我们遵循 Standard 的规范,不使用分号。

关于应不应该使用分号的讨论有很多,本规范认为非必要的时候,应该不使用分号,好的 JS 程序员应该清楚场景下是一定要加分号的,相信你也是名好的开发者。

1
2
3
4
5
6
7
8
9
10
11
// bad
const test = "good";
(function () {
const str = "hahaha";
})();

// good
const test = "good";
(() => {
const str = "hahaha";
})();

提升:设计模式和编程思想

前后端通用架构模式 MVC MVP MVVM 介绍

MVC

MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑(C)、数据(M)、界面显示(V)分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

图示的常用代表为 PHP 思想的代码。View 通过控制器直接操作 Model.

对于 MVC 架构有多种框架不同的定义和变种。但是整体的思路是不变的。

当用户对视图层做出操作后,视图层捕获到这个操作,然后把权力交给管理层去进行预处理,从而决定调用哪个模型层去执行相关的业务逻辑,最后视图层观察到模型层变更了后,去重新更新界面

优点:
耦合性低,重用性高,部署快,可维护性高,有利于软件工程化管理
缺点:
由于模型 model 和视图 view 要严格的分离,给前端程序带来了很大的困难,每次操作需要彻底的测试。
这个模式有个问题就是这三者是之间都有关联,但是 model 发生变化后被 view 通过观察者模式监控到,从而渲染新的视图,这也就导致了 view 强依赖与特定的 model 层

MVP

图示的常用代表为 NEXT,JSP 等后端渲染模式.

① 各部分之间的通信,都是双向的。

②View 与 Model 不发生联系,都通过 Presenter 传递。可以将一个 Presenter 用于多个视图,而不需要改变 Presenter 的逻辑。这个特性非常的有用,因为视图的变化总是比模型的变化频繁。

③View 非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而 Presenter 非常厚,所有逻辑都部署在那里。而 view 又由于 view 和 model 之间没有直接的关联,就导致后期 model->view 的手动同步逻辑麻烦,维护困难。

MVVM

前后端分离后,采用了前身 MVP 的结构延续。在大多前端框架上使用的是此架构。

Model:数据模型。普通的 javascript 数据对象(其实就是一个对象,对象里放了数据),用于对数据的操作。

View:视图。表示 UI 组件,作用是将模型的数据转换成 UI 展示出来,也就是前端展示页面。

ViewModel:视图-模型。用于同步 Model 和 View,用于双向绑定数据与页面,就是 vue 的实例。

总结

不论是 MVC MVP MVVM 或者以后出的更多架构设计,总体的思路是不变的。

视图展示->业务逻辑->数据处理

理清楚这条线,逐步逐层分析。编程过程中会越来越简单化和清晰化。

设计模式介绍

设计模式在代码开发中的重要性
  1. 通过设计模式,可以提高代码的可读性和可维护性,使代码更易于理解和修改。
  2. 设计模式可以提高代码的灵活性和可扩展性,使系统更易于扩展和修改,从而更好地适应需求变化。
  3. 应用设计模式可以促进代码重用,减少重复编码,提高开发效率。
  4. 设计模式有助于规划和组织代码结构,降低系统复杂度,提高稳定性和性能。
编程中常用的设计模式
单例模式(Singleton Pattern)

单例模式是一种创建型设计模式,其主要目的是确保一个类只有一个实例,并提供一个全局访问点。这意味着无论在何处创建该类的实例,都将获得相同的实例。单例模式通常在需要严格控制某个类的实例数量的情况下使用,例如线程池、缓存、日志对象等。

实现单例模式的关键在于将类的构造函数私有化,以防止外部直接实例化该类。然后,通过一个静态方法或静态变量来控制实例的创建和访问。以下是一个简单的单例模式示例:

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
let Singleton = (function () {
let instance;

function createInstance() {
// 在这里可以进行一些初始化操作
return {
// 返回实例的方法和属性
};
}

return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
},
};
})();

// 使用单例模式获取实例
let instance1 = Singleton.getInstance();
let instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // true,两个实例相同
工厂模式(Factory Pattern)

工厂模式是一种创建型设计模式,旨在提供一种统一的方式来创建对象,而无需指定具体的类。这种模式通过定义一个创建对象的接口来封装对象的实例化过程,从而使得代码更具灵活性和可维护性。

以下是一个使用 JavaScript 实现的简单工厂模式示例:

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 ShapeFactory() {}

// 在工厂原型上添加创建不同形状的方法
ShapeFactory.prototype.createShape = function (type) {
let shape;

if (type === "circle") {
shape = new Circle();
} else if (type === "square") {
shape = new Square();
} else if (type === "triangle") {
shape = new Triangle();
}

return shape;
};

// 定义不同形状的构造函数
function Circle() {
this.type = "circle";
this.draw = function () {
console.log("画一个圆形");
};
}

function Square() {
this.type = "square";
this.draw = function () {
console.log("画一个正方形");
};
}

function Triangle() {
this.type = "triangle";
this.draw = function () {
console.log("画一个三角形");
};
}

// 使用工厂创建对象
let factory = new ShapeFactory();
let circle = factory.createShape("circle");
let square = factory.createShape("square");
let triangle = factory.createShape("triangle");

circle.draw(); // 输出:画一个圆形
square.draw(); // 输出:画一个正方形
triangle.draw(); // 输出:画一个三角形

在这个示例中,我们定义了一个 ShapeFactory 工厂类,它包含一个 createShape 方法,根据传入的参数来创建不同的形状对象。然后,我们定义了 Circle、Square 和 Triangle 三个具体的形状类,它们都具有相同的 draw 方法,但实现方式不同。

通过工厂模式,我们可以通过统一的接口来创建不同的形状对象,而无需直接使用具体的类名。这种方式使得代码更具灵活性,当需要添加新的形状时,只需修改工厂类即可,而无需修改调用方的代码。

工厂模式在实际开发中经常用于创建对象的场景,特别是在需要根据一些条件来创建不同对象时,工厂模式可以提供一种清晰的创建过程,并将对象的创建逻辑封装起来,使得代码更易于维护和扩展。

观察者模式(Observer Pattern)

观察者模式是一种行为设计模式,它定义了一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都会得到通知并自动更新。这种模式使得对象之间的解耦,让主题对象(被观察者)和观察者对象可以独立地交互。

以下是一个使用 JavaScript 实现的简单观察者模式示例:

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
// 定义主题对象(被观察者)
class Subject {
constructor() {
this.observers = [];
}

// 添加观察者
addObserver(observer) {
this.observers.push(observer);
}

// 通知所有观察者
notify(message) {
this.observers.forEach((observer) => {
observer.update(message);
});
}
}

// 定义观察者对象
class Observer {
constructor(name) {
this.name = name;
}

// 接收通知并更新
update(message) {
console.log(`${this.name} received message: ${message}`);
}
}

// 创建主题对象
let subject = new Subject();

// 创建观察者对象
let observer1 = new Observer("Observer 1");
let observer2 = new Observer("Observer 2");

// 添加观察者到主题对象
subject.addObserver(observer1);
subject.addObserver(observer2);

// 主题对象通知所有观察者
subject.notify("Hello, observers!");

在这个示例中,我们定义了一个 Subject 主题类和一个 Observer 观察者类。主题对象维护一个观察者列表,并提供添加观察者和通知观察者的方法。观察者对象包含一个 update 方法,用于接收主题对象的通知并进行相应的更新操作。

通过观察者模式,当主题对象状态发生变化时,只需调用 notify 方法通知所有观察者,观察者对象将自动更新自身状态。这种方式实现了主题对象和观察者对象之间的解耦,使得它们可以独立地进行交互,从而提高了代码的灵活性和可维护性。

观察者模式在实际开发中经常用于事件处理、UI 组件通知等场景,可以有效地实现对象之间的解耦和消息传递。

策略模式(Strategy Pattern)

策略模式是一种行为设计模式,它定义了一系列算法,将每个算法封装起来,并使它们可以互相替换,使得算法可以独立于使用它的客户端而变化。这种模式可以使得算法的变化不会影响到使用算法的客户端。

以下是一个使用 JavaScript 实现的简单策略模式示例:

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
// 定义策略类
class PaymentStrategy {
pay(amount) {
// 策略类中定义的支付方法
}
}

// 定义不同的支付策略
class CreditCardPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid $${amount} with credit card`);
}
}

class PayPalPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid $${amount} with PayPal`);
}
}

class AliPayPayment extends PaymentStrategy {
pay(amount) {
console.log(`Paid $${amount} with AliPay`);
}
}

// 定义上下文类
class PaymentContext {
constructor(strategy) {
this.strategy = strategy;
}

// 设置支付策略
setStrategy(strategy) {
this.strategy = strategy;
}

// 执行支付
executePayment(amount) {
this.strategy.pay(amount);
}
}

// 使用策略模式进行支付
let paymentContext = new PaymentContext(new CreditCardPayment());
paymentContext.executePayment(100); // 输出:Paid $100 with credit card

paymentContext.setStrategy(new PayPalPayment());
paymentContext.executePayment(200); // 输出:Paid $200 with PayPal

paymentContext.setStrategy(new AliPayPayment());
paymentContext.executePayment(300); // 输出:Paid $300 with AliPay

在这个示例中,我们定义了一个 PaymentStrategy 策略类,以及三种不同的支付策略:CreditCardPayment、PayPalPayment 和 AliPayPayment。然后,我们定义了一个 PaymentContext 上下文类,它包含一个策略对象,并提供了设置策略和执行支付的方法。

通过策略模式,我们可以根据不同的需求选择不同的支付策略,而无需修改客户端的代码。这种方式使得支付策略可以独立于客户端而变化,从而提高了代码的灵活性和可维护性。

策略模式在实际开发中经常用于需要根据不同条件选择不同算法的场景,例如排序算法、计算方式等。它使得算法的变化不会影响到使用算法的客户端,从而提高了代码的可扩展性和可维护性。

装饰器模式(Decorator Pattern)

装饰器模式是一种结构型设计模式,它允许向对象动态添加新功能,同时又不改变其结构。这种模式通过创建一个包装对象,也就是装饰器,来包裹真实的对象,并在保持原有对象类结构不变的情况下,动态地添加新的行为或责任。

以下是一个使用 JavaScript 实现的简单装饰器模式示例:

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
// 定义一个基础组件类
class Coffee {
cost() {
return 10;
}
}

// 定义装饰器类
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}

cost() {
return this.coffee.cost() + 5;
}
}

// 定义另一个装饰器类
class WhipDecorator {
constructor(coffee) {
this.coffee = coffee;
}

cost() {
return this.coffee.cost() + 2;
}
}

// 使用装饰器模式
let myCoffee = new Coffee();
console.log(myCoffee.cost()); // 输出:10

let coffeeWithMilk = new MilkDecorator(myCoffee);
console.log(coffeeWithMilk.cost()); // 输出:15

let coffeeWithMilkAndWhip = new WhipDecorator(coffeeWithMilk);
console.log(coffeeWithMilkAndWhip.cost()); // 输出:17

在这个示例中,我们定义了一个基础的 Coffee 组件类,它具有一个 cost 方法用于返回咖啡的价格。然后,我们定义了两个装饰器类 MilkDecorator 和 WhipDecorator,它们分别用于向咖啡中添加牛奶和奶泡,并在原有价格的基础上增加相应的费用。

通过装饰器模式,我们可以动态地向对象添加新的功能,而无需修改原有对象的结构。在示例中,我们可以根据需要组合不同的装饰器,从而实现不同的功能组合,而基础组件类的结构保持不变。

装饰器模式在实际开发中经常用于需要动态地添加新功能或责任的场景,例如对已有的对象进行功能扩展、动态地组合对象等。它使得对象的功能可以灵活地组合和扩展,从而提高了代码的灵活性和可维护性。

适配器模式(Adapter Pattern)

适配器模式是一种结构型设计模式,它允许将一个类的接口转换成客户端所期望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的类可以一起工作。

以下是一个使用 JavaScript 实现的简单适配器模式示例:

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
// 定义一个目标接口
class Target {
request() {
// 目标接口的请求方法
}
}

// 定义一个适配者类
class Adaptee {
specificRequest() {
console.log("适配者的特殊请求");
}
}

// 定义适配器类
class Adapter extends Target {
constructor(adaptee) {
super();
this.adaptee = adaptee;
}

request() {
this.adaptee.specificRequest();
}
}

// 使用适配器模式
let adaptee = new Adaptee();
let adapter = new Adapter(adaptee);

adapter.request(); // 输出:适配者的特殊请求

在这个示例中,我们定义了一个目标接口 Target,它包含一个 request 方法。然后,我们定义了一个适配者类 Adaptee,它具有一个特殊的方法 specificRequest。接着,我们定义了一个适配器类 Adapter,它继承自目标接口 Target,并在内部持有一个适配者对象。适配器类的 request 方法实际上调用了适配者对象的特殊请求方法。

通过适配器模式,我们可以将适配者类的接口转换成目标接口,使得适配者类可以与客户端一起工作。在示例中,适配器类 Adapter 将适配者类 Adaptee 的特殊请求方法适配成目标接口 Target 的请求方法,从而实现了接口的兼容性。

适配器模式在实际开发中经常用于需要将已有类与其他类进行协同工作的场景,特别是在接口不兼容的情况下。它使得不兼容的类可以无缝地协同工作,提高了代码的复用性和灵活性。

模板方法模式(Template Method Pattern)

模板方法模式是一种行为设计模式,它定义了一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

以下是一个使用 JavaScript 实现的简单模板方法模式示例:

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
// 定义一个模板类
class AbstractClass {
templateMethod() {
this.baseOperation1();
this.requiredOperation1();
this.baseOperation2();
this.hook();
}

baseOperation1() {
console.log("AbstractClass: baseOperation1");
}

baseOperation2() {
console.log("AbstractClass: baseOperation2");
}

hook() {}
}

// 定义具体子类
class ConcreteClass1 extends AbstractClass {
requiredOperation1() {
console.log("ConcreteClass1: requiredOperation1");
}

hook() {
console.log("ConcreteClass1: hook");
}
}

class ConcreteClass2 extends AbstractClass {
requiredOperation1() {
console.log("ConcreteClass2: requiredOperation1");
}
}

// 使用模板方法模式
let concrete1 = new ConcreteClass1();
concrete1.templateMethod();
// 输出:
// AbstractClass: baseOperation1
// ConcreteClass1: requiredOperation1
// AbstractClass: baseOperation2
// ConcreteClass1: hook

let concrete2 = new ConcreteClass2();
concrete2.templateMethod();
// 输出:
// AbstractClass: baseOperation1
// ConcreteClass2: requiredOperation1
// AbstractClass: baseOperation2

在这个示例中,我们定义了一个抽象类 AbstractClass,它包含一个模板方法 templateMethod,以及一些基本操作和钩子方法。然后,我们定义了两个具体子类 ConcreteClass1 和 ConcreteClass2,它们分别实现了抽象类中的抽象方法和钩子方法。

通过模板方法模式,我们可以在抽象类中定义算法的骨架,而将一些具体步骤延迟到子类中实现。在示例中,templateMethod 方法定义了算法的骨架,而 requiredOperation1 方法和 hook 方法则由具体子类实现。

模板方法模式在实际开发中经常用于定义一些通用的算法骨架,而将具体的实现延迟到子类中。这种方式使得算法的结构保持不变,同时又可以灵活地扩展和修改具体步骤,提高了代码的复用性和可维护性。

总结

编程模式和语言无关,是开发人员在实陃项目中总结出的一套通用的最佳实践,它可以帮助开发人员更好地组织和规划代码结构,提高代码的质量和可维护性,从而提高软件开发的效率和质量。

我们要经常学习和了解前人总结的编程模式思想,在项目中多做对比。多看流行的开源代码,学习他们的设计模式和最佳实践,并将其应用到自己的项目中。这样不论什么语言,什么方向,都可以做到得心应手。

调试心得分享

框架篇: 浏览器调试框架介绍

浏览器的调试框架是指 DevTools,它是内置的一套 Web 开发和调试工具,可以帮助开发人员检查页面元素、调试 JavaScript、优化性能等。DevTools 包括各种面板。

Elements(元素)

元素区域图

样式优先级覆盖图

计算样式图

屏幕尺寸适配图

Console(控制台)

调试控制台图(以及相关配置)

层级选择点图

Sources(源代码)

源代码文件分域列表

源代码文件视图以及断点位置

堆栈信息,变量监视等

启动暂停单步面板

Network(网络)


网络部分操作台

请求查看,包含 http,ws 等

操作面板以及请求瀑布流

其他等

性能分析面板

内存分析面板

应用面板,存储,持久化,service worker 等

lighthouse 报告

总结

使用上述的调试工具,可以带给我们以下的便利性:

  1. 检查和编辑页面元素,实时查看页面结构和样式。
  2. 调试 JavaScript 代码,查看变量值、执行上下文等。
  3. 分析网络请求和响应,优化页面加载性能。
  4. 检测页面性能,识别并解决性能瓶颈。
  5. 模拟移动设备,进行响应式设计和开发调试。
  6. 跟踪内存使用情况,发现内存泄漏和优化内存管理。

有使用调试工具的思想,并且熟练使用各种调试工具,对我们来说很重要,在我们工作开发过程中也是必不可少的一部分。

原理篇:堆栈分析原理

我们常常说堆栈堆栈,但是堆和栈其实是完全不同的两个概念。栈其实完全是为了函数调用而设计的,那么函数调用如何通过栈实现的呢?不用函数调用方式,栈在行为上有什么区别呢?

系统栈的工作原理

内存的不同用途

如果您关注网络安全问题,那么一定听过缓冲区溢出这个术语。简单说来:缓冲区溢出就是在大缓冲区中的数据向小缓冲区复制的过程中,由于没有注意小缓冲区的边界,“撑爆”了较小的缓冲区,从而冲掉了和小缓冲区相邻内存区域的其他数据而引起的内存问题。缓冲溢出是最常见的内存错误之一,也是攻击者入侵系统时所用到的最强大、最经典的一类漏洞利用方式。

成功地利用缓冲区溢出漏洞可以修改内存中变量的值,甚至可以劫持进程,执行恶意代码,最终获得主机的控制权。要透彻地理解这种攻击方式,我们需要回顾一些计算机体系架构方面的基础知识,搞淸楚 CPU、寄存器、内存是怎样协同工作而让程序流畅执行的。

根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行。但是不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能大致分成以下 4 个部分。

(1)代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令并执行。
(2)数据区:用于存储全局变量等。
(3)堆区:进程可以在堆区动态地请求一定大小的内存,并在用完之后归还给堆区。动态分配内存和回收内存是堆区的特点。
(4)栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行。

在 Windows 平台下,高级语言写出的程序经过编译链接,最终会变成所谓的 PE 文件。当 PE 文件被装载运行后,就成了所谓的进程。

PE 文件代码段中包含的二进制级别的机器代码会被装入内存的代码区.text,处理器将到内存的这个区域一条一条地取出指令和操作数,并送入算术逻辑单元进行运算;如果代码中请求开辟动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;当函数调用发生时,函数的调用关系等信息会动态地保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返冋母函数。

以上是 windows 平台,对于浏览器平台,这部分由浏览器内核接管,高级语言通过 V8 内核接管,去执行上述的操作。

如果把计算机看成一个有条不紊的 1:厂,我们可以得到如下类比。

  • CPU 是完成工作的工人。
  • 数据区、堆区、栈区等则是用来存放原料、半成品、成品等各种东西的场所。
  • 存在代码区的指令则告诉 CPU 要做什么,怎么做,到哪里去领原材料,用什么工具来做,做完以后把成品放到哪个货舱去。
  • 值得一提的是,栈除了扮演存放原料、半成品的仓库之外,它还是车间调度主任的办公室。
  • 程序中所使用的缓冲区可以是堆区、栈区和存放静态变景的数据区。缓冲区溢出的利用方法和缓冲区到底属于上面哪个内存区域密不可分。
栈与系统栈

从计算机科学的角度来看,栈指的是一种数据结构,是一种先进后出的数据表。栈的最常见操作有两种:压栈 PUSH、弹栈 POP :用于标识栈的属性也有两个:栈顶 TOP 、栈底 BASE。
可以把栈想象成一摞扑克牌。

  • PUSH:为栈增加一个元素的操作叫做 PUSH,相当于在这摞扑克牌的最上面再放上—张。
  • POP:从栈中取出一个元素的操作叫做 POP,相当于从这摞扑克牌取出最上面的一张。
  • TOP:标识栈顶位置,并且是动态变化的。每做一次 PUSH 操作,它都会自增 1;相反,每做一次 POP 操作,它会自减 1。栈顶元素相当于扑克牌最上面一张,只有这张牌的花色是当前可以看到的。
  • BASE:标识栈底位置,它记录着扑克牌最下面一张的位置。BASE 用于防止栈空后继续弹栈(牌发完时就不能再去揭牌了)。很明显,一般情况下,BASE 是不会变动的。
    内存的栈区实际上指的就是系统栈。系统栈由系统自动维护,它用于实现高级语言中函数的调用。对于类似 C 语言这样的高级语言,系统栈的 PUSH、POP 等堆栈平衡细节是透明的。

—般说来,只有在使用汇编语言开发程序的时候,才需要和它直接打交道。

函数调用时发生了什么
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int func_B(int arg_B1, int arg_B2)
{
int var_B1;
int var_B2;
var_B1 = arg_B1 + arg_B2;
var_B2 = arg_B1 - arg_B2;
return var_B1 * var_B2;
}
int func_A(int arg_A1, int arg_A2)
{
int var_A;
var_A = func_B(arg_A1, arg_A2) + arg_A1;
return var_A;
}
int main(int argc, char** argv, char** envp)
{
int var_main;
var_main = func_A(3, 4);
return 0;
}

这段代码经过编译器编译后,各个函数对应的机器指令在代码区中可能是这样分布的

根据操作系统的不问、编译器和编译选项的不同,同一文件不同函数的代码在内存代码区中的分布可能相邻,也可能相离甚远,可能先后有序,也可能无序;但它们都在同一个 PE 文件的代码所映射的一个“节”里。我们可以简单地把它们在内存代码区中的分布位置理解成是散乱无关的。
当 CPU 在执行调用 func_A 函数的时候,会从代码区中 main 函数对应的机器指令的区域跳转到 func_A 函数对应的机器指令区域,在那里取指并执行;当函数执行完闭,需要返会的时候,又会跳回到 main 函数对应的指令区域,紧接着调用 func_A 后面的指令继续执行 main 函数的代码。

那么 CPU 是怎么知道要去 func_A 的代码区取指,在执行完 func_A 后又是怎么知道跳回到 main 函数(而不是 func_B 的代码区)的呢?这些跳转地址我们在 C 语言中并没有直接说明,CPU 是从哪里获得这些函数的调用及返回的信息的呢?
原来,这些代码区中精确的跳转都是在与系统栈巧妙地配台过程中完成的。当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。

在函数调用的过程中,伴随的系统栈中的操作如下。

  • 在 main 函数中调用 func_A 的时候,首先在自己的栈帧中压入函数返回地址,然后为 func_A 创建新栈帧并压入系统栈。
  • 在 func__A 调用 func_B 的时候,同样先在自己的栈帧中压入函数返回地址,然后为 func_B 创建新栈帧并压入系统栈。
  • 在 func_B 返回时,func_B 的栈帧被弹出系统栈,func_A 栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到 func_A 代码区中执行。
  • 在 func_A 返同时,func_A 的栈帧被弹出系统栈.macn 函数栈帧中的返回地址被“露” 在栈顶,此时处理器按照这个返回地址跳到 main 函数代码区中执行。
寄存器与函数栈帧

每一个函数独占自己的栈帧空间。当前正在运行的函数的栈帧总是在栈顶。CPU 系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧。

(1) ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈地上面-个栈帧的栈顶。
(2) EBP:基址指针寄存器(extended base pointer)-其内存放着一个指针,该指针永远指向系统栈展上面一个栈帧的底部。

注意:EBP 指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念,本文在叙述中将坚特使用“栈帧底部”这一提法以示区别;ESP 所指的栈帧顶部和系统栈的顶部是同一个位置,所以后面叙述中并不严格区分“栈帧顶部”和“栈顶”的概念。请您注意这里的差异,不要产生概念混淆。

函数栈帧:ESP 和 EBP 之间的内存空间为当前栈帧.EBP 标识了当前栈帧的底部.ESP 标识了当前栈帧的顶部。
在函数栈帧中,一般包含以下几类重要信息。

(1) 局部变量:为函数局部变量开辟的内存空间。

(2)栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本帧被弹出后恢复出上一个栈帧。

(3) 函数返回地址:保存当前函数调用前的“断点”信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

除了与栈相关的寄存器外,您还需要记住另一个至关重要的寄存器。
EIP:指令寄存器(Extended Instruction Pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址,可以说如果控制了 EIP 寄存器的内容,就控制了进程——我们让 EIP 指向哪里,CPU 就会去执行哪里的指令。

函数调用约定与相关指令

函数调用约定描述了函数传递参数方式和栈协同工作的技术细节。不同的操作系统、不同的语言、不同的编译器在实现函数调用时的原理虽然基本相同,但具体的调用约定还是有差别的。这包括参数传递方式,参数入栈顺序是从右向左还是从左向古,函数返回时恢复堆栈平衡的操作在子函数中进行还是在母函数中进行。

函数调用大致包括以下几个步骤.

(1) 参数入栈:将参数从右向左依次压入系统栈中。

(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。

(3) 代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。

(4)栈帧调整:具体包括。

保存当前栈帧状态值,已备后面恢复本栈帧时使用(EBP 入栈):
将当前栈帧切换到新栈帧(将 ESP 值装入 EBP.更新栈帧底部):
给新栈帧分配空间(把 ESP 减去所需空间的大小,抬高栈顶):

类似地,函数返回的步骤如下:
(1) 保存返回值:通常将函数的返回值保存在寄存器 EAX 中。
(2) 弹出当前栈帧,恢复上一个栈帧。
具体包括:

  • 在堆栈平衡的基础上,给 ESP 加上栈帧的大小,降低栈顶,回收当前栈帧的空间。
    将当前栈帧底部保存的前栈帧 EBP 值弹入 EBP 寄存器,恢复出上一个栈帧。
    将函数返回地址弹给 EIP 寄存器。

(3) 跳转:按照函数返回地址跳同母函数中继续执行。

还是以 C 语言和 Win32 平台为例,函数返回时的相关的指令序列如下。

add esp, xxx ;降低栈顶,回收当前的栈帧
pop ebp ;将上一个栈帧底部位置恢复至 ebp.
retn ;这条指令有两个功能:

a)弹出当前栈顶元素,即弹出栈帧中的返回地址。栈帧恢复工作完成。
b)让处理器跳转到弹出的返回地址,恢复调用前的代码区。

好处以及优点

心里对栈和堆有一定的概念后,对编程思想方面有什么帮助呢。举几个例子。

  1. 尾部递归

例子,普通递归,计算斐波那契数列:

1
2
3
4
5
6
7
8
9
function fibonacci(n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}

console.log(fibonacci(6)); // 输出 8

接下来是一个尾递归的例子,同样是计算斐波那契数列:

1
2
3
4
5
6
7
8
9
function fibonacciTail(n, a = 0, b = 1) {
if (n === 0) {
return a;
} else {
return fibonacciTail(n - 1, b, a + b);
}
}

console.log(fibonacciTail(6)); // 输出 8

在普通递归的情况下,每次递归调用都会将当前的状态(包括局部变量、返回地址等)压入调用栈中,直到达到递归结束条件后,才开始依次弹出栈顶的状态并执行。这意味着普通递归的调用栈会随着递归深度的增加而增加,当递归深度过大时,可能会导致栈溢出。

而在尾递归的情况下,由于递归调用是整个函数体中的最后一个操作,一些编译器和解释器可以对其进行优化,将其转化为循环结构,这样就不会占用额外的栈空间。因此,尾递归函数在执行时具有更好的性能和更低的内存消耗,不会导致栈溢出问题。

  1. 复杂性优化

时间复杂度是衡量算法执行时间随输入规模增长而变化的度量。它通常用大 O 符号表示,表示算法执行时间的增长趋势。比如:

  • O(1)表示常数时间复杂度.
  • O(log n)表示对数时间复杂度.
  • O(n)表示线性时间复杂度.
  • O(n^2)表示平方时间复杂度等.

时间复杂度越低,算法执行速度越快。

总结

在程序调试中,堆栈分析是一种重要的调试技术:

堆栈分析原理思想,不仅仅可以基于调用栈的结构和异常信息记录,通过工具解析和展示堆栈信息,帮助开发人员定位和修复程序中的错误。还可以从思想层面上去优化代码,如果不了解堆栈的原理,就不会有优化代码的想法,想法都不会有。

优化代码逻辑,提高代码质量,提高代码可读性,减少代码复杂性。优化代码的内存使用率,是我们逐步学习和成长的必经道路。

工具篇:调试工具的探索

react 工具: redux-dev-tools

Redux DevTools 是一款 Redux 官方提供的浏览器调试工具,
可以让我们很方便的对 Redux 保存的状态进行追踪调试

具体高阶使用方法请自行探索视频教程。

Vue 工具: vue-dev-tools

Vue Devtools 是开发 Vue.js 应用时必备的调试工具,能够帮助开发者更高效地调试代码。

blockly 中使用 vuetools 看 dom 结构和组件数据

例子: 实时追踪组件内部数据信息,加载走向

timeline 信息


全局状态信息监控

具体高阶使用方法请自行探索视频教程。

技巧篇:真实调试技巧举例

这里作为抛砖引玉,举例说明如何在外部调试 3D 组件。

3D 组件在加载之后需要和 pro 通讯,由 pro 下发各种指令,和实时的位姿信息。这种插件形式的组件,在调试的时候和 pro 进行了强耦合,增加了调试的难度。

如果不连接 pro 而直接打开,则会什么都不显示。具体的功能也无法进行实现。

进行解耦的调试可以进行其他原因的隔离,避免 pro 发送的信息影响到调试的功能,做到单一问题的调试。

第一步 创建 ws 服务端

模拟 pro 进行发送数据,需要创建一个 ws 服务端。在服务端进行数据的模拟,相关的调试代码如下:

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
//依赖关系
var express = require("express");
var router = express.Router();

var mitt = require("mitt");
const emitter = mitt();

const ws = require("ws");
var net = require("net");

const wss = new ws.WebSocketServer({ port: 9988 });
console.log("ws server on listening 9988");

wss.on("connection", function connection(ws) {
let timer = null;
console.log("客户端已连接!");

ws.on("message", function message(data) {
console.log("received: %s", data);
});

// 监听客户端断开连接事件
ws.on("close", function close() {
clearInterval(timer);
console.log("客户端已断开连接!");
});

// 发送消息
emitter.on("wsSend", (e) => {
ws.send(JSON.stringify(e));
});

emitter.on("wsSendInterval", (e) => {
clearInterval(timer);
timer = setInterval(() => {
ws.send(JSON.stringify(e));
}, 50);
});
});

//主页路由
router.get("/", function (req, res, next) {
res.render("index", { title: "测试页面" });
});

// 发送一个消息给ws
router.post("/ws", function (req, res, next) {
emitter.emit("wsSend", req.body);
res.send({ success: "ok" });
});

// 定时发送一个消息给ws
router.post("/wsInterval", function (req, res, next) {
emitter.emit("wsSendInterval", req.body);
res.send({ success: "ok" });
});

//导出模块接口
module.exports = router;

使用 express 启动服务,使用 mitt 做消息订阅,使用接口服务做对外的数据接口。这样一个简易的服务端就可以搭建完毕了。

第二步 进行数据的抓取分析

在初始化过程中,由于对代码的不了解。难以在短时间内完全熟悉所有的业务逻辑。但是这个组件是由消息去加载数据,去完成逻辑的。

在 3d 组件内部进行打印数据方法名称和具体的数据,并白盒查看代码,了解数据含义,并封装到 Postman 中做数据指令,作为服务器的控制终端。

如图所示,整理出来的消息,以及需要发送的消息体。

说明:这里的消息发送有部分接口具有特殊性,主要是获取位姿需要间隔一定的时间去实时的发送,3d 部分会按照此发送的间隔去渲染界面。服务端针对此部分做了循环发送的处理。

第三步 进行调试,满足业务功能

如果所示,通过开启服务器,发送相关指令,即可在外部浏览器进行相关的调试。

根据改变下发的参数,可以做到细节调整,以及准确的,无干扰的调试。满足了业务需求。

根据 React 官方提供的增强型的调试工具,可以直观的分析页面内的大多数的数据信息(由于 react 版本或者其他原因还有一部分数据未能完全分析到,不过可以分析到绝大部分)。

可以打断点,进行变量监控,堆栈信息查看,单步调试等。(由于未开启 sourcemap,此功能断点的编译后的文件,后续如果要使用此功能还是需要更新项目配置)。

技巧总结

使用多种工具的相关配合,可以做到针对性强,解耦性强(这个看需求,需求复杂建议解耦,需求简单可以直接调试),快速,准确的调试。

这个例子只是调试过程中的一个小方面,各种技巧五花八门,能够确定问题,解决问题才是最终的目的。

总结

编码规范的目的是确保代码的一致性、可读性和可维护性。它有助于团队成员之间的协作,减少错误和 bug,并提高代码的质量和可靠性。此外,编码规范还有助于提高代码的可移植性和可扩展性,从而为项目的长期发展打下良好的基础。

学习调试工具和调试技巧的目的是为了更高效地发现和解决软件程序中的错误和问题。通过掌握调试工具和技巧,开发人员可以快速定位并修复代码中的 bug,提高软件的质量和稳定性,从而节省时间和精力,加速开发周期,并提升用户体验。大家如果有更好的调试技巧和方案鼓励多多分享。

多向大厂学习,多遵循规范,多总结经验,多实践技能,多阅读优秀的代码,多吸收优秀的思想。不断学习并付诸实践,逐步优化自己的能力和水平。

  • 版权声明: 本博客所有文章,未经许可,任何单位及个人不得做营利性使用!转载请标明出处!如有侵权请联系作者。
  • Copyrights © 2015-2024 翟天野

请我喝杯咖啡吧~