想写这篇文章已经很久了,作为一个技工,怎么能免的了一直学习呢! 也换过两份工作,所以深知每次面试就是一次大考。写这篇文章的目的是
对技术的回顾,也是对自己成长的记录。(对于某些知识点如果我理解的不对,大家有更好的理解角度,也希望大家指出来,大家相互讨论,学习吧!)。
闲话休提,书归正传。面试就是用较短的时间做到双方相互了解,所以我尽量用简短语言来描述事物本质,至于对于某些重要知识点的深入理解,我会在后续逐步整理出文,请关注、点赞、收藏!(更新日期:2023-08-06)
这篇文章我断断续续写了两个月的时间,也是总结了我面试过程中的遇到的90%的内容,看完这篇文章,足以应对市面上70% - 80% 的面试题,再剩下算法类和一些公司自己出的题就需要大家再多面试,多总结经验了。我此时已经完成了所有的编辑内容,后面如果有新的题也会做补充, 截止到现在已经五万多字了,现在这个编辑器已经卡的不行了,如果出新的文章,也希望大家多多关注哈!码字不易啊!!!
面试路上互助交流群
如果你也在找工作或者你准备换工作,那么可以一起交流一下呀,也不介意潜水哈 😄,我只是希望面试的路上不是你我一个人独立奋斗,在大城市生活久了人都变的孤独了许多,我不希望面试完想找人分享找不到。在群里我们可以共享资源、一同避一下那些踩雷的公司、📣 吐槽生活、互相激励等等,下面是群二维码,欢迎加群交流呦!💪 加油 准备冲刺2023!需要进群的后台私信找我要群二维码
一、http 浏览器相关
http相关知识,前端必备网络相关知识。关于浏览器这一部分,我整理成一篇完整的文章,请看这里
二、性能优化
1.前端性能优化指标RAIL
| RAIL | 关键指标 | 用户操作 |
|---|---|---|
| 响应 | 输入延迟时间(从点按到绘制)小于100毫秒 | 用户点按按钮(例如打开导航) |
| 动画 | 每个帧的动画(从js到绘制)完成时间小于16毫秒 | 用户滚动页面,拖动手指(例如,打开菜单)或看到动画。 拖动时,应用的响应与手指位置有关(例如,拉动刷新、滑动轮播)。 此指标仅适用于拖动的持续阶段,不适用于开始阶段。 |
| 空闲 | 主线程工作分成不大于50毫秒的块 | 用户没有与页面交互,但主线程应足够用于处理下一个用户输入。 |
| 加载 | 页面可以在1000毫秒内就绪 | 用户加载页面并看到关键路径内容 |
2. 前端性能优化常用的方法有哪些 ?
js方面:
- 减少
http请求 :节流、防抖、缓存(keep-alive); - 及时消除对象引用,清除定时器,清除事件监听器;
- 使用常量,避免全局变量;
- 减少
dom操作, - 删除冗余代码(没有使用到的代码)
- 推迟js 加载:defer
css 方面
- 使用
<link>不使用@import - 减少重绘和回流,减少
table表格布局,html层级嵌套不要太深; - 合理配置图片加载方式(图片压缩上传、iconfont、base64、file文件、cdn、预加载、懒加载)
- 开启硬件加速(GPU加速)
工程化方面
webpack:打包压缩、Loader、插件;- 合理利用浏览器缓存(首次缓存)
- 开启gzip压缩(减少文件访问体积)
- 使用
ssr服务端渲染。 - 路由、组件、长页面使用懒加载
- 减少重定向请求
3. 大量图片加载优化策略
- 可以考虑上传到图片服务器
- 图片压缩再进行上传
- 使用懒加载(滚动加载,当滚动到可视区时再进行加载)
- 使用预加载(先将部分图片下载到本地,使用时进行替换,设定一个阀值,后面判断数据中剩余的图片阀值再进行下载)
- 在
webpack配置图片优化策略:小于指定大小使用base64 - 如果是大量的图标,可以使用
iconfont
4. 关于页面白屏如何排查原因 ?
下面提供几种排查场景
- 检查网络连接是否异常,看
network资源加载情况,传输数据量是否过大; - 检查控制台是否有报错;
- 看
dom结构,是否正常加载,是否是静态资源加载失败; - 在其他电脑打开是否一样白屏;
- 更换浏览器,检查是否是浏览器兼容问题。
5. 什么是服务端渲染?
服务端渲染
SSR指的是Server-side Rendering,意思为服务端渲染就是在浏览器请求页面url的时候,服务端将我们需要的html文本组装好,并返回给浏览器,这个html文本被浏览器解析之后,不需要经过js脚本的执行,即可直接构建出DOM树,并展示到页面中,这个服务端组装html的过程叫做服务器渲染。
三、HTML 相关
1. 浅谈前端工程化介绍:模块化、组件化、规范化、自动化
前端工程化:指使用软件工程的技术与方法对前端开发的技术、工具、流程、经验、方案等指标
标准化,它具备模块化、组件化、规范化、自动化四大特性,主要目的是降低成本与增加效率。
-
模块化:是指在文件层面上对代码与资源实现拆分与组装,将一个大文件拆分为互相依赖的小文件,再统一拼装与加载。各个模块功能独立,分模块来维护,组合方式更灵活,多人协作也互不干扰。例如:接口模块、资源模块、路由模块等。
-
组件化:是指在功能开发场景中,将具备通用功能的交互设计划分为模板、样式和逻辑组成的功能单元,是具体某个功能的封装,实现了代码更高层次的复用性,提升开发效率。组件的封装也是对象的封装,同样要做到高内聚低耦合,例如分页器、
table表格、form表单等。 -
规范化:将一系列预设规范接入工程各个阶段,通过各项指标标准化开发者的工作流程,为每个开发者指明一个方向,引领着成员往该方向走。例如:
eslint、stylelint、pre-commit等,拉齐代码标准,形成规范底线,方便不同人员等交叉维护。 -
自动化:指将一系列繁琐重复的工作流程交由程序根据预设脚本自动处理,常见
自动化场景包括但不限于自动化构建、自动化测试、自动化打包、自动化发布和自动化部署等。在保证效率的同时,又解放了双手。
总结:前端工程化不是某个具体的工具,而是对项目的整体架构与整体规划,使开发者能在未来可判断时间内动态规划发展走向,以提升整个项目对用户的服务周期。最终的目的是从手动处理流程全部替换为自动处理流程,以解放团队双手,让其他成员更专注于自身业务需求。
2. html5 的新特性
语义化标签: <header> <footer> <nav> <article> <aside>
新增的表单属性: placeholder 、 required 、 pattern、 min/max 、 autofocus、 mulitiple
音视频标签: <audio>、 <video>
绘图标签:<canvas>
存储: sessionStorage 、 localStorage
通信: webSocket
语义化标签有什么作用?
- 比较利于开发人员阅读,结构清晰明了;
- 利于
SEO搜索引擎优化,搜索引擎也要分析我们的网页,可以很方便的寻找出网页的重点部分,排名靠前; - 有利于特殊终端的阅读(盲人阅读器)。
四、CSS 相关
1. css 盒模型:
W3C盒模型
当box-sizing:content-box时 ,为W3C盒模型,又名标准盒模型,元素的宽高大小表现为内容的大小
box = content
IE 盒模型
当 box-sizing:border-box时,为IE 盒模型,又名怪异盒模型,元素的宽高表现为内容 + 内边距 + 边框:
box = content + padding + border
2. 什么是BFC?
BFC全称Block formatting context(块级格式化上下文)。是web布局的css渲染模式,是一个独立的渲染区域或一个隔离的独立容器。
特性:
- 块级元素,内部一个一个垂直排列
- 垂直方向的距离由两个元素中
margin的较大值决定 bfc区域不会与浮动的容器发生重叠- 计算元素的高度时,浮动元素也会参与计算
bfc容器中,子元素不会影响外边元素- 属于同一个
bfc的两个相邻元素的外边距发生重叠,
如何触发BFC
可以通过设置
css属性来实现
- 设置浮动
float但不包括none - 设置定位,
absoulte或者fixed - 行内块显示模式,设置
display为inline-block - 设置
overflow为hidden、auto、scroll - 弹性布局,
flex
BFC解决了哪些问题:
- 阻止元素被浮动元素覆盖
- 可以利用
BFC解决两个相邻元素的上下margin重叠问题; - 可以利用
BFC解决高度塌陷问题; - 可以利用
BFC实现多栏布局(两栏、三栏、圣杯、双飞翼等)。
3. CSS3 新特性总结
-
选择器:
E:last-child 、E:nth-child(n)、E:nth-last-child(n) -
边框特性:支持圆角、多层边框、彩色和图片边框
-
背景图增加多个属性:
background-image、background-repeat、background-size、background-position、background-origin和background-clip -
支持多种颜色模式和不透明度:加了
HSL、HSLA、RGBA和不透明度opacity -
支持过渡与动画:
transition、animation -
引入媒体查询:
mediaqueries,支持为不同分辨率的设备设定不同的样式 -
增加阴影:文本阴影:
text-shadow,盒子阴影:box-shadow -
弹性盒模型布局:
flex新的布局方式,用于创建具有多行和多列的灵活界面布局。
4. 几种让元素隐藏的方法?
-
overflow:hidden隐藏除宽高外的内容 -
opacity:0占地了,可以点击 -
visibility:hidden占地了,但看不见 -
display:none不占地,也看不见
5. display 有哪些值?
| 值 | 描述 |
|---|---|
| none | 此元素不会被显示 |
| block | 此元素将显示为块级元素,前后带有换行符 |
| inline | 默认。此元素会被显示为内联行内元素,元素前后没有换行符 |
| inline-block | 行内块元素,可设置宽高,没有换行符 |
| table | 此元素会作为块级表格来显示,表格前后带有换行符 |
| inherit | 规定应该从父元素继承 display 属性的值 |
| flex | 弹性盒模型,支持各种对齐方式,支持自适应 |
| grid | 网格布局,最新的布局方式,ie不支持 |
6. position 有哪些值?
-
absolute绝对定位,脱离文档流,相对于父级元素。 -
relative相对定位,不脱离文档流,参考自身静态位置定位。 -
fixed固定定位,这里他所固定的对象是浏览器可视窗口而并非是body或是父级元素。 -
static:默认值,无定位。 -
sticky: 粘性定位,相当于relative和fixed的结合,元素在跨越特定阈值前为相对定位,之后为固定定位。适用于顶部导航栏、标题、操作栏、底部评论等。
7. 怎么用css实现 品 字形布局?
满屏品字形
html:
<div class='div1'>1</div>
<div class='div2'>2</div>
<div class='div3'>3</div>
css:
.div1{
width:100%;
height:200px;
margin:auto;
background:red;
}
.div2{
width:50%;
height:200px;
float:left;
background:green;
}
.div3{
width:50%;
height:200px;
float:left;
background:blue;
}
固定宽高品字形:
html:
<div class='div1'>1</div>
<div class='div4'>
<div class='div2'>2</div>
<div class='div3'>3</div>
</div>
css:
.div1{
width:200px;
height:200px;
background:red;
margin:0 auto;
}
.div4{
border:1px solid red;
display: flex;
justify-content: center;
}
.div2{
width:200px;
height: 200px;
background:green;
}
.div3{
width:200px;
height: 200px;
background:blue;
}
8. flex怎么实现一部分固定高度,一部分自适应?
下面就列举几个常用场景
左侧固定,右侧自适应:
html:
<div class='box'>
<div class='div1'>1</div>
<div class='div2'>2</div>
</div>
css:
*{
margin:0;
padding:0;
}
.box{
display: flex;
width: 100%;
height: 500px;
}
.div1{
width: 200px;
height:100%;
background:red;
}
.div2{
height:100%;
flex:1;
background:green;
}
左右固定,中间自适应:
html:
<div class='box'>
<div class='div1'>1</div>
<div class='div2'>2</div>
<div class='div3'>3</div>
</div>
css:
*{
margin:0;
padding:0;
}
.box{
display: flex;
height: 200px;
width: 100%;
}
.div1{
width: 200px;
height: 100%;
background:red;
}
.div2{
height:100%;
flex:1;
background:green;
}
.div3{
width: 200px;
height: 100%;
background:blue;
}
顶部固定,底部自适应:
html:
<div class='box'>
<div class='div1'>1</div>
<div class='div2'>2</div>
</div>
css:
*{
margin:0;
padding:0;
}
.box{
display: flex;
min-height: 100vh;
width: 100%;
flex-direction: column;
}
.div1{
flex:0 0 100px;
background:red;
}
.div2{
height: 100px;
flex: auto;
background:green;
}
顶部和底部固定高度,中间自适应
html:
<div class='box'>
<div class='div1'>1</div>
<div class='div2'>2</div>
<div class='div3'>3</div>
</div>
css:
*{
margin:0;
padding:0;
}
.box{
display: flex;
min-height: 100vh;
width: 100%;
flex-direction: column;
}
.div1{
flex:0 0 100px;
background:red;
}
.div2{
height: 100px;
flex: auto;
background:green;
}
.div3{
flex:0 0 200px;
background:blue;
}
9. 居中的布局
在
css面试题中,关于水平 + 垂直居中布局这个问题,可能是我在前端面试中被问到最多的,哈哈 那我们就来实现一下吧!
html
<div class='box'>
<div class='center'></div>
</div>
第一种:(flex最方便,有兼容性问题:ie 8 以下不支持)
.box{
width:500px;
height: 500px;
background-color: yellow;
display: flex;
justify-content: center;
align-items: center;
}
.center{
width:200px;
height: 200px;
background-color: blue;
}
第二种:(四个方向拉扯)
.box{
width:500px;
height: 500px;
background-color: yellow;
position: relative;
}
.center{
width:200px;
height: 200px;
background-color: blue;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
第三种:(使用计算属性calc)
.box{
width:500px;
height: 500px;
background-color: yellow;
position: relative;
}
.center{
width:200px;
height: 200px;
background-color: blue;
position: absolute;
left: calc(50% - 100px);
top: calc(50% - 100px);
}
第四种:(使用转换属性transform)
.box{
width:500px;
height: 500px;
background-color: yellow;
position: relative;
}
.center{
width:200px;
height: 200px;
background-color: blue;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
第五种:(定位 + margin-left:盒子的一半)
.box{
width:500px;
height: 500px;
background-color: yellow;
position: relative;
}
.center{
width:200px;
height: 200px;
background-color: blue;
position: absolute;
left: 50%;
top: 50%;
margin-left: -100px;
margin-top: -100px;
}
统一效果如下:
10. 聊聊你了解的 flex?
Flex是Flexible Box的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。2009年,W3C提出了一种新的方案----Flex布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。
目标:可以提供一个更高效的布局、对齐方式,并且能够使父元素在子元素的大小未知或动态变化情况下仍然能够分配好子元素之间的间隙。
场景:布局是一部分固定宽高,一部分自适应,例子在上面👆。除此之外呢,flex还可以用于:各种对齐,顶对齐,底部对齐,居中对齐等各种对齐的场景。
由于我们日常的工作中多使用ui 组件库,组件库中都有删格这个组件,这个删格底层使用的就是我们的flex,这样说是不是恍然大悟了,ui组件在一定程度上对于我们日常使用比较多的场景做了一些封装,所以下次不要说你不会用flex了哦,下面两个链接是阮一峰大佬的,里面有具体如何使用的代码,大家自取吧,我就不赘述了。
11. css 的选择器有哪些?
对于常常写样式的同学,肯定都比较熟了,下面就列一些比较常用的选择器,并不是全部哦!
a. 常用选择器
通配符选择器
对全局样式生效
<p>段落1</p>
...
*{
// 具体样式
}
id选择器
<p id="a">段落1</p>
...
#a{
// 具体样式
}
class 选择器
<p class="a">段落1</p>
...
.a{
// 具体样式
}
标签选择器
<p>段落1</p>
...
p{
// 具体样式
}
b. 关系选择器
后代选择器
<div id="a">
<p class="b"></p>
</div>
...
div p{
// 具体样式
}
或者
#a p{
// 具体样式
}
或者
div .b{
// 具体样式
}
或者
#a .b{
// 具体样式
}
相邻兄弟选择器:
只会去找相邻的,例如:只会去找.box紧邻的p
<div class="box"></div>
<p></p>
...
.box+p{
// 具体样式
}
通用兄弟选择器:
会去寻找指定选择器后面的所有的指定元素,例如:下面的例子会选择.box 后面所有的p标签
<div class="box"></div>
<p></p>
<p></p>
<p></p>
...
.box~p{
// 具体样式
}
c. 伪类选择器
-
:first-child 第一个子元素
-
:last-child 最后一个子元素
-
:nth-child() 选中第n个元素(可以传参数)
-
:first-of-type 第一个子元素
-
:last-of-type 最后一个子元素
-
:nth-of-type() 选中第n个元素(可以传参数)
-
:hover 鼠标移入后元素的状态
-
:active 鼠标点击后,元素的状态
d. 选择器的优先级和权重
!import(最大) > 行内样式(1000) > id选择器(100) > class 选择器(10) 、 属性选择器(10) 、 伪类选择器(10) > 标签选择器(1) > 通配符选择器(最小)
12. 清除浮动的几种方式
浮动元素会影响父级元素,使之产生高度塌陷的问题
- 父元素添加
overflow:hidden - 父元素设置
height - 在父元素中最后设置一个块级元素, 设置
clear:both - 父元素添加
clearfix,样式如下:
.clearfix::after{
display:block;
content:"";
clear:both;
}
13. 关于文本溢出
- 单行文本溢出
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
white-space: nowrap; // 规定段落中的文本不进行换行
- 多行文本溢出
overflow: hidden; // 溢出隐藏
text-overflow: ellipsis; // 溢出用省略号显示
display:-webkit-box; // 作为弹性伸缩盒子模型显示。
-webkit-box-orient:vertical; // 设置伸缩盒子的子元素排列方式:从上到下垂直排列
-webkit-line-clamp:3; // 显示的行数
五、JS 相关
1. 节流和防抖实现及其区别
- 防抖
是指触发事件后在
n秒内函数只能执行一次,如果在n秒内再次触发,则会重新计算函数执行时间,即重新计时,每次触发事件时都取消之前的延时调用方法,这样一来,只有最后一次操作能被触发。
function debounce(func, delay) {
let timer;
return function() {
let _this = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
func.apply(_this, args);
},delay)
}
}
例如:输入搜索,在400毫秒只能执行一次,如果又输入的搜索内容,则重新开始计算400秒
- 节流
就是指连续触发事件但是在
n秒中只执行一次函数。节流会稀释函数的执行频率。每次触发事件时都判断当前是否有等待执行的延时函数。
间隔时间执行,不管事件触发有多频繁,都会保证在规定时间内一定会执行一次,也就是第一次的执行,当后续再次执行时 ,先判断时间是否超过delay的间隔,当时间大于,就再次执行。否则不操作。
// 节流
function throttle(func, delay) {
let curTime = new Date();;
return function() {
let context = this;
let args = arguments;
let nowTime = new Date();;
// 判断时间是否超过设置的delay 时间,超过即可执行
if (nowTime - curTime >= delay) {
curTime = new Date();;
return func.apply(context, args);
}
}
}
例如:点击事件连续点击了n次,但在400毫秒内只能执行一次
2. dom 事件流是什么,如何阻止事件捕获
3. js的es系列的新语法有哪些?
包括但不限于
es6、7、8、9....,它最大的特性就是:方便开发,提高工程性,开发效率高,更不容易犯错误
关于es6以上新特性兼容性: 大部分的新特性ie浏览器不支持,如需支持,可使用babel在线编译
配置:
script type='text/babel'
- Number.isSafeInteger(): 是否在安全数内
检测数字是否在安全数内,返回布尔值
Number.isSafeInteger(234334343434343); // true
Number.isSafeInteger(999999999999999999); // false
- Number.isNaN(): 检查是否为 NaN
结果返回布尔值。与
isNaN的区别是,Number.isNaN会先检测参数是否为Number类型,如果不是直接返回false,只有当参数为Number类型,才会再去判断是不是NaN。
Number.isNaN(NaN) // true
Number.isNaN(1) // false
Number.isNaN(true) // false
Number.isNaN(undefined) // false
Number.isNaN({}) // false
Number.isNaN("abc") // false
Number.isNaN("") // false
- Math.sign(): 返回一个数字的符号,指示数字是正数、负数还是零
一共有 5 种返回值,分别是 1, -1, 0, -0, NaN. 代表的各是正数,负数,正零,负零,NaN。
Math.sign(3); // 1
Math.sign(-3); // -1
Math.sign("-3"); // -1
Math.sign(0); // 0
Math.sign(-0); // -0
Math.sign(NaN); // NaN
Math.sign("foo"); // NaN
Math.sign(); // NaN
- Math.imul(): 两数相乘
返回两个参数的类
C的32位整数乘法运算的结果
Math.imul(2, 4); // 8
Math.imul(-1, 8); // -8
Math.imul(-2, -2); // 4
Math.imul(0xffffffff, 5); // -5
- 允许顶层 await
之前我们在写
await的时候,一定要在前面加一个async,现在我们支持在顶层直接使用。Top await本身就是esModule里的规范,所以需要在script里加上type=“module”
- at(-1)可以直接拿到数组或者字符串的最后一位元素
支持数组和字符串,再也不用使用这种冗余的写法了
arr[arr.length-1]
[2,3,5].at(-1); // 5
'aazvxcvher'.at(-1); // r
- Object.hasOwn
代替 Object.prototype.hasOwnProperty.call
// 原来
Object.prototype.hasOwnProperty.call(obj, 'name');
// 现在
Object.hasOwn(obj, 'name');
- 允许 JavaScript 的数值使用下划线(
_)作为分隔符
可以间隔3位,也可以间隔两位,看起来比较好读
let budget = 1_000_000_000_000;
budget === 10 ** 12 // true
4. 关于 ES6 的模块化
发展历史:
没有模块 ->
CommonJS同步加载 ->AMD异步加载 ->UMD = CommonJS + AMD->ES Module是标准规范, 取代UMD,是大势所趋。
export:导出的几种方式
// 导出普通变量(边定义边导出)
export let a=5;
// 导出常量
export const a=6;
// 导出函数
export function xx(){};
// 导出类
export class xxx(){} ;
// 导出默认成员
export default 'xxx';
// 从另一个模块导出
export * from './xx';
// 导出模块中的部分内容
export {xxx,xx,xx} from './xxx';
// 导出模块中的default
export {default} from './xx';
import:引入的几种方式
// 全部引入并重命名
import * as mod from '/xxx';
// 引入指定成员
import {a,b,c} from './xxx';
// 引入默认成员,配合 export default xxx;
import xxx from './xxx';
// 将模块的代码引入,不引入内部成员
import './x.jpg';
import './x.css';
// 异步引入
let promise = import ('./xx');
5. Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()判断数组的方法,区别?
如果只是用来判断数组,
Array.isArray优于instanceof。
- Object.prototype.toString.call():
这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined 。
- instanceof
instanceof 的内部机制是通过判断对象的原型链中是不是能找到Array 的原型 prototype,找到返回 true,否则返回 false。
- Array.isArray():
是es6新增的方法,可以检测出 iframes,运行效率比较高,所以当真正需要检测一个变量是不是数组时,先会检测浏览器是否支持Array.isArray(), 之后再用toString.call()方法。
5. typescript 中的type 和 interface 的区别 ?
一般情况下定义接口用
interface,定义常量用type,定义对象两个都可以
相同点:
- 都能描述对象(含数组、函数、包装对象)
- 都能用于扩展一个类型,
interface是通过extends,type是通过&;
关于区别:
- 同名
interface会合并,而同名type会报错 type可用于 可以定义基本类型 / 联合类型 / 元祖类型(string、number、bool、undefined、null),而interface只能描述对象(含数组、函数、包装对象、元组)。type声明的是类型别名,而interface声明的是新类型。
例:
// object (对象)
type PartialPointX = { x: number };
type PartialPointY = { y: number };
// union(联合)
type PartialPoint = PartialPointX | PartialPointY;
// tuple(元祖)
type Data = [PartialPointX, PartialPointY];
// primitive(原始值)
type Name = Number;
6. Object.assign() 、 Object.create() 、 Object.defineProperty 分别的用处 ?
Object.assign
方法用于将自身所有可枚举属性的值从一个或多个源对象复制到目标对象,并且会返回一个新的目标对象。参考链接
Object.create
创建一个新对象,是把现有对象的属性,挂到新建对象的原型上,第一个参数添加到原型上,第二个参数,为添加的可枚举属性(即添加自身属性,不是原型上的) 例:
复制对象:
var a = {name:'a'};
a.__proto__.lastName="b";
// 想要创建一个一模一样的 a
b = Object.create(a.__proto__,Object.getOwnPropertyDescriptors(a))
console.log(b); // { name:'a' }
Object.create()方法创建的对象时,属性是在原型下面的
var a={ name:'hh'};
var b = Object.create(a);
console.log(b); // {}
console.log(b.name); // 'hh'
当创建一个以另一个空对象为原型,第二个参数是添加到新创建的对象的可枚举属性:
var a = Object.create({}, { p: { value: 19 } })
console.log(a); // {p: 19}
Object.defineProperty
直接在一个对象上定义一个新属性,或者修改一个对象现有的属性,并返回这个对象。
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
object1.property1 = 77; // throws an error in strict mode 不允许被修改
console.log(object1.property1); // 42
区别:
Object.defineProperty用于给对象添加新属性Object.assign用于将自身所有可枚举属性的值从一个或多个源对象复制到目标对象,并且会返回一个新的目标对象Object.create()创建一个新对象,是把现有对象的属性,挂到新建对象的原型上
7. 什么是Object属性的可枚举性 ?
- 可枚举属性是指那些内部
可枚举(enumerable)标志设置为true的属性 - 对于通过直接的赋值和属性初始化的属性,该标识值默认为
true - 对于通过
Object.defineProperty等定义的属性,该标识值默认为false - 可枚举的属性可以通过
for...in循环进行遍历。
8. 浅拷贝和深拷贝 ?
- 浅拷贝 如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址(新旧对象共享同一块内存),所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 深拷贝 将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原对象。
9. 深拷贝的方法 ?
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象(新旧对象不共享同一块内存),且修改新对象不会影响原对象
- JSON.parse(JSON.stringify())
let obj1 = {
name: 'Li',
age:18
}
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1 === obj2); // false
关于JSON.parse(JSON.stringify())的缺点
obj里面有new Date(),深拷贝后,时间会变成字符串的形式。而不是时间对象;obj里有RegExp、Error对象,则序列化的结果会变成空对象{};obj里有function,undefined,则序列化的结果会把function或undefined丢失;obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null;JSON.stringify()只能序列化对象的可枚举的自有属性,如果obj中的对象是由构造函数生成的实例对象, 深拷贝后,会丢弃对象的constructor;
- 手写方案:
function cloneDeep3(obj = {}, newObj = null) {
// 判断边界情况
if (typeof obj == 'object' && obj !== null) {
newObj = Array.isArray(obj) ? [] : {}
for (var i in obj) {
newObj[i] = cloneDeep3(obj[i])
}
}
// 非对象直接赋值
else {
newObj = obj
}
return newObj
}
cloneDeep3(obj)
- lodash中的cloneDeep()
import lodash from 'lodash';
let obj = {
a: {
c: 2,
d: [1, 9, 9, 2],
e:'张三'
},
b: 99
}
const newObj = lodash.cloneDeep(obj);
obj.b = 5;
console.log(newObj.b); // 99 不会被影响
10. var 如何实现 let ?
var 存在的问题
- 可以重复声明
- 没有块级作用域
- 不能限制修改
普通的var使用:
for(var i=1;i<4;i++){
setTimeout(function(){
console.log(i);
},1000);
}
改造后的实现:
for (var i = 1; i < 4; i++) {
(function f(a) {
setTimeout(function () {
console.log(a);
}, 1000);
})(i);
}
11. 什么是作用域 ?
作用域指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限
在js中大部分情况下,分为以下三种作用域:
全局作用域: 全局作用域为程序的最外层作用域,一直存在。
函数作用域: 函数作用域只有函数被定义时才会创建,包含在父级作用域内。
块级作用域 : 所有语法块{}都会形成独立的块级作用域,例如:if 、for。
由于作用域的限制,每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量。
ES6标准提出了使用let和const代替var关键字,来“创建块级作用域”。
/* 全局作用域开始 */
var a = 1;
function func () {
/* func 函数作用域开始 */
var a = 2;
console.log(a);
}
/* func 函数作用域结束 */
func(); // => 2
console.log(a); // => 1
/* 全局作用域结束 */
除了全局作用域,我们是不能在一个作用域中访问其他作用域中的内容的。 那么如果我们需要获取其他作用域中的变量,那怎么办?闭包就这样诞生来了!
12. 什么是闭包?什么场景使用?有什么缺点?
官方解释:闭包是指有权访问另一个函数作用域中的变量的函数。例如:函数
a可以拿到函数b内部作用域的变量。
闭包除了和作用域有关,也和垃圾回收的机制有关,正常的垃圾回收过程是:当一个函数执行时会给它分配空间,而当执行结束后会把空间收回。
但是当一个局部变量被另一个函数正在使用,那么它就不会被释放,也就不会被垃圾回收,就是形成了闭包环境。
例如:
function foo() {
var a = 2;
document.onclick=()=>{
alert(a); // a 不会被回收
}
}
show();
我们了解作用域之间是可以嵌套的,我们把这种嵌套关系称为 作用域链。闭包的执行看起来像是开发者使用的一个小小的 “作弊手段” ——绕过了作用域的监管机制,从外部也能获取到内部作用域的信息。闭包的这一特性极大地丰富了开发人员的编码方式,也提供了很多有效的运用场景。
用途:
- 能缓存作用域变量,私有化数据,避免全局变量的污染;
- 延长变量的生命周期
- 利于代码封装:防抖、节流、选项卡等
缺点:
当变量一直保存在内存中,有可能导致内存泄露(不是一定,因为我们使用闭包是已知行为),我需要做的事:合理使用闭包,避免无限循环,所以在不需要用到的时候及时把变量设置为null。
13. 什么是垃圾回收机制?
程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,及时去释放无用对象的内存,否则,内存占用过高,轻则影响系统性能,重则就会导致进程崩溃。
清除哪些内容?
在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,我们需要清理的就是那些不可达的值所占用的内存, JavaScript 垃圾回收机制的原理说白了也就是定期找出那些不再用到的内存(变量),然后释放其内存。
垃圾回收的两种方法:
- 标记清除(常用)
- 引用计数。
标记清除
javascript中最常用的垃圾回收方式。
整个标记清除算法大致过程:
- 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为
0 - 然后从各个根对象开始遍历,把不是垃圾的节点改成
1 - 清理所有标记为
0的垃圾,销毁并回收它们所占用的内存空间 - 最后,把所有内存中对象标记修改为
0,等待下一轮垃圾回收
标记清除的缺点:
-
内存碎片化:空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
-
分配速度慢:因为即便是使用
First-fit策略,其操作仍是一个O(n)的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢
标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
v8 对垃圾回收机制的优化
V8的垃圾回收策略主要基于分代式垃圾回收机制,V8中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器也就是不同的策略管理垃圾回收
分代式的目的:
分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接受检查,新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率。
14. 什么情况下会导致内存泄露 ?
内存一般有三个阶段的生命周期: 分配期 -> 使用期 -> 释放期
程序的运行需要占用内存,当这些程序没有用到时,还不释放内存,就会引起内存泄漏。而内存泄漏,会让系统占用极高的内存,让系统变卡甚至崩溃。
可能导致的原因:
- 意外的全局变量
- 被遗忘的定时器、回调函数(例如:未使用的闭包)、事件监听器
- 由于不正确使用递归调用,导致无限循环的调用函数(死循环)
console保存大量数据在内存中
14. 如何避免内存泄露 ?
其实总结一句话就是:尽早释放无用对象的引用,下面是具体方法:
- 减少不必要的全局变量,使用严格模式避免意外创建全局变量(现在编辑器都会提示)。
- 在你使用完数据后,及时解除引用(闭包中的变量,
dom引用,定时器清除)。 - 梳理清楚逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。
- 避免过度使用闭包(尽量减少逻辑嵌套)。
15. 数组中各遍历方法的返回值
-
filter返回一个判断结果为true组成的数组; -
forEach会改变原数组,没有返回值; -
map返回每次函数调用的结果组成的新数组; -
reduce迭代数组所有项,然后构建一个最终返回的值; -
some如果该函数任意一项返回true,则返回true; -
every如果该函数对每一项都返回true,则返回true;
16. map 和 filter 的区别
map:
作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的数组中。
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4]
filter
作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,我们可以利用这个函数删除一些不需要的元素
let array = [1, 2, 4, 6];
let newArray = array.filter(item => item !== 6) ;
console.log(newArray) // [1, 2, 4]
17. Event loop事件循环机制
我们知道
js是单线程语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环。
在JavaScript中,所有的任务都可以分为:
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
- 异步任务:异步执行的任务,比如
ajax网络请求,setTimeout定时函数等
在执行任务时会先执行同步任务,然后再执行异步任务,在异步任务中分为为宏任务和微任务队列,秉承先入先出的规则。
在JavaScript中,异步任务可以分为:
- 宏任务:包括整体代码
script,定时器:setTimeout,setInterval - 微任务:
Promise.then(非new Promise),process.nextTick(node中)
Event Loop 执行顺序如下所示:
- 首先执行同步代码,也就是
script中可以立即执行的代码,例如:console.log,新程序执行归为宏任务; - 当执行完所有同步代码后,执行栈为空;
- 优先执行微任务,例如
promise.then里的回调函数; - 当执行完所有微任务后,如有必要会渲染页面;
- 然后开始下一轮
Event Loop,执行宏任务中的异步代码,也就是setTimeout中的回调函数。
18. setTimeout、Promise、Async/Await 的区别
事件循环中分为宏任务队列和微任务队列。
-
promise.then里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行 -
async函数表示函数里面可能会有异步方法,await后面跟一个表达式,async方法执行时,遇到await会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。 -
settimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;(最后执行)
24. js 异步解决方案的发展历程?
异步操作:多个操作可以一起进行,而互不干扰,同步操作:一次只能处理一个请求
- 回调函数:当嵌套层级过多,容易产生回调地狱,强耦合,一旦有所改动,牵一发而动全身,错误管理困难。
Promise就是为了解决callback的问题而产生的,Promise实现了链式调用,也就是说每次then后返回的都是一个全新Promise,如果我们在then中return,return的结果会被Promise.resolve()包装。让Promise大放异彩的是Promise.all。缺点:无法取消promise,如果有逻辑,不能解决,如果有错误需要通过回调函数来捕获。Generator可以控制函数的执行,可以有顺序,可以中途停止,停止后再执行,写起来相对麻烦。async、await是异步的终极解决方案,当然也是语法糖,内部实现仍然是回调嵌套回调。优点是:代码清晰,不用像Promise写一大堆then链,处理了回调地狱的问题,用同步的书写方式写异步。缺点:await将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用await会导致性能上的降低。
21. 什么是Promise?
Promise是异步编程的一种解决方案,是一种语法糖,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,解决回调地狱难题,一旦新建它就会立即执行,无法中途取消Promise。
Promise是一个对象,它代表了一个异步操作的最终完成或者失败。由于它的then方法和catch、finally方法会返回一个新的Promise所以可以允许我们链式调用,解决了传统的回调地狱问题。
Promise有三种状态:
pending,进行中。resolved(也可以叫fulfilled),已成功。rejected,已失败。
Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected,then 和 catch 都会返回一个新的promise
then()
方法可以接收两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,处理操作成功后的业务,第二个回调函数是Promise对象的状态变为rejected时调用,处理操作异常后的业务。
catch()
只接受一个参数,用于处理操作异常后的业务, 一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作, 方法的回调函数不接受任何参数, 这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果,最终也是返回一个promise 函数
all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。Promise.all方法接收一个数组作为参数,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调,一旦抛出异常,也只有第一个抛出的错误会被捕获,但不影响其他异步任务。
举例:
p1、p2、p3都是 Promise 实例
const p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况。
- 只有
p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。 - 只要
p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
race()
方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,就会返回结果。race含有竞速的意思,将多个Promise放在一个数组中,数组中有一个promise最先得到结果,不管是" 完成(resolved)"还是" 失败(resolved)" , 那么这个 race 方法就会返回这个结果。
race()、all() 、 allSettled的区别:
race()根据第一个请求来返回结果,若第一个成功,全局都成功,第一个失败,全局都失败,返回结果的顺序,时间快的先返回。
all() 是全部成功,才会成功,只有一个失败,就是失败,成功返回的结果根据请求的参数的顺序,无关请求的快慢。
allSettled 状态总是返回成功,并且返回一个数组,数组中包含对象,对象里有每一项的状态和值。
22. 事件委托/事件代理?
事件委托是利用事件的
冒泡原理来实现的
何为事件冒泡呢?
就是事件从最深的节点开始,然后逐步向上传播事件
只指定一个事件处理程序,就可以管理某一类型的所有事件。注册事件的话应该注册在父节点上。假如我们要给100个li同时添加一样的事件,那么我们就需要和浏览器交互100次,如果要用事件委托,就会将所有的操作放到js程序里面,只对它的父级(如果只有一个父级)这一个对象进行操作,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,效率高,不用for循环去添加事件,提高性能。
23. 什么是跨域?
跨域的产生来源于现代浏览器所通用的同源策略限制,当一个请求
url的协议、域名、端口三者均一样的情况下,才允许访问相同的cookie、localStrage,以及访问页面dom或者发送ajax请求。原因是为了保证用户信息安全,防止恶意网站窃取数据。
同源:
http://www.example.com:8080/index.html
http://www.example.com:8080/home.html
跨域:
http://www.example.com:8080/index.html
http://www3.example.com:8080/index.html
但是还有两种情况,http默认的端口为80,https默认的端口号为443,所以以下不是跨域:
http://www.example.com:80 === http://www.example.com
https://www.example.com:443 === https://www.example.com
如何解决跨域 ?
其实跨域请求产生时,请求是发出去了,也是有响应的,只是因为浏览器同源策略,它认为此时是不安全的,拦截了我们的返回结果,不将数据传递我们使用罢了。
- CORS 全称是"跨域资源共享"(Cross-origin resource sharing)
CORS支持所有类型的HTTP请求,是跨域http请求的根本解决方案。
后端来处理,修改响应头,允许任何网站访问:
'Access-Control-Allow-Origin': '*',
- jsonp
jsonp是利用
<script>标签没有跨域限制的漏洞,jsonp的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。缺点:jsonp只支持GET请求,且容易收到xss攻击,因为它不能被限制,且无任何校验,所以已经逐步被淘汰了。
<script src='其他网站.js'></script>
实现步骤:
- 声明一个回调函数,用于接收数据;
- 创建一个
script标签,把script的scr赋值为跨域的api接口地址; - 将回调函数的名字当参数挂在
src上,告诉对方我的回调函数的名称,用于执行完成后来调用我的函数
配合jquery来实现:
...
$.ajax({
url:"http://sp0.baidu.com.23d8s8dfj923kjs7823jshdsdf/su",
data:{wd:'qq'},
dataType:"jsonp",
jsonp:"cb", // 告诉它回调函数的名字,以便于它生成一个函数来给我们用
}).then(({s})=>{
console.log(s);
},res=>{
console.log('失败')
);
...
- websocket
跨域问题的产生无非就是去服务器上获取数据;而
websocket是和服务器建立了双工连接,连接只要建立了,就能通讯了,既然能通讯,那我想要什么数据跟后台说,后台再发送给前端数据,这样利用通信就不会有跨域的问题啦。这种方式是通过建立长链接来请求,相比普通请求会比较耗费性能!
Websocket 支持跨域,因websocket原生api用起来不太方便,推荐使用socket.io的库,并使用node.js配合前端。
- webpack
通过代理的方式,一般用于开发环境
module.exports = {
devServer: {
proxy: {
"/api": {
target: "http://localhost:3000",
pathRewrite: {"/api": ""} // 将/api替换掉
}
}
}
}
访问 http://localhost:8080/api/test 就会被代理到 http://localhost:3000/test 上,而如果JSESSIONID设置了httpOnly不能重写cookie到target,故无效
24. 一般跨域发送请求时,会发两个请求,为啥?
所有跨域的
js在提交post请求的时候,如果服务端设置了可跨域访问,都会默认发送两次请求,第一次是预检请求(options请求),查询是否支持跨域,第二次才是真正的post提交。
产生options请求的原因:
- 产生了复杂请求(自定义
header头,携带用户信息) - 发生了跨域
25. call 、apply、bind的区别是什么,哪个性能更好 ?
一般来说,
this总是指向调用某个方法地对象,使用上述三个方法都可以改变this指向,当想使用一个对象中的某个方法时不用重写,使用call和apply可以实现“劫持”。
- 三者第一个参数都是
this要指向的对象,如果没有这个参数或参数为undefined或null,则默认指向全局window - 三者都可以传参,但是
apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入 bind是返回绑定this之后的函数,是一个新函数,apply、call则是立即执行。
call 的性能更好些,因为它内部少了一次将apply第二个参数解构的过程。
add.call(this,5,8);
add.apply(this,[12,34]);
add.bind(this,2);
26. 关于重绘和重排 ?
- 重绘:某些元素的外观被改变,例如:元素的填充颜色
- 重排:局部或者整体布局发生改变,需要重新生成布局,重新排列元素。
重绘不一定导致重排,但重排一定会导致重绘。重排的开销要远远大于重绘,会破坏用户体验,并且让UI展示非常迟。所以我们要尽量减少页面重排次数。
导致重排的场景:
- 添加或删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代
- 页面一开始渲染的时候(这避免不了)
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
如何减少重排:
- 对于那些复杂的动画,对其设置
position: fixed/absolute,尽可能地使元素脱离文档流,从而减少对其他元素的影响 - 避免频繁操作样式
- 动画样式启用GPU加速,
transform、opacity、filters这些动画不会引起回流重绘 - 减少重排范围,尽可能将样式加在具体元素上,而不是它的父级
- 避免使用
table布局
27. 进程和线程的区别 ?
一个程序至少有一个进程,第一个叫主进程,其余都是子进程,一个进程可以包含多个线程。他们是包含的关系。
stateDiagram-v2
程序 --> 进程1
程序 --> 进程2
进程1 --> 线程1
进程1 --> 线程2
进程2 --> 线程3
进程2 --> 线程4
- 线程的性能高,但安全性低,同一个进程之内的线程之间共享内存,共享计数器。
- 进程性能低,但安全性高,每个进程有自己独立的内存,独立的计数器,进程和进程之间一般是不共享数据的;
- 进程要分配一大部分的内存,而线程只需要分配一部分栈就可以了。
- 进程是资源分配的最小单位,线程是程序执行的最小单位。
- 一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行
nodejs是多进程语言
js: new Worker()
node:cluster.for()
28. 单页面应用,初始进入正常,当跳转后再刷新,页面404,是发生了什么问题?
前端单页面应用的场景中的
history的模式下,url发生改变,此时如果手动去刷新页面,浏览器会认为是请求了一个新的页面,会发起http请求,而新的页面是不存在的(后端没配置的话),所以就会导致404。
nginx 方式解决
当按下回车之后,浏览器发出的
http去请求html文件,在通过一系列的转发和寻址解析后,被目标IP所在服务器上的80端口(默认)接收,这个时候,服务器的80接口拿到http请求后,它不知道要去返回什么,这个时候就需要Nginx进行静态资源代理,告诉服务器返回什么静态文件。
server{
listen 8080;
server_name localhost;
root /usr/share/nginx/lhtml/dist;
try_files $uri /index.html; // 添加这一条
index index.html;
charset utf-8;
}
使用webpack解决
去开启
historyApiFallback即可
devServer:{
historyApiFallback:true
}
29. Map 和Weakmap 的区别?
WeakMap是ES6中新增的一种集合类型,叫做弱映射。它和Map是兄弟关系,与Map的区别在于这个弱字,API还是Map那套API。
Map的键可以是任意类型,WeakMap只接受对象作为键,不接受其它类型的值作为键Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键;WeakMap的键是弱引用,如果创建了一个弱引用对象,不会被垃圾回收关注,如果不再需要,weakmap中的键名对象和所对应的键值对会自动消失,不再手动删除引用。Map可以被遍历,WeakMap不能被遍历
恭喜你!你已经学完了一大半的内容,应对面试的70% 左右的问题已经不在话下了,加油!你马上就成功了!
六、React 相关
1. vue or react 有什么相同点? 又有什么区别?
关于
vue和react的讨论一直是舆论的重头,拒一位不愿透露姓名的大佬说:现在的现状是一般大的公司用react的较多,小公司用vue的较多,但究竟哪个更优没有一个绝对的说法,都是相对而言,我个人观点是:哪个用的舒服用哪个就可以,下面就分析一下关于两个框架的区别。
使用虚拟dom代替真实dom的思路相同
Vue与React都使用了 Virtual DOM + Diff算法, 不管是Vue的Template模板+options api 写法, 还是React的Class或者hooks写法,最后都是生成render函数,而render函数执行返回VNode(虚拟DOM的数据结构,本质上是棵树)。
当每一次UI更新时,总会根据render重新生成最新的VNode,然后跟以前缓存起来老的VNode进行比对,再使用Diff算法(框架核心)去真正更新真实DOM(虚拟DOM是JS对象结构,同样在JS引擎中,而真实DOM在浏览器渲染引擎中,所以操作虚拟DOM比操作真实DOM开销要小的多)。
对diff算法的优化基本上思路是相同的:
tag不同认为是不同节点- 只比较同一层级,不跨级比较
- 同一层级的节点用
key唯一标识,tag和key都相同则认为是同一节点
dom的更新策略不同
react会自顶向下全diff。vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。Diff算法借助元素的Key判断元素是新增、删除、修改,从而减少不必要的元素重渲染。
react:
在
react中,当状态发生改变时,组件树就会自顶向下的全diff, 重新render页面, 重新生成新的虚拟dom树, 然后新旧dom树进行比较, 进行patch打补丁方式,局部更新dom。所以react为了避免父组件更新而引起不必要的子组件更新, 可以在shouldComponentUpdate做逻辑判断,减少没必要的render, 以及重新生成虚拟dom,做差量对比过程。
vue:
Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
书写方式的区别:
vue:
vue推荐的做法是webpack+vue-loader的单文件组件格式,vue保留了html、css、js分离的写法,使得现有的前端开发者在开发的时候能保持原有的习惯,更接近常用的web开发方式,对于熟悉原生的开发者上手更容易。其中<style>标签还提供了一个可选的scoped属性,它会为组件内CSS指定作用域,用它来控制仅对当前组件有效还是全局生效。
react:
用过
react的开发者可能知道,react是没有模板的,直接就是一个渲染函数,它中间返回的就是一个虚拟DOM树,React推荐的做法是JSX + inline style, 也就是把HTML和CSS全都写进JavaScript了,即'all in js'。JSX实际就是一套使用XML语法,用于让我们更简单地去描述树状结构的语法糖。在react中,所有的组件的渲染功能都依靠JSX。你可以在render()中编写类似XML的语法,它最终会被编译成原生JavaScript,JSX是基于JS之上的一套额外语法,学习使用起来有一定的成本。
关于数据绑定:
vue :
是以双向数据绑定著称,但又是单向数据流,这是为什么?其实这两个并不冲突,单向数据流是指数据的流向是单一个方向的,即:从父组件 => 子组件,当子组件接收到父组件传递过来的数据后,想修改父组件的数据是不可以的,只可以由父组件修改,然后再传递给子组件。
数据流向:
graph TD
父组件 --> 子组件
那么双向数据绑定又是怎么回事呢?
双向绑定的关键点在于
data如何更新view,因为view更新data其实可以通过事件监听即可,比如input标签监听'input'事件就可以实现了。如何知道数据变了,其实就是通过Object.defineProperty()对属性设置一个set函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面就可以实现data更新view了。
react:
react同样为单向数据流,但数据绑定是单向绑定,不能直接修改state,要求我们使用setState来改变数据,它不是马上就会生效的,它是异步的。所以不要认为调用完setState后可以立马获取到最新的值。多个顺序执行的setState不是同步的一个接着一个的执行,会加入一个异步队列,然后最后一起执行,即批处理。相对vue的双向绑定来说,react的数据操作更清晰。
总结:
关于是用 vue 还是 react ,各个公司都有自己的选择,无论是哪个都是为了提升我们的工作效率而生的,我用过vue ,也用过react,vue的社区大部分是作者尤雨溪或作者的团队运营,周边的方法都是官方推荐的,提供了一系列的api;React JS 库是由 Facebook 创建的,拥有大量的贡献者以及一个庞大的开发者社区,为各种问题贡献他们的解决方案,社区相当活跃的,灵活性也更高。所以无论哪个,各有优缺点。
2. 如何理解受控组件和非受控组件?
React中受控和非受控的概念通常跟form表单组件相关,比如input,通过区分与input进行数据交互的方式,组件被分成两个不同的派系,受控与非受控。
-
受控组件:
React中受控组件的是指表单元素的控制是交给React,表单元素的值是完全交由组件的state控制。 -
非受控组件: 非受控组件指表单元素的状态并不受
React组件状态的影响,表单元素的值存储于DOM元素中。如果要React组件要获取DOM元素的值,需要通过绑定ref的方式去获取。
3. 使用React 组件开发有哪些好处?
- 速度快:
React通过对DOM的模拟,最大限度地减少与DOM的交互,解决了以往需要大量操作DOM带来的性能问题; - 组件复用:组件化开发,一切皆组件,提高代码复用率;
jsx:扩展了js的书写方式,更符合趋势,提高开发效率;
4. react 中的那些hooksapi?
5 . React中Fiber 机制怎么理解
为什么会出现fiber?
react 16之前使用的是stack架构,递归遍历组件树成本很高,由于递归执行,更新一旦开始,中途就无法中断。会造成主线程被持续占用,结果就是主线程上的布局、动画等周期性任务就无法立即得到处理。当层级很深时,递归更新时间超过16ms,会造成视觉上的卡顿,影响用户体验。
fiber解决了哪些问题?
官方的一句话解释是
React Fiber是对核心算法的一次重新实现。Fiber架构任务分解,就是把一个耗时长的任务分解为一个个的工作单元,每个单元运行时间很短,在执行工作单元之前,由浏览器判断是否有空余执行时间,有时间就执行工作单元,执行完成后继续判断是否有空余时间,有时间就从终止的地方继续执行工作单元,一直重复到任务结束,这样处理解决了主线程的持续占用造成的卡顿问题。(好比:凌波微步)
关于fiber的生命周期分为 render阶段 和 commit阶段:
render阶段包括调度器和协调器两部分:
scheduler调度器
调度任务优先级,高优先级的优先
diff,Fiber使用requestAnimationFrame来处理优先级较高的更新,使用requestIdleCallback处理优先级较低的更新。rIC的执行时机是由浏览器控制的,能更好的保证体验,优化性能,是fiber的核心,。
名词解释:
requestAnimationFrame:通知浏览器某些 JavaScript 代码要执行动画了,这样浏览器就可以在运行某些代码后进行适当的优化,是为了实现更流畅和性能更好的动画。
requestIdleCallback:是在 eventLoop 空闲 时候被唤起,将任务切分到多个帧之间,保证子任务不会出现成为 long task,为了在渲染空闲时间执行优先级不高的操作,以避免阻塞渲染。
reconciler协调器
在
DOM需要更新的时候,通过diff算法可以计算出虚拟DOM中真正变化的部分,只针对变化的部分进行更新渲染,而不是重新渲染整个页面,减少不必要的性能浪费。
React通过自己的优化,将O(N^3)的时间复杂度降到了O(N)
(时间复杂度不是计算程序具体运行的时间,而是算法执行语句的次数)
React具体的优化策略:
tree diff(树策略)
对树进行分层比较,两棵树只会对同一层次的节点进行比较。
component diff(组件策略)
-
如果是同一类型的组件,按照原策略继续比较虚拟
DOM树,React向用户提供了shouldComponentUpdate()来判断该组件是否需要进行diff。 -
如果不是同一类型的组件,则将该组件判断为
dirty component,从而替换掉整个组件下面的所有子节点
element diff(元素策略)
对于同一层级的一组子节点,通过唯一id进行区分。
commit阶段:
又叫
renderer渲染器,主要工作内容是把render阶段更新diff的结果进行渲染。
工作内容包括: 执行dom 操作前、执行dom 操作、执行操作后的工作,细节部分为:遍历effectList,变量赋值,状态重置,ref 的绑定和解绑,替换fiber树,调度useEffect 执行和销毁等。
注意:协调器和调度器可能会被中断,因为在内存中,不会影响页面,commit 阶段是不可中断的。
6. React-hooks 原理
hooks是react16.8的新特性,它可以让你在不编写class的情况下使用state以及其他的react特性
hooks 主要是利用闭包来保存状态,state 当做一个链表的形式,用一个useState 就去表里取一下,使用链表保存一系列hooks,将链表中的第一个hooks 与 fiber关联,在fiber 树更新时,就能从hooks 中计算出最终输出的状态和执行相关的副作用。
7. useState 和 this.setState 对比有什么区别?
这两个是
react两种用在不同组件的管理状态的方案。useState是使用在函数组件中的,this.setState是用在类组件中。
在更新之前有区别,useState有优化策略,如果你上次的value和下一次的一样,会触发eagerState策略,不会走到更新。this.setState则不会处理,会走到scheduleUpdateOnFiber。这也是他们在触发时最大的不同。
8. hooks 的使用限制是什么?
在react 16.8 之后,推荐使用
hooks函数式组件的方式来书写,其优势是相对简洁,且合并了之前众多的生命周期,那hooks有哪些使用限制呢?
- 不能在
for循环中使用 - 不能在类组件中使用
- 不能在
if判断中使用
原因:hooks 的结构是链表结构,上述操作会打断结构,导致取值错误等问题。
这里推荐一些react好用的hooks库,提升效率杠杠哒 !
9. react 的错误边界概念?
当部分
js出现错误后,不应该导致整个程序崩溃,为了解决这个问题,react 16引入了一个错误边界的概念。
错误边界是一种 class 组件,这种组件可以捕获发生在其子组件树任何位置的 js 错误,并打印这些错误,同时展示降级 UI,而并不会渲染那些发生错误的子组件树。错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法以及构造函数中的错误。
如果一个 class 组件中定义了 static getDerivedStateFromError()或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch()打印错误信息。
只有 class 组件才可以成为错误边界组件,大多数情况下, 你只需要声明一次错误边界组件, 并在整个应用中使用它。不能在hooks组件中使用。
10. useEffect 和 useLayout的区别?
作为组件的副作用,本质上是一样的,区别是最终的执行时机不同,一个异步一个同步,这使得
useEffect不会阻塞渲染,而useLayoutEffect会阻塞渲染
注意:不要在服务端渲染组件中引入 useLayoutEffect 代码时会触发React 告警。解决这个问题,需要将代码逻辑移至 useEffect 中即可。
react建议:尽可能使用标准的useEffect以避免阻塞视觉更新。绝大部分场景只用到useEffect就可以,只有当它出问题的时候再尝试使用useLayoutEffect。
11. class组件的生命周期
常用的生命周期如下:
在渲染前调用:
componentWillMount在第一次渲染后调用:
componentDidMount在组件完成更新前调用:
componentWillUpdate在组件完成更新后立即调用:
componentDidUpdate在组件接收到一个新的 prop (更新后)时被调用:
componentWillReceiveProps根据判断来决定是否需要
diff:shouldComponentUpdate在组件从 DOM 中移除之前立刻被调用:
componentWillUnmount
12. 使用hooks可以模拟class的哪些生命周期?
这个问题是我真实遇到过多次的问题,我刚开始很是不理解,现在都在使用
hooks,为啥要问我如何来用hooks去实现class,我现在猜想,可能他们现在都有些项目还有在用class的项目需要维护吧。
关于两个生命周期的不同:
class是从页面的整体渲染来划分生命周期;hooks是从状态改变来划分生命周期,class提供的这些生命周期其实是足够细的,包含渲染的各个时期;hooks更为简单粗暴一些,只提供了useEffect,作为函数组件,确实也不用去关心页面的渲染,重点只关心状态改变即可。
尝试用hooks 来实现 class 的生命周期:
-
当
useEffect第二个参数不存在时,每次渲染后都会触发回调,类似于componentDidUpdate。 -
当
useEffect第二个参数的数组存在并有值时,如果数组中的任何值发生更改,则每次渲染后都会触发回调,类似于componentDidUpdate -
当
useEffect第二个参数是一个return的函数时,在组件卸载前触发,类似于componentWillUnmount。 -
useLayoutEffect(fn,[])类似于componentDidMount -
使用
React.memo的第二个参数来模拟shouldComponentUpdate
总结如下:
| Class | Hooks |
|---|---|
componentDidMount | useLayoutEffect(fn,[]) |
componentDidUpdate | 当useEffect第二个参数不存在 / 数组存在并有值时 |
componentWillUnmount | 当useEffect第二个参数是一个return的函数时 |
shouldComponentUpdate | 通过React.memo第二个参数来对比是否需要render |
扩展的知识:
一般网上关于类似于componentDidMount声明周期的方法,都会说是可以使用useEffect(fn,[])代替。这里我想说他们是不同的,useEffect(fn,[]) 是在commit阶段完成后异步调用的,componentDidMount 是在commit 阶段完成试图更新后,在layout 阶段同步调用的。
如果要说和componentDidMount 调用一致的,那就是useLayoutEffect(fn,[])。
这三个生命周期(
componentWillMount、componentWillUpdate、componentWillReceiveProps)hooks模拟不了。因为hooks关注的是自身或者父组件状态变化后的要做哪些改变。
七、webpack 相关
1. 关于tree shaking?
什么是tree shaking?
Tree-shaking 这一术语在前端社区内,起初是 Rich Harris 在 Rollup 中提出。简单概括起来,Tree-shaking 可以使得项目最终构建(Bundle)结果中只包含你实际需要的代码。我们都知道
JavaScript绝大多数情况需要通过网络进行加载再执行,所以加载的文件越小,整体执行时间将会更短。
Tree Shaking 具体指的就是当我引入一个模块的时候,我不引入这个模块的所有代码,我只引入我需要的代码,本质是消除无用的js代码,提高加载速度。
原理:
ES6 Module引入进行静态分析,故而编译的时候正确判断到底加载了那些模块
静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码
Webpack中如何使用tree shaking?
webpack里面自带Tree Shaking这个功能来帮我们实现。
生产环境配置: 会自动开启tree shaking
mode: 'production'
开发环境配置:需要手动配置 optimization 选项,才能够 Tree Shaking
usedExports: true
2. webpack 中的 loader 和plugin的区别?
webpack唯一的功能就是打包,那么在打包前和打包过程中loader和plugin又是起着什么作用呢?
使用功能不同:
loader: 帮webpack 预处理文件,因为webpack 只认识js 文件,其他都不认,loader 增强了webpack 的功能,帮助webpack 识别其他的文件。
plugin: 并不是直接操作单个文件,它直接对整个构建过程起作用,从打包优化到压缩,在webpack 的声明周期会广播出许多事件,plugin 监听这些事件,在不同的时机通过webpack 提供的api 改变输出结果,开启gzip、开发环境去警告等
运行时机不同:
loader 在打包之前,plugin 在整个编译声明周期起作用。
3. webpack 的babel-loader都做了哪些事情?
-
将
Es6+的js代码转为es5,以便在更多的浏览器上运行 -
将
jsx ts等转为普通的js -
代码压缩,以减小项目体积
cacheCompression -
代码缓存,为了使重新编译的时间更短
cacheDirectory -
支持异步操作,以便在项目中更好地处理异步操作
4. npm run build 之后,webpack整个构建过程做了什么?
-
初始化参数: 解析
webpack配置参数,合并shell传入和webpack.config.js文件配置的参数,形成最后的配置结果; -
开始编译:上一步得到的参数初始化
compiler对象,注册所有配置的插件,插件监听webpack构建生命周期的事件节点,做出相应的反应,执行对象的run方法开始执行编译; -
确定入口:从配置的
entry入口,开始解析文件构建AST语法树,找出依赖,递归下去; -
编译模块:递归中根据文件类型和loader配置,调用所有配置的
loader对文件进行转换,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理; -
完成模块编译:递归完成后,得到每个文件结果,包含每个模块以及他们之间的依赖关系,根据
entry或分包配置生成代码块chunk; -
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
chunk,再把每个chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。 -
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
八、其他业务层面
1. 创建一个组件库,除基本组件外,应该关注的点有哪些?组件发布时,关注的点有?
- 文档:保持和代码同步的更新频率,同步最新更新的记录到文档中,设定一个主要负责人,定期审查;
- 更新规则:非
bug fix的问题,提前收集整理多方需求,确认上线时间要求,合理安排排期,最多一迭代一更新; - 测试:在测试资源充足的情况下,安排测试人员对组件进行测试;如无测试资源,保证组件功能说明文档详尽,待业务提测后一同进行测试。
2. 按钮级别的权限怎么做 ?
-
首先将权限分层来管理 例如:菜单权限、模块权限、按钮权限。再将其进行分层判断,层层拦截,如果没有菜单权限,即不会再进行模块和按钮权限判断。一定程度上避免一些
bug产生。 -
关于具体的按钮权限: 可以通过配置来实现,将按钮做成字典表,然后实现一个鉴权的函数,通过判断当前角色是否有权限来显示按钮,当角色没有权限时,不显示操作权限。
九、算法
1. 实现找到最大字符并输出其个数
const array1 =[1,3,5,6,8,8,8];
function foo(arr) {
const maximum = Math.max(...arr);
const maximumArr = arr.filter(function(item,index){
return item === maximum;
});
return {maximum, "length":maximumArr.length}
}
打印结果:
2. 数组去重
去重的方法不止一种,下面推荐几个比较常用的方法:
使用es6新增的方法:new Set
使用filter + indexOf 方法: filter
使用 includes : includes
3. 冒泡排序
第一种:
let arr = [10, 9, 7, 5, 7, 8, 9, 3, 2, 0];
let len = arr.length;
for (let j = 0; j < len - 1; j++){
for (let i = 0; i < len - 1 - j; i++){
if (arr[i]>arr[i+1]) {
[arr[i + 1], arr[i]] = [arr[i], arr[i + 1]];
}
}
}
// [0, 2, 3, 5, 7, 7, 8, 9, 9, 10]
第二种:
let arr = [10, 9, 7, 5, 7, 8, 9, 3, 2, 0];
let len = arr.length;
for (var i = 0; i < len; i++){
for (var j = i + 1; j < len; j++){
if (arr[i] > arr[j]) {
[arr[i],arr[j]]=[arr[j],arr[i]]
}
}
}
// [0, 2, 3, 5, 7, 7, 8, 9, 9, 10]
4. 展开多维数组
arr.flat()方法可以展开多维数组,但当面试官要求使用其他方法实现,你有什么想法吗?
最简洁:
let arr = [1, 2, 4, 6,[5, 4, 5,[98, 3], [34], [7]]];
arr.toString().split(',').map(Number);
步骤解析:
es6 的扩展运算符
let arr = [1, 2, 4, 6,[5, 4, 5,[98, 3], [34], [7]]];
function flatten(arr){
while(arr.some(item=>Array.isArray(item))){
arr = [].concat(...arr);
}
return arr;
}
var sunArr = [1,2,3,[4,[5,[6]]]];
flatten(sunArr);
使用递归来处理:
let arr = [1, 2, 4, 6,[5, 4, 5,[98, 3], [34], [7]]];
function f(arr=[],newArr=[]) {
arr.forEach((item) => {
if (!Array.isArray(item)) {
newArr.push(item);
} else {
f(item, newArr);
}
});
return newArr;
}
f(arr);
打印结果:
十、其他随机问题
这项记录的是一些开放性问题,没有标准答案,但也不能全靠随机应变,我也只是给大家提供一些思路和小的方法,大家仅供参考!
1. 自我介绍
-
首先自报家门,然后告诉面试官自己从事本项工作时长,分别在哪几家公司,分别负责的项目和具体使用到的技术栈。
-
加分项:如果有博客或者开源项目可以介绍一下,或者最近自己学习了哪些新的技能知识
-
加分项:自己平常工作中的一些好的习惯,例如:沟通能力、管理能力、学习能力
2. 你的优缺点?
这个问题是个开放性的问题,也是面试官让你自己充分介绍他所不了解的你。在平常的生活中如果有人问到这个问题,我们可能不太重视,想到哪点说哪点,也不会做整理和优化,当然我也不是教给大家说谎话,我们要从实际出发,真实阐述自己的(与当前应聘职位相关的)优点和缺点。下面我给大家分享一篇我认为可以很好阐述这个问题的文章:链接地址
3. 以后的职业规划
这个问题同样开放性比较大,大概是可以分为两个方向: 技术的深度or广度、管理层
两个方向,有不同的重点,在技术领域,无论是深度还是广度,都需要的是耐心、自律能力、持续学习能力等硬技能,而在管理方向,则需要精进自己的沟通能力、共情能力、协调能力等软技能,根据自己的想法应答即可。
4. 你平常的学习途径 ?
这个问题因人而异,不同的人有不同的学习习惯,进而就会选择不同的学习途径,下面就列出我的学习途径给大家参考。
-
掘金社区 / 掘金小册 / csdn / sf / 知乎
-
b站 学习资料
-
某专项技术的官网
-
微信公众号
-
犀牛书
推荐阅读: