自我介绍

面试官您好,我名字叫林凯迪,来自华南理工大学大学软件学院。在IHCI实验室和导师做开发将近半年多的时间。

本人抗压能力较好,并且学习能力较强,在跟做老师的项目的时候,可以及时跟进项目进度,协助老师做项目管理,并有针对性快速展开工作。让我对前端的整个开发流程更加熟悉。我对react的setState机制和diff算法较为熟悉,也对js的事件循环,作用域和作用一定的认识。最近也在开始学习HTTP缓存相关的知识,我的讲话就到这里,谢谢。

职业规划

你的职业规划

我的计划是能在前端开发这个行业里面做好,多学一些组件插件,接触一些新技术,新业务。然后再来接触深入学习一门后端语言,比如Java,Python的Django框架等,以便于入职后能更好的适应工作节奏,能更好在工作中被人需要。另外能多跟人沟通,锻炼自己的个人协作能力。

为什么选择前端

前端写出来就可以立刻看到,更有成就感。

js这门语言写前端和后端都是比较容易的,可以用js进行全栈开发,非常方便,技术栈不会太复杂,学生团队做项目学习成本不会太高。

我个人看来前端现在也是越来越旺,现在的产品对于用户体验是越来越重要了,而且对于APP,小程序,桌面应用都可以用前端来开发,比较多样。前端现在是一个风口期,是值得被看好的,所以选择了前端。

怎么学习前端的

初期的基础学习比较依赖视频,熟练度高了之后,更加偏向于查询文档学习,文档的示例是是最好用的【除了react文档】。有时候找错误,去谷歌查找翻译一下就可以找到错误。

为什么选择react

客观原因是老师的前端选型就是使用react,之后就往react这个方向去学习

react是有几个好处的,因为react跟vue的理念是不同的:vue是集成了几乎所有的魔法,而react就是消灭所有的魔法,只要你有原生js功底,学习react就十分简单

react的antd的颜值很高

react的社区很强大,react也很自由,开发轮子方便,react的轮子几乎是vue的千倍左右。

项目相关

1 项目介绍

跟老师做的智能咨询平台,目前还未上线。当初选型技术前端以react为主,后端以基于nodejs的express框架为主。

这个项目是为了解决市民办事咨询难的问题。整个广州人社局有866个事项,市民人工服务的时候往往都难以找出自己需要的事项,为了缓解人工服务的压力,并且让市民能快速查到自己需要办理的事项,才创建的这个项目。

项目的前端有两个部分,一个是客户端,一个是后台管理系统。客户端主要用于供市民的事项导航和事项的检索;后台主要用于事项的指南创建,事项的规则创建。

事项规则是在客户端中,一个事项的引导,比如说,点击,然后就会一步一步顺着关键词找到自己需要办理的事项,然后查到办理方式。

2 项目学到的东西

在项目中,写前端的时候,刚开始接触hooks编程,在useEffect里面想要设置状态,但是设置状态之后发现在useEffect里面状态没有立刻改变。后面查到,这是一个经典的闭包陷阱问题。因为设置状态的函数在里面被引用了,形成了闭包一直被保存。要解决这个问题的时候,想要拿到最新的值,有两种方法,一种就是重新开一个useEffect,等待上一个useEffect执行完毕,等闭包的数据改变,以改数据的变化做捕获处理,虽然这种不推荐。还有一种方法就是使用useRef,因为初始化的useRef执行之后,返回的都是同一个对象,用的是同一个内存空间,因此才可以立刻拿到最新的值。

  • 因为useRef可以用于多次渲染中,每次渲染的state和props保持独立的特点给打破,可以避免粗暴的浅比较render

3 项目功能的实现

目前实现的功能有用户注册登录,用户权限管理,系统日志管理,还有核心的业务:具体为事项指南管理,事项规则管理

4 项目难点

项目的难点主要是在管理员端的部分还有用户检索的部分,在事项规则管理这一块业务会有较为复杂的数据控制还有渲染优化的一些问题,但用户检索的部分并非由我负责,我参与的是事项规则管理模块的开发。

  • 因为客户端是基于点击跳转导航来确定事项的,所以规则是呈树形结构的,放在后端。但是基于事项有866个,所以这颗树子节点有866个,那么这棵树是很复杂的,如果是通过管理员端的创建规则来进行搜索下一级,那么会导致每一次的点击都直接访问接口,都会有十分明显的卡顿。后面经过分析,我们需要的是根本上减少请求的次数,直接一次性请求到前端,存储到state的一个属性中,前端再来做跳转的展示控制。
  • 另外,因为树形结构也是比较复杂的,项目初期讨论是否建立规则的树型数据结构,但是对于树的管理是很复杂的,特别是对于多叉树,而且子节点个数还是动态的【因为可以随时添加】,深度也随时可变,那么会变得极难管理,而且state也最好不要管理比较复杂的数据。而且,往往用户不需要导航到最后一级,出现了少数的事项,也可以甄别筛选自己需要的事项办理。后面讨论,采用”一事项一路径”的方法,类比文件管理器的路径,直接将规则项改为规则路径,也可以一定程度上模拟树形结构,根据规则正则来确定事项,结合上述一次性发送所有数据,数据也相比之前的树形结构变得扁平,这种数据处理会更加方便前端的控制展示。

5 简单讲一下自己独立实现的功能

我主要是负责server端的,前端的事项规则管理这部分我也有参与。

在这里就着重讲一下事项的业务。在初期的时候

6 你是怎么实现登录功能的

因为我不是主要负责这个方面的,所以我在登录功能中目前只是写前端。后端的实现部分是和同学对接的。

  • 用生成jwt的依赖包调用api来生成token。传入的是要加密的json和jwt的过期时间。我们设置了1小时之后就过期。就放到body传输
  • 我们也不仅放到请求体里面,也放到cookie里,然后前端第一次登录的时候,就在请求体拿到token来判断自己的登录状态
  • 然后使用redis,也还有一个token,这个token是用来刷新的,也就是说,当第一个token过期,而第二个token未过期时,则会将两个token更新,然后重新发给前端。

7 对于权限管理,你们是怎么控制的

  • 初步粒度,根据侧边栏控制。初期会在sessionStorage里面放一份对应的用户角色名称,然后要预渲染侧边栏组件的时候就开始请求含有侧边栏名称和侧边栏url的树形结构,用于递归渲染侧边栏及侧边栏的树型结构
  • 越权访问问题:跳转时,先拿取url,将url用来判断决定是否跳转,如果url在请求的树形结构找不到的话,那么就意味着越权访问,就会重定向到首页
  • 细粒度:比如一级审核员和二级审核员,他们是同样的侧边栏,能进入同样的页面。但是他们在《事项审核》这个页面中,是会显示不同的事项来供人审核,这个时候也是根据他们的字段名称来判断渲染的数据是什么。也就是说,每一次请求都需要携带这个字段。然后返回不同的数据供显示,供操作就可以了

HTML

1 标签嵌套

  • ul>li

  • ol>li

  • dl>dt

  • a不能嵌套a

2 HTML5新表单元素

  • datalist:规定输入域选项列表,一般用于给表单预设定义值
1
2
3
4
5
<input list="browsers">
<datalist id="browsers">
<option value="IE">
<option value="Firefox">
</datalist>
  • keygen:提供一种验证用户的可靠方法

    提交表单的时候,会生成两个键,私钥和公钥

    私钥存储在客户端,公钥发送到服务器,公钥用于验证用户的客户端证书

  • output:用于不同类型的输出,在此用于侧重输出

    在from内部的oninput规定好即可

1
2
3
4
5
<form oninput="x.value=parseInt(a.value)+parseInt(b.value)">0
<input type="range" id="a" value="50">100
+<input type="number" id="b" value="50">
=<output name="x" for="a b"></output>
</form>

3 a标签发送邮件

1
<a href=”mailto:ming.zhou@nowcoder.com”>发送邮件</a>

4 置换元素

一个不受CSS视觉格式化模型【层叠样式表】控制,CSS渲染模型并不考虑对此内容的渲染,且元素本身一般拥有固有尺寸的元素被称之为置换元素。

也就是说,置换元素是浏览器根据元素的标签和属性来决定元素的具体显示内容。

常见的有:img、input、textarea、select、object

5 语义化标签

语义化指的就是根据标签的根据内容,选择合适的标签。

好处在于:

  • 它对于机器友好,便于SEO,便于爬取信息

  • 对于开发者也很友好,对于有标签语义化的html,可读性将会大大增加,便于后面维护

6 块级元素和行级元素 || 空元素

块级元素:在浏览器中占据整行,排斥其他元素同行。

行内元素:在浏览器中可以和其他的行内元素一起排一行,行内元素如果在一行排满了,就会自动换到下一行。

常用的行内元素:img,a,span,input,select等

常用的块级元素:p,h1~h6

注意,img属于行级元素,但是它可以设置宽高

行级元素和块级元素是通过display来操控的,如果想要行排,又想要设置宽高,可以使用inline-block

块级元素不止不可以设置宽高,也不可以设置margin和padding,还有width也是不能设置的

7 img行内元素

img属于行级元素,但是它可以设置宽高

属性:

  • src:图片资源的URL
  • alt:图片未加载出来时的替代文字

8 a标签

a标签属于行级元素,属性有href和target

  • href:规定链接指向的页面URL
  • target:规定在何处打开链接文档
    • _blank:在新窗口中打开被链接的文档
    • _self:在相同的框架中打开被链接的文档
    • _parent:在父框架集中打开被链接的文档
    • _top:在整个窗口中打开被链接的文档

9 HTML5相关

  • canvas
  • video & audio

CSS

1 选择器优先级

判断优先级时,先判断一条属性声明是否有权重,也就是是否有:!important。如果加上权重,那么优先级最高,前提是没有相同优先级的声明。

样式优先级从左到右为:行内样式,id选择器,class选择器,标签选择器

一条规则优先级是从左到右的样式种类选择叠加而成的,注意,叠加不会产生进位,只是一个判定优先级的手段。叠加完毕之后,再从两条规则从左往右比较,谁的数大谁的优先级大。如果经过选择器比较,优先级仍然相同,那么后面的选择器的样式会覆盖前面的选择器的样式

2 盒子模型

所有html元素都可以看作盒子

盒模型有四个部分组成:margin,padding,content还有border

盒子模型有两种,标准模型和怪异模型

标准模型,是W3C的标准模型;怪异模型,是IE盒模型

标准模型中,width和height只包含content的内容,不包含border和padding

而怪异模型中,width和height全部包含除margin的内容,也就是包含border,padding和content

box-sizing可以控制盒子为标准模型还是怪异模型,如果设置为content-box,为标准模型;如果设置为border-box,则为怪异模型

在html中,如果没有声明DOCTYPE的话,在IE浏览器将会被识别为怪异盒子,有添加的话,就会被识别为标准盒子

3 重绘重排

先简单说一下浏览器的解析渲染机制:

浏览器解析HTML会生成一棵DOM树,解析CSS会生成一棵CSS规则树,两者结合,生成一棵渲染树。然后根据生成的渲染树。再根据生成的渲染树,得到节点的位置和大小这些布局相关的信息。再根据这些信息得到节点的像素值,通过GPU渲染到页面上。

重排,就是渲染树的一部分元素的属性改变了,导致了布局改变,进而重新构建的过程。

重绘,就是渲染树的一部分元素的属性改变了导致了元素重新改变外观,比如颜色,大小等,就是重绘。

重排一定会重绘,而重绘不一定会重排。比如当只有颜色改变时,重绘了,但是没有进行重排。

避免重绘重排几个方法:

  • 改变class类名来设定样式,可以将css变化一次性重绘重排,避免多次重绘重排
  • 避免设置多项内联样式
  • 将多次改变的效果合并计算成最后的结果来进行重绘重排,减少重绘重排次数
  • 元素频繁改动时,可以让元素取走,再放回,即使用:display: none之后,再设置回去,再设置回去,可以一定程度上减少重绘重排开销
  • 动画元素较为复杂的时候,可以使用display: flex/absolute来脱离文档流,避免对其他的元素造成影响,从而减少开销
  • JS动态创建多个节点的时候,可以使用DocumentFragment,一次性添加,也可以避免开销

4 浮动 清除浮动

浮动元素可以脱离常规流随便移动,直到遇到另一个元素或者遇到它外边缘的包含框。元素浮动之后,不会影响块级元素布局,只会影响内联元素布局。当包含框的高度小于浮动框的时候,甚至包含框没有设置高度的时候,就会溢出,没有设置高度的时候【比如设置为auto】甚至会引起高度变化从而影响常规流的布局,俗称”高度坍塌”。

清除浮动是为了清除使用浮动元素产生的影响。

清除浮动的方式:

  • 使用clear属性

    1
    2
    3
    .clear {
    clear: both // 清除浮动带来的影响
    }
  • 使用BFC块级格式化上下文来清除浮动

5 样式单位

  • px:1像素点
  • em:相对于你的高度/长度的倍数,可以叠加
  • rem:相对于你的高度/长度的背书,不可叠加大小,比较方便计算最后的盒子宽高的值
  • vw:值为1-100,50vw为其整个浏览器大小的宽度的一半【会随着浏览器的宽高变化而变化的】
  • vh:值为1-100,50vw为其整个浏览器大小的高度的一半【会随着浏览器的宽高变化而变化的】
  • vmin:值为1-100,代表比例,50vw为其整个浏览器短边的一半。适合做永不超出画面的内容。
  • vmax:值为1-100,代表比例,50vw为其整个浏览器长边的一半。适合做滚动内容。

6 画三角形

使用了border的特性

1
2
3
4
5
6
div {
border: 40px;
width: 0;
height: 0;
border-color: #000000 #000000 #000000 black
}

7 定位

定位:position属性进行操控

静态定位:static,默认值,文档流正常显示,不会受left、right、top、bottom、影响

相对定位:relative,相对于自己的默认位置来进行上下左右的调整

绝对定位:absolute,相对于自己父类的非默认定位的位置来进行上下左右的调整

固定定位:fixed,相对于整个浏览器窗口的位置,并且随着滚动也不会在窗口有移动

粘性定位:sticky,是相对定位和固定定位的结合体,当元素可以正常在页面中完全存在时,采用相对定位;当元素原位置将要被浏览器遮盖时,采用固定定位。

8 文字最多不超过三行

1
2
3
4
5
6
7
#root {
overflow-y: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp:3;
-webkit-box-orient: vertical;
}

9 伪类 伪元素

CSS中引入伪类和伪元素是为了修饰不在文档树的内容。

伪类用于当已有的元素处于某个特殊状态,比如有悬停,或者是聚焦时,为其添加其他的样式。这个样式是受用户行为变化的。伪类可以存在多个,只要不要互相排斥即可。

伪元素用于创建一些不在文档中的元素,并且为其添加样式。比如说,使用::before,就可以在该元素前方创建文本,并设定样式。通常一个选择器是不能同时使用两个伪元素的

通常伪元素使用::而伪类使用:,但是早期是没有规范的,通常:也可以表示伪元素。

10 水平垂直居中

  • 直接设置margin为auto

  • 变成行块盒,然后使用text-align: center

  • 绝对定位:四个位置参数全部调成0之后,设置margin为auto

  • 未知的宽高,可以通过设置left参数百分比

  • transform属性也可以:translate(-50%, -50%)

  • flex布局:再将align-items和justify-content为center

11 Grid布局

Grid布局,被称作网格布局。比起Flex,它是一种二维的布局方式。由纵横相交的两组网格线形成的框架性的布局结构。能够将一个页面划分几个区域,定义这些位置的大小,位置和层次关系。

设置方式为:display: grid

grid-template-columns: 200px 200px 200px 代表横向跨度为200px,重复三次。【也就是将600网格分割成三份】

grid-template-rows: 同上,一样的道理,两者可以使用repeat()函数来进行简写

grid-template-areas: 用于定义区域,一个区域由一个或多个单元格组成,HTML的元素都放在这些划分好的区域

repeat:

  • auto-fill:自动填充,让一行中尽可能的容纳更多的单元格
  • fr:表示比例用

grip-gap: 代表分割之后的分割带宽度

  • grip-row-gap: 分隔带行宽度
  • grip-column-gap: 分隔带列宽度

grid-auto-flow:划分网格之后,按照顺序来放置网格,默认为行,如果修改为column,则默认按列顺序放HTML元素

grid-row:用于定义元素长度跨度从第几条分割线开始,并从第几条分割线结束

grid-column:用于定义元素高度从第几条分割线开始,并从第几条分割线结束

12 BFC

什么是BFC

BFC:块级格式化上下文

它决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用,当涉及到可视化布局时,BFC会提供一个环境,HTML在这个环境中按照一定的规则进行布局。也就是说,BFC是一个完全独立的空间,让里面的子元素不会影响到外面的布局

什么叫触发BFC

父元素达到了BFC的触发条件,也就是有特定CSS时,父元素和整个子元素都是触发了BFC。其中父元素不同,则BFC不同;父元素相同,则BFC相同。

BFC的特点

BFC,也叫做块级格式化上下文,它是页面中的一块渲染区域,并且有一套自己的渲染规则

  • 内部的盒子会在垂直方向上接连放置
  • 对于同一个BFC,两个相邻盒子的margin会相互重叠,与方向无关
  • 每个元素的左外边距与包含块的左边界相接触
  • BFC的区域不会与浮动float元素区域重叠
  • 计算BFC高度时,浮动子元素也会参与计算
  • BFC就是页面上的一个隔离的独立容器,容器里面的元素不会影响到外面的元素,反之也是

BFC的目的就是为了形成一个相对于外界完全独立的空间,让内部的子元素不会影响到外部的元素

BFC的触发条件

  • 根元素中添加
  • 浮动元素:float值为left或者right
  • overflow的值不为visible,为auto、scroll、hidden
  • display的值为inline-block|flex|inline-flex|table-cell|grid
  • position的值为absolute或者fixed

BFC的应用

  • 去除margin重叠:只需要再其中一个盒子添加另一个不同的BFC,就可以margin相互不重叠
1
2
3
4
<div class="container">
<div class="box1"></div>
</div>
<div class="box2"></div>
  • 去除浮动:利用BFC计算高度的时候,浮动元素也会参与,只需要将父元素触发BFC就可以避免高度坍塌
1
2
3
4
5
6
7
8
9
10
11
12
13
.fix {
height: auto;
width: 400px;
border: 1px solid black;
background-color: aqua;
overflow: hidden
}
.box3 {
width: 100px;
height: 100px;
background-color: black;
float: left;
}
  • 自适应多栏布局:浮动元素BFC会对齐左边距,弄成两个不同的BFC,就可以实现不重叠的效果,做成自适应多栏布局

13 transition animation

  • transition:用于设置元素的样式过度
    • 设置效果的属性名称【一般来说该属性名称规定了动作】+ 完成动画的时间长度 + 速度曲线 + 开始时间
    • 缺点:
      • 必须依赖事件触发,不能做网页初始渲染的动画加载
      • 是一次性的,不能重复发生,除非反复触发
      • 只能定义开始状态和结束状态,不能定义中间状态
      • 只能添加一个属性的变化,不能添加多个属性
  • animation:用于设置动画属性,是transition属性的扩展。结合keyframe来实现更自由的效果
    • keyframe名称 + 持续时间 + 速度效果曲线 + 浏览器执行动画的等待时间 + 动画播放次数 + 播放方向 + 元素动画的播放状态【暂停和继续】+ 动画结束后元素的样式
    • keyframe可以切割持续时间里面的哪一个帧执行的动作,比transition更加灵活

14 flex布局

flex:弹性布局

  • flex-direction:决定主轴的方向
  • flex-wrap:决定换行的方式,决定是排满上方还是排满下方,还是不换行
  • flex-flow:flex-direction和flex-wrap的简写方式
  • justify-content:决定了一行flex布局的元素的对齐方式
  • align-items:决定了元素在垂直轴的方向如何对齐
  • align-content:定义了元素有多条轴线时的对齐方式

flex: 1是?

flex-grow flex-shrink flex-basis 分别为1 1 0%

flex-grow:规定子元素之间的放大比例,也就是存在剩余空间也不会放大

flex-shrink:表示空间如果不足,子元素就收缩元素。

flex-basis:决定flex元素在主轴方向的初始大小

1
2
3
4
5
6
7
8
.parent {
width: 100px;
height: 100px;
display: flex;
}
.child {
flex: 1
}

15 margin塌陷

在标准文档流中,竖直方向的margin会出现叠加现象,较大的margin会覆盖掉较小的margin,竖直方向上的两个盒子中间只有一个较大的margin

往往在兄弟和父子关系的元素会产生margin塌陷

解决方法:

  • 为父盒子设置border
  • 为父盒子设定padding值,抵消掉子元素设置margin的方式
  • 父元素添加overflow: hidden
  • 父盒子添加position: fixed
  • 为父盒子添加 display:table
  • 利用伪元素给子元素的前面添加一个空元素。
  • 还可以使用BFC方法

JS

1 介绍js的基本数据类型

js的基本数据类型有NumberundefinednullStringBoolean

ES6增加的数据类型为Symbol

  • symbol的用法:

    • 轻易创建全局变量,可用于解决全局变量的冲突问题

    • 使用symbol,定义类的私有属性/方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      const PASSWORD = Symbol()

      class Login {
      constructor(username, password) {
      this.username = username
      this[PASSWORD] = password
      }

      checkPassword (pwd) {
      return this[PASSWORD] === pwd
      }
      }

      const login = new Login('admin', '123456')

      login.checkPassword('123456') // true

      // undefined
      login.PASSWORD
      login[PASSWORD]
      login["PASSWORD"]
    • 作为对象属性名,用于优雅展示不需要对外暴露的属性

      1
      2
      3
      4
      5
      6
      7
      let obj = {
      [Symbol('name')]: '一斤代码',
      age: 18,
      title: 'Engineer'
      }

      Object.keys(obj) // ['age', 'title']
    • 注册全局symbol对象用于联系

      注意!在这里gs2在另一个文件,也会表达成gs1===gs2的

      1
      2
      3
      4
      5
      6
      //注册一个全局Symbol
      let gs1 = Symbol.for('global_symbol_1')
      //获取全局Symbol
      let gs2 = Symbol.for('global_symbol_1')

      gs1 === gs2 // true

ES10增加了BigInt内置对象,表示大于2^53 - 1的整数

  • 可以在整数字面量后面加上n定义BigInt
  • BigIntNumber不是严格相等的,但是是宽松相等
  • 一般用于表示整数,没有位数限制

2 js有几种类型的值

  • 原始数据类型:除了ObjectArrayDatefunction
  • 引用数据类型:ObjectArrayDatefunction

两种类型的区别是存储的位置不同:

原始数据类型直接在栈地址中存放本身数据,占据空间小,大小固定,被频繁使用。解释器寻找原始数据类型的时候直接在栈地址中找到该数据即可。

引用数据类型数据种类较多,占据空间大,存储在栈中会影响性能,通常是将内部的数据放在一个堆里。引用数据类型本身存储的是堆地址起始地址。当解释器寻找引用值,先找栈中存储的地址,在通过该地址去检索实体

3 堆-栈问题

  • 堆:是一个优先队列,按优先级进行排序
  • 栈:是一种先进后出的数据存取方式

栈区内存由编译器自动分配释放,存放函数参数值或者局部变量的值,操作方式类似于数据结构的栈,效率高于堆很多

堆区内存一般由程序员分配释放,若程序员不释放,则依赖垃圾回收机制来释放,一般空间可自己定义,普遍较大

4 内部属性[[class]]是什么

所有typeof返回值为object的对象都包含一个内部属性[[class]]。这个属性可以当成一个内部分类来看。该属性是无法访问的,可以通过Object.prototype.toString.call(Ans)来查看。

1
2
Object.prototype.toString.call([1, 2]);
// '[object Array]'

如果是自己创建的类,就查看不到具体的,因为默认情况下类的内部属性[[class]]的返回值为object。

1
2
3
class class1 {}
Object.prototype.toString.call(new class1());
// '[object Object]'

往往需要定制[[class]],用toStringTag实现:

1
2
3
4
5
6
7
class Class2 {
get [Symbol.toStringTag] () {
return 'Class2';
}
}
Object.prototype.toString.call(new class2());
// '[object Class2]'

5 typeof 和 instanceof

两者都是用于判断对象的数据类型

  • typeof:可以判断绝大部分的基础数据类型,用法为typeof A,根据返回的字符串断定类型,但是这个有一定的缺陷就是无法判断引用数据类型,比如Array,Object,Date,Function,甚至是null,都会一律返回object字符串

  • instanceof:用于判断引用数据类型,比如({}) instanceof Object,用于细致判断引用数据类型

    1
    2
    const date = new Date()
    const isDate = date instanceof Date // true

    当然,在上述例子中,date instanceof Object也是正确的

难点:instanceof可以用来判断基本数据类型吗

可以:可以通过自定义对象来判定基本数据类型:使用Symbol.hasInstance

自定义Symbol.hasInstance,就是规定这个对象使用instanceof之后返回的是true还是false

1
2
3
4
5
6
7
class P {
static [Symbol.hasInstance] (x) {
return typeof x === 'string'
}
}

console.log('' instanceof P); // true

6 0.1+0.2 !== 0.3问题

  • 因为js使用64位的双精度浮点,故只有53位有效数字,在计算机内部编码的时候,0.1和0.2在转换为二进制的时候都是无限循环的,因此在第54位有效数字就已经被截取掉了,这是第一个造成精度丢失的地方。

  • 浮点数相加的时候,会进行对阶的处理,也就是将小数点对齐,在这里一般是小阶向大阶对齐,小阶对齐的过程中有效数字会向右移动,移动后有效数字会被截取,这是第二个精度丢失的地方

  • 浮点数相加完毕,得到的结果可能超过53位有效数字,那么超过的位数也会被截取掉

解决方法:

  • 使用toFix(num),num表示保留小数的位数
  • 使用es6的Number.EPSILON:0.1 + 0.2 - 0.3 < Number.EPSILON ? true : false

7 js的隐式转换

隐式转换规则

  • ToString:在这里,除了数组,普通对象,还有科学计数法的数字,其他的全部都转变为对应的字符串,比如true转换为"true"

    • 1e10转换为"1e+10"
    • 数组转变为字符串是将所有元素用,连接起来。比如[1, 2]转变为"1,2"[1, null, undefined]转换为"1,,"【在这里null和undefined都被过滤了】
    • 普通对象:转换为[object Object]【注意,这是没有定义valueOf方法和toString方法导致的,toString方法会返回该字符串】
  • ToNumber:其他类型转换为数字类型

    • null转换成0
    • undefined转换成NaN
    • "123.12"转换成123.12,"123no"视为处理失败,转换成NaN
    • true转换成1,false转换成0
    • 数组和对象,需要首先用ToPrimitive转换:比如[1]转换成1,{}转换成NaN。如果经过转换不是原始类型时,就报错
  • ToBoolean:falsenullundefined''0NaN转化为false,其他的全为true

  • ToPrimitive:对象类型需要转换为原始类型的时候,先查找Symbol.toPrimitive()方法,再查找对象的valueOf方法,如果返回值不是原始数据类型或者没有该方法,那么就查找toString方法

    • 数组:valueOf为本身,toString为数组的join方法,[].toString() === ''
    • 对象:valueOf和toString需要自己定义
    • 注意:Date对象会优先尝试toString()方法来实现转换
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const obj = {
    valueOf () {
    return {}
    },
    toString () {
    return "123"
    }
    }
    Number(obj) // 123
    // 过程:首先查找valueOf
    // 发现不是原始类型时,就继续查找toString方法
    // toString返回"123",那么就返回"123"
    // 经过Number转换,得到的为123
    // 注意:Date对象会优先尝试toString()方法来实现转换

    const q = {}
    Number(q) // NaN
    // 过程:首先查找valueOf,没有
    // 再查找toString方法,返回'[object Object]'
    // 经过Number转换,字符串转换为NaN

宽松相等比较规则

  • 布尔类型域其他类型比较:两边全部转换成Number类型

    1
    2
    3
    4
    5
    6
    7
    8
    const obj = {
    valueOf () {
    return '1'
    }
    }

    obj == true // true
    true == 2 // false

    平时使用if判断的时候,不写清楚,一样是隐式转换:

    1
    2
    3
    4
    5
    const x = 10;
    if (x) {
    console.log(x)
    }
    // 在这里不会打印,因为x == true 值为false
  • 数字类型和字符串类型比较:字符串类型会转换为数字类型

    1
    2
    3
    4
    5
    6
    1 == '1' // true
    1 == 'Once' // false
    1e21 == '1e21' // true
    1e21 == '1e+21' // true
    Infinity == 'Infinity' // true
    // 注意,Infinity 为超出浮点数限制的数字
  • 对象类型和原始类型的比较

    先将对象转换成原始类型,再来根据上面的法则进行比较

  • null和undefined都是假值:

    1
    2
    3
    null == false // false
    null == true // false
    null == undefined // true[ECMA规定]

大小比较

同样的道理,根据上述的隐式转换规则,只不过是将 == 换成了别的符号而已

8 创建数组的方式、数组相关的API

创建方式

  • 字面量:const arr = [1, 2, 3]
  • 构造函数创建:const arr = new Array(5).fill("FU")

API

  • length:数组长度

  • 数组字符串转换:toString()、toLocaleString()【三位一分隔】、join(),其中join方法可以指定转换字符串时的分隔符【就是替换,变成另一个符号】

  • 数组尾部操作:pop()、push()【可传入多个参数】

  • 数组首部操作:shift()、unshift()

  • 数组排序:sort()【注意!这个比较的是字符串!】

  • 数组倒置:reverse()

  • 数组连接:concat()【不影响原来的数组】

  • 数组截取:slice(start, end)【截取index为start到end数组,左闭右开,不影响原数组】

  • 数组插入:splice(start, removeNumber, …insearchArray) 就在起始部分开始删除和添加

  • 数组遍历:map、forEach【没有返回值,直接在原数据修改】、every、someone、filter

9 预编译与执行上下文

预编译

在js文件执行的时候,会先进行语法分析,判断语法是否有错误

接着再进行预编译:

全局预编译:在js开始加载的前一刻执行

局部预编译:预编译阶段【发生在函数执行的前一刻】,在这个阶段,会进行变量的声明提升和函数的声明提升

最后再由解释器逐行解释并转换为其他语言来进行编译

在预编译的前期:如果声明变量没有声明就赋值,那么这个变量就会被隐式定义为全局变量

所有的全局变量都可以在window中访问获取

1
2
3
4
5
6
function test () {
var a = b = 100;
}
test();
// a = undefined, 因为赋值之后,局部变量会被垃圾回收
// b = 100, 因为b没有声明就被赋值,在这里视为将b隐式定义为全局变量,不会被回收

预编译步骤

首先,会先创建一个AO【执行期上下文】

在函数体内部,如果有多个属性重名,将参照该方式预编译:

  • 找形参和变量声明【注意,不找函数声明,因为这个在最后一步执行】,该属性值首先为undefined
  • 将实参和形参统一,将实参赋予该属性
  • 查看是否进行同名的函数定义【函数表达式不进行赋予】,如果有,则继续将该函数赋予该属性

预编译成功之后,属性值便是默认值,接着再从上到下执行:

注意,因为预编译,内部已经做过一次变量、函数的提升了,不需要再做提升,执行的时候将声明部分省略就行

1
2
3
4
5
6
7
8
9
10
function test (a) {
console.log(a); // 经过预编译,值为function
var a = 123;
console.log(a); // 重新赋值,值为123
function a () {}
console.log(a); // 值为123
var b = function () {} // 因为是函数表达式,所以初始AO的时候值为undefined,但是在解释运行的时候,就会赋值
console.log(b);
function d () {} // 函数声明,初始AO为 d(){}
}

同理,不在函数内也可以,只不过是在全局作用域执行同样的步骤罢了

执行上下文

运行上下文

在函数创建时,会将函数创建的执行环境放入作用域中

什么是执行上下文

执行上下文指的是当前执行环境中的变量、函数声明、参数、作用域链和this等信息

我们可以理解成执行上下文就是当前代码的运行的环境

如何创建

在预编译阶段创建,也就是说,在函数执行的前一刻创建。

但是作用域的确定,是在函数定义时就已经确定了,函数作用域的链式关系跟定义该函数所处的上下文相关

种类

执行上下文分类有两种:全局执行上下文和函数执行上下文。

全局执行上下文是执行js代码时创建的上下文

函数执行上下文是在函数执行的时候创建的一个新的函数执行上下文

工作流程

利用执行上下文栈来工作,因为js是单线程的,因此只能做一件事情,其他事情就在指定的上下文栈中等待执行

JS解释器在初始化代码的时候,首先会创建一个新的全局执行上下文,压入执行上下文栈顶,然后每次函数调用都会创建一个新的执行上下文放入到栈顶,随着函数执行完毕之后被执行上下文栈顶弹出

10 作用域-作用域链

  • 作用域:是一个让某些函数和变量生效的区域。也是一个只能通过js引擎存取的一个对象,称之为:[[scope]]
  • 作用域链,[[scope]]存储执行期上下文对象的集合,并呈链式链接,我们叫做作用域链
    • 函数被定义但未执行的时候作用域链将所在环境的执行上下文加入
    • 函数被执行的时候将函数自身内部环境的执行上下文加入
  • 执行环境**[EC]**:函数在运行的时候都会产生一个执行环境,并且JS引擎还会产生一个

遵照先进先出法则,至上向下寻找变量,如果找不到,就向下一层的作用域,来进行寻找。

函数执行完毕,函数内部环境的上下文将会被销毁

注意:预编译阶段出现于函数执行的前一刻,而作用域的开辟则是在函数创建的时候

作用域和执行上下文的区别:作用域在函数创建的时候确定,执行上下文在函数执行的时候创建。执行上下文维护一个作用域链的[[scope]]对象

11 闭包

什么是闭包:当函数有权访问另一个函数作用域的变量的时候,该函数,变量还有作用域就构成一个闭包。通常当内部函数被保存到外部时,就会产生闭包。比如,在一个函数中的返回值是另一个函数的时候,另一个函数在内部执行的过程中,找不到一些变量的时候,就会根据作用域向上查找,也就是查找到创建该函数的外部,这时候这个返回的函数就可以访问另一个函数的作用域中的变量

通常,闭包会导致原有的函数作用域不释放,造成内存泄漏

  • 误区:闭包一定造成内存泄露:是闭包导致的作用域不释放才会造成内存泄漏
  • 闭包的缺点:导致变量不会被垃圾回收机制回收,造成了内存消耗。通常,闭包内部没有在全局被赋值引用的时候,也是可以被清除的

通常闭包的用法:

  • 公有变量
  • 封装,属性私有化
  • 用于模块化开发
  • 防止污染全局变量

作用域如何才会释放

当发现仍然有函数访问该作用域内部的变量的时候,该作用域不会释放,反之就会立刻释放。

12 原型和原型链

原型【prototype】:函数内置对象,描述函数创建时的公共祖先

  • 公共祖先都是空对象{},我们可以通过往空对象添加属性方法,原来的构造函数进行实例化的时候,每一个实例化对象都会继承同一个公共祖先

  • 当构造函数内部有与公共祖先对象相同的属性,但是值和方法不一样的时候,这个时候以构造函数内部的属性的值为主

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function Person (name) {
    this.name = name
    }

    Person.prototype.LastName = 'zhang'
    Person.prototype.say = function () {
    console.log('hi')
    }

    const p1 = new Person('wo');
    const name = p1.name // 'wo'
    console.log(name)
    console.log(p1.LastName) // zhang

我们有时候想要根据实例来直接拿到原型,应该怎么做?

使用【__proto__】属性【我们称为隐式属性】,就可以直接从实例拿到对应的构造函数的原型了!并且也可以修改。

1
2
3
p1.__proto__.LastName = 'qw'
const p2 = new Person("newP")
p2.LastName // qw [我们可以发现这个将p2对应的构造函数的原型修改了]

当然,我们甚至有时候想要根据原型来拿到对应的构造函数,应该怎么做?

每一个原型都内置了一个构造器对象,用于方便查看是哪个构造函数使用该原型

1
console.log(Person.prototype.constructor)

构造函数原型.constructor -> 构造函数

实例.proto -> 构造函数原型

构造函数.prototype -> 构造函数原型

对于Object原型对象来说,构造函数的原型是它的一个实例,那么有:

构造函数原型->Object .__proto__

Object原型对象跟Object构造函数相对应,Object构造函数.prototype得到Object原型对象

因为Object原型对象已经不再是任何实例了,因此proto指向null

对于原型链而言的作用,当一个实例对象找不到属性和方法的时候,就会往原型上面找,找不到就再往上面找【比如toString方法可以在任何的实例使用】

13 函数的this指向

this,就是指向调用的对象

this的绑定形式:

  • 默认绑定:独立函数调用、setTimeout、setInterval时采取默认绑定,this直接指向window

  • 隐式绑定:函数调用在对象内触发,this指向该对象【对象属性链里面,只有最后一层会影响调用位置】

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function sayHi(){
    console.log('Hello,', this.name);
    }
    var person1 = {
    name: 'YvetteLau',
    sayHi: function(){
    setTimeout(function(){
    console.log('Hello,',this.name);
    });
    }
    }
    var person2 = {
    name: 'Christina',
    sayHi: sayHi
    }
    var name = 'Wiliam';
    person1.sayHi();// Wiliam
    setTimeout(person2.sayHi,100); // Wiliam
    setTimeout(function(){
    person2.sayHi(); // Christina
    },200);

    第一个输出,因为setTimeout内部的回调函数的this绑定为window

    第二个输出,setTimeout内部的回调函数可以理解为被作为参数传给了setTimeout,因此做过赋值操作,为默认绑定

    第三个输出,虽然有setTimeout,但是sayHi不是作为回调函数传入,而是在回调函数内部执行,因此是隐式绑定

  • 显式绑定:通过call、apply、bind,显式指定this的指向对象,

    在this判断的时候,把函数当变量传值一般需要经过绑定,因为函数作为变量传进函数会导致该函数进行”赋值”,导致丢失隐式绑定,变成默认绑定

    注意:传入对象为null或者undefined时,这些值会被忽略,并且执行默认绑定规则

  • new绑定

    • 创建一个空对象,构造函数的this指向该对象

    • 这个新对象被执行__proto__链接

    • 执行构造函数,属性和方法添加到该空对象【this引用的对象】

    • 如果构造函数中没有返回其他对象,那么返回this,即创建的新对象,否则返回构造函数中规定的其他对象

      1
      2
      3
      4
      5
      function sayHi(name){
      this.name = name;
      }
      var Hi = new sayHi('Yevtte');
      console.log('Hello,', Hi.name);// Yevtte,优先级new比默认高

因此,this指向绑定是有优先级的:

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

箭头函数的this指向:this指向箭头函数创建时,所在作用域的this,另外,箭头函数的this是不可以换绑的

js的this只会在函数作用域全局作用域,不要与块级作用域混淆

14 call apply bind

作用:改变this的指向

call和apply的区别:

  • 两者都是换绑this指向,但是两者仅仅只是传参的方式不同
  • apply传入的是argument数组,而call传入的是若干个参数
1
2
3
4
5
6
7
8
9
10
var name = 'window'
var a = {
name: 'obj'
fn: function (a, b) {
console.log(a, b, this.name)
}
}
var func = a.fn // 进行了赋值操作,隐式绑定断裂,变成默认绑定
func.call(a, 1, 2) // 绑定到a中
func.apply(a, [1, 2])

而bind是创建一个新函数,因此需要重新去调用

1
2
3
4
5
6
7
8
9
var name = 'window'
var a = {
name: 'obj'
fn: function (a, b) {
console.log(a, b, this.name)
}
}
var func = a.fn // 进行了赋值操作,隐式绑定断裂,变成默认绑定
func.bind(a, 1, 2)() // 绑定了,但是需要再使用一次执行符号,以此执行,否则为未执行。

实现call方法

1
2
3
4
5
6
7
8
9
10
Function.prototype.call_ =  function (context, ...args) {
if (context === undefined || context === null) {
context = window;
}
// 原型this指向实例对象,call方法本质上就是在该上下文创建一个一模一样的函数,再进行删除
context.fn = this;
const res = context.fn(...args);
delete context.fn;
return res;
}

实现bind方法

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.bind_ = function (context, ...initArgs) {
if (context === undefined || context === null) {
context = window;
}
// bind方法也是一样的,只不过是返回一个函数,要写一个闭包的构造方式
const _this = this;
return function (...args) {
context.fn = _this;
const res = context.fn(...initArgs, ...args);
delete context.fn;
return res;
}
}

15 JS判断是否为数组

正统方法是isArray方法,原理是通过Object.prototype.toString()来判断对象内部属性[[Class]]是否为"Array"来实现的

  • typeof方法:不行,因为对数组,会返回"object",无法判断是不是数组

  • instanceof方法:可以,但是有缺陷

    1
    [] instanceof Array // true
  • 原型链和构造函数查找方法:

    1
    2
    3
    const arr = [1]
    console.log(arr.__proto__.constructor === Array)
    console.log(arr.constructor)

    但是可能不准确:也就是当某些函数的原型指向了数组

    1
    2
    3
    4
    5
    function Fn () {}
    Fn.prototype = new Array()
    const fn = new Fn()
    fn.constructor === Array // true
    fn.constructor === Fn // false

使用instanceof和constructor的局限性:

两个页面之间是不可以进行相互判断数组的。

1
2
3
4
5
6
7
8
9
10
11
12
13
var iframe = document.createElement('iframe')
document.body.appendChild(iframe)
var xArray = window.frames[window.frames.length - 1].Array //切换
var xarr = new xArray()
var arr = new Array()

// 不同页面,结果并非我们所预期的 true,而是 false 哦!
console.log(xarr instanceof Array) // false
console.log(xarr.constructor === Array) // false

// 同页面才是 true 哦!
console.log(arr instanceof Array) // true
console.log(arr.constructor === Array) // true
  • isArray原理:

    1
    2
    3
    function isArray (arr) {
    return Object.prototype.toString().call(arr) === '[object Array]'
    }
  • Object.prototype.toString.call()方法:兼容性最好

16 防抖与节流

防抖:每一次的触发都要消除上一次的异步处理,如果最后没触发,将会实现【只执行高频最后一次动作】

1
2
3
4
5
6
7
8
9
10
11
12
function Debounce (fn, time) {
time = time || 100;
var timer = null;
return function () {
if (!timer) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, time)
}
}
}

节流:触发变得更加稀疏,不到时间点就将一个数记为非空,时间到了再撤去

1
2
3
4
5
6
7
8
9
10
11
12
function Throttle (fn, time) {
time = time || 100;
var timer;
return function () {
if (timer === null) {
fn.apply(this, args)
timer = setTimeout(() => {
timer = null
}, time)
}
}
}

操作缓慢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Consumer (fn, time) {
let tasks = [],
timer
return function (...args) {
// 和apply的区别就是bind不会立即执行,apply会立即执行
task.push(fn.bind(this,...args))
if (timer == null) {
timer = setInterval(() => {
tasks.shift().call(this)
if(task.length <= 0) {
clearInterval(timer)
timer = null
}
}, time)
}
}
}

17 箭头函数的特点以及普通函数的区别

  • 语法更简洁

  • 箭头函数没有prototype,故箭头函数本身没有this

  • 箭头函数本身的this是由创建箭头函数的作用域的this决定,箭头函数的this和创建箭头函数的作用域的this一致

  • call、apply、bind方法不可以动态修改箭头函数内部的this指向

  • 因为没有自己的this,所以是不可以作为构造函数使用的,因为构造函数创建实例的时候会将函数中的this指向该对象,但是箭头函数this是永远不会改变的,因此构造函数不能用箭头函数定义

  • 箭头函数没有自己的argument对象,用rest参数代替,箭头函数内部访问argument实际上获得的是外部函数的argument

    • rest是一种方法,而不是一个像argument的固定对象
  • 箭头函数不能用Generator参数,不能用yield关键字

18 new的过程

  • 创建一个空对象,构造函数的this指向该对象
  • 将该对象用__proto__关联实例
  • 将this的属性方法添加到空对象
  • 如果构造函数没有规定返回对象,则this为创建的对象,反之为该创建的对象
1
2
3
4
5
6
function myNew (obj, ...rest) {
let newObj = Object.create(obj.prototype);
newObj.__proto__ = obj.prototype;
const result = obj.apply(newObj, rest);
return typeof result === 'object' ? result : newObj;
}

19 事件循环机制

JS是单线程的,因此同一个时间只能做一件事情,但是如果没有事件循环机制,处理延时过长的IO的时候,JS不会停止,会造成假死状态,将会影响用户的交互体验

因此设计出了事件循环机制

如果是同步任务,那么将会扔进执行栈中,如果是异步任务,就扔进任务队列里面

执行的时候,将会先执行执行栈中的同步任务

执行栈中的东西执行完毕之后,就会去任务队列里面寻找

任务队列

  • 宏队列:用于放异步任务用的,比如setTimeout、setInterval、I/O、UI交互、Promise回调内部的异步、setImmediate(Node.js 环境)等
    • 宏任务内部的异步应用将放在微队列中,内部的同步应用放在执行栈中
  • 微队列:Primise.then()、process.nextTick(Node.js 环境)

宏队列和微队列都只有一个!

微队列特性,微任务内部的微任务也会直接放在队列里面:

1
2
3
4
5
6
7
假设a任务有ABC三个子微任务
b任务有DE子微任务
c任务就自己是微任务。
一开始队列肯定是abc
然后执行,发现有三个子微任务,队列放后面,变成bcABC
同理然后变成cABCDE
然后c没有子任务,反而先执行了。

例子如下

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
new Promise((res, rej) => {}).then(
setTimeout(() => {
setTimeout(() => {
console.log("A")
}, 0)
setTimeout(() => {
console.log("B")
}, 0)
setTimeout(() => {
console.log("C")
}, 0)
}, 0)
).then(
setTimeout(() => {
setTimeout(() => {
console.log("D")
}, 0)
setTimeout(() => {
console.log("E")
}, 0)
}, 0)
).then(
setTimeout(() => {
console.log('c')
}, 0)
)

关于async和await的处理顺序

async是同步的,因为async返回的是一个隐式Promise,那么在await出现之前,都是可以像在Promise内部执行任务,async内部执行异步任务,一样是放在宏队列

但是,如果里面有await的话,执行完await的任务,那么就会立刻中断async,在继续往下执行,执行完毕之后,再回头执行await后面的任务。【注意,await后面的任务都会放到微队列去执行!】

如果还有await,将会继续中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
console.log('A')

async function async1() {
setTimeout(() => {console.log("Q")}, 0)
await console.log("@")
await async2()
console.log('B')
console.log("C")
}
async function async2() {
console.log('D')
}
async1()
new Promise((resolve) => {
resolve()
}).then(() => {
console.log('F')
})
console.log('G')
setTimeout(() => {console.log("W")}, 0)
// A@GDFBCQW 这是新版,但是这个是不标准的,按照标准应该是:A@GFDBCQW

本来应该是G@FDBCQW,因为是以微任务的形式塞入,F在D之前,但是因为chrome的优化导致,await变得更快了,但是这种做法其实是已经违反了规范。

关于node的事件循环机制

区别:浏览器的微任务是在每个相应的宏任务进行的,而nodejs中的微任务是在不同阶段之间进行的

20 v8垃圾回收机制

垃圾回收的针对对象

垃圾回收针对的是引用数据类型,因为基础数据类型可以通过操作系统自动分配和自动回收的。

因为引用数据类型大小不固定,不在栈中,系统无法自动释放回收,因此需要js引擎的垃圾回收机制。

为什么要垃圾回收?

在chrome中,v8被限制了内存的使用

  • 可能用不到占用大量内存的场景

  • 如果内存占用很大,清理的时候,会很耗时间,会引起js线程暂停时间过长,性能下降

为什么需要垃圾回收呢?因为对象和数组没有固定大小,当他们大小已知的时候才能对他们进行动态存储分配。只要进行过动态分配,那么就要进行内存释放以便后续还能继续使用这些内存,否则,将会导致js应用和操作系统的性能下降,甚至将会消耗完所有的内存造成系统崩溃。

垃圾回收的方法

v8的垃圾回收机制基于分代回收机制,这个机制基于世代假说:新生的对象容易死亡、不死的对象活得更久

分代回收就是:v8将堆分为两个空间,一个叫新生代,一个叫老生代,新生代存放存活周期短的对象,老生代存放存活周期长的对象

  • 新生代:scavange算法【from空间和to空间命名为A、B】
    • 将新生代区分为两个区A和B,B区是空的,将A区的所有活动对象全部复制,并且按内存顺序放到B区,之后清除A区的所有对象,最后将AB两个区交换命名,也就是交换用法,这样方便用于下一次垃圾回收
    • 分辨是否为活动对象?根据可达性来判断,活动对象往往就是可达的
      • 可达性:从window开始向下搜索子节点,当一个对象被搜索到了,那么这个对象就是可达的,就是活动对象,反之就是非活动对象
  • 老生代:Mark-Sweep算法和Mark-Compact算法
    • 为什么不能用新生代的垃圾回收算法?因为老生代的内存空间很大,使用新生代的方法将会造成将近一半的内存浪费
    • 对老生代对象进行扫描,判断可达性,然后再进行删除非活动对象
    • 与新生代垃圾回收不同的地方在于不需要进行复制清理,只要进行判断可达性,并标记,就可以清理非活动对象了
    • 当然,我们需要内存整理,不然会产生常见的碎片问题,因此才使用Mark-Compact算法

新生代晋升到老生代

  • 判断对象是否已经经过一次scavange回收。如果经历过,则从对象直接移植到老生代中,反之移植到新生代的另外半边的区域用于整理
  • 判断to空间的内存使用占比是否超过限制,如果移植到to的时候,发现超过25%,则立刻移植到老生代中,不然会影响后续新生代新对象的空间分配。

垃圾回收可能造成的全停顿问题

因为js代码运行需要js引擎,而垃圾回收也需要js引擎,当你两者都需要进行该怎么办?先执行垃圾回收,垃圾回收完成才执行js代码。这个过程会让js代码暂停运行,称作全停顿

往往新生代全停顿时间较短,老生代时间可能较长,当老生代内存整理的对象较多的时候就需要较长的时间,造成卡顿现象

全停顿的解决方法

  • 增量标记:将时间分片,依次轮询给js垃圾回收和js代码运行
  • 惰性清理:当垃圾回收器发现不清理垃圾也能保证内存足够的时候,就延迟清理,或者只清理部分垃圾而不清理全部
  • 并发与并行

21 js继承[五种]

原型链继承

直接通过原型链来继承,子实例之间方法唯一,不需要重新创建,但是这样子实例之间的属性不私有

借用构造函数继承

通过构造函数继承,虽然子实例之间属性已经私有,但是子实例之间方法却要重复创建

组合继承

综合了上述两者的优点

原型式继承

通过传入一个对象来创建一个类

寄生式继承

创建一个封装继承过程的函数,该函数在内部增强对象,最后返回对象

但是也有缺点,就是每次创建实例都会再来创建一次方法

寄生组合式继承

组合继承几乎是完美的,但是美中不足的就是它会调用两次父构造函数,一次是设置子类型原型的时候,另一次是创建子类型实例的时候

解决方法:直接让Child.prototype直接指向Parent.prototype

思想:直接创建一个空实例,并且将这个空实例的原型和构造函数进行继承的绑定,这样就不需要多次调用父类的构造函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建空实例
function object (o) {
function F () {};
F.prototype = o;
return new F();
}

// 构建原型链关系并立刻创建
function create (child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}

// 直接使用,相当于已经做好原型和构造函数之间的绑定了
prototype(Child, Parent)

22 JS事件流和事件委托

事件模型的种类

  • IE事件模型:只有目标阶段冒泡阶段。
  • DOM0级模型:不会传播,没有事件流的概念,使用attachEvent来绑定事件
  • DOM2级模型:有捕获阶段,目标阶段和冒泡阶段,使用addEventListener来绑定事件

事件流

事件流描述的是从页面中接受事件的顺序

事件流一共有三个阶段

  • 捕获阶段:在DOM树的根部向下搜索事件触发的DOM节点
  • 目标阶段:处理捕获到的DOM节点的事件
  • 冒泡阶段:事件触发后,根据DOM树的结构一级一级往上传播

事件委托

也叫事件代理,本质上是利用了浏览器事件冒泡的机制,将事件监听设在父节点,因此子节点触发事件会冒泡到父节点,进而触发父节点绑定的事件

好处:不需要为每一个子元素设置一个绑定事件,这样可以有效减少内存消耗。并且我们动态添加子元素,也是可以触发事件的

阻止事件冒泡

e.stopPropagation()

23 属性描述符

描述对象属性的设置,就是属性描述符

数据描述符

  • configurable:是否可删除
  • enumerable:是否可遍历(枚举)
  • value:属性值
  • writable:是否可修改

我们一般使用Object.defineProperty来进行属性的添加设置操作

1
2
3
4
Object.defineProperty(window, 'a', {
value: 2,
writable: false
})

window.a我们可以读到是2,但是输出window我们看不到a

并且,我们发现不可修改

访问器描述符

用getter和setter替换value和writable属性而已,使用了setter表示了writable为true

24 获取宽高的各种API

offset系列[只读]

  • offsetWidth:包含了padding,border,scrollbar,以及css的width的值
  • offsetHeight:包含了padding,border,scrollbar,以及css的height的值
  • offsetParent:返回一个指向最近的包含该元素的定位元素。如果没有定位的元素,则offsetParent为最近的table,table cell或者根元素,如果display属性设置为none时,offsetParent返回null
  • offsetTop:返回和offsetParent的顶部距离
  • offsetLeft:返回和offsetParent的左侧距离

注意,如果是行元素,会自己截断换行的元素,使用该系列API指定的是第一个边界框的位置

因此用上述API来对应span元素的盒子边界各项的值是不行的

client系列[只读]

  • clientWidth:返回元素的内部宽度,只包括内边距,也就是padding和width,不包括scroll
  • clientHeight:返回元素的内部高度
  • clientTop:返回上边框宽度
  • clientLeft:返回左边框宽度

scroll系列

  • scrollWidth:返回值为padding,width和溢出部分的和

  • scrollHeight:返回值为padding,width和溢出部分的和

  • scrollLeft:方框左边到最左边的内容的距离

  • scrollTop:方框顶部内容到最顶部的内容的距离

  • scrollX:整个页面水平方向滚动的像素值

  • scrollY:整个页面垂直方向滚动的像素值

inner outer系列

  • inner代表浏览器页面内部宽高
  • outer代表整个浏览器的宽高

25 JS为什么是单线程的

JS的主要用处就是用户交互和操作DOM,因此决定了它只能是单线程,否则会带来很复杂的同步问题,浏览器不知道应该以哪个线程为主,因此为了避免问题的复杂性,就将浏览器设置为单线程

后面,为了利用多核CPU的计算能力,提出了HTML5 的web worker标准,允许JS创建多个线程,但是子线程受到主线程控制,并且不能操作DOM,因此本质上还是单线程的本质

26 fetch ajax axios区别

fetch

是ES6提供的原生跨域方法,返回格式为Promise,比较底层,一般都是向上封装

__优点:__处理跨域十分简单

缺点:

  • 但是它只对网络请求报错,对于400和500都当作成功,需要自行封装,只有网络中断才会执行reject

  • 不会携带cookie,需要添加配置项

  • fetch不支持请求中断,不支持超时控制,往往定时器和Promise.reject不能阻止请求过程继续在后台运行,造成了流量的浪费【现在使用AbortController 可以了】

  • fetch无法使用原生检测请求的进度

ajax

__优点:__对原生XHR封装做了兼容处理,简化了使用,并且可以简单处理部分跨域【比如JSONP】

缺点:

  • 有多个请求并且有依赖关系,将会形成回调地狱
  • 为了使用ajax直接调用整个jquery,是不好的
  • 设计初衷本身针对MVC,对于MVVM浪潮较为老旧

axios

优点:

  • 支持Promise

  • 支持并发操作

  • 可在nodejs使用

  • 可以拦截响应

  • 可以设置超时时间,取消请求

  • 自动转换JSON数据

  • 可以抵御CSRF攻击

__缺点:__不支持现代浏览器

27 async和defer的区别

  • async:会使脚本异步加载,不会阻塞页面的解析,但是其他脚本加载完成后立刻执行该脚本,这个时候文档没有解析完成的话同样会阻塞。多个async属性的脚本执行顺序也是不可预测的,一般不会按照代码顺序执行
  • defer:这个属性会让脚本的加载和文档解析同步,然后文档解析完成之后再执行这个脚本文件,这样可以使页面的渲染不被阻塞,通常按规范来说defer是最后执行的

28 DOM的API

  • DOM:被称为文档对象模型,它指的是把文档当作一个对象来看,这个对象主要定义了处理网页内容的方法和接口
    • 当然,它应该有两层含义,一个是文档建模出来的一个树形模型,另一个是操作文档的API
    • 浏览器首次解析文档的时候,会将文档上面的一个个元素解析成对应的树节点,元素的包含关系对应树的父子关系,dom树变化,浏览器会去跟踪树的变化,并跟随DOM树的变化做出相应的变化

增加 [都需要appendChild添加进dom树中]

  • createElement:其中react的JSX的虚拟DOM格式创建元素也是基于它来实现的
  • createTextNode:创建一个文本节点
  • cloneNode:复制一个文本节点,传入一个bool类型值,值为true则可以一起复制子节点,反之不复制子节点

删除

  • remove:删除该节点
  • removeChild:删除该节点的子节点

插入

  • appendChild【添加一个,有返回值】、append【可添加多个】
  • insertBefore【前插】、replace【替换】

查找

  • document.getElementById:根据id获取
  • document.getElementByTagName:根据标签名获取【一般为集合】
  • document.getElementByClassName:根据类名获取【一般为集合】【可传入多个类名】
  • document.querySelector:选择器【选择第一个符合条件的元素】【使用深度优先搜索】
  • document.querySelectorAll:选择器【返回一个元素集合】【返回一个非即时的元素集合,也就是说结果不会随着文档树变化】
  • 父子节点系列:parentNode、parentElement、childern、childNodes
  • 兄弟节点系列:previousSibling、previousElementSibling、nextSibling、nextElementSibling
1
2
3
4
// 下面是例子,支持id,类名,还有标签名选择
document.querySelectorAll('#span');
document.querySelectorAll('div');
document.querySelector('.clear');
  • 特殊元素获取
    • bodydocument.body
    • htmldocument.document.Element

29 Promise

什么是Promise

Promise是一种新技术,是ES6的实现异步编程的新的解决方案

Promise是一个构造函数,接收一个函数作为参数,在内部封装了异步操作,并返回一个Promise对象实例

使用Promise,可以让指定回调函数变得更加灵活,摆脱js回调地狱的问题

Promise的结构

  • 状态:Pending【等待执行】、resolve【成功】、reject【失败】
  • 值:PromiseState:用于存储resolve或者reject传输的值
  • then方法:是Promise的其中一个方法,它返回一个Promise对象,获取resolve或者reject传输的值再来执行不同的回调
    • 回调顺序问题:根据宏任务微任务的方法来进行判定,当resolve或者reject在异步里面,就会先执行then方法再执行resolve或者reject来改变状态【注意:Promise是在new之后就会直接执行的】
    • then可以进行链式调用,并且通过返回一个Pending对象来进行链式中断,并且我们也可以用catch方法把失败回调集中捕获处理

30 深拷贝

在这里有一个坑,就是要准确判断是否为数组

1
2
3
4
5
6
7
8
9
10
11
const deepClone = (obj) => {
let res = obj instanceof Array ? [] : {};
for (let key in obj) {
if (obj[key] && typeof(obj[key]) === 'object') {
res[key] = deepClone(obj[key]);
} else {
res[key] = obj[key]
}
}
return res;
}

浅拷贝,就是复制一份原对象的指针值,两个指向的是同一个堆地址,如果一个改变了,同理浅拷贝的另一个对象也会改变

深拷贝的几种方式

  • Object.assign:表示将两个对象合并,默认是对对象进行深拷贝的,不过是对最外层进行深拷贝,如果内部还有对象,那么就只是进行浅拷贝,这点需要注意。
  • 用JSON.parse和JSON.stringify来做。本质是执行JSON会构建新的内存来存放新对象
    • 会忽略undefinedsymbol
    • 不可以对Function进行拷贝,因为JSON字符串不支持function,在序列化的时候自动删除
    • Map,Set和其他内置类型在进行序列化也会丢失
    • 不支持循环引用对象的拷贝
  • 用JQ的extend方法:$.entend(true, [], array)
  • 使用MessageChannel
1
2
3
4
5
6
7
function deepClone (obj) {
return new Promise((resolve) => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
})
}

缺点,异步,并且也不支持function

  • lodash方法:lodash.cloneDeep方法:是最完美的方法,不过要引入lodash依赖

31 jsonp

jsonp是JSON with padding的简称,是一种非官方的跨域解决方案,通过客户端的script标签发出的请求方式。

在浏览器中,有同源安全机制,就是浏览器会将协议端口域名三者都相同的视为同源。如果不依赖ajax请求,浏览器会将其非同源的响应数据丢弃。

但是有些标签发出的请求是不会进行同源检测的。比如script标签和img标签等。这样,后端就不需要做解决跨域的响应头。

当然,jsonp是有局限的,它只能发送get请求,而且必须要与后端规定好一些参数,比如回调参数名,因为后端的url返回的是一个参数调用

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
// 转换对象数据为url
function formatData (data) {
const res = [];
for (let i in data) {
res.push(encodeURIComponent(key) + '=' + data[i]);
}
return res.join('&');
}

// jsonp设置
function jsonp (params) {
return new Promise((resolve, reject) => {
params = params || {};
// 插入script标签到head
var script = document.createElement('script');
var head = document.querySelector('head');
script.src = `${params.url}?${formatData(params.data)}`
// 回调参数名,服务端会直接返回一个该函数的调用
const callbackName = params.jsonp;

window[callbackName] = function (jsonData) {
head.removeChild(script);
clearTimeout(script.timer);
window[callbackName] = null;
params.success && params.success(jsonData);
};
if (params.time) {
script.timer = setTimeout(() => {
window[callbackName] = null;
head.removeChild(script);
params.error && params.error({
message: "超时"
})
}, params.time)
}
head.appendChild(script);
})
}

32 手写Promise.all

Promise.all接收一个数组,数组里面有Promise还有常量,它通常做的操作就是把里面的Promise执行了,执行的成功回调的值放进一个Promise里面返回出来。Promise会等待所有的Promise执行完毕之后才会返回结果。如果都是成功的回调,Promise.all就会返回一个数组,如果里面会有一个失败,那么将直接进行catch回调,执行reject,返回的值是第一个失败的Promise回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function newPromiseAll (PromiseArray) {
return new Promise((resolve, reject) => {
if (!Array.isArray(PromiseArray)) {
reject("不是数组,请稍后再试呢")
}
let res = [];
for (var i = 0; i < PromiseArray.length; i++) {
let variable = Promise.resolve(PromiseArray[i]).then(value => {
res.push(value);
if (res.length === PromiseArray.length) {
resolve(res);
}
}).catch(e => {
reject(e)
});
}
})
}

33 手写Promise.race

Promise也是接收一个数组,数组里面可以有常量值,也可以有Promise值。race方法返回的是第一个执行出来的resolve或者reject方法的数据,谁先输出,race方法就是谁。在这里,我们只处理Promise数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function PromiseRace (PromiseArr) {
return new Promise((resolve, reject) => {
if (!Array.isArray(PromiseArr)) {
reject("不是数组呢")
}
for (let p of PromiseArr) {
Promise.resolve(p).then((res) => {
resolve(res);
}).catch((err) => {
reject(err);
})
}
})
}

34 CommonJS 和ES Module

CommonJS

  • require避免重复加载和循环加载的原理
    • 每个文件模块被执行了之后,判断是否之前加载过,如果之前没有加载过,那么就会异步缓存该模块,并且立即执行;如果加载过,那么就不会执行了。正是通过这个原理,才得以避免循环依赖等问题
    • require可以在任意的上下文动态加载模块,因为它的本质就是一个函数,在运行时实现
  • exports:规定导出对象,本质上就是module.exports,只不过module.exports可以装进对象中暴露,而exports只能一个一个引用,并且还会被module.exports覆盖【所有的exports都会】
  • module.exports:有一些缺点,就是循环引用的时候导致的,在循环引用的情况下,会导致有几个文件共享一个module.exports的情况,可能很容易造成人为的属性的丢失

ES Module

  • export:正常导出
  • export default:正常导出,但是唯一的
  • import:引入,支持重命名,懒加载等,也支持只运行模块,同样,也支持返回Promise的动态导入
    • 导入的数据是不支持修改的,只能通过暴露的函数来进行修改
  • 因为ES Module的引入和导出是静态的,import会自动提升到代码顶层。import和export不能放到块级作用域或者条件语句
  • ES Module会提前加载并执行模块文件【会提升到最上方】

双方的特性与区别

CommonJS:

  • CommonJS模块由运行JS时出现
  • CommonJS是单个值导出,本质上是导出exports属性
  • CommonJS可以动态加载,对每一个加载都设置缓存,避免循环依赖问题
  • CommonJS同步加载并执行模块文件
  • nodejs只支持commonjs,不支持es module

ES Module:

  • ES Module是静态的,不能放在块级作用域内
  • ES Module的值是动态绑定的,可以通过导出方法修改,但不能直接修改
  • ES Module的导入导出非常灵活,可以导入导出多个属性方法
  • ES Module提前加载并执行模块文件
  • ES Module导入模块在严格模式下

35 let和const

let和const都是只在声明的块级作用域有效

但是,let是可以进行变量值的修改的,而const是不可以进行变量的栈地址的值的修改,也就是说,普通类型的数值是不可以被修改的,但是引用类型,比如数组,可以采用push等方式改变数组,因为这样子栈地址并没有改变

如果想让堆地址的数值也不发生改变的话,可以采用object.freeze(obj)进行冻结,这样堆地址的也不可以进行了。

let和const比起var,也添加了一个暂时性死区的特性。在var中,变量在声明前调用,会被视为undefined,而使用let和const的时候,这样的操作是非法的,会报错,这就叫做暂时性死区

36 预加载和懒加载

懒加载就是延时加载,通常在用户想要继续往下浏览时,触发某个事件再来进行按需加载的方法就叫懒加载

资源预加载

提前加载所需要的图片资源,加载完毕之后会缓存到本地,当需要的时候会立马显示出来,以达到预览时就不需要加载就直接预览

资源懒加载

  • window.onscroll:滚动浏览器视口的时候触发的函数,频繁触发也是不好的,因此需要一个节流处理
  • offsetTop:图片到浏览器顶部的距离:img.offsetTop获取
  • scrollTop:滚动的距离:document.documentElement.scrollTop获取
  • clientHeight:视口距离:document.documentElement.clientHeight获取

如何触发?当scrollTop + clientHeight > offsetTop时,就会触发。但是记住,不要重复触发,也就是:!img.src

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
const imgs = document.querySelector('img');
function lazyLoad (imgs) {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
for (let i = 0; i < imgs.length; i++) {
if (clientHeight + scrollTop >= imgs[i].offsetTop && !imgs[i].src) {
imgs[i].src = imgs[i].dataset.src;
}
}
}

function Throttle (fn, time) {
time = time || 500;
var timer = null;
return function () {
if (!timer) {
fn.apply(this, args);
timer = setTimeout(() => {
timer = null;
}, time);
}
}
}

window.onscroll = Throttle(lazyLoad(imgs), 500);

路由懒加载

在开发过程中运行资源,会把所有页面加载,这样会有首页初次渲染较慢的问题。所以使用路由懒加载,就是你点击此页面,就再来加载这个页面,这样可以提高首次加载整个应用的等待时间有一定程度的缩短。

在react如何实现路由懒加载:使用React.lazy函数即可。在这里,需要搭配ES Module的加载方式,因为ES Module支持懒加载的方式

37 洗牌函数

可以说一个加分点:用网上的文章洗牌函数都是有问题的

1
2
3
4
5
6
7
8
9
10
const shuffle = (array) => {
const res = [];
while (array.length > 0) {
let index = Math.floor(Math.random() * array.length);
array = array.filter((item, index_) => {
return index === index_;
});
res.push(array[index]);
}
}
1
2
3
function shuffle(arr) {
arr.sort(() => Math.random() - 0.5);
}

38 一些正则

  • 千分位:/(?!^)(?=(\d{3}+$))/g
  • 邮箱:^[a-zA-Z0-9_-]+@[a-zA-Z0-9]+(\.[a-zA-Z0-9_-]+)+$

39 数组去重

1
2
3
arr = arr.filter((item, index, array) => {
return array.indexOf(item) === index;
})

40 JS中如何展示大量数据呢?

第一个办法:只发第一页,然后需要继续向下看数据,就继续请求。也就是后端对数据进行分页处理

第二个办法:利用setTimeout和setInterval的特性来做,将大量数据分组,然后再放到微任务,几乎一起同步执行,不过这个的缺点就在于你必须要保证是按照顺序来加载,不然容易出现大量数据的乱序问题

第三个办法:懒加载,当即将访问到窗口底部的数据,就会请求后端服务加载新数据

41 实现拖拽效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
div.onmousedown = function (e) {
const x = e.pageX;
const y = e.pageY;
// 得到元素视口位置
const {left, top} = div.getBoundingClientRect();
window.onmousemove = function (e) {
const disX = e.pageX - x;
const disY = e.pageY - y;
const newLeft = left + disX;
const newTop = top + disY;
div.style.left = newLeft + 'px';
div.style.top = newTop + 'px';
}
}

div.onmouseup = function (e) {
window.onmousemove = null;
}

42 尾调用优化

尾调用:就是函数的最后一个步骤是调用另外一个函数,还不能是闭包

尾调用优化:即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是”尾调用优化”的意义。

尾递归:递归会造成函数的多层级调用,容易引起内存不足的问题,因为每一个函数执行都会产生一个执行帧,压入执行栈,而尾递归就是尾调用自身,就会将之前的执行帧参数清空并重新赋值,就不会引起栈溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 尾调用
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

// 常规递归
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}

factorial(5) // 120

43 for in 和for of

for in可以遍历原型上的属性,而for of是不可以的

44 Proxy

Proxy用于创建一个事项的代理,是用于监听一个对象的相关操作。代理对象可以监听我们对原对象的操作

Proxy对象需要传入两个参数,分别是需要被Proxy代理的对象和一系列的捕获器

1
2
3
4
5
6
7
8
9
10
const obj = {
name: '1'
}

const objProxy = new Proxy(obj, {
get: function (target, key) {
console.log(target[key]);
}
});
console.log(objProxy.name);

会输出两个值,一个是触发了get之后输出的值,一个是正常输出

也就是说,Proxy可以用来捕获对象的一些操作,比如获取值,设置值等等

另外,Proxy的this和原对象的this是不相同的

所以,Proxy可以用于一些前端框架中数据改变之后的触发事件,使框架实现响应式

45 JS面向对象和Java面向对象的区别

1 面向对象的能力

JS只有一种访问作用域public,不支持静态作用;而java支持public,private等

2 修改对象定义

JS可以修改对象定义,比如重写方法,都是可以被允许的。而Java不可以

JS可以在对象实例化之后再来定义方法。

React

1 说说对React的理解,有哪些特性

什么是react

react是一个构建用户界面的js库,只提供了UI层面的解决方案

它遵循了组件的设计模式,声明式的编程范式和函数式编程概念,使前端应用程序更加高效

使用的虚拟DOM操控真实DOM,遵循高阶组件到低阶组件的单向数据流

react也易于组件化,组件之间也可以进行组合

特性

  • JSX语法
  • 单向数据绑定
  • 虚拟DOM
  • 声明式编程:表达逻辑不需要显示将步骤展示,只需要声明出需要的结构即可
  • 组件化:在react中,一切皆为组件。通常将应用程序的整个逻辑分解为功能独立的各个部分,这些就叫组件。通常组件可以是一个函数或者一个类,接受数据输入,并根据数据的变化处理UI展示
    • 组件该有的特点:可组合,可重用,可维护【每个组件逻辑相对独立,更容易被理解和维护】

react的优点

高效灵活,声明式开发较为简单,组件化提高代码复用率

2 虚拟DOM和真实DOM的区别

相关介绍

真实DOM,为文档对象模型,在页面渲染出的每一个节点都是以一个真实的DOM

虚拟DOM,本质上是JS对象形式的对DOM的描述,用对象的属性来描述节点,并且该对象最少包含tag,attrs和children三个属性,最终可以通过一系列操作使这棵树映射到真实环境中

虚拟DOM轻量,不比真实DOM有其他重要的属性,只需包含XML内部需要的

在react中,有JSX格式,可以直接使用XML的格式直接声明界面的DOM结构,并可以插入到真实DOM,并渲染到页面上。是因为JSX格式是React.createElement()的简化的语法糖,使用JSX格式书写,会被babel转换成React.createElement()的JS代码【也是一个虚拟DOM】。进而操作真实DOM

区别

  • 虚拟DOM不会进行排版与重绘操作,而真实DOM会频繁重排重绘

  • 虚拟DOM的总损耗是:虚拟DOM增删改的花费时间+真实DOM差异部分的增删改的花费时间+排版与重绘,真实DOM的总损耗是完全的增删改的花费时间+排版与重绘

优缺点

真实DOM

  • 优点:易用
  • 缺点:
    • 效率低,解析速度慢,内存占用过高
    • 性能差,频繁操作真实DOM,易于导致重绘与回流

虚拟DOM

  • 优点:
    • 简单方便,如果使用真实DOM,操作繁琐,难以维护
    • 性能方面,可以有效避免真实DOM的频繁刷新,减少多次引起的重绘与回流,一定程度上可以提高性能
    • 跨平台:React借助虚拟DOM带来了跨平台的能力,一套代码多端运行
  • 缺点:
    • 性能要求极高的应用中,虚拟DOM无法进行针对性的极致优化
    • 首次渲染大量DOM时,需要一层虚拟DOM计算,所以首次渲染会稍微慢一些

3 state和props的区别

组件的UI展示与数据直接相关,而数据的来源有两种,一种是内部的数据状态,另一种是外部数据

state

state用于管理内部数据状态,用于函数组件,一般在constructor中初始化

当我们需要通过一些事件来修改state的值的时候,我们使用setState来进行修改state,当state变化就会导致render重新调用,从而达到变化组件的效果

props

props是一个收集外部数据的对象,因为react的数据是单向数据流,所以会产生父组件传值给子组件的情况,父组件的值传进子组件,就会被子组件捕获并放到props中。当然,数据也包括函数与对象。

props在子组件是不可以改变的,只能通过父组件的传值来进行改变,否则子组件的props是不会改变的

相同点和不同点

相同点:

  • 两者都是js对象,并且都是保存信息的
  • props和state都可以触发更新

不同点:

  • props的数据是外部传入,而state的数据是内部constructor构造的
  • props在组件内部不能修改,而state可以修改
  • state是多变的,可以修改

4 setState的执行机制

工作流

在react中,如果一个动作有多次调用setState,那么react会将状态放入一个队列,最后将状态进行合并。如果是赋值操作,则取该属性最后赋值的值;如果是回调函数操作,那么每次的状态改变都需要进行额外计算

在同步任务内部,setState是异步的,但是当setState写在setTimeout内部时,就会有setState的同步现象。在这里,setTimeout帮助了setState脱离了react的管控,因此在内部变成了同步处理。

setState为何异步

react内部有一个对象:batchingStrategy,其内部的isBatchingUpdates属性直接决定了要进行更新流程,还是排队等待。当它的值为false的时候,表示不进行批量更新操作。每当react想要调用batchingStrategy.batchingUpdates方法的时候,会将isBatchingUpdates置为true,然后这个时候任何需要更新的组件都只能暂时进入队列里面等候批量更新

setState为何能脱离react管控

因为batchingStrategy.batchingUpdates方法,不仅仅会在setState中会被调用,与更新相关的地方也是会被调用的。React中为了保证组件上的事件触发的setState也能有效,React会在事件内部开启批量更新。

然而,因为setState在异步任务里面,将会导致事件内部的isBatchingUpdates的开启和关闭都会先执行,因为在此会先执行同步任务再执行异步任务,因此执行setTimeout的时候,isBatchingUpdates已经是false了,此时setTimeout内部的setState就相当于是脱离了isBatchingUpdates的管控,会随着每次调用都执行。

大致用法

在react中,state操控组件内部的状态,而setState就是用于修改state的值而存在的,起到了更新组件数据的作用

setState本身是一个异步的操作,因此如果需要获得更新后的state,我们需要再setState内部指定一个回调函数来进行

1
2
3
4
5
this.setState({
mes: '1'
}, () => {
console.log(this.state.mes) // 1 因为回调已经实时更新了
})

此外,setState在进行相同事件的密集的执行的时候,是有进行防抖的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
handleClick = () => {
this.setState({
count: this.state.count + 1,// 2
})
console.log(this.state.count) // 1

this.setState({
count: this.state.count + 1,// 2
})
console.log(this.state.count) // 1

this.setState({
count: this.state.count + 1, // 2
})
console.log(this.state.count) // 1
}

上面的操作只取了最后一次,但是如果我们不想要这样的机制,我们就必须得在setState传的不是数据,而是携带前一个state的回调函数,并且返回一个新对象

1
2
3
4
5
6
7
handleClick = () => {
this.setState((prev, pro) => {
return {
count: prev.count + 1
}
})
}

5 React的事件机制

React基于浏览器的事件机制自身实现了一套事件机制,包括事件注册,事件合成,事件冒泡,事件派发等。这套事件机制被称为合成事件

合成事件是React模拟原生DOM事件所有能力的一个事件对象,如果想获得原生DOM事件,可以通过e.nativeEvent属性获取

为什么需要合成事件?因为合成事件可以抹平浏览器之间导致的事件差异,对开发者暴露一个稳定的事件接口,从而不再需要关注事件的兼容性问题。

React事件并不是直接将事件直接绑定到真实的DOM上,而是绑定到结构的最外层,然后使用一个统一的事件去监听。这个事件监听器维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或者卸载的时候,只是在这个统一的事件监听器上面插入或者删除一些对象。当事件发生的时候,首先被这个统一的事件监听器处理,然后在映射里面找到真正的事件处理函数并调用。这样能简化事件处理和回收机制,性能大幅提高。

关于React事件的输出顺序:

  • React事件都挂载在document上面
  • 当真实DOM元素触发事件,会冒泡到document事件后,再来处理React事件
  • 所以会先执行原生事件,再执行React事件
  • 最后执行document上面挂载的事件

阻止合成事件的冒泡:e.stopPropagation

阻止合成事件与最外层document的冒泡:e.nativeEvent.stopImmediatePropagation

阻止合成事件与非最外层的原生事件的冒泡:通过判断e.target避免

6 React的事件绑定有哪些,区别

  • render中直接使用bind进行绑定:这样会每次都会在render之后重新绑定bind,影响性能

  • render方法中直接使用箭头函数:这样也会每次在render之后都要产生新方法,影响性能

  • 构造函数内部bind绑定:这样会在直接在第一次render之后也能无需重新加载使用之前的方法,但代码编写较为冗杂

  • 定义箭头函数之后再绑定:最优的绑定

7 React的构建组件的方式有哪些,区别

  • 函数式创建:没有hooks之前,被视为无状态组件,因为只能通过props的数据来渲染,而没有自己的状态

  • 继承React.Component创建:之前被视为有状态组件,内部有state管理组件的内部状态,状态变化会导致render函数重新执行渲染

  • React.createClass创建:React官方很久之前推荐的写法,是使用函数创建的方式,返回一个createElement的函数,但是过于冗杂

我们在选择组件的时候,最好是能使用无状态组件就使用无状态组件,有的话就通过hooks的函数式编程,因为class继承会让代码过重

8 React组件之间如何通信

React是通过props来进行通信的

父组件跟子组件之间通信,父组件会将数据直接放到子组件中,子组件会从props获得父组件的数据

子组件跟父组件之间通信,父组件会将一个函数放到子组件中,子组件会使用props获得的函数将子组件内部的数据通过该函数传递给父组件

兄弟组件之间通信,需要一个父组件来作为媒介来传递,子组件通过回调函数来传值给父组件,父组件再直接传给另一个兄弟组件

当然,复杂的组件关系,可以采用状态管理工具来实现,比如Redux

如果是单向数据流,但是是父组件向后代组件通讯的时候,可以使用Provider和Consumer组件来进行通信

9 React Hooks的理解

对我而言,hooks是为了让函数也能有状态。对于一个项目,初期对于一些组件的构想肯定是不会做状态改变,只会做渲染工作,通常选型都会使用函数组件,但是随着后面的业务变动,会导致该函数组件也需要状态,可是重新使用类组件的成本会很高,所以需要hooks,相当于给函数组件注入能够让自己有状态,而且有声明周期的功能,下面是对一些常用hooks的介绍

  • useCallback:它的原理是,当变量触发变化的时候,才会导致函数的重新构建,否则每一次都会重新使用该函数。我们一般配合memo优化子组件的渲染次数。

    render-props模式:父组件刷新,子组件全部都必须刷新一次。

    我们可以使用React.memo,用于避免一些组件因为父组件直接render导致不必要的刷新,但是只是对父传给子的props来进行浅比较判断,甚至如果是传入一个方法给子组件,都会导致每次的函数创建,就会导致每次的方法的存放地址不相等,进而导致了子组件的无意义render。

    useCallback一般用于render-props模式中,它让每次父组件刷新之后,方法都被保存下来,供子组件复用。使用了React.memo的子组件识别了props一致的时候,就可以避免子组件的无意义的render

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import { useState, useCallback, memo } from 'react';
    const Parent = () => {
    const [parentState, setParentState] = useState(0);
    const parentFunction = () => {
    console.log('parent');
    }

    const childFunction = () => {
    console.log('child');
    }

    const childFunctionCallback = useCallback(childFunction, []);

    return <>
    <button>parentState: {parentState}</button>
    <Child handleClick={childFunctionCallback}/>
    </>
    }

    const Child = memo(({handleClick}) => {
    console.log('child render');
    return <button onClick={handleClick}>child</button>
    })
  • useMemo:根据变量是否变化,以此来决定useMemo里面的值是否要重新返回一个新的值,这里其中包括了普通的值还有对象和数组这一类的值。还是因为万恶的render-props模式,父组件的强制全刷新导致的。有一些值,计算量大,每次render整个父组件之后还要重新计算一次大规模计算,这显然是不好的。因此,useMemo就是为了解决粗暴render,从而反复大规模计算的问题。让大规模计算的结果缓存,等到组件真的触发了依赖刷新,再来进行重新计算

  • useState:让函数组件也能拥有状态的钩子

  • useEffect:让函数组件也能拥有类似于生命周期的特性,比如能在组件初始加载的时候执行一些任务,也可以来做一些数据的监听,该数据改变,就可以触发一些事件的执行

react hooks的使用是有一点需要注意的,就是要规避闭包陷阱问题。

原理

在使用hooks的函数组件中,有两种状态,一种是mountState,另一种是updateState。其中,mountState是首次渲染,在这个阶段会构建hooks链表并渲染;updateState阶段会依次遍历链表并进行渲染

hooks的相关所有信息都放在一个hooks对象里面,而hooks对象会以单向链表的形式相互串联

hooks的渲染是按顺序遍历之前构建好的链表,取出对应的数据信息进行渲染。然后调用hooks也是只记住了对应的索引,根据索引来取出数据来进行渲染。如果把hooks放到逻辑判断中导致了链表读取顺序的差异,那将会导致渲染的结果是不可控的。

10 Redux的理解与工作原理

11 说说对React Router的理解

react-router和前端路由的原理大致是相同的,可以实现无刷新条件下切换显示不同的页面

路由的本质就是页面的URL发生改变的时候,页面的显示结果可以根据URL的变化而变化,但URL不会刷新

因此是可以通过前端路由来实现单页SPA引用

一些常见的组件来使用:

  • HashRouter和BrowerRouter:表明路由在浏览器中是history模式还是hash模式
  • Routes:当适配到第一个组件的时候,后面的组件将不再继续适配
  • Route:用于路径的匹配,然后进行组件的渲染,内部的属性有:path、还有element、exact等【exact表达的是精准匹配路由】
  • redirect:用于路由的重定向

12 React Router有几种模式?实现原理

  • hash模式:url有锚点。对应的是HashRouter
  • history模式:url没有锚点,允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录。对应的是BrowserRouter

实现原理

路由描述的是URL和UI之间的关系,这种映射是单向的,即URL的变化引起UI刷新

  • BrowserRouter使用的是H5的history API,不兼容IE9以下的版本
  • HashRouter使用的是URL的哈希值

BrowserRouter本身是一个类组件,在内部创建一个全局的history对象用于监听整个路由的变化,并将history作为props传递给react-router的Router组件

HashRouter只是一个容器,没有DOM结构,它渲染的就是子组件,并向下传递location,如果hash改变了,就会根据hash改变pathname的值

13 说说immutable的理解,如何应用到React项目中

immutable,指一旦创建,就不能被更改的数据。除非用immutable的特定方式,API来修改。

对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象

Immutable的部分修改,会避免全部数据的修改,它使用了结构共享,如果一个对象树只改变了一个部分,只修改这个节点和受它影响的父节点,可以避免大量的修改。

immutable优点

  • 降低可变性带来的代码复杂度,就算数据经过多次处理,我们也可以知道这个数据是在不通过immutable的API来做改动的话,也是不变的
  • 节省内存:immutable会尽量复用内存。当你对一个immutable对象进行浅拷贝的时候,你修改它的属性值,两个的属性值是不一样的,但是它们的immutable的内部其他的值是共享的。
  • 易于回退数据
  • 并发安全:数据天生不可变,不需要并发锁了
  • 拥抱函数式编程

应用到react项目中

  • 提供了简洁高效的判断数据变化的方法,可以用于判断前一个状态和后一个状态是否一致【毕竟有些值都共享了】,来判断是否render。这样就可以使用Immutable的迅捷的比对方法,无需做像基于深度一样的比较。比如:我们搭配React.memo,因为memo是基于props的浅比较来决定是否刷新的,但是有时候会导致两个props的对象仅仅只是引用值不同而内部数据相同却导致了粗暴的render,或者是引用值相同但是内部值不相同而触发了memo,虽然memo可以用深拷贝,但是深拷贝太浪费资源。但是使用了immutable之后,就可以很轻松的确定两个props是不是同一个props,进而决定是否render,可以节省大量的资源,提高性能
  • 易于回退数据,可以用于做添加撤销功能,非常方便
  • 也可以与redux配合使用,来做状态管理

原理

我的理解是immutable是基于一种数据结构,类似于32叉树,兄弟之间用一个数组存放,在结构发生变化的时候,就对变化的部分的相应路径上的所有节点重新生成,不变化的内容保持不变,重新创建。

位分区机制

首先会将对象的key进行hash转换,变成一个唯一的数字。然后用该数字的每一组位来对每一层进行依次搜索。在这里最好使用2 ** n 叉树,因为这样便于我们使用位运算来计算位数。比如,626转换为10 01 11 00 10,然后依次找每一层对应的数据

使用了位分区机制,使得对象属性的创建和删除变得更加简单,避免了整个值的重新拷贝。但是对于查找,从以前的O(1)变成了O(log 7)

内存优化

另外,32叉树如果不经过优化整理,肯定是不行的,因为内存很大,有两种压缩方法:树高压缩和内部节点压缩

树高压缩

不产生混淆的情况下,使用最少的二进制位去标识key

如果添加值,末位也是0,那么在这里再来进行开拓空间,增加或者减少节点就可以了

内部节点压缩

使用bitmap机制:去维护一个bitmap数字,比如:00010010,意味着只有下标1和4位置有数字,这样就只需要开辟一个长度为2的数组,可以节省内部节点的空间,计算bitmap的1的数量,再来开辟数组的长度

哈希冲突

使用HashCollisionNode节点去处理发生冲突的键值,然后将hash冲突的值放在entries数组里

14 说说render方法的原理,在什么时候被触发

render方法有两种,在类组件中,指的是render方法,在函数组件中,指的是函数本身

首先,会将JSX格式的虚拟DOM转化为createElement方法,再来转换为真实DOM。然后,在render的过程中,react将新调用的render函数返回的虚拟DOM树与旧版本进行diff算法比对,再来更新DOM树。

在类组件中,只要执行了setState方法,就一定会导致render,但是函数组件使用useState更改状态不一定导致重新render

组件的props改变了不一定触发render执行,但是props来自祖先的state时,这时候会因为state的变化导致重新render

所以,在此,一旦执行了setState就一定会执行render方法,但是执行了useState会根据值是否改变再来执行render方法。一旦父组件发生了渲染,子组件也会渲染

15 如何提高组件的渲染效率?如何避免不必要的render?

  • shouldComponentUpdate方法,来比对state和props,如果该函数返回true,则重新render,否则则不render
  • PureComponent方法:当组件更新的时候,如果组件的props和state都没有发生改变,那么则不会render,省去VDOM的生成和比对过程。不过PureComponent是基于浅比较的,所以还是会导致一些问题
    • PureComponent也有shouldComponentUpdate方法,会优先使用shouldComponentUpdate函数作为render依据,没有该生命周期时会使用PureComponent的内置方法来判断render
  • React.memo:只能用于函数组件,用于缓存组件的渲染,避免不必要的渲染。memo可以根据props的不同来判断是否进行render。但是它只是根据前后的props进行浅比较,如果是栈地址相同而堆内的数据不同,也会导致一些问题,或者是堆内数据相同而栈地址不同时,仍然粗暴render。因此往往会搭配immutable,或者直接传入第二个参数,该参数是一个返回值为bool值的函数,传入的参数为前后的props,比较的规则就是自己定的,这样也可以避免一些问题。(不过这样还要再来进行深比较一次,还是immutable会更好一些)
1
2
3
4
5
6
function arePropsEqual(prevProps, nextProps) {
// 自定义比较规则,返回bool值,来判断是否render
return prevProps === nextProps;
}x

export default memo(Button, arePropsEqual);

16 React diff的原理是什么

React引入了虚拟DOM的概念,虚拟DOM就是让React自己维护一个对象,这个对象能够跟真实DOM的元素能够抽象的一一对应。并且通过虚拟DOM的变化,来对真实DOM进行变化部分的重新渲染,而对其他部分不受影响

diff算法就是高效的比较新的虚拟DOM和旧的虚拟DOM,并且找出两者的变化之处

diff的策略前提

  • DOM节点跨层级的移动操作特别少,可以忽略不计
  • 拥有相同类的两个组件有相似结构,拥有不同类的没有相似树形结构
  • 对于同一层级的一组子节点,它们可以通过唯一的key来进行区分

React diff的原理

diff算法主要遵循三个层级的策略:

  • tree层级:React对于同层的节点进行比较,如果节点之间不同,直接将该部分和对应的子节点删除,重新构建。因为对不同层级节点的操作没有移动,只有直接删除,因此不要随意进行DOM节点跨层级移动的操作

  • component层级:如果是同一类型的组件,则直接继续按照原来的策略去比较虚拟DOM树,如果不是同一类型的组件,则直接删除该组件下所有子节点。对于同一类型的组件,虚拟DOM可能没有任何变化,我们不想让该组件进入diff算法中以节省时间,可以使用shouldComponentUpdate来判断组件是否需要diff

  • element层级:当节点处于同一层级,diff有移动,插入和删除三种节点操作。

    在这里,可以让开发者进行同级别的元素中添加唯一key。这样就可以通过diff差异化对比,可能发现仅调换位置的元素节点,一般而言,都是做向后调换的方法,所以尽量不要将最末端的元素向前提,否则会造成后面几乎所有元素的变动

注意

于简单的列表渲染,使用key反而开销变得更大,本来可以重写,但使用了key,就会考虑重排,增大开销

react diff是深度优先算法

跟vue一样,react也有diff算法,而元素key属性的作用是判断元素是新创建的还是被移动的元素,从而减少不必要的diff

因此key的值需要为每一个元素赋予一个确定的标识,如果组件重新加载,发现key相同的元素其他的也相同,那么在react中是不会被销毁和重新加载的,会被保留,减少变化的开销;如果找不到对应的完全相同的元素,将会大规模修改组件。

key较为适合列表元素频繁增删的适合会更加有用处,如果只是单纯修改内部列表元素,key反而不是一个很好的选择

此外,key不推荐使用index来赋予,因为如果是数组的头插的话,会导致所有数组的index变化,反而对react的性能优化会更加不利

key也要避免用随机数来使用,不然也是不利于性能优化

设置key,是为了不通过diff算法,让组件有一个更加简单的更新真实DOM的机制

17 说说对Fiber架构的理解,解决了什么问题

JS引擎和页面渲染引擎两个线程是互斥的,其中一个执行的时候,另一个往往需要等待

如果JS线程长时间占用主线程,导致页面渲染不及时,用户容易感受卡顿。当组件较大的时候,JS线程一直执行,等到整颗虚拟DOM计算完成才会开始渲染,那么容易有卡顿的情况

什么是Fiber

从架构角度看,Fiber是对React核心算法的重写

从编码角度看,Fiber是React内部定义的一种数据结构,它是Fiber树结构的节点单位,也就是React16新架构下的虚拟DOM

从工作流的角度来看,Fiber保存了组件需要更新的状态和副作用,一个Fiber对应了一个工作单元

它是react核心算法的一次重新实现

  • 为每个任务增加了优先级,优先级高的能中断优先级低的,然后再重新执行优先级低的任务
  • 增加了异步任务,调用requestIdleCallback api,浏览器空闲时执行
  • dom diff树变成了fiber链表,中断执行的部分是diff阶段,commit阶段渲染真实DOM是规定不可以中断执行fiber的。fiber上有保存中断effect状态的组件,可供用户保存中断前后的状态

解决了什么问题

fiber把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有,则将继续执行下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候再继续执行,使得加载大组件时消除用户感觉卡顿的现象

18 说说JSX转换为真实DOM的过程

  • 首先,babel会将JSX格式的虚拟DOM转换为React.createElement这种形式,React.createElement将会返回一个ReactElement的JS对象
  • createElement函数会先对key、ref、self、source四个属性值二次处理,然后再遍历config,筛选可以提入props的属性,然后再提取子元素放入childArray数组,最后格式化defaultProps,返回一个ReactElement对象
  • ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转化为真实DOM

19 React性能优化的手段有哪些

  • React.memo:用于props仅仅只是栈地址的内容不相符而导致的粗暴render
  • 避免使用内联函数:如果直接使用内联函数,这样会存在render时,会重新创建这个函数,这样对性能是无益的。因此需要在组件内部创建这个函数,再将事件绑定这个函数,这样就不会因为render导致的函数的重新构建
    • 当然的,也避免使用非箭头函数的创建,因为这样,你在render中使用时,需要进行bind函数进行换绑操作,是影响性能的,因为每次都需要进行一次bind操作
  • 可以使用React.Fragments来避免额外标记
  • 使用Immutable,可以让很多数据共享某些堆地址的数据,进而减少数据大幅度的改动,从而达到减少数据渲染的目的
  • 懒加载:在前端页面运行时再来进行动态加载
  • 服务端渲染,也可以更快看到渲染页面。不过这里需要将自己编写的react组件给重新读取成html

20 说说React的服务端渲染怎么做,原理是什么

如果要使用React做服务端渲染,那么有两种方案:

  • 使用next.js
  • 自己手动搭建一个SSR框架

使用React做后端渲染,我们需要在webpack来配置babel-loader使得服务端能解析JSX语法,然后使用renderToString方法转换解析成html,最后拼接进模板字符串发到浏览器。最后,浏览器开始渲染和节点对比,然后执行完成组件内事件绑定和一些交互浏览器重用了后端输出的html节点。

21 说说你在使用React过程中遇到的常见问题,如何解决

使用react-hooks的时候,常常会有闭包陷阱的问题,在useEffect中进行useState的状态更改,不会在该useEffect立刻获得更改后的反馈,在对于一些特定需求的时候会产生一些问题。但是也是可以解决的,通常使用多一个useEffect来跟踪这个状态,进而控制。【这个方法继续衍生下去,就是自定义该状态的hooks】

22 react中,key有什么作用

跟vue一样,react也有diff算法,而元素key属性的作用是判断元素是新创建的还是被移动的元素,从而减少不必要的diff

因此key的值需要为每一个元素赋予一个确定的标识,如果组件重新加载,发现key相同的元素其他的也相同,那么在react中是不会被销毁和重新加载的,会被保留,减少变化的开销;如果找不到对应的完全相同的元素,将会大规模修改组件。

key较为适合列表元素频繁增删的适合会更加有用处,如果只是单纯修改内部列表元素,key反而不是一个很好的选择

此外,key不推荐使用index来赋予,因为如果是数组的头插的话,会导致所有数组的index变化,反而对react的性能优化会更加不利

key也要避免用随机数来使用,不然也是不利于性能优化

设置key,是为了不通过diff算法,让组件有一个更加简单的更新真实DOM的机制

23 说说refs的理解,应用场景

refs是一种供我们访问DOM节点或者render中的react元素的一种方式。

创建的方法有以下几种:

  • 传入字符串:根据this.refs传入的字符串名称获取该元素
  • 传入对象:通过React.createRef创建,可以通过this.refs.current获取元素
  • 传入函数:通过React.createRef创建,可以通过this.refs获取元素
  • 使用hooks:使用useRef获取元素

不过呢,平时开发中最好不要使用refs去更新组件【虽然会有一些极端情况会使用refs】,因为用这种方式会违反组件封装的原则

24 类组件与函数组件的理解与区别

  • 类组件:通过使用ES6类的编写形式去编写组件
  • 函数组件:通过函数编写的形式去实现一个React组件

区别:

  • 类组件和函数组件的编写形式不同,类组件的JSX格式的组件写在render中,而函数组件写在自己的函数里面
  • 状态管理:类组件使用的是setState,而函数组件使用的是useState的hooks
  • 生命周期:类组件有着不同的生命周期,而函数组件全部都依赖着useEffect和钩子触发的变量来模拟
  • 调用方式:如果是类组件,就需要实例化,并且调用对象的render方法,而函数组件就可以直接调用本函数
  • 获取渲染的值:类组件中,需要大量的this指向来判断【因为this是可变的,因此可能每次props都是不相同的】,而函数组件不需要

25 高阶组件的理解与应用场景

26 react引入css的方式

27 受控组件和非受控组件

在传统的html中,表单元素的标签input等的值通常是根据用户的输入来进行更新的

受控组件

在react中,可变状态都放在state中来管理,并且只能使用setState来进行更新,而呈现表单的react组件,有人为输入的时候,react会控制表单元素进而改变其值的方式,称为受控组件。受控组件依赖于react的状态,更新依赖于react的事件处理,并通过state来实时反应元素的情况

非受控组件

指表单数据由DOM本身来处理,就是不受setState的限制,与传统的html表单输入相似,input的值为显示最新值

在非受控组件里面,通常用ref来获取里面的值。非受控组件通常是自己内部维护了这个状态,这样用户输入任何值都会反应到元素上面

区别

受控组件 非受控组件
依赖于状态 不受状态控制
数据都在state进行管理,修改值是通过state的修改来映射 获取数据相当于操作DOM,从ref中获取
只有继承React.Component或者使用hooks才可以使用受控组件 更容易与第三方结合,更容易集成React和非React部分的代码
一般用于有初始值时的情况 一般用于没有初始值的情况

28 类组件的生命周期

组件的生命周期分为三个阶段:挂载,更新,卸载

挂载阶段

  • constructor:初始化props,初始化state,也可以用来绑定事件
  • shouldComponentUpdate:组件是否更新钩子,一般返回值为true或者false
  • render:用于展示渲染
  • componentDidMount:组件挂载完毕的钩子,用于挂载完毕执行某些项

更新阶段

  • shouldComponentUpdate:判断是否要进行更新

  • componentDidUpdate:组件更新完毕的钩子,用于更新时执行某些项

卸载阶段

  • componentWillUnmount:在组件即将被移除或者销毁后执行,一般用于清理定时器、取消网络请求等事件

执行顺序

部分钩子被废弃的原因

componentWillMountcomponentWillReceivePropscomponentWillUpdate

因为出现了react Fiber,Fiber会对任务进行中断处理,这个过程暂停之后继续执行,所以挂载和更新之前的生命周期钩子就有可能不执行或者多次执行

29 首页白屏优化如何解决

  • 服务端渲染:直接将处理好的HTML发送给前端来展示,但是有不好的地方就是可能会给服务器造成一定的压力,还有前后端的耦合性会较高
  • 预渲染:使用一些预渲染插件,比如prerender-spa-plugin,在编译阶段就将对应好的路由编译好插入到App节点,这样就能在js文件解析过程中有一些内容展示
  • 对于一些静态资源本地存储,或者图片压缩
  • 路由懒加载:使用lazy()的API

30 react16和react17的区别

新的JSX转换

在react16中,babel-loader会将JSX预编译成React.createElement

而在react17中,JSX转换不会转换成React.createElement,而是自动从React的package中引入新的函数调用

这次改动也不会改变新的JSX语法

事件代理进行修改

在react16中,事件代理不会冒泡到html标签,也就是不会将事件传播到整个文档级别。而是将事件传播到React树对应的根容器

31 闭包陷阱

闭包陷阱的本质,就是每次render之后,都会有自己的事件处理和Effect钩子。也就是每次render之后,事件和钩子都分别隔离了,拿到的都只是当前隔离的值

解决的方法:

  • 使用useRef,使得每次render之后的状态都不相互隔离
  • 使用immutable,让状态的值进行复用

网络

1 HTTP和HTTPS的区别

虽然HTTP和HTTPS之间只差了SSL/TLS,但是HTTPS的通信安全得到了极大保障

  • HTTP:超文本传输协议,特点如下:
    • 支持:客户端 - 服务器模式
    • 简单快速:客户端请求服务,只需传送请求方法和路径。由于HTTP协议较为简单,使得HTTP服务器的程序规模小,通信速度快
    • 灵活:HTTP允许传输任意类型的数据对象
    • 无连接:HTTP在传输一个请求,并且发送一个应答的时候,就会断开连接。但是这样会有无谓的TCP连接的建立断开,有额外开销,也有增加过多的往返时延。
    • 无状态:HTTP协议不会根据之前的状态进行本次的请求处理
  • HTTPS:是以HTTP作为基础,使用TLS/SSL来在传输中间的数据进行加密

SSL的实现这些功能主要依赖以下手段:

  • 对称加密:加密和解密的密钥是同一个。只要保证密钥的安全,就能保证通信过程的机密性
  • 非对称加密:有公钥和私钥,公钥是公开的,私钥是私密的,公钥加密可以用私钥解密,私钥加密可以用公钥解密
  • 混合加密:HTTPS通信过程中,采用对称加密和非对称加密,公钥私钥对对称密钥进行加解密。但是黑客可以伪造身份发布公钥,如果你获得了假的公钥,你的数据仍然是不安全的。因此,数据加密的基础上,还需要完整性和身份验证的特性来实现真正的安全

​ 这样就相当于你跟黑客通信了,数据不安全了

  • 摘要算法【保证完整性】:将数据压缩成固定长度并独一无二的字符串,放进明文中加密;接收方解密之后拿到明文和字符串,再将收到的明文和字符串比对,如果相同,则说明传输数据是没被修改的
  • 数字签名【身份验证】:数字签名能够确认消息确实是发送方发出来的,别人仿造不了发送方的签名。签名和公钥一样,任何人都可以获取,但是签名只有用私钥对应的公钥才可以解开。验证成功之后就可以拿到摘要了。

​ 数字签名一般用于验证服务端是安全的,服务端发送给客户端的时候就使用数字签名,让客户端鉴别发送方

​ 一般,将明文压缩成固定长度的字符串,并私钥加密,就是数字签名了

  • CA验证机构:防止黑客发布假的公钥,就使用CA验证。CA本质是非对称加密。

    私钥在CA,公钥在浏览器中。通信时向CA申请证书【公钥和数字签名的绑定体】,通信方进行通信的时候直接把这份证书传给接收方,这样接收方会认证证书上的数字签名了,认证成功了之后,就能表明通信方身份

    建立连接

2 TCP与UDP的区别,视频连接采用什么协议

TCP是一个面向连接的、可靠的、基于字节流的传输层协议

UDP是一个面向无连接的传输层协议

和UDP相比,TCP有着三大特性:

  • 面向连接:通信的时候需要三次握手简历连接,而UDP没有
  • 可靠性:TCP有超时重传,流量控制和拥塞控制机制,比UDP发送数据更加可靠
  • 面向字节流:UDP的数据传输是基于数据报的,而TCP为了维护状态,将每一个IP包都变成了字节流,便于拥塞控制

视频采用的协议两者都有,常规看视频的时候,是使用TCP,因为允许等待,如果是视频通话,使用UDP。使用webSocket的话,就是采用TCP协议了。

3 为什么HTTP采用TCP协议

有用TCP的,也有用UDP的。HTTP 3.0就是基于UDP的协议

4 输入URL后敲回车发生的事情

URL解析

首先判断你的输入是一个合法的URL还是一个搜索关键字,并且根据你的输入内容进行对应操作。

目标是提取出域名,供DNS解析

DNS解析

在这里,浏览器将域名发给本地域名服务器,如果本地服务器在本地的高速缓存没有查询到,那么就会到根域名服务器查找。

接着,再不断往根节点不断向下往下级服务器来查找域名,最后查询到该域名,那么本地就会获取到域名对应的IP地址,并且写入高速缓存中,再将IP地址给浏览器。

如果在高速缓存找到域名对应的IP地址,那么就直接将IP地址给浏览器

DNS的目标就是获得IP地址,以建立TCP连接

发起TCP连接

当我们DNS解析完毕,获取到了端口,就可以进行TCP连接了。

对于TCP连接有三次握手,三次握手成功之后,双方就建立连接了。

对于TCP的连接拆除,有四次挥手。

  • 三次握手:

    • 第一次:客户端发送syn报文,并发送需要x 【服务端知道,客户端的发送能力和服务端的接收能力正常】
    • 第二次:服务端发送syn + ack报文,并设置发送序号为y,确认序号为x + 1 【客户端知道,服务端的接收发送正常,客户端的接收发送正常】
    • 第三次:客户端发送ack报文,并发送序号z,确认序号为y + 1 【服务端知道,客户端的接收能力正常,服务端自己的发送能力正常】

    采用三次握手,是为了防止已经失效的连接报文重新传给服务器因而产生错误。

    为什么不能两次握手?:如果只两次握手,那么服务端是不可能知道客户端是否已经接收到自己的信号,一旦该信息丢了,两边的序列号,还有连接状态可能就不一致

    四次握手可以吗?:理论上可以。三次握手是在不可靠信道进行可靠传输的最小确认数。但是4次确认,虽然也能建立可靠传输,这样会造成对网络资源的额外开销,是不推荐的。

发送HTTP请求

发送HTTP协议的过程就是将HTTP的请求报文通过TCP协议发送到服务器指定端口。

不过在这里,我觉得,应该先去请求静态资源。【也就是页面资源】

服务器处理响应请求并返回HTTP报文

服务器会对TCP连接进行处理,并对收到的HTTP协议进行解析,并且按照报文格式进一步封装成HTTP Request对象供上层使用。

浏览器解析渲染页面

浏览器获取返回的HTTP报文资源之后,首先对资源进行解析。

  • 查看是否需要重定向,存储cookie,缓存资源,还有解析方式

对浏览器进行页面的渲染。

  • 解析HTML构建DOM树
  • 解析CSS生成CSS规则树
  • 合并DOM树和规则树,生成render树
  • 布局render树,计算元素的尺寸和位置
  • 绘制render树,绘制页面的像素信息,比如颜色
  • 浏览器将信息发送GPU,GPU解析信息,显示在屏幕上

5 HTTP 1.0 - HTTP 1.1

  • HTTP 1.0的问题:

    • 每次传输数据完成就要做一次连接拆除,不仅会有链接消耗,而且还会消耗过多的RTT,造成资源浪费
    • 队头阻塞:由于HTTP 1.0规定了下一个请求必须在前一个请求的响应收到了才可以发出,如果前一个请求的响应一直没有到达,那么就会造成下一个请求无法发送的问题,这样就会产生阻塞
  • HTTP 1.1

    • 在HTTP 1.0的基础上,传输数据完成不进行连接拆除,保持长连接。【通过增加Keep-Alive属性】如果客户端想要关闭HTTP连接,只需要在请求头携带Connection: close,服务器将会关闭连接

    • HTTP 1.1为了解决队头阻塞问题,弄了管道化运输。也就是可以不必等待上一个请求回来,下一个请求就可以发出,只不过是接收响应的时候,要按照请求的顺序。虽然它解决了队头阻塞造成的时间开销较大的问题,但是它还是无法解决队头阻塞的问题。因为服务器仍然还是要按照顺序逐个响应送回,也不允许存在两个并行的响应。而且很多浏览器并不支持管道化运输。

      后面,浏览器厂商采取了允许多个TCP会话,实现了真正的并行

    • HTTP 1.1加入了缓存处理,比如强缓存和协商缓存,添加了新字段:cache-control

    • 支持断点传输

    • 增加了Host字段(使得一个服务器能用来创建多个Web站点)

6 HTTP 2.0

二进制分帧

HTTP 2.0 的出现是为了在1.x的基础上,改进传输性能,实现低延迟和高吞吐量

HTTP 2.0通过在应用层和传输层之间增加了一个二进制分层帧,在二进制分层帧上,HTTP 2.0将传输信息分成更小的帧,然后采用二进制格式编码,原HTTP 1.x的头部封装到Header帧,原HTTP 1.x的body封装到了Data帧中。突破了HTTP 1.1的性能限制,改进了传输性能

因为是分帧传输,服务器接收的帧都是乱序的,然后再自行组合起来,这样的好处就是避免了HTTP队头阻塞问题。

什么是帧

HTTP 2.0通信的最小单位,所有的帧都共享着一个8字节的头部,其中包含帧的长度、类型、标志位和保留位,并且有标识当前帧所属的流的标识符

什么是消息

比帧大的通信单位,是指逻辑上的http消息,比如请求和响应,至少由一个帧构成

什么是流

比消息大的通信单位。是TCP连接中的一个虚拟通道,可以承载双向的消息。每个流都有一个唯一的整数标识符

HTTP 2.0中所有加强性能的核心是二进制传输,在HTTP 1.x中,是通过文本的方式传输数据,有很多缺陷,而且还要考虑文本文件的兼容性问题,而二进制传输只有01,健壮性很好。

多路复用

所有的HTTP 2.0通信都在一个TCP链接上完成,可以承载任意流量的双向数据流。

每个数据流以消息的形式发送,而消息由一个或多个帧组成。这些帧可以乱序发送,然后再根据每个帧头部的流标识符重新封装

多路复用可能会导致关键字被阻塞,HTTP 2.0里每个数据流都可以设置优先级和依赖,优先级高的数据可以优先处理优先返回客户端,数据流还可以依赖其他的子数据流。

HTTP 2.0实现了真正的并行传输,它能够在一个TCP上进行任意数量的HTTP请求

头部压缩

在HTTP 1.X中,头部元数据都是纯文本的形式发送的,通常会给每个请求增加很多字节的负荷。比如cookie,默认情况下都会将cookie放到请求头中发送给服务器。HTTP 2.0中,通讯双方维护一个动态表和一个静态表。然后发送请求的时候只把请求头变化的部分加入到header帧中,然后响应请求时再来解压请求头,双方更新维护两个表,将新添加的部分添加到动态表里面。

服务器推送

除了服务器响应,服务器还可以向客户端额外推送资源,而无需客户端的明确需求。也就是说,可以一个请求多个响应。

缺点

只解决了HTTP的队头阻塞问题,但是没有解决TCP的队头阻塞问题。如果某一个数据包没有按照顺序到达,那么接收端会一直等待这个数据包返回,这样会阻塞后续请求,发生了TCP的队头阻塞

7 HTTP 3.0之前HTTP的缺点

HTTP 1.0

  • 每次传输数据要做连接拆除,消耗RTT
  • 收到服务器的响应,才能继续发送请求,容易造成队头阻塞

HTTP 1.1

  • Connection: keep-alive,用于长连接,使其不会立刻做连接拆除

  • 做了管道运输,可以不必等上一个请求的响应才能发请求,可以连续发送请求,但是响应必须有序。而且管道通信并没有做到真正的并行响应,也没有真正解决队头阻塞问题

  • 后面也做了域名分片,使得这些二级域名都可以指向同样的服务器,然后能发送的请求更多了,也比较好解决队头阻塞的问题

  • 需要检测服务器发送的消息的时候,只有通过轮询请求,会造成严重的性能问题

  • 请求报文和响应报文头部信息冗余量大,数据未压缩,传输量也较大

HTTP 2.0

  • 使用了二进制分帧来传输数据,数据包可以无序传输,但最后会依序拼接,解决了HTTP的队头阻塞问题。但是HTTP 1.1可以使用多个TCP,而2.0只有一个TCP,当数据包丢包,那么就会造成服务器一直在等待,就导致了TCP的队头阻塞。而且因为底层是TCP,如果丢包,会导致超时重传,一定程度上影响效率

HTTP 3.0

后面为了优化,就更改了HTTP的底层协议为UDP

  • 连接上数据包丢失,可以直接只发丢失的包即可,不需要重传。

  • 在移动端表现上,3.0更好,因为3.0是基于ID识别链接。网络环境如何变化,只要ID不变,都能迅速连接

  • 加密程度也比2.0及之前的版本好

  • 3.0有一个非常独特的特性,为【向前纠错机制】,每个数据包除了本身的内容之外还包括了其他数据包的数据,因此少量丢包可以通过其他包的冗余数据直接组装无需重传【也就是可以通过其他包的数据来计算出丢失的包的数据】

    向前纠错机制牺牲了每个数据包发送数据的上限,但是带来的提升高于丢包带来的超时重传

    当然,如果丢失多个包,就不好找了

基于QUIC

8 get和post的区别

直观的,就是语义上的区别

从缓存的角度,get请求会被浏览器主动缓存下来,留下历史记录,而post默认不会

从参数的角度,如果get使用一般情况,那么他的数据是直接暴露在url中的,并且浏览器是有url的长度限制的,因此只能在一般使用情况发送小批量数据,但是post放在请求体中【虽然也可以暴露在url】,一般用于放敏感信息

为什么浏览器url有长度限制?因为url需要浏览器解析,那么需要分配一块有限的连续内存。如果url过长并发又过高的时候,服务器很容易报错503【服务器过载,无法处理请求】

从幂等性的角度,get是幂等的,而post不是幂等的

安全性:两者都是不安全的,只要使用http请求,就是明文传输,就没有所谓谁更安全一说

网上的两个tcp包,这个是部分框架的特定行为,已经经过证实,在chrome中是没有这么一说的

9 说一下计网的层次和相关协议

OSI七层模型分别有:应用层,表示层,会话层,传输层,网络层,数据链路层和物理层

应用层

用于通过应用程序之间的交互来完成特定的网络应用

应用层协议定义了应用进程之间的交互规则,通过不同的应用层来提供不同的服务。比如HTTP,DNS和SMTP等

表示层

会话层

传输层

网络层

数据链路层

物理层

10 说一下Cookie相关的字段属性

什么是cookie

  • cookie是客户端的一种解决请求无状态问题的方案,它是服务器发送到web浏览器上面的一小块数据。大小一般限制在4kb
  • cookie是一个在服务器和客户端之间来回传送文本的内置机制。当服务器接收到客户端发出的HTTP请求时,服务器可以发送带有响应的Set-Cookie标头,然后将cookie与HTTP请求头一起发送请求
  • cookie安全性较低,一般需要加密。用JS操作cookie也较为复杂。并且每次请求总会发送cookie,会加重服务器处理的负担。
  • cookie的创建:document.cookie=”key=value”

cookie的相关字段属性

  • Name和Value:键值对,Name为cookie名称,Value为cookie值
  • Domain:决定了cookie在哪个域是有效的
  • path:是cookie的有效路径,决定了cookie在一个域里的哪个路径是有效的 【子能访问父】
  • Expires / Max-Age:两个均为cookie的有效期,Expire是该Cookie被删除时的时间戳,格式为GMT。若设置成以前的时间,则该Cookie被立刻删除。而Max-Age是Cookie的有效期,表示几秒之后就立刻失效
  • Size:Cookie的大小。在所有浏览器中,任何cookie大小超过限制都会被忽略,且永远不会被设置。
  • HttpOnly:如果为true,则不允许通过脚本document.cookie去更改这个值,这个值在document.cookie中也不可见,但请求时依旧会发送。一般用于防止XSS攻击。
  • Secure:为Cookie的安全属性,若设置为true,则浏览器只会在HTTPS和SSL等安全协议中传输Cookie,不会在不安全的HTTP协议传输Cookie
  • SameSite:用来限制第三方cookie,从而减少安全风险。
    • Strict:完全禁止第三方获取Cookie,跨站点时任何情况下都不会发送Cookie
    • Lax:规则放宽,不完全禁止第三方获取Cookie,但是导航到目标网址的GET请求除外
    • None:关闭SameSite属性,但是必须同时设置Secure属性,否则是无效的
  • Priority:定义了三种优先级,当cookie数量超出的时候,低优先级的cookie就会被清除。在Firefox中没有该属性

11 状态码返回的是200的情况下,来源一定是服务端吗

直接从本地副本比对读取,不去请求服务器,返回的状态码是 200

12 一个js文件怎么设置HTTP缓存

13 HTTP状态码

状态码 效果
200 请求被正常处理
204 请求已成功处理,但在返回的响应报文不含实体的主体部分
206 服务端进行了范围请求
301 永久重定向
302 请求的资源已经分配到了新的URI,临时移动
303 表示由于请求对应的资源存在着另一个URI,必须要用GET请求获取
304 表示客户端发送附带条件的请求,服务器允许访问资源但是附带条件不满足【一般用于协商缓存命中】
307 和302有相同含义,但是不会从POST变成GET
400 请求报文中存在语法错误
403 资源不允许被访问
404 服务器上没有该资源
405 请求方法出错
500 服务器故障
503 服务器过载,无法处理请求

14 HTTP缓存

HTTP缓存机制

HTTP缓存机制是根据HTTP报文的缓存标识进行的。HTTP缓存分为强缓存和协商缓存。优先级最高的是强缓存,在命中强缓存失败的情况下才会进行协商缓存。

强缓存

强缓存是利用http中的Expires和Cache-Control两个字段来控制的。强缓存中,浏览器根据这两个字段来判断是否命中了强缓存,如果命中了该强缓存,则直接从缓存中获取数据,不会与服务端发生通信。命中强缓存的情况下,返回的HTTP状态为200

Expires是一个事件戳,代表一个缓存的过期时间。发起请求的时候,就会用Expires的时间戳和本地时间做校验,如果还没到过期时间,则直接从缓存中,绕过服务器获取资源。

但是这个字段,对时间的一致性太高了,而且,客户端和服务端传输数据是有时延的,因此这个方法有时候会因为时间而出一些问题,因此会使用Cache-Control来做强缓存【Cache-Control的优先级比Expires高】

Cache-Control的值:

  • public:响应可以被任何对象缓存

  • private:响应只会被客户端缓存

  • no-cache:跳过强缓存,直接进入协商缓存

  • no-store:直接禁用缓存

  • max-age=:设置缓存存储的最大周期,到达该最大周期时,缓存就会被认为过期

  • s-maxage=:该属性会覆盖max-age=和Expires。如果s-maxage会过期,则向代理服务器请求缓存内容

虽然Cache-Control强大,但是如果想要向下兼容,还是需要Expires属性

协商缓存

协商缓存,也叫对比缓存。协商缓存的机制下,浏览器需要向服务器去询问缓存的相关信息,进而判断是否需要重新发请求下载完整响应,还是使用本地缓存。

如果服务端返回的响应提示资源未改动,资源就会被重定向到浏览器缓存中,状态码为304

在上图我们可以明白,协商缓存是必须依赖于服务端和客户端之间的通信。

协商缓存的表示在响应头中返回给浏览器,其中有:Last-Modified和Etag

  • Last-Modified & If-Modified-Since【在这里我表示成LM和IMS】:

    • LM表示资源的最后修改时间,是一个时间戳,如果使用协商缓存,它会在首次请求的时候随着响应头部返回;
    • IMS是一个请求的头部字段,并且只能用于GET和HEAD中。【1.0的HTTP嘛】请求头包含这个字段,后面跟着在缓存中获取资源的最后修改时间
    • 根据LM和IMS,两者已经根据两个字段确定了是否获取缓存数据的标识了。如果两个字段在客户端判断是相等的,则请求服务器发送缓存,否则将发送数据和缓存规则,让服务器写入缓存,以用于下一次协商缓存
    • 缺点:
      • 资源更新频度过高,会导致缓存不能使用,因为IMS只能检查到以秒为最小单位的时间差
      • 如果文件通过服务器动态生成,但是文件没有变化时,尽管文件没有变化,但是还是因为更新了导致IMS的更新,就起不到缓存作用
      • 编辑文件,文件内容不变,但导致了LM的改变,也会导致起不到缓存作用,不该重新请求的时候也被请求了
  • Etag & If-None-Match:是为了解决上面无法正确感知文件变化导致的不缓存,无意义请求的问题

    • Etag是一串哈希字符串,标识资源的状态,服务端生成,文件的改变会导致Etag的改变
    • INM是一个条件式的请求头,如果INM和上一次响应的Etag相同,则表示资源没有变化,进入协商缓存。反之,不同的话服务端就要携带响应,并且可能将Etag修改
    • Etag的生成是需要服务器做额外开销的,会一定程度上影响服务器性能。而且,Etag不能完全替代LM和IMS,只会作为LM和IMS捕获文件状态的加强。因此往往Etag优先级会更高

15 webSocket的原理

webSocket是一种网络传输协议,位于OSI模型的应用层。可在单个TCP连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通讯

客户端和服务器只需要完成一次握手,两者之间就可以创建长连接,并且双向进行数据传输

webSocket服务器与客户端通过握手连接,连接成功后,两者都能主动向对方发送或者接受数据

在没有出现webSocket的时候,就是使用HTTP请求,双方轮询传输接收数据,以近似达到实时通信的效果,但这样会耗费大量带宽和CPU资源

特点:

  • 全双工:通信允许数据在两个方向同时传输,基本上双方发送数据是瞬时同步的
  • 二进制帧:采用二进制帧结构,语法语义与HTTP完全不兼容。它更加侧重实时通信。
  • 协议端口:80/443,几乎和HTTP/HTTPS一致
  • 握手:比起HTTP,webSocket需要一次握手进行双方校验,再来进行数据收发
  • cookie是客户端的一种解决请求无状态问题的方案,它是服务器发送到web浏览器上面的一小块数据。大小一般限制在4kb
  • cookie是一个在服务器和客户端之间来回传送文本的内置机制。当服务器接收到客户端发出的HTTP请求时,服务器可以发送带有响应的Set-Cookie标头,然后将cookie与HTTP请求头一起发送请求
  • cookie安全性较低,一般需要加密。用JS操作cookie也较为复杂。并且每次请求总会发送cookie,会加重服务器处理的负担。
  • cookie的创建:document.cookie=”key=value”

session

  • 浏览器第一次访问服务器时,服务器就会返回一个Session对象,这个对象有唯一的ID,也就是sessionID,服务器将sessionID用cookie的方式送到浏览器

  • 浏览器再次访问服务器的时候,会将sessionID发送过来,服务器依照sessionID就可以找到对应的session对象

  • 但是session有一个缺点,当用户过多时,就会给服务器造成过多负担。如果负载均衡,那么就要备份大量的session给其他服务器,服务器的资源开销无故增加

localStorage

它允许访问一个Document的对象storage,存储的数据将永久保存到浏览器会话中,除非用户手动删除

sessionStorage

sessionStorage属性允许访问一个对应当前源的sessionStorage对象。与localStorage相似,但是它有过期会话设置,一单关闭会话或者会话过期,该数据将会自行删除

localStorage和sessionStorage都是用来存储客户端临时信息的对象,并且只能存储字符串对象,能存储的大小为5MB。

不同的浏览器是不会共享Storage的信息的。相同浏览器的不同页面共享localStorage,但不会共享sessionStorage

localStorage的生命周期是永久,除非用户手动清除,而sessionStorage的生命周期为当前窗口,一旦关闭,就销毁sessionStorage

17 跨域

跨域我目前只了解两种方式:jsonp和CORS方式:

为什么要跨域

在这里,浏览器有一个策略,叫同源策略。同样的协议域名端口才可以相互传输数据。如果不同源来请求数据,不做任何防范,就容易导致CSRF攻击。

CSRF

跨站请求伪造,攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击的网站发送跨域请求。利用受害者在被攻击网站已经获取的注册凭证,比如session或者cookie,就可以绕过后台验证,达到冒充用户的目的,用用户的信息对被攻击网站执行某项操作。所以后面为了防范CSRF攻击,就不允许非同源的网站获取信息。但是这样也会造成很多麻烦,所以规定了CORS协议,以便于良性的跨域请求

JSONP

利用了一些能够跨域请求的标签,比如img和script等,我们只需要规定好服务端的参数,就可以利用JSONP来进行请求

CORS

现在的跨域都是基于CORS的标准来进行的,它允许浏览器向服务器发送XMLHttpRequest请求,从而克服Ajax只能同源使用的限制

CORS需要浏览器和服务器同时支持,浏览器一旦发现跨域请求,就会添加一些附加的头的信息

CORS不支持较老旧版本的浏览器,比如IE不能低于10版本

浏览器将CORS请求分为两类:简单请求和非简单请求

  • 简单请求:GET,POST,HEAD。对于简单请求来说,浏览器发送CORS请求,就是在头部信息中添加一个origin字段,表明自己的身份,然后交给服务器判断是否可以获得服务器返回的数据。一般在服务器使用:Access-Control-Allow-Origin来规定允许跨域访问服务器的URI。也就是说,简单请求将识别身份和传输数据都合在了一起。

  • 非简单请求,就是除去上述请求的其他请求。比如PUT方式。非简单请求的CORS请求会在正式通信之前会进行一次预检,也就是OPTION。浏览器会先询问服务器,当前网页所在的域名是否可以请求服务器,以及可以使用HTTP的信息,只有获得正确的回复,才能进行下一步的请求。在服务器收到预检请求,会先检查Origin,Access-Control-Request-Method和Access-Control-Request-Headers字段之后,确认允许跨域请求。如果浏览器否定预检,那么会返回一个正常的HTTP响应,但是没有任何CORS的头的相关信息,之后浏览器认定服务器不允许此次访问;当预检请求通过了,那么服务器发出正常的HTTP请求

Nginx

Nginx相当于一个跳板,做了个反向代理,将服务器收到的请求转发给代理服务器,这样可以使得让客户端和服务器满足同源策略

18 TCP的三次握手和四次挥手

什么是TCP

  • TCP是一种传输层的协议,它提供一种面向连接的、可靠的字节流服务。
  • TCP使用了校验和,确认和重传机制保证可靠传输
    • 注意:校验和是无法确认可靠的,只能排除不可靠的
  • TCP给数据分节进行排序,并使用累计确认保证数据的顺序
  • TCP使用滑动窗口机制来实现流量控制,通过动态改变滑动窗口大小来进行拥塞控制
  • TCP并不能保证数据一定会被对方接收到,他所能提供的是数据的可靠传递和故障的可靠通知

三次握手

三次握手,指的是服务器和客户端建立连接时,需要客户端和服务器总共发送三个包

  • 第一次握手:客户端发送一个SYN标记的包,Seq初始序列号x,发送完之后,客户端进入SYN_SEND状态
  • 第二次握手:服务端返回确认包(ACK)应答,应答结果为x+1,同时还要发送一个SYN包回去,序列号为y,客户端进入SYN_RCVD状态
  • 第三次握手:客户端返回确认包(ACK)应答,应答结果为y+1,然后客户端进入ESTABLISHED状态,服务端接收到了这个ACK,也进入了ESTABLISHED状态

为什么不两次握手?

只有两次握手的话,服务端无法判断客户端的接收能力是否合格。

四次挥手

四次挥手,指的是服务端和客户端拆除连接时,需要客户端和服务器总共发送四个包

  • 第一次挥手:客户端发送一个FIN标记包,表示自己不用发送数据了,此时的序列号为x
  • 第二次挥手:服务端发送ACK应答,应答结果为x+1,表示自己收到请求,不会再等待接收数据
  • 第三次挥手:服务端发送一个FIN标记包,表示自己不用发数据了,此时的序列号为y
  • 第四次挥手:客户端发送ACK应答,应答结果为y+1,表示自己收到请求,不会再等待接收数据,服务端收到ACK包之后关闭连接

为什么第二次挥手和第三次挥手要有等待?

这意味着:为什么不三次挥手?

只有三次挥手的话,意味着第二次和第三次是需要合并的,也就是ACK应答和FIN包一起发给客户端,这样会造成服务端还有数据没有发送完,就造成了数据的丢失。所以第二次和第三次的空档是为了等待服务器把剩下的数据发完

为什么最后服务器要等待2MSL?

MSL: 最大报文生存时间

这意味着考TIME_WAIT状态:

如果直接关闭,而不等待2MSL的话。可能会有第四次挥手中,因为网络问题,服务端收不到客户端的ACK,导致服务端会不断发送FIN包的情况。这样就可能导致关闭连接之后还会有这些FIN包去不断干扰。

另外,也是为了避免前次连接的包对下一次连接进行干扰,才设置2MSL让上一次连接的包失效

19 XSS和CSRF

XSS

跨站脚本攻击,攻击者通常在被攻击页面里面插入恶意script代码,当用户浏览该页的时候,嵌入里面的scipt代码会执行,从而达到攻击用户,拿到用户的信息并进行操作。

XSS可以做到什么事情:

  • 窃取Cookie
  • 监听用户行为,比如输入账号密码的时候直接发送到黑客服务器
  • 修改DOM伪造登录表单
  • 在页面中生成浮窗广告

通常XSS有三种:存储型,反射型和DOM型

  • 存储型:攻击者将恶意代码提交到目标网站的数据库中,然后服务器发送到前端,将恶意代码拼接在前端页面中,前端解析执行之后,可能会窃取用户数据,等等

  • 反射型:攻击者将恶意代码以URL的方式发给服务器,服务器解析之后,返回给前端,之后渲染恶意脚本后,窃取到数据

    • 这种攻击方式一般得用户打开恶意的url才会生效,攻击者结合多种手段诱导点击
    • POST也可以触发反射型XSS,不过条件较为苛刻
  • DOM型:攻击者将恶意代码以url伪协议的形式放入,用户打开带有恶意代码的url的时候,浏览器会将URL的恶意代码执行

    • DOM型是前端的DOM浏览器模型本身的漏洞,通过DOM操作修改内容,和服务端是没有关系的

CSRF

跨站请求伪造,攻击者通常诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台用户验证,达到冒充该用户在该网站执行某项操作的目的

通常CSRF有三种类型:GET型,POST型还有链接型

  • GET型:只需要一个HTTP请求
  • POST型:通常使用表单,自动获取用户数据提交,并且隐藏表单
  • 链接型:需要用户点击嵌在网页上的恶意链接或者恶意图片才会触发,当用户登录信任网站并保存登录状态,就会被窃取信息

常见解决方案

XSS解决方案

  • 不要相信用户的输入,一定要对用户的输入进行转码或者过滤

  • 内容安全策略:限制其他域下的资源下载,禁止向其他域提交数据,提供上报机制以快速发现XSS攻击

  • 输入内容的长度限制【只能增加XSS难度,不能防止XSS】

  • HTTP-only:禁止JS读取敏感Cookie,即使XSS攻击之后也不会获取敏感信息

  • 使用验证码,防止脚本冒充用户提交危险操作

CSRF解决方案

  • 增加防护能力
    • 阻止不明外域的访问
      • 提交时要求附加本域才有的信息来做验证
  • 增加预防能力
    • 严格管理所有的上传接口,防止任何预期之外的上传内容
    • 添加请求头:X-Content-Type-Options: nosniff,防止黑客上传HTML内容资源被解析成网页
    • 对于用户上传的图片进行转存或者校验,不要直接使用用户填写的图片链接
    • 打开其他链接时提前告知有风险
    • samesite:禁止向第三方请求携带cookie

20 CDN

实现方法

通过在网络各处节点放置节点服务器所构成的在现有互联网的基础上的一层智能虚拟网络。

CDN系统能够实时根据网络流量和各处节点的连接和负载状况以及到用户的距离和响应时间等综合信息,将用户的请求重新导向离用户最近的服务节点上,加快访问速度

目的

使用户可以就近取得所需内容,解决internet网络拥挤情况,提高网站的响应速度

优势

  • CDN节点解决了跨运营商和跨地域访问的问题,访问延时大大降低
  • 大部分在CDN边缘节点完成,CDN起到了分流作用,减轻了源站的负载

工作流程

  • 用户访问域名,先经过DNS解析,如果本地DNS命中,则直接返回给用户一个IP地址
  • 本地DNS如果没命中,则转发授权DNS查询
  • 网站授权DNS,返回域名CNAME来对应IP地址
  • 域名解析请求发送到DNS调度系统,DNS调度系统为请求分配最佳节点IP地址
  • 返回该IP地址给用户,用户对该缓存服务器发起请求
  • 如果本地DNS有缓存这个域名的解析结果,则直接响应用户的解析请求

21 HTTP的报文结构

HTTP的报文结构是:

  • 起始行:包含了请求方法,请求路径和http的版本号【响应部分叫状态行:版本号 状态码 Reason】
  • 头部
  • 空行:用于区分头部和实体,如果在头部中间加一行空行,那么空行后面的部分强制识别成实体
  • 实体:一般用于传输数据时,存放数据的部分

22 Cookie跨域与获取

可以设置cookie的domain的两个域的父域,这样就可以让两个域拿到这个cookie

Cookie的获取

前端

前端的清除可以直接将其expire的值设置为过期时间

1
document.cookie=" username=JavaScript  ; expire= " + cookieExpire + " ;path=/;domain=www.itxueyuan.org";

后端[express]

后端获取cookie,值直接拿到set-Cookie部分,在删除cookie也是在请求头的clearCookie方法来清除的

1
res.clearCookie(cookieName);

操作系统

进程和线程

进程是资源分配的最小单位,线程是CPU调度的最小单位

  • 进程:进程是资源分配的基本单位,进程中包括可执行的代码、打开的文件描述符、挂起的信号、进程的状态、内存地址空间、存放全局变量的数据段,以及一个或者多个执行线程等
  • 线程:线程是进程中活动的对象,是一个独立调度的基本单位。每个线程都有一个独立的程序计数器、线程堆栈和寄存器
  • 协程:是一种比线程更加轻量级的存在。一个线程可以拥有多个协程。

进程和线程的区别

  • 线程共享本进程的地址空间,而进程之间是独立的地址空间
  • 线程共享本进程的资源比如内存、I/O、CPU等,不利于资源的管理保护;而进程之间的资源是独立的,能很好的进行资源管理和保护
  • 多进程比多线程健壮,一个进程崩溃,是可以不会对其他进程产生影响的,但是一个线程崩溃将会导致整个进程全部死掉
  • 进程有程序运行入口、程序入口等,执行开销大;而线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,执行开销小
  • 进程切换消耗的资源大,效率高。涉及到频繁切换进程时,使用线程较优。如果还需要共享变量,则只能使用进程

进程的几种状态

  • 创建状态:进程正在被创建,尚未到达就绪状态
  • 就绪状态:进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到了处理器资源就会开始运行
  • 运行状态:进程在处理器上运行
  • 阻塞状态:进程正在等待某一事件而暂停运行,比如等待资源,或者等待IO操作完成
  • 结束状态:进程从系统中消失,可能是正常结束,也可能是其他原因中断了执行

webpack

1 webpack中loader和plugin的原理及其区别

2 babel的用处?

3 npm run build的编译流程

4 tree shaking

5 webpack的构建流程

6 webpack的热更新如何做到的,原理是什么

7 webpack proxy的原理,为什么能解决跨域

8 如何借助webpack优化前端性能

Git

  • rebase:用于将一个分支合到另一个分支的下节点

    下面就是合并bugfix到main下面

    1
    [bugfix*]: git rebase main
  • checkout:用于回溯到任何一个git节点

  • 修改已提交:amend

设计模式

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
// 观察者的容纳容器
class subject {
constructor () {
this.observerList = [];
}

addObserver (observer) {
this.observerList.push(observer);
}

deleteObserver (observer) {
const index = this.observerList.findIndex(o => o.name === observer.name);
this.observerList.splice(index, 1);
}

notifyObservers (message) {
const observers = this.observerList;
observers.forEach((item) => {
item.notified(message);
})
}
}

// 被观察者
class observer {
constructor (name, subject) {
this.name = name;
if (subject) {
subject.addObserver(this);
}
}
notified (message) {
console.log(this.name, ' is sent a message', message);
}
}

2 发布订阅模式

发布订阅是一种消息范式,消息的发送者不会将消息发送给特定的接收者,而是将发布的消息分为不同的类别,无需了解哪些订阅者可能存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class PubSub {
constructor () {
this.messages = {};
this.listeners = {};
}
publish (type, content) {

}
subscribe (type, callback) {

}
notify (type) {

}
}

class Publisher {
constructor (name, context) {
this.name = name;
this.context = context;
}
publish (type, content) {

}
}

class Subscriber {
constructor (name, context) {
this.name = name;
this.context = context;
}
subscribe (type, callback) {

}
}