导图社区 前端面试-总-2
前端面试-总-2,详尽地拆解为讲概念、说用途、优缺点以及延伸等多个方面,无论是初学者还是有一定经验的前端开发者,都可以从这份流程图中获得宝贵的指导和启示。
编辑于2024-11-17 08:28:55前端面试 讲概念;说用途;优缺点;延伸
1. HTML+CSS
1.盒模型了解吗?
a. 标准盒模型; b. 怪异盒模型; c. flex弹性伸缩盒模型;d. column多列布局盒模型 标准盒模型和IE盒模型的区别在于设置width和height时,所对应的范围不同 标准盒模型的width和height属性的范围只包含了content; IE盒模型的width和height属性的范围包含了border、padding和content。
● box-sizeing: content-box表示标准盒模型(默认值) ● box-sizeing: border-box表示IE盒模型(怪异盒模型)
2. felx:1; 是哪些属性的缩写
flex-grow(设置了对应元素的增长系数) flex-shrink(指定了对应元素的收缩规则,只有在所有元素的默认宽度之和大于容器宽度时才会触发) flex-basis(指定了对应元素在主轴上的大小)
3. 隐藏元素的方法
display: none 不会在页面中占据位置 visibility: hidden 在页面中仍占据空间 opacity: 0 在页面中仍然占据空间 position: absolute 用绝对定位将元素移除可视区域内 z-index: 负值使其他元素遮盖住该元素 clip/clip-path 使用元素裁剪的方法;仍在页面中占据位置,但是不会响应绑定的监听事件。 transform:scale(0,0) 将元素缩放为 0;仍在页面中占据位置,但不会响应绑定的监听事件
4.src和href的区别
src和href都是用来引用外部的资源 src:表示对资源的引用,它指向的内容会嵌入到当前标签所在的位置。src会将其指向的资源下载并应⽤到⽂档内,如请求js脚本。当浏览器解析到该元素时,会暂停其他资源的下载和处理,直到将该资源加载、编译、执⾏完毕,所以⼀般js脚本会放在页面底部。 href:表示超文本引用,它指向一些网络资源,建立和当前元素或本文档的链接关系。当浏览器识别到它他指向的⽂件时,就会并⾏下载资源,不会停⽌对当前⽂档的处理。 常用在a、link等标签上。
5.iframe 的优缺点
iframe 元素会创建包含另外一个文档的内联框架(即行内框架)。 优点: ● 用来加载速度较慢的内容(如广告) ● 可以使脚本可以并行下载 ● 可以实现跨子域通信 缺点: ● iframe 会阻塞主页面的 onload 事件 ● 无法被一些搜索引擎索识别 ● 会产生很多页面,不容易管理
6.Canvas和SVG的区别
SVG:可缩放矢量图形。 特点: ● 不依赖分辨率 ● 支持事件处理器 ● 最适合带有大型渲染区域的应用程序(比如谷歌地图) ● 复杂度高会减慢渲染速度(任何过度使用 DOM 的应用都不快) ● 不适合游戏应用 Canvas:画布,通过Javascript来绘制2D图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。 特点: ● 依赖分辨率 ● 不支持事件处理器 ● 弱的文本渲染能力 ● 能够以 .png 或 .jpg 格式保存结果图像 ● 最适合图像密集型的游戏,其中的许多对象会被频繁重绘
7.script标签中defer和async的区别
区别: 执行顺序:多个带async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行; 脚本是否并行执行:async属性,表示后续文档的加载和执行与js脚本的加载和执行是并行进行的,即异步执行;defer属性,加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),js脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded事件触发执行之前。
8.transition和animation的区别
transition是过度属性,强调过度,它的实现需要触发一个事件(比如鼠标移动上去,焦点,点击等)才执行动画。它类似于flash的补间动画,设置一个开始关键帧,一个结束关键帧。 animation是动画属性,它的实现不需要触发事件,设定好时间之后可以自己执行,且可以循环一个动画。它也类似于flash的补间动画,但是它可以设置多个关键帧(用@keyframe定义)完成动画。
9.CSS 优化和提高性能的方法
a. 加载性能
1)css压缩:将写好的css进行打包压缩,可以减小文件体积。 2)css单一样式:当需要下边距和左边距的时候,很多时候会选择使用 margin:top 0 bottom 0;但margin-bottom:bottom;margin-left:left;执行效率会更高。 3)减少使用@import,建议使用link,因为后者在页面加载时一起加载,前者是等待页面加载完成之后再进行加载。
b.选择器性能
(1)关键选择器(key selector)。选择器的最后面的部分为关键选择器(即用来匹配目标元素的部分)。CSS选择符是从右到左进行匹配的。当使用后代选择器的时候,浏览器会遍历所有子元素来确定是否是指定的元素等等; (2)如果规则拥有ID选择器作为其关键选择器,则不要为规则增加标签。过滤掉无关的规则(这样样式系统就不会浪费时间去匹配它们了)。 (3)避免使用通配规则,如*{}计算次数惊人,只对需要用到的元素进行选择。 (4)尽量少的去对标签进行选择,而是用class。 (5)尽量少的去使用后代选择器,降低选择器的权重值。后代选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每一个标签元素。 (6)了解哪些属性是可以通过继承而来的,然后避免对这些属性重复指定规则。
c.渲染性能
(1)慎重使用高性能属性:浮动、定位。 (2)尽量减少页面重排、重绘。 (3)去除空规则:{}。空规则的产生原因一般来说是为了预留样式。去除这些空规则无疑能减少css文档体积。 (4)属性值为0时,不加单位。 (5)属性值为浮动小数0.**,可以省略小数点之前的0。 (6)标准化各种浏览器前缀:带浏览器前缀的在前。标准属性在后。 (7)不使用@import前缀,它会影响css的加载速度。 (8)选择器优化嵌套,尽量避免层级过深。 (9)css雪碧图,同一页面相近部分的小图标,方便使用,减少页面的请求次数,但是同时图片本身会变大,使用时,优劣考虑清楚,再使用。 (10)正确使用display的属性,由于display的作用,某些样式组合会无效,徒增样式体积的同时也影响解析性能。 (11)不滥用web字体。对于中文网站来说WebFonts可能很陌生,国外却很流行。web fonts通常体积庞大,而且一些浏览器在下载web fonts时会阻塞页面渲染损伤性能。
d.可维护性、健壮性
(1)将具有相同属性的样式抽离出来,整合并通过class在页面中进行使用,提高css的可维护性。 (2)样式与内容分离:将css代码定义到外部css中。
10.z-index属性在什么情况下会失效
(1)浮动: 在设置z-index同时设置浮动,会失效 解决办法:去浮动,改为display:inline-block; (2)父元素的position:父元素position为relative时,子元素的z-index失效。 解决办法:父元素position改为absolute或static; (3)元素的position:元素没有设置position属性为非static属性。 解决办法:将position设置为relative、absolute或fixed中的一种
11.对媒体查询的理解
媒体查询由⼀个可选的媒体类型和零个或多个使⽤媒体功能的限制了样式表范围的表达式组成,例如宽度、⾼度和颜⾊。媒体查询,添加⾃CSS3,允许内容的呈现针对⼀个特定范围的输出设备⽽进⾏裁剪,⽽不必改变内容本身,适合web⽹⻚应对不同型号的设备⽽做出对应的响应适配。 媒体查询包含⼀个可选的媒体类型和满⾜CSS3规范的条件下,包含零个或多个表达式,这些表达式描述了媒体特征,最终会被解析为true或false。如果媒体查询中指定的媒体类型匹配展示⽂档所使⽤的设备类型,并且所有的表达式的值都是true,那么该媒体查询的结果为true。那么媒体查询内的样式将会⽣效。
<!-- link元素中的CSS媒体查询 --> <link rel="stylesheet" media="(max-width: 800px)" href="example.css" /> <!-- 样式表中的CSS媒体查询 --> <style> @media (max-width: 600px) { .facet_sidebar { display: none; } } </style>
简单来说,使用 @media 查询,可以针对不同的媒体类型定义不同的样式。@media 可以针对不同的屏幕尺寸设置不同的样式,特别是需要设置设计响应式的页面,@media 是非常有用的。当重置浏览器大小的过程中,页面也会根据浏览器的宽度和高度重新渲染页面。
12.两栏布局的方法
(1)利用浮动:将左边元素宽度设置为200px,并且设置向左浮动。将右边元素的margin-left设置为200px,宽度设置为auto(默认为auto,撑满整个父元素) .outer { height: 100px; } .left { float: left; width: 200px; background: tomato;} .right { margin-left: 200px; width: auto; background: gold;}
(2)利用浮动:左侧元素设置固定大小,并左浮动,右侧元素设置overflow: hidden; 这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。 .left{ width: 100px; height: 200px; background: red; float: left;} .right{ height: 300px; background: blue; overflow: hidden;}
(3)利用flex布局:将左边元素设置为固定宽度200px,将右边的元素设置为flex:1
(4)利用绝对定位:将父级元素设置为相对定位。左边元素设置为absolute定位,并且宽度设置为200px。将右边元素的margin-left的值设置为200px .outer { position: relative; height: 100px; } .left { position: absolute; width: 200px; height: 100px; background: tomato;} .right { margin-left: 200px; background: gold;}
(5)利用绝对定位:将父级元素设置为相对定位。左边元素宽200px,右边元素绝对定位,左边定位200px,其余方向定位为0。 .outer { position: relative; height: 100px; } .left { width: 200px; background: tomato; } .right { position: absolute; top: 0; right: 0; bottom: 0; left: 200px; background: gold; }
13.三栏布局:如何实现双飞翼(圣杯)布局?
(1)利用绝对定位:左右两栏设置为绝对定位,中间设置对应方向大小的margin的值。 .outer { position: relative; height: 100px; } .left { position: absolute; width: 100px; height: 100px; background: tomato; } .right { position: absolute; top: 0; right: 0; width: 200px; height: 100px;background: gold;} .center { margin-left: 100px; margin-right: 200px; height: 100px; background: lightgreen;}
(2)利用flex布局:左右两栏设置固定大小,中间一栏设置为flex:1 .outer { display: flex; height: 100px; } .left { width: 100px; background: tomato; } .right { width: 100px; background: gold;} .center { flex: 1; background: lightgreen;}
(3)利用浮动:左右两栏设置固定大小,并设置对应方向的浮动。中间一栏设置左右两个方向的margin值,注意这种方式,中间一栏必须放到最后: .outer { height: 100px; } .left { float: left; width: 100px; height: 100px; background: tomato;} .right { float: right; width: 200px; height: 100px; background: gold; } .center { height: 100px; margin-left: 100px; margin-right: 200px; background: lightgreen;}
(4)圣杯布局(利用浮动和负边距来实现):父级元素设置左右的 padding,三列均设置向左浮动,中间一列放在最前面,宽度设置为父级元素的宽度,因此后面两列都被挤到了下一行,通过设置 margin 负值将其移动到上一行,再利用相对定位,定位到两边。 .outer { height: 100px; padding-left: 100px; padding-right: 200px;} .left { position: relative; left: -100px; float: left; margin-left: -100%; width: 100px; height: 100px;background: tomato; } .right { position: relative; left: 200px; float: right; margin-left: -200px; width: 200px; height: 100px; background: gold; } .center { float: left; width: 100%; height: 100px; background: lightgreen;}
(5)双飞翼布局:相对于圣杯布局来说,左右位置的保留是通过中间列的 margin 值来实现的,而不是通过父元素的 padding 来实现的。本质上来说,也是通过浮动和外边距负值来实现的。 .outer { height: 100px; } .left { float: left; margin-left: -100%; width: 100px; height: 100px; background: tomato;} .right { float: left; margin-left: -200px; width: 200px; height: 100px; background: gold;} .wrapper { float: left; width: 100%; height: 100px; background: lightgreen;} .center { margin-left: 100px; margin-right: 200px; height: 100px;}
(1)五种法案的优缺点?
浮动:缺点(因为脱离文档流,要清除浮动);优点(兼容性好) 绝对定位:缺点(脱离文档流,子元素也要脱离文档流,可使用性差);优点(快捷) flex布局:优点(css3的,为了解决上面2个的缺点,手机端都用这个) table布局:优点(很多场景都可以用、兼容性好,当flex解决不了可以用table布局, 比如IE8不支持flex,此时可以用表格布局);缺点(历史上的诟病以外,比如此例子, 两侧的高度不需要随中间高度变化而变化,表格就不行) 网格布局:现如今很多进行12列网格布局
(2)如果高度不确定,中间内容比较多, 要求左右高度随中间高度变化,以上哪 种能用?哪种不能用?
浮动、绝对定位、网格布局都不能用; flex布局、table布局没问题
(3)为什么浮动的中间超出来了?
因为浮动的基本原理:左侧有遮挡物,就随着遮挡物排列; 超出高度后,左侧没有遮挡物了,就左侧排列。 如何解决?答:创建BFC
效果
14.盒子水平垂直居中?
1.容器 flex布局 + 纵、纵对齐方式
.father { display: flex; // 主轴对齐方式 justify-content: center; // 纵轴对齐方式 align-items: center; }
2.容器设置 display: flex; 子项设置 margin: auto;
.box { width: 200px; height: 200px; border: 1px solid; display: flex; } .child { margin: auto; // 水平垂直居中 }
3.绝对定位配合transform 优点:不用关心子元素的长和宽 要考虑浏览器兼容问题
.father { position: relative;} .son { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
4.绝对定位配合margin:auto 缺点:需要知道子元素的宽高
.father { position: relative;} .son { width: 100px; height: 100px; position: absolute; top: 0; left: 0; bottom: 0; right: 0; margin: auto; }
5.绝对定位的方式 absolute + 负margin 缺点:需要知道子元素的宽高
.parent { width: 500px; height: 500px; border: 1px solid #000; position: relative; } .child { width: 100px; height: 100px; border: 1px solid #999; position: absolute; top: 50%; left: 50%; margin-top: -50px; margin-left: -50px; }
6.tabel-cell
.box { width: 200px; height: 200px; border: 1px solid; display: table-cell; // 设置元素在垂直方向上的对齐方式 vertical-align: middle; text-align: center; } .child { background: red; display: inline-block; }
7.grid设置居中 兼容性较差,不推荐
给容器设置 display: grid; align-items: center; justify-content: center; .box { width: 200px; height: 200px; border: 1px solid; display: grid; align-items: center; justify-content: center; } .child { background: red;}
8.grid给子项设置 某些浏览器会不支持grid布局方式, 兼容性较差,不推荐
给容器设置 display: grid; 子项设置 margin: auto; .box { width: 200px; height: 200px; border: 1px solid; display: grid; } .child { background: red; margin: auto; }
9.给容器加给伪元素 适合单行文本,不能换行
给容器加给伪元素,设置line-height等于容器的高度。 给孩子设置display: inline-block; .box { width: 200px; height: 200px; border: 1px solid; text-align: center; } .box::after { content: ""; line-height: 200px; } .child { display: inline-block; background: red; }
如何垂直居中一个 img?
{ display:table-cell; text-align:center; vertical-align:middle; }
15. Localstorage、sessionStorage、cookie 的区别
共同点:都是保存在浏览器端、且同源的 区别: 1、cookie 数据始终在同源的 http 请求中携带(即使不需要),即 cookie 在浏览器和服务器间来回传递,而 sessionStorage 和 localStorage 不会自动把数据发送给服务器,仅在本地保存。cookie 数据还有路径(path)的概念,可以限制 cookie 只属于某个路径下 2、存储大小限制也不同,cookie 数据不能超过 4K,同时因为每次 http 请求都会携带 cookie、所以 cookie 只适合保存很小的数据,如会话标识。sessionStorage 和 localStorage虽然也有存储大小的限制,但比 cookie 大得多,可以达到 5M 或更大 3、数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效;localStorage: 始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie:只在设置的 cookie 过期时间之前有效,即使窗口关闭或浏览器关闭 4、作用域不同,sessionStorage 不在不同的浏览器窗口中共享,即使是同一个页面; localstorage 在所有同源窗口中都是共享的;cookie 也是在所有同源窗口中都是共享的 5、web Storage 支持事件通知机制,可以将数据更新的通知发送给监听者 6、web Storage 的 api 接口使用更方便
16.HTTP协议
HTTP报文的组成部分
HTTP响应报文
响应⾏ / 响应头 / 空⾏ / 响应体
- 响应⾏:由网络协议版本,状态码和状态码的原因短语组成,例如 HTTP/1.1 200 OK 。
- 响应头:响应部⾸组成
- 响应体:服务器响应的数据
http协议中,从用户输入url地址,会发生什么?
会发生从解析url到渲染页面的过程。 1. 首先浏览器会解析url,以及缓存判断,查看本地缓存(浏览器缓存、系统缓存、路由缓存等),如果有则直接显示。 2. 如果都没有,则进行DNS域名解析,解析出对应的IP地址。 3. 浏览器与服务器三次握手,成功建立TCP连接。 4. 浏览器发出http请求。 5. 服务器收到请求,返回资源。 6. 浏览器接收数据后,进行解析、渲染。 7. 最后显示出页面。 8.TCP四次挥手
浏览器是怎么渲染的呢?
过程如下。 1. 解析HTML,生成DOM树。 2. 解析CSS,生成CSSOM树。 3. 将DOM和CSSOM合并,生成渲染树(Render-Tree)。 4. 计算渲染树的布局Layout, 将布局Paint渲染到屏幕上。
http协议中,缓存机制有哪些,如何命中?
强缓存 响应头中的 status 是 200,相关字段有expires(http1.0),cache-control(http1.1),两者同时出现时 cache-control 优先级更高。主要是利用 cache-control 的max-age值来进行判断。 浏览器再次请求服务器时,浏览器会先判断max-age,如果到期则直接请求服务器,否则直接从缓存中读取。
协商缓存 响应头中的 status 是 304,相关字段有 Etag / If-No-Match,Last-Modified / If-Modified-Since。 (1) 服务器的 ETag 和 浏览器的 If-None-Match 对应: Etag(服务器): 上次加载时,服务器的 response header 返回的; If-No-Match(浏览器): 浏览器请求时的 request header 里的,取的是 Etag 的值。 如果服务器为请求的资源确定的 ETag 标头的值与浏览器请求中的 If-None-Match 值相同,则服务器将返回 304 Not Modified。表示资源文件没有发生改变,命中协商缓存。 服务器优先考虑Etag。 Etag 与 Last-Modifed 均是服务器的响应头返回的,如图所示:
2) 服务器的 Last-Modified 和 浏览器请求的 If-Modified-Since 对应: Last-Modified(服务器):该资源文件最后一次更改时间,服务器的 response header 里返回; If-Modified-Since(浏览器):浏览器请求时的 request header 里的,取的是 Last-Modify 的值。 在下一次发送请求时,服务器在接收到会做比对,如果相同,则命中协商缓存。
http协议中,常见的状态码有哪些?
1xx: 指示信息 - 表示请求已接收,继续处理 2xx: 成功 - 表示请求已被成功接收 3xx: 重定向 - 要完成请求必须进行更进一步的操作 4xx: 客户端错误 - 请求有语法错误或请求无法实现 5xx: 服务器错误 - 服务器未能实现合法的请求
200 请求成功。 301 永久重定向,请求的网页已永久移动到新位置,浏览器会自动重定向到新的 url 地址。 302 临时重定向,服务器目前从不同位置的网页响应请求,可使用原有 url 地址。 303 查看其它位置,重定向。 304 Not Modified,资源未作修改。协商缓存。 305 所访问资源必须通过代理访问。 400:请求报文语法有误,服务器无法识别 401:请求需要认证 403:请求的对应资源禁止被访问 404:服务器无法找到对应资源 500 服务器内部错误。 501 服务器不支持请求的功能。 502 网关错误,通常需要后端找原因。 503 服务器超载或系统维护。
http协议 与 https协议 有什么区别?
http协议是明文传输,https协议是http协议+ssl协议构建的,加密传输协议。 1. http 的连接很简单,是无状态的。 2. http 传输的数据都是未加密的,是明文传输。 3. https 协议是由 http和ssl 协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。可防止数据在传输过程中不被窃取、改变,确保数据的完整性。 4. https 协议需要ca证书,费用较高。ssl 证书也需要钱,功能越强大的证书费用越高。
TCP和UDP的区别
1. TCP是面向连接的,UDP 是无连接的,即发送数据前不需要先建立链接。 2. TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。 并且因为tcp可靠,面向连接,不会丢失数据因此适合大数据量的交换。 3. TCP是面向字节流,UDP面向报文,并且网络出现拥塞不会使得发送速率降低(因此会出现丢包,对实时的应用比如IP电话和视频会议等)。 4. TCP只能是1对1的,UDP支持1对1,1对多。 5. TCP是面向连接的可靠性传输,而UDP是不可靠的。
三次握手、四次挥手 详细过程
三次握手(TCP连接建立)
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
1.第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN,此时客户端处于 SYN_SEND 状态。 首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。 第二次握手:服务器收到SYN包后,回应客户端一个SYN-ACK(同步-确认)包,表示同意建立连接,并确认收到的SYN包。此时,服务器进入SYN_RECEIVED状态。 在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y 第三次握手:客户端收到服务器的SYN-ACK包之后,再次向服务器发送一个ACK(确认)包,表示连接的建立。此时,客户端进入ESTABLISHED(已连接)状态,服务器也进入ESTABLISHED状态。 Client --> Server: ACK 完成这三步后,客户端和服务器之间的TCP连接就建立成功了。
四次挥手(TCP连接终止)
四次挥手用于在客户端和服务器之间优雅地关闭TCP连接。过程如下: 第一步挥手: 客户端向服务器发送FIN(结束)包,表示它已经没有数据要发送了,要求关闭连接。此时,客户端进入FIN_WAIT_1状态。 Client --> Server: FIN 第二步挥手: 服务器收到FIN包后,发送ACK包给客户端,确认收到FIN包,并且进入CLOSE_WAIT状态。 Server --> Client: ACK 第三步挥手: 服务器准备好关闭连接时,向客户端发送FIN包,表示它也没有数据要发送了。此时,服务器进入LAST_ACK状态。 Server --> Client: FIN 第四步挥手: 客户端收到FIN包后,发送ACK包给服务器,确认收到FIN包。此时,客户端进入TIME_WAIT状态,以确保服务器收到了ACK包,然后最终进入CLOSED状态。服务器在接收到ACK包后,也进入CLOSED状态,连接彻底关闭。 Client --> Server: ACK 在四次挥手的过程中,确保了双方都能完全断开连接,不会导致数据丢失。以上就是TCP的三次握手和四次挥手的详细过程。
网站性能优化
1. 优化资源加载
压缩文件:使用工具(如 Gzip 或 Brotli)压缩 HTML、CSS 和 JavaScript 文件。 合并文件:将多个 CSS 和 JS 文件合并成一个,减少 HTTP 请求。 使用 CDN:通过内容分发网络(CDN)加速静态资源的加载。
2. 图片优化
图片格式选择:使用适当的图片格式(如 WebP、SVG)以减少体积。 延迟加载:对非视口内的图片使用懒加载技术,以降低初次加载时的负担。 自适应图片:根据不同设备和屏幕尺寸定义不同分辨率的图片。
3. 减少 HTTP 请求
CSS Sprites:将小图标整合成一张大图,减少请求次数。 使用字体图标:利用 FontAwesome 等图标库,替代图片。
4. 使用缓存
合理设置 Cache-Control:为静态资源设置合适的缓存策略。 Service Workers:利用 Service Worker 提高离线能力和资源缓存。
5. 代码优化
移除不必要的代码:删除未使用的 CSS 和 JavaScript 代码。 使用异步加载:对于非关键的 JavaScript 脚本,使用 async 或 defer 属性进行异步加载。 Tree Shaking:在构建过程中剔除无用的代码(主要针对现代的模块打包工具,如 Webpack)
6. 渲染优化
避免重排与重绘:能够减少 DOM 操作,尤其是批量更新时,尽量减少触发重排的操作。 使用虚拟 DOM:如 React 等框架,可以提高渲染性能。
7. 性能监测
使用 Performance API:借助浏览器提供的性能监测工具,检测加载时间与运行效率。
分析工具:使用 Lighthouse、WebPageTest 等工具评估页面性能,并获取优化建议。
8. SEO 与可达性
虽然这不直接关联于性能,但良好的 SEO 实践可以帮助提升站点的访问速度与排名。确保网站结构友好,重视可达性也有助于改善整体用户体验。
post 和 get 的区别
1. 请求用途
get主要用于请求数据; post: 用于提交数据或创建资源, 常用于表单提交和上传文件
2.数据传输方式
GET:参数附加在 URL 的查询字符串中(即 URL 后面的部分)。 受限于 URL 长度,通常不适合发送大量数据。 POST:数据通过请求体(body)发送,通常不显示在 URL 中。 可以发送大量数据,包括文本和文件,不受 URL 长度限制。
3. 安全性
GET:相对不安全,因为参数暴露在 URL 中,容易被截取、缓存或者保存到浏览器历史记录中。 POST:较为安全,数据不在 URL 中,但并不意味着绝对安全。还需配合 HTTPS 等技术提升安全性。
4. 缓存
GET:默认可以被缓存,浏览器可能会保存 GET 请求的响应以提高性能。 POST:通常不会被缓存,请求的数据是一次性的。
DNS完整的查询过程
1.首先会在浏览器的缓存中查找对应的IP地址,如果查找到直接返回,若找不到继续下一步
2.将请求发送给本地DNS服务器,在本地域名服务器缓存中查询,如果查找到,就直接将查找结果返回,若找不到继续下一步
3.本地DNS服务器向根域名服务器发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址
4.本地DNS服务器向顶级域名服务器发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址
5.本地DNS服务器向权威域名服务器发送请求,域名服务器返回对应的结果
6.本地DNS服务器将返回结果保存在缓存中,便于下次使用
7.本地DNS服务器将返回结果返回给浏览器
比如要查询 www.baidu.com 的 IP 地址,首先会在浏览器的缓存中查找是否有该域名的缓存,如果不存在就将请求发送到本地的 DNS 服务器中,本地DNS服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。
17.CSS预处理器/后处理器是什么?为什么要使用它们?
预处理器,如:less,sass,stylus,用来预编译sass或者less,增加了css代码的复用性。层级,mixin,变量,循环,函数等对编写以及开发UI组件都极为方便。 后处理器,如: postCss,通常是在完成的样式表中根据css规范处理css,让其更加有效。目前最常做的是给css属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。 css预处理器为css增加一些编程特性,无需考虑浏览器的兼容问题,可以在CSS中使用变量,简单的逻辑程序,函数等在编程语言中的一些基本的性能,可以让css更加的简洁,增加适应性以及可读性,可维护性等。 其它css预处理器语言:Sass(Scss), Less, Stylus, Turbine, Swithch css, CSS Cacheer, DT Css。 使用原因: 1.结构清晰,便于扩展 2.可以很方便的屏蔽浏览器私有语法的差异 3.可以轻松实现多重继承 4.完美的兼容了CSS代码,可以应用到老项目中
18.Webpack 能处理 CSS 吗?如何实现?
Webpack 能处理 CSS 吗: 1.Webpack 在裸奔的状态下,是不能处理 CSS 的,Webpack 本身是一个面向 JavaScript 且只能处理 JavaScript 代码的模块化打包工具; 2.Webpack 在 loader 的辅助下,是可以处理 CSS 的。 如何用 Webpack 实现对 CSS 的处理:Webpack 中操作 CSS 需要使用的两个关键的 loader:css-loader 和 style-loader a.css-loader:导入 CSS 模块,对 CSS 代码进行编译处理; b.style-loader:创建style标签,把 CSS 内容写入标签。 在实际使用中,css-loader 的执行顺序一定要安排在 style-loader 的前面。因为只有完成了编译过程,才可以对 css 代码进行插入;若提前插入了未编译的代码,那么 webpack 是无法理解这坨东西的,它会无情报错。
19.如何判断元素是否到达可视区域
1.window.innerHeight 是浏览器可视区的高度; 2.document.body.scrollTop: 浏览器已滚动到高度 3.imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离); 4.内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop
20.画一条0.5px的线
(1)采用transform: scale() transform: scale(0.5,0.5) (2)采用meta viewport <meta name="viewport" content="width=device-width, initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5"/> 这样就能缩放到原来的0.5倍,如果是1px那么就会变成0.5px。viewport只针对于移动端,只在移动端上才能看到效果
21.设置小于12px的字体
(1)使用Webkit的内核的-webkit-text-size-adjust的私有CSS属性来解决,只要加了-webkit-text-size-adjust:none;字体大小就不受限制了。但是chrome更新到27版本之后就不可以用了。所以高版本chrome谷歌浏览器已经不再支持-webkit-text-size-adjust样式,所以要使用时候慎用 (2)使用css3的transform缩放属性-webkit-transform:scale(0.5); 注意-webkit-transform:scale(0.75);收缩的是整个元素的大小,这时候,如果是内联元素,必须要将内联元素转换成块元素,可以使用display:block/inline-block/...; (3)使用图片:如果是内容固定不变情况下,使用将小于12px文字内容切出做图片,这样不影响兼容也不影响美观。
22.如何解决 1px 问题
1px 问题指的是:在一些 Retina屏幕 的机型上,移动端页面的 1px 会变得很粗,呈现出不止 1px 的效果。 原因很简单——CSS 中的 1px 并不能和移动设备上的 1px 划等号。它们之间的比例关系有一个专门的属性来描述:window.devicePixelRatio = 设备的物理像素 / CSS像素。 打开 Chrome 浏览器,启动移动端调试模式,在控制台去输出这个 devicePixelRatio 的值。这里选中 iPhone6/7/8 这系列的机型,输出的结果就是2 这就意味着设置的 1px CSS 像素,在这个设备上实际会用 2 个物理像素单元来进行渲染,所以实际看到的一定会比 1px 粗一些。
思路一:直接写 0.5px
<div id="container" data-device={{window.devicePixelRatio}}></div> #container[data-device="2"] { border:0.5px solid #333 }
缺陷:兼容性不行,IOS 系统需要8及以上的版本,安卓系统则直接不兼容
思路二:伪元素先放大后缩小
#container[data-device="2"] { position: relative;} #container[data-device="2"]::after{ position:absolute; top: 0; left: 0; width: 200%; height: 200%; content:""; transform: scale(0.5); transform-origin: left top; box-sizing: border-box; border: 1px solid #333; }
思路三:viewport 缩放来解决
<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no"> 整个页面被缩放
23.rem和em的区别
rem,系统处理时(将rem转化为px)只会看html的font-size
em,系统处理时(将em转化为px)只会看父元素的font-size
24.display:none与visibility:hidden的区别
这两个属性都是让元素隐藏,不可见
1)在渲染树中
● display:none会让元素完全从渲染树中消失,渲染时不会占据任何空间; ● visibility:hidden不会让元素从渲染树中消失,渲染的元素还会占据相应的空间,只是内容不可见。
2)是否是继承属性
● display:none是非继承属性,子孙节点会随着父节点从渲染树消失,通过修改子孙节点的属性也无法显示; ● visibility:hidden是继承属性,子孙节点消失是由于继承了hidden,通过设置visibility:visible可以让子孙节点显示;
3)修改常规文档流中元素的 display 通常会造成文档的重排,但是修改visibility属性只会造成本元素的重绘;
4)如果使用读屏器,设置为display:none的内容不会被读取,设置为visibility:hidden的内容会被读取。
2. JavaScript
1.数据类型
1.类型
7种基本类型
Undefined、Null、Boolean、Number、String、Symbol、BigInt
保存在栈内存中,可以直接访问它的值,占据空间小,属于被频繁使用的数据。
Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
引用类型Object
Object(Object本质上是由一组无序的名值对组成的),里面包含 function、Array、Date等
保存在堆里面,栈里面保存的是地址,通过栈里面的地址去访问堆里面的值。占据空间大
2. 如何判断 JavaScript 的数据类型
1.typeof
用来区分除了 Null 类型以外的原始数据类型
typeof undefined // "undefined" typeof null // "object" typeof 1 // "number" typeof "1" // "string" typeof Symbol() // "symbol" typeof function() {} // "function" typeof {} // "object" typeof [] // 'object' typeof console // 'object' typeof console.log // 'function'
其中数组、对象、null都会被判断为object
问题一:typeof 不能识别 null,如何识别 null? 答:可以直接使用===全等运算符来判断(或者使用下面的Object.prototype.toString 方法): let a = null a === null // true
问题二:typeof 作用于未定义的变量,会报错吗? 答案:不会报错,返回"undefined"。 typeof randomVariable // "undefined"
问题三:typeof Number(1)的返回值是什么? 答案:"number"。
问题四:typeof new Number(1)的返回值是什么? 答案:"object"。 typeof new String(1) // "object"
2.instanceof
其内部运行机制是判断在其原型链中能否找到该类型的原型
a instanceof B判断的是 a 和 B 是否有血缘关系,而不是仅仅根据是否是父子关系
不能用于判断原始数据类型的数据 3 instanceof Number // false '3' instanceof String // false true instanceof Boolean // false
可以用来判断对象的类型 var date = new Date() date instanceof Date // true var number = new Number() number instanceof Number // true var string = new String() string instanceof String // true
需要注意的是,instanceof 的结果并不一定是可靠的,因为在 ECMAScript7 规范中可以通过自定义 Symbol.hasInstance 方法来覆盖默认行为。
3.Object.prototype.toString.call(xx)
判断基本数据类型
实现原理:若参数(xx)不为 null 或 undefined,则将参数转为对象,再作判断 转为对象后,取得该对象的 [Symbol.toStringTag] 属性值(可能会遍历原型链)作为 tag,然后返回 "[object " + tag + "]" 形式的字符串。
var a = Object.prototype.toString; console.log(a.call(2)); // [object Number] console.log(a.call(true)); // [object Boolean] console.log(a.call('str')); // [object String] console.log(a.call([])); // [object Array] console.log(a.call(function(){})); // [object Function] console.log(a.call({})); // [object Object] console.log(a.call(undefined)); // [object Undefined] console.log(a.call(null)); // [object Null]
obj.toString()的结果和 Object.prototype.toString.call(obj) 的结果不一样,这是为什么?
因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
4.constructor
有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了
console.log((2).constructor === Number); // true console.log((true).constructor === Boolean); // true console.log(('str').constructor === String); // true console.log(([]).constructor === Array); // true console.log((function() {}).constructor === Function); // true console.log(({}).constructor === Object); // true ''.constructor === String // true true.constructor === Boolean // true new Number(1).constructor === Number // true function Fn(){}; Fn.prototype = new Array(); var f = new Fn(); console.log(f.constructor===Fn); // false console.log(f.constructor===Array); // true
Array.isArray
可以用来判断 value 是否是数组
Array.isArray([]) // true Array.isArray({}) // false
3.null 和 undefined 的区别
undefined:表示变量声明但未初始化时的值(未定义) undefined典型用法是: 1、变量被声明了,但没有赋值时,就等于 undefined 2、调用函数时,应该提供的参数没有提供,该参数等于 undefined 3、对象没有赋值的属性,该属性的值为 undefined 4、函数没有返回值时,默认返回 undefined null:表示准备用来保存对象,还没有真正保存对象的值。(表示一个对象被定义了,值为“空值”)(空对象) 从逻辑角度看,null 值表示一个空对象指针 典型用法是: 1) 作为函数的参数,表示该函数的参数不是对象 2) 作为对象原型链的终点
3.如何区分数组和对象?
Array.isArray
Array.isArray([]) //true Array.isArray({}) //false
instanceof
[] instanceof Array //true {} instanceof Array //false
constructor
{}.constructor //返回 object [].constructor //返回 Array
Object.prototype.toString.call() 可以获取到对象的不同类型
Object.prototype.toString.call([]) //["object Array"] Object.prototype.toString.call({}) //["object Object"]
XXX.__proto__ === Array.prototype
// 原型链方法
4. 0.1+0.2 ! == 0.3
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...(1100循环),0.2的二进制是:0.00110011001100...(1100循环),这两个数的二进制都是无限循环的数
+(0.1 + 0.2).toFixed(1) // 0.3
5. isNaN 和 Number.isNaN
函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
6. JavaScript 中的包装类型
const a = "abc"; a.length; // 3 a.toUpperCase(); // "ABC" 在访问'abc'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。 JavaScript也可以使用Object函数显式地将基本类型转换为包装类型: var a = 'abc' Object(a) // String {"abc"} 也可以使用valueOf方法将包装类型倒转成基本类型: var a = 'abc' var b = Object(a) var c = b.valueOf() // 'abc'
var a = new Boolean( false ); if (!a) { console.log( "Oops" ); // never runs } 答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。
7.类型转换
强制
转化成字符串 toString() String() 转换成数字 Number()、 parseInt()、 parseFloat() 转换成布尔类型 Boolean()
隐式
拼接字符串,如 var str = "" + 18
=== ==
== 在允许强制转换的条件下检查值的等价性
而 === 是在不允许强制转换的条件下检查值的等价性; 因此 === 常被称为「严格等价」。 (“55” == 55 true, “55” === 55 false 把字符串转为数值)
哪些非 boolean 值被强制转换为一个 boolean 时,它是 false ?
""(空字符串) 0, -0, NaN (非法的 number ) null, undefined
8.如何在 JavaScript 中比较两个对象?
① 方法一:通过JSON.stringify(obj)来判断两个对象转后的字符串是否相等 优点:用法简单,对于顺序相同的两个对象可以快速进行比较得到结果 缺点:这种方法有限制就是当两个对象内容是否一致时就没用了 想要比较两个对象内容是否一致,思路是要遍历对象的所有键名和键值是否都一致 因此,为了克服此缺点,引入了一个名为“ lodash ”的JavaScript库: _.isEqual(obj1, obj2)
② 方法二: Object.getOwnPropertyNames该方法可以将Object对象的第一层key获取到并返回一个由第一层key组成的数组。 优点:相对方法一进行了优化,可以应对不同顺序的Object进行比较,不用担心顺序不同而对比出错 缺点:从方法中可以看到只能获取到第一层的key组成的数组,当对象是复合对象时无法进行多层对象的比较
9.深拷贝和浅拷贝
浅拷贝:只复制一层,引用类型仍然共享。
1. Object.assign()
const obj1 = { a: 1, b: { c: 2 } }; const shallowCopy = Object.assign({}, obj1); shallowCopy.b.c = 3; // 修改了共享的引用 console.log(obj1.b.c); // 输出 3
2. 扩展运算符 (...)
const obj1 = { a: 1, b: { c: 2 } }; const shallowCopy = { ...obj1 }; shallowCopy.b.c = 3; // 修改了共享的引用 console.log(obj1.b.c); // 输出 3
深拷贝:递归复制全部层级,完全独立。
1. 使用 JSON.parse() 和 JSON.stringify()
const obj1 = { a: 1, b: { c: 2 } }; const deepCopy = JSON.parse(JSON.stringify(obj1)); deepCopy.b.c = 3; // 修改仅影响 deepCopy console.log(obj1.b.c); // 输出 2
注意:这种方式不适用于包含函数、日期对象、不规则对象等复杂类型,且会丢失原型链的信息。
2. 手动递归实现
function deepClone(obj) { if (obj === null || typeof obj !== "object") { return obj; } if (Array.isArray(obj)) { return obj.map(deepClone); } const clone = {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { clone[key] = deepClone(obj[key]); } } return clone; } const obj1 = { a: 1, b: { c: 2 } }; const deepCopy = deepClone(obj1); deepCopy.b.c = 3; console.log(obj1.b.c); // 输出 2
2. es6
10.var、let、const之间有什么区别
1.变量提升
var 声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
// var console.log(a) // undefined var a = 10 // let console.log(b) // Cannot access 'b' before initialization let b = 10 // const console.log(c) // Cannot access 'c' before initialization const c = 10
2.暂时性死区
暂时性死区的定义:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区
var不存在暂时性死区 let和const存在暂时性死区,只有等到声明变量的那一行代码出现, 才可以获取和使用该变量
// var console.log(a) // undefined var a = 10 // let console.log(b) // Cannot access 'b' before initialization let b = 10 // const console.log(c) // Cannot access 'c' before initialization const c = 10
3.块级作用域
var不存在块级作用域 let和const存在块级作用域
// var { var a = 20 } console.log(a) // 20 // let { let b = 20 } console.log(b) // Uncaught ReferenceError: b is not defined // const { const c = 20 } console.log(c) // Uncaught ReferenceError: c is not defined
4.重复声明
var允许重复声明变量 let和const在同一作用域不允许重复声明变量
// var var a = 10 var a = 20 // 20 // let let b = 10 let b = 20 // Identifier 'b' has already been declared // const const c = 10 const c = 20 // Identifier 'c' has already been declared
5.修改声明的变量 (指针指向)
var和let可以 const声明一个只读的常量。一旦声明,常量的值就不能改变
// var var a = 10 a = 20 console.log(a) // 20 //let let b = 10 b = 20 console.log(b) // 20 // const const c = 10 c = 20 console.log(c) // Uncaught TypeError: Assignment to constant variable
6.初始值设置
在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值
11.如果new一个箭头函数的会怎么样
箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。
new操作符的实现步骤如下
1. 创建一个对象 2. 将构造函数的作用域赋给新对象(也就是将对象的proto属性指向构造函数的prototype属性) 3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法) 4. 返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
12.箭头函数和普通函数有啥区别? 箭头函数能当构造函数吗?
箭头函数不会创建自身的this,只会从上一级继承this,箭头函数的this在定义的时候就已经确认了,之后不会改变。同时箭头函数无法作为构造函数使用,没有自身的prototype,也没有arguments。
1、语法更加简洁、清晰
2、箭头函数不会创建自己的this(重要!!深入理解!!)
箭头函数不会创建自己的this,所以它没有自己的this,它只会从自己的作用域链的上一层继承this。
箭头函数没有自己的this,它会捕获自己在定义时(注意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数中this的指向在它被定义的时候就已经确定了,之后永远不会改变。
3、箭头函数继承而来的this指向永远不变(重要!!深入理解!!)
对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。
var id = 'GLOBAL'; var obj = { id: 'OBJ', a: function(){ console.log(this.id); }, b: () => { console.log(this.id); } }; obj.a(); // 'OBJ' obj.b(); // 'GLOBAL' new obj.a() // undefined new obj.b() // Uncaught TypeError: obj.b is not a constructor
4、.call()/.apply()/.bind()无法改变箭头函数中this的指向
.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽然这么做代码不会报错。
var id = 'Global'; let fun1 = () => { console.log(this.id) }; fun1(); // 'Global' fun1.call({id: 'Obj'}); // 'Global' fun1.apply({id: 'Obj'}); // 'Global' fun1.bind({id: 'Obj'})(); // 'Global'
5、箭头函数不能作为构造函数使用
我们先了解一下构造函数的new都做了些什么?简单来说,分为四步: ① JS内部首先会先生成一个对象; ② 再把函数中的this指向该对象; ③ 然后执行构造函数中的语句; ④ 最终返回该对象实例。 但是!!因为箭头函数没有自己的this,它的this其实是继承了外层执行环境中的this,且this指向永远不会随在哪里调用、被谁调用而改变,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错!
6、箭头函数没有自己的arguments
在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值
7、箭头函数没有原型prototype
let sayHi = () => { console.log('Hello World !') }; console.log(sayHi.prototype); // undefined
8、箭头函数不能用作Generator函数,不能使用yeild关键字
13.说一下this、call、apply、bind区别
this
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
● 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。 ● 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。 ● 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。 ● 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
call、apply、bind作用是改变函数执行时的上下文, 简而言之就是改变函数运行时的this指向。
示例:求数组中的最大值
apply
var arr=[1,10,5,8,3]; console.log(Math.max.apply(null, arr)); //10
apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入 改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
call
var arr=[1,10,5,8,3]; console.log(Math.max.call(null,arr[0],arr[1],arr[2],arr[3],arr[4])); //10
call方法的第一个参数也是this的指向,后面传入的是一个参数列表 跟apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
bind
var arr=[1,10,5,8,12]; var max=Math.max.bind(null,arr[0],arr[1],arr[2],arr[3]) console.log(max(arr[4])); //12,分两次传参
bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入) 改变this指向后不会立即执行,而是返回一个永久改变this指向的函数
区别
1.三者都可以改变函数的this对象指向 2.三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window 3.三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入 4.bind是返回绑定this之后的函数,apply、call 则是立即执行
14.Proxy
在 Vue3.0 中通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { setBind(value, property) return Reflect.set(target, property, value) } } return new Proxy(obj, handler) } let obj = { a: 1 } let p = onWatch( obj, (v, property) => { console.log(`监听到属性${property}改变为${v}`) }, (target, property) => { console.log(`'${property}' = ${target[property]}`) } ) p.a = 2 // 监听到属性a改变 p.a // 'a' = 2
通过自定义 set 和 get 函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。
Promise
是一种用于处理异步操作的对象。它表示一个可能尚未完成的操作及其结果值
有三种状态
Pending(待定):初始状态,既不是成功,也不是失败。
Fulfilled(已兑现):操作成功完成。
Rejected(已拒绝):操作失败。
Promise 提供了几个方法来处理其结果:
then():用于处理成功状态的结果。 catch():用于处理失败状态的错误。 finally():无论结果是成功还是失败,都会执行的操作。
myPromise .then(result => { console.log(result); // "操作成功" }) .catch(error => { console.error(error); }) .finally(() => { console.log("操作结束"); });
当需要并行处理多个 Promise 时
Promise.all():接受一个包含多个 Promise 的数组,只有在所有 Promise 都成功时才会成功,以及返回一个数组包含每个 Promise 的结果。
Promise.all([promise1, promise2]) .then(results => { console.log(results); // [result1, result2] }) .catch(error => { console.error(error); });
Promise.race():接受一个 Promise 的数组,返回第一个成功或失败的 Promise 的结果。
Promise.race([promise1, promise2]) .then(result => { console.log(result); // 第一个解决的 Promise 的结果 }) .catch(error => { console.error(error); });
15.async/await 和 Promise 有什么关系
Promise
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
async/await
es2017的新语法,async/await就是generator + promise的语法糖
async/await 和 Promise 的关系非常的巧妙,await必须在async内使用,并装饰一个Promise对象,async返回的也是一个Promise对象。
async/await中的return/throw会代理自己返回的Promise的resolve/reject,而一个Promise的resolve/reject会使得await得到返回值或抛出异常。
如果方法内无await节点
return 一个字面量则会得到一个{PromiseStatus: resolved}的Promise。 throw 一个Error则会得到一个{PromiseStatus: rejected}的Promise。
如果方法内有await节点
async会返回一个{PromiseStatus: pending}的Promise(发生切换,异步等待Promise的执行结果)。 Promise的resolve会使得await的代码节点获得相应的返回结果,并继续向下执行。 Promise的reject 会使得await的代码节点自动抛出相应的异常,终止向下继续执行。
16.common.js和es6中模块引入的区别?
Common]S是一种模块规范,最初被应用于Nodejs,成为Nodejs 的模块规范。 运行在浏览器端的JavaScript由于也缺少类似的规范,在ES6出来之前,前端也实现了一套相同的模块规范(例如: AMD),用来对前端模块进行管理。 自ES6起,引入了一套新的ES6 Module规范,在语言标准的层面上实现了模块功能,而且实现得相当简单,有望成为浏览器和服务器通用的模块解决方案。
在使用上的差别主要有: CommonJS模块输出的是一个值的拷贝,ES6模块输出的是值的引用CommonJS模块是运行时加载,ES6模块是编译时输出接口。 CommonJs是单个值导出,ES6 Module可以导出多个 CommonJs是动态语法可以写在判断里,ES6 Module静态语法只能写在顶层CommonJs的this是当前模块,ES6 Module的this是undefined
17.什么是 let 的临时性死区?
let 会产生临时性死区,在当前的执行上下文中,会进行变量提升,但是未被初始化,所以在执行上下文执行阶段,执行代码如果还没有执行到变量赋值,就引用此变量就会报错,此变量未初始化。
18.Object.defineProperty 与 Proxy 的区别
在 Vue2.x 的版本中,双向绑定是基于 Object.defineProperty 方式实现的。而 Vue3.x 版本中,使用了 ES6 中的 Proxy 代理的方式实现。
Object.defineProperty(obj, prop, descriptor) 使用 Object.defineProperty 会产生三个主要的问题:
1.不能监听数组的变化
在 Vue2.x 中解决数组监听的方法是将能够改变原数组的方法进行重写实现(比如:push、 pop、shift、unshift、splice、sort、reverse),举例:
// 我们重写 push 方法 const originalPush = Array.prototype.push Array.prototype.push = function() { // 我们在这个位置就可以进行 数据劫持 了 console.log('数组被改变了') originalPush.apply(this, arguments) }
2.必须遍历对象的每个属性
可以通过 Object.keys() 来实现
3.必须深层遍历嵌套的对象
通过递归深层遍历嵌套对象,然后通过 Object.keys() 来实现对每个属性的劫持
Proxy
1.Proxy 针对的整个对象,Object.defineProperty 针对单个属性,这就解决了 需要对对象进行深度递归(支持嵌套的复杂对象劫持)实现对每个属性劫持的问题
// 定义一个复杂对象 const obj = { obj: { children: { a: 1 } } } const objProxy = new Proxy(obj, { get(target, property, receiver){ console.log('-- target --') return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { console.log('-- set --') return Reflect.set(target, property, value, receiver) } }) console.log(objProxy.obj) // 输出 '-- target --' console.log(objProxy.a = 2) // 输出 '-- set --'
2.Proxy 解决了 Object.defineProperty 无法劫持数组的问题
const ary = [1, 2, 3] const aryProxy = new Proxy(ary, { get(target, property, receiver){ console.log('-- target --') return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { console.log('-- set --') return Reflect.set(target, property, value, receiver) } }) console.log(aryProxy[0]) // 输出 '-- target --' console.log(aryProxy.push(1)) // 输出 '-- set --'
3.比 Object.defineProperty 有更多的拦截方法,对比一些新的浏览器,可能会对 Proxy 针正对性的优化,有助于性能提升
3.JavaScript基础
19.new操作符具体做了什么
const a = new Foo(); //以下为new 操作符干的事情 var o = new Object(); //新建一个空对象 o.__proto__ = Foo.prototype;//将该空对象的原型指向构造函数的原型对象 Foo.call(o); //在空对象上调用构造函数 a = o; //赋值给变量
new操作符的执行过程: (1)首先创建了一个新的空对象 (2)设置原型,将对象的原型设置为函数的 prototype 对象。 (3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性) (4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
20.正则表达式
// (1)匹配 16 进制颜色值 var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; // (2)匹配日期,如 yyyy-mm-dd 格式 var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; // (3)匹配 qq 号 var regex = /^[1-9][0-9]{4,10}$/g; // (4)手机号码正则 var regex = /^1[34578]\d{9}$/g; // (5)用户名正则 var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
21.遍历数组的方法
for
for...in
拿到的是数组下标
能遍历对象,拿到的是对象的key
for...of
拿到的是数组的值
不能遍历对象,会报错
forEach
会改变原数组
map
返回一个新的数组
some
如果有其中一项满足,就返回true。只有全部都不满足,才会返回false
every
只有全部都满足,才会返回 ** true **, 一旦其中某一项不符合条件,就中断循环,直接返回 false
find
到第一个满足条件的元素时,则直接返回该元素。如果都不满足条件, 则返回 undefined
filter
返回符合条件的新数组
不会影响原数组
reduce
一般用于求和
不会影响原数组。
findIndex
找到符合条件的返回当前项的下标,没找到返回 - 1
22.JS数组去重
1.indexOf
新建一个数组,遍历要去重的数组,当值不在新数组的时候(indexOf 为 -1)就加入该新数组中
function unique(arr){ var newArr = []; for(var i = 0; i < arr.length; i++){ if(newArr.indexOf(arr[i]) == -1){ newArr.push(arr[i]) } } return newArr; } var arr = [1,2,2,3,4,4,5,1,3]; var newArr = unique(arr); console.log(newArr);
2.利用两层循环+数组的splice方法
通过两层循环对数组元素进行逐一比较,然后通过splice方法来删除重复的元素。此方法对NaN是无法进行去重的,因为进行比较时NaN !== NaN。
function removeDuplicate(arr) { let len = arr.length for (let i = 0; i < len; i++) { for (let j = i + 1; j < len; j++) { if (arr[i] === arr[j]) { arr.splice(j, 1) len-- // 减少循环次数提高性能 j-- // 保证j的值自加后不变 } } } return arr } let arr = [1,3,4,3,2,1,1,1] removeDuplicate(arr) console.log(arr)
3、new Set
set的特点就是不会有重复元素
function unique(arr){ //Set数据结构,它类似于数组,其成员的值都是唯一的 return Array.from(new Set(arr)); // 利用Array.from将Set结构转换成数组 // 或者 return [...arr]; } var arr = [1,2,2,3,5,3,6,5, null, null, undefined, undefined]; var res = unique(arr) console.log(res ); // [1, 2, 3, 5, 6, null, undefined]
23.JS的事件循环(EventLoop)和宏任务和微任务
Event Loop
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
事件循环主要与任务队列有关,所以必须要先知道宏任务与微任务。
任务队列
宏任务
script 标签中的js整体代码、setTimeout、setInterval、ajax、IO操作、UI交互、postMessage等
微任务
process.nextTick(node.js中进程相关的对象)、Promise
在事件循环中,每进行一次循环操作称为tick
1.同步任务最先执行,然后进入任务队列。 2.从任务队列中取出一个宏任务并执行。 3.检查微任务队列,执行并清空微任务队列,如果在微任务的执行中又加入了新的微任务,也会在这一步一起执行。
总结
注意,在一次事件循环中,实际上宏任务是先于微任务的,但是微任务会一次性把队列的执行完,所以如果要优先执行的话,应该放到微任务里。
24.如何判断对象具有某属性?
如:let obj={name:'zhangsan',age:21} 有以下方法 ( property 为属性名的变量,实际上是key,键名):
1. 'xxx' in obj
2. Reflect.has(obj, property)
3. obj.hasOwnProperty(property)
可以判断是否是对象的自有属性,若有,返回true,否则返回 false(原型链上的返回false)。 所有继承了 Object 的对象都会继承到 hasOwnProperty 方法。用来检测一个对象是否含有特定的自身属性;和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。
4. Object.hasOwn(obj, property)
是 Object 的方法,也是判断自有属性的。 不过要注意浏览器版本兼容问题,谷歌 93 以上版本才支持。
5. Object.prototype.hasOwnProperty.call 方法
25. ajax、axios、fetch的区别
AJAX
异步 JavaScript 和 XML
一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新
Fetch
号称是AJAX的替代品
Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
Axios
是一种基于Promise封装的HTTP客户端
4.原型与原型链
26.原型链
原型
在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。
原型链
原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。 因此可以利用__proto__一直指向Object的原型对象上,而Object原型对象用Object.prototype.__ proto__ = null表示原型链顶端。如此形成了js的原型链继承。
通俗的讲:
每个对象都可以有一个原型_proto_,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找...... 这个操作被委托在整个原型链上,这个就是我们说的原型链了。
特点
JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
27.JS继承有哪些方式
1.原型链继承
function Parent1() { this.name = 'parent1'; this.play = [1, 2, 3] } function Child1() { this.type = 'child2'; } Child1.prototype = new Parent1(); console.log(new Child1());
// 潜在的问题 let s1 = new Child1(); let s2 = new Child1(); s1.play.push(4); console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4] // 两个实例使用的是同一个原型对象。它们的内存空间是共享的,当一个发生变化的时候,另外一个也随之进行了变化,这就是使用原型链继承方式的一个缺点。
特点: 1.非常纯粹的继承关系,实例是子类的实例,也是父类的实例 2.父类新增原型方法/原型属性,子类都能访问到 3.简单,易于实现 缺点: 1.要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中 2.无法实现多继承 3.来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的) 4.创建子类实例时,无法向父类构造函数传参
2.构造函数继承(借助 call)
function Parent1(){ this.name = 'parent1'; } Parent1.prototype.getName = function () { return this.name; } function Child1(){ Parent1.call(this); this.type = 'child1' } let child = new Child1(); console.log(child); // 没问题 console.log(child.getName()); // 会报错
特点: 1.解决了1中,子类实例共享父类引用属性的问题 2.创建子类实例时,可以向父类传递参数 3.可以实现多继承(call多个父类对象) 缺点: 1.实例并不是父类的实例,只是子类的实例 2.只能继承父类的实例属性和方法,不能继承原型属性/方法 3.无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3.组合继承(前两种组合)
function Parent3 () { this.name = 'parent3'; this.play = [1, 2, 3]; } Parent3.prototype.getName = function () { return this.name; } function Child3() { // 第二次调用 Parent3() Parent3.call(this); this.type = 'child3'; } // 第一次调用 Parent3() Child3.prototype = new Parent3(); // 手动挂上构造器,指向自己的构造函数 Child3.prototype.constructor = Child3; var s3 = new Child3(); var s4 = new Child3(); s3.play.push(4); console.log(s3.play, s4.play); // [1, 2, 3, 4]; [1, 2, 3]; console.log(s3.getName()); // 正常输出'parent3' console.log(s4.getName()); // 正常输出'parent3'
特点: 1.弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法 2.既是子类的实例,也是父类的实例 3.不存在引用属性共享问题 4.可传参 5.函数可复用 缺点: 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
4.原型式继承
let parent4 = { name: "parent4", friends: ["p1", "p2", "p3"], getName: function() { return this.name; } }; let person4 = Object.create(parent4); person4.name = "tom"; person4.friends.push("jerry"); let person5 = Object.create(parent4); person5.friends.push("lucy"); console.log(person4.name); // tom console.log(person4.name === person4.getName()); // true console.log(person5.name); // parent4 console.log(person4.friends); // ['p1', 'p2', 'p3', 'jerry', 'lucy'] console.log(person5.friends); // ['p1', 'p2', 'p3', 'jerry', 'lucy']
通过 Object.create 这个方法可以实现普通对象的继承,不仅仅能继承属性,同样也可以继承 getName 的方法。 第一个结果“tom”,比较容易理解,person4 继承了 parent4 的 name 属性,但是在这个基础上又进行了自定义。 第二个是继承过来的 getName 方法检查自己的 name 是否和属性里面的值一样,答案是 true。 第三个结果“parent4”也比较容易理解,person5 继承了 parent4 的 name 属性,没有进行覆盖,因此输出父对象的属性。 最后两个输出结果是一样,其实 Object.create 方法是可以为一些对象实现浅拷贝的。
5.寄生式继承
let parent5 = { name: "parent5", friends: ["p1", "p2", "p3"], getName: function() { return this.name; } }; function clone(original) { let clone = Object.create(original); clone.getFriends = function() { return this.friends }; return clone; } let person5 = clone(parent5); console.log(person5.getName()); // parent5 console.log(person5.getFriends()); // ['p1', 'p2', 'p3']
它不仅仅有 getName 的方法,而且可以看到它最后也拥有了 getFriends 的方法。
6.寄生组合式继承
function clone (parent, child) { // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程 child.prototype = Object.create(parent.prototype); child.prototype.constructor = child; } function Parent6() { this.name = 'parent6'; this.play = [1, 2, 3]; } Parent6.prototype.getName = function () { return this.name; } function Child6() { Parent6.call(this); this.friends = 'child5'; } clone(Parent6, Child6); Child6.prototype.getFriends = function () { return this.friends; } let person6 = new Child6(); console.log(person6); // child6 {name: "parent6",play: [1, 2, 3], friends: "child5"} console.log(person6.getName()); // parent6 console.log(person6.getFriends()); // child5
这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销. 总体看下来,这六种继承方式中,寄生组合式继承是这六种里面最优的继承方式
28.闭包
讲概念
两个函数嵌套,内部函数引用外部函数变量
function fn1(){ let a = "1" let fn2 =function (){ console.log(a) } return fn2 } let fn3 = fn1()
特性
1.函数套函数;
2.函数内部可以直接使用外部函数的变量或者参数;
3.变量或参数不会被垃圾回收机制回收
说用途
1.读取函数内部的变量;
2.这些变量的值始终保存在内存中,不会在外层函数调用后被自动清除。
优缺点
优点
1.可以访问到函数内部的局部变量;
2.可以避免全局变量的污染;
3.这些变量的值始终保持在内存中,不会在外层函数调用后被自动清除。
缺点
会增大内存使用量,滥用闭包会影响性能,导致内存泄漏等问题。
延伸
怎么清除闭包的内存泄漏
确定不再使用闭包里面的变量的时候,let f=fn1(); f(); f=null;通过这种方法释放内存,解决闭包缺陷
内存泄漏
闭包强大的原因
闭包函数对作用域链 (或作用域层级)的访问
1.在它自身声明之内声明的变量
function outer() { function inner() { // 内部函数中定义变量 let a = 1; console.log(a); // <---- 输出: 1 } inner(); }
2.对全局变量的访问
// 定义全局变量 let global = "global"; function outer() { function inner() { let a = 1; console.log(a); console.log(global); // <---- 输出: global } inner(); }
3.对外部函数变量的访问 !important
let global = "global"; function outer() { // 外部函数中定义变量 let outer = "outer"; function inner() { let a = 1; console.log(a); console.log(global); console.log(outer); // <---- 输出: outer } inner(); }
闭包的实际应用
封装成高阶函数
代码
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
连续打印出 5个6
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000) }
连续打印出 1 2 3 4 5
29.对作用域、作用域链的理解
一个变量或者函数在其内能够被访问的“可见区域”
全局作用域
● 最外层函数和最外层函数外面定义的变量拥有全局作用域 ● 所有未定义直接赋值的变量自动声明为全局作用域 ● 所有window对象的属性拥有全局作用域 ● 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
函数作用域
● 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到 ● 作用域是分层的,内层作用域可以访问外层作用域,反之不行
块级作用域
● 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段) ● let和const声明的变量不会有变量提升,也不可以重复声明 ● 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
作用域链
在当前作用域中查找所需变量,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。 当在某个作用域中尝试访问一个变量时,JS引擎会从当前作用域开始,沿着作用域链向上逐级查找,直到找到该变量为止,如果全局未找到就抛出错误。 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。 作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。 当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
30.对执行上下文的理解
1. 执行上下文类型
1)全局执行上下文
任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文。
2)函数执行上下文
当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个
3)**eval**函数执行上下文
执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍。
2. 执行上下文栈
JavaScript引擎使用执行上下文栈来管理执行上下文
当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
let a = 'Hello World!'; function first() { console.log('Inside first function'); second(); console.log('Again inside first function'); } function second() { console.log('Inside second function'); } first(); //执行顺序 //先执行second(),在执行first()
3. 创建执行上下文
创建阶段
1)this绑定
● 在全局执行上下文中,this指向全局对象(window对象) ● 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined
2)创建词法环境组件
● 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。 ● 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域
3)创建变量环境组件
变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
执行阶段
此阶段会完成对变量的分配,最后执行完代码
简单来说执行上下文就是指
在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。 在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。 ● 全局上下文:变量定义,函数声明 ● 函数上下文:变量定义,函数声明,this,arguments
31.说一下this、call、apply、bind区别
this
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
● 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。 ● 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。 ● 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。 ● 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
call、apply、bind作用是改变函数执行时的上下文, 简而言之就是改变函数运行时的this指向。
示例:求数组中的最大值
apply
var arr=[1,10,5,8,3]; console.log(Math.max.apply(null, arr)); //10
apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入 改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
call
var arr=[1,10,5,8,3]; console.log(Math.max.call(null,arr[0],arr[1],arr[2],arr[3],arr[4])); //10
call方法的第一个参数也是this的指向,后面传入的是一个参数列表 跟apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
bind
var arr=[1,10,5,8,12]; var max=Math.max.bind(null,arr[0],arr[1],arr[2],arr[3]) console.log(max(arr[4])); //12,分两次传参
bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入) 改变this指向后不会立即执行,而是返回一个永久改变this指向的函数
区别
1.三者都可以改变函数的this对象指向 2.三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window 3.三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入 4.bind是返回绑定this之后的函数,apply、call 则是立即执行
32.setTimeout、 Promise、 Async/Await
setTimeout
console.log('script start') setTimeout(function(){ console.log('settimeout') }) console.log('script end') // 输出顺序:script start->script end->settimeout
Promise
当执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。
console.log('script start') let promise1 = new Promise(function (resolve) { console.log('promise1') resolve() console.log('promise1 end') }).then(function () { console.log('promise2') }) setTimeout(function(){ console.log('settimeout') }) console.log('script end') // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
当JS主线程执行到Promise对象时
● promise1.then() 的回调就是一个 task ● promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue ● promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中 ● setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
async/await
sync/await 实际上是对 Generator(生成器)的封装,是一个语法糖。
*/yield和async/await看起来其实已经很相似了,它们都提供了暂停执行的功能,但二者又有三点不同: async/await自带执行器,不需要手动调用 next()就能自动执行下一步 async 函数返回值是 Promise 对象,而 Generator 返回的是生成器对象 await 能够返回 Promise 的 resolve/reject 的值
不管await后面跟着的是什么,await都会阻塞后面的代码
Generator
Generator 实现的核心在于上下文的保存,函数并没有真的被挂起,每一次 yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个 context 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样。
用babel编译后生成regeneratorRuntime mark()方法为生成器函数绑定了一系列原型 wrap()相当于是给 generator 增加了一个_invoke 方法
async function async1(){ console.log('async1 start'); await async2(); console.log('async1 end') } async function async2(){ console.log('async2') } console.log('script start'); async1(); console.log('script end') // 输出顺序:script start->async1 start->async2->script end->async1 end
async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。
两者的区别
Promise的出现解决了传统callback函数导致的地域回调问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。 而async await代码看起来会简洁些,使得异步代码看起来像同步代码,await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句。
async/await与Promise一样,是非阻塞的。
async/await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。
33.async/await 和 Promise 有什么关系
Promise
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
async/await
es2017的新语法,async/await就是generator + promise的语法糖
async/await 和 Promise 的关系非常的巧妙,await必须在async内使用,并装饰一个Promise对象,async返回的也是一个Promise对象。
async/await中的return/throw会代理自己返回的Promise的resolve/reject,而一个Promise的resolve/reject会使得await得到返回值或抛出异常。
如果方法内无await节点
return 一个字面量则会得到一个{PromiseStatus: resolved}的Promise。 throw 一个Error则会得到一个{PromiseStatus: rejected}的Promise。
如果方法内有await节点
async会返回一个{PromiseStatus: pending}的Promise(发生切换,异步等待Promise的执行结果)。 Promise的resolve会使得await的代码节点获得相应的返回结果,并继续向下执行。 Promise的reject 会使得await的代码节点自动抛出相应的异常,终止向下继续执行。
34.垃圾回收与内存泄漏
垃圾回收
概念
JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制
● Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。 ● JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。 ● 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
回收的方式
标记清除
● 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。 ● 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
引用计数
● 这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。 ● 这种方法会引起循环引用的问题:例如:obj1和obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
● 对数组进行优化:在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。 ● 对**object**进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 ● 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
什么情况会引起内存泄漏?
虽然有垃圾回收机制但是我们编写代码操作不当还是会造成内存泄漏。 1.意外的全局变量引起的内存泄漏。 原因:全局变量,不会被回收。 解决:使用严格模式避免。 2.闭包引起的内存泄漏 原因:闭包可以维持函数内局部变量,使其得不到释放。 解决:将事件处理函数定义在外部,解除闭包,或者在定义事件处理 函数的外部函数中,删除对dom的引用。 3.没有清理的DOM元素引用 原因:虽然别的地方删除了,但是对象中还存在对dom的引用 解决:手动删除。 4.被遗忘的定时器或者回调 原因:定时器中有dom的引用,即使dom删除了,但是定时器还在, 所以内存中还是有这个dom。 解决:手动删除定时器和dom。 5.子元素存在引用引起的内存泄漏 原因:div中的ul li 得到这个div,会间接引用某个得到的li, 那么此时因为div间接引用li,即使li被清空,也还是在内存中, 并且只要li不被删除,他的父元素都不会被删除。 解决:手动删除清空
35. JavaScript为什么要进行变量提升,它导致了什么问题
本质原因
本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
JS在拿到一个变量或者一个函数的时候,会有两步操作 即解析和执行
在解析阶段,JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。 ○ 全局上下文:变量定义,函数声明 ○ 函数上下文:变量定义,函数声明,this,arguments
在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?
1)提高性能
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。 在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
2)容错性更好
a = 1; var a; console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。 虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结
var tmp = new Date(); function fn(){ console.log(tmp); if(false){ var tmp = 'hello world'; } } fn(); // undefined
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
var tmp = 'hello world'; for (var i = 0; i < tmp.length; i++) { console.log(tmp[i]); } console.log(i); // 11
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11
谈谈对面向对象的理解
答1: 面向对象编程,即OOP,是一种编程范式,满足面向对象编程的语言, 一般会提供类、封装、继承等语法和概念来辅助我们进行面向对象编程。 类型被设计为将数据和行为捆绑在一起的一种东西,数据和行为被称之 为类型的成员。我们可以创建类型的实例,不同的实例包含不同的数据, 从而其表现出来的行为也会不同,尽管其代码是一样的。 封装使得类的成员得以有选择性的暴露,一些成员只在类型的内部使用, 被称之为私有的(private),一些成员可以被派生类型使用,称之为 受保护的(protected),一些成员可以被任何东西使用,称之为公开 的(public)。而某些语言还提供了内部的(internal)这样的访问 修饰符来标识一些只能被同一个程序集或者包使用的成员。 继承可以从一个现有类型派生出新的类型来,派生类继承了基类的所有 成员,也可以新增只属于自己的成员。在任何情况下,派生类类型的实 例可以被当做基类类型的实例来使用。 虚方法为派生类修改基类的行为提供了一个途径,通过重写(override) 虚方法可以修改基类某些方法的行为。当派生类实例被当做基类实例来使 用时,这一行为的区别将会被体现出来,这种在运行时不同类型的实例在 同样的代码中呈现出完全不同行为的现象被称之为多态。 面向对象编程最初是为了解决GUI程序设计问题所提出的,后来面向对象 编程被发现也比较适合用于许多特定领域的开发。面向对象编程是目前运 用最为广泛的一种编程范式,从而也产生了非常多的解决代码复用的技巧 ,其中相当一部分技巧在程序中反复出现而被提炼为设计模式。
答2: 他问你面向对象编程,面试官想知道的是你的理解。 不是概念! 不是概念! 不是概念! 你那样回答没有错,但是不是面试官想要的,概念,特征都会讲,看你 怎么描述了: 网上讲的详细的很多,我讲个我当时面试的回答,一个瞎扯淡的例子: “假设我是女娲,我准备捏一些人, 首先,人应该有哪些基本特征: 1.有四肢 2.有大脑 3.有器官 4.有思想 我们就有了第一个模型,这就是抽象。 其次,我和西方上帝是好友,我想我的这个想法能够提供给他用,但是我不想 让他知道里面细节是怎么捏出来的,用的什么材料,他也不用考虑那么多,只 要告诉我他要捏什么样的人就可以了。这就是封装。 然后,我之后创造的人都以刚才的模型做为模板,我创造的人都有我模型的特征 这就是继承。 最后,我觉得为了让人更丰富多彩,暗合阴阳之原理,可以根据模型进行删减, 某些人上半身器官多突起那么一丢丢,下面少那么一丢丢。某些人,下半身多 突起那么一丢丢。这就是多态。 嘿嘿,当然为了,更丰富多彩,那么一丢丢大小也是可以有区别的。。。” 此时,面试官要是男的你可以露出你懂得的表情! 程序员面试都很枯燥,你可以适当弄点笑点,是加分项。
答3: 通过封装 继承 多态 组合等手段把N 变为1的方法,, 封装:主要是暴露接口,你不用关心内部实现。 继承:主要是让你不用重复造轮子了。 多态:让代码可读性更强,让编译器做更多的事。 这个问题很简单嘛,直接回答如下: 面向对象隐藏了面向过程具体实现的细节, 把属性和行为封装成一个抽象模型,即对象,以便用专业的方法做专业的事情。 就好比,面向过程是部门员工,他们具体怎么完成工作内容我不关心了,我只 关心是谁来做这些事,谁叫我是部门经理呢?——这就是面向对象。
答4: “人”是类。 “人”有姓名、出生日期、身份证号等属性。 “人”有约会、么么哒、啪啪啪等功能(方法)。 “男人”、“女人”是“人”的子类。继承“人”的属性和功能。但也有 自己特有的属性和功能。你、我是对象。
答5: 是一种编程的思想,主要是为了解决代码复用的问题。 封装:把属性值、红蓝条、攻击、走位、放技能、清兵、游走等行为都塞在一个英雄里。 继承:攻击+10 的装备可以升级到攻击+20,以后还可能升级到攻击+30 并带有吸血效 果。不管升级成什么,都携带着攻击+10 这部分属性。 多态:一个团队需要一个辅助,我们只需要一个辅助英雄,并不关心来的是哪个辅助 英雄,能加血就行。 具备这三种特性的编程思想,叫做面向对象。
3.性能优化
1.第一次访问时的优化(把第一次的加载速度变快)
1.首屏优化速度、白屏时间
最大效果就是要减少首屏资源体积
- 打包工具的压缩 - 异步加载(体积大但是又不是马上要用的) - 更新为提及更小的新版本,能不用第三方库的就不用,自己编写,比如时间格式化的,编写代码尽量减少体积 - 去除大的base64体积:打包后,小图片会转成base64,但是一些配置如果配错的话,大图片也会转成的
收效不大或者特殊情况的优化操作
首屏数据尽量并行,如果可行小数据接口合并; 页面包含大量dom可以分批滚动渲染; 骨架屏,Loading:先让屏幕不白,减少用户焦虑
页码渲染优化
优化 html 代码
js 外连接放在底部; css 外连接放在顶部; 减少DOM数量
优化js/css代码
使用 webworker; 场任务分片执行; 减少重拍、重绘降低 css选择器复杂性
优化动画效果
资源加载优化
减少资源大小
代码压缩; Gzip 图片压缩 代码拆分
减少 Http 请求次数
http强缓存; serviceWorker; 本地存储; 合并请求(雪碧图等)
提升 Http 请求响应速度
CDN
Http 弱缓存
DNS Prefetch
Http2
优化资源加载时机
按需加载;懒加载;预加载
优化资源、内容加载方式
客户端内H5页面可以考虑离线宝等方式
内容直出
UI 框架按需加载
2.如果此时文件依然很大,通过webpack进行资源拆分
3. 图片/图标资源的处理,将一定大小的文件转换成base64、或者使用阿里的字体图标库进行图标的渲染
4.如果想做SEO的优化,也可以采用服务端渲染的方式来加快首屏渲染的速度;
2.第n次访问时的优化(把已经访问过的资源缓存);
一般情况下,访问过的页面/资源,想要在后面访问时加快访问速度,可以想到的方式是利用缓存或本地存储;
前端本身我们可以通过不同的业务逻辑利用localStorage或sessionStorage 就可以了
如何涉及到服务端,我们也可以采用http的缓存,一般有两种方式,一个是强缓存、另一个是协商缓存,强缓存的优先级高于协商缓存,我们可以通过相关key去设置缓存时间(那么多属性不一定记得住,但一定要知道有这个东西)
除了本地存储和http缓存,也可以尝试采用indexDB去做前端的数据存储
除了indexBD,Service workers也可以作为缓存方案
3.让用户感觉很快,很流畅(通过交互手段优化体验)
让用户感觉很快,顾名思义,就是并没有实际上的提升速度,而是优化了用户体验,可以采用骨架屏、懒加载、合理loading,防抖、节流
什么情况下造成操作卡顿和渲染慢?
一次性操作大量dom,比如长列表渲染,异步渲染
进行复杂度很高的运算(比如循环)
vue和react项目中,不必要的渲染太多
- v-show:频繁切换的;v-if :一次性的 - 循环和动态切换内容加好key值 - keep-alive 缓存,要慎用 - 区分请求粒度,减少请求范围,也能减少更新(没必要更新的接口就不要更新了,比如删除后,只更新列表,搜索分类的就没必要了)
css加载会造成阻塞吗?
结论
css加载不会阻塞DOM树的解析; css加载会阻塞DOM树的渲染; css加载会阻塞后面js语句的执行。
为了避免让用户看到长时间的白屏时间,我们应该尽可能的提高css加载速度
1.使用CDN(因为CDN会根据你的网络状况,替你挑选最近的一个具有缓存内容的节点为你提供资源,因此可以减少加载时间) 2.对css进行压缩(可以用很多打包工具,比如webpack,gulp等,也可以通过开启gzip压缩) 3.合理的使用缓存(设置cache-control,expires,以及E-tag都是不错的,不过要注意一个问题,就是文件更新后,你要避免缓存而带来的影响。其中一个解决防范是在文件名字后面加一个版本号) 4.减少http请求数,将多个css文件合并,或者是干脆直接写成内联样式(内联样式的一个缺点就是不能缓存)
原理解析
浏览器渲染的流程如下: HTML解析文件,生成DOM Tree,解析CSS文件生成CSSOM Tree 将Dom Tree和CSSOM Tree结合,生成Render Tree(渲染树) 根据Render Tree渲染绘制,将像素渲染到屏幕上。
从流程我们可以看出来: DOM解析和CSS解析是两个并行的进程,所以这也解释了为什么CSS加载不会阻塞DOM的解析。 然而,由于Render Tree是依赖于DOM Tree和CSSOM Tree的,所以他必须等待到CSSOM Tree构建完成,也就是CSS资源加载完成(或者CSS资源加载失败)后,才能开始渲染。因此,CSS加载是会阻塞Dom的渲染的。 由于js可能会操作之前的Dom节点和css样式,因此浏览器会维持html中css和js的顺序。因此,样式表会在后面的js执行前先加载执行完毕。所以css会阻塞后面js的执行。
什么是内存泄漏? 什么原因会导致呢?
内存泄露的解释:程序中己动态分配的堆内存由于某种原因未释放或无法释放。
根据JS的垃圾回收机制,当内存中引用的次数为0的时候内存才会被回收 全局执行上下文中的对象被标记为不再使用才会被释放
场景
全局变量过多
通常是变量未被定义或者胡乱引用了全局变量
闭包
未手动解决必包遗留的内存引用。定义了闭包就要消除闭包带来的副作用。
事件监听未被移除
缓存
建议所有缓存都设置好过期时间
SPA(单页应用)首屏加载速度慢怎么解决?
一、什么是首屏加载
指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容 首屏加载可以说是用户体验中最重要的环节
二、加载慢的原因
网络延时问题
- cdn,用户节点就近
- preload 预加载
prereder 预渲染
资源太大
a.包分chunk
b.懒加载
c.公共资源 vender
d.缓存(不变动的使用强缓存;变动的使用协商缓存;离线环境下使用策略缓存,service-worker)
e.服务端渲染(DOM树在服务端生成,然后返回给前端。)
资源是否重复发送请求去加载了
加载脚本的时候,渲染内容堵塞了
三、解决方案
减小入口文件积
静态资源本地缓存
UI框架按需加载
图片资源的压缩
组件重复打包
开启GZip压缩
使用SSR,也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
什么情况下会重绘和回流,常见的改善方案
回流
当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程
重绘
当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程
情景
浏览器请求到对应页面资源的时候,会将HTML解析成DOM,把CSS解析成CSSDOM,然后将DOM和CSSDOM合并就产生了Render Tree。在有了渲染树之后,浏览器会根据流式布局模型来计算它们在页面上的大小和位置,最后将节点绘制在页面上。 那么当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变,浏览器就会重新渲染页面,这个就是浏览器的回流。常见的回流操作有:页面的首次渲染、浏览器窗口尺寸改变、部分元素尺寸或位置变化、添加或删除可见的DOM、激活伪类、查询某些属性或调用方法(各种宽高的获取,滚动方法的执行等)。 当页面中的元素样式的改变不影响它在文档流的位置时(如color、background-color等),浏览器对应元素的样式,这个就是回流。
可见:回流必将导致重绘,重绘不一定会引起回流。回流比重绘的代价更高。
常见改善方案
1.在进行频繁操作的时候,使用防抖和节流来控制调用频率。 2.避免频繁操作DOM,可以利用DocumentFragment,来进行对应的DOM操作,将最后的结果添加到文档中。 3.灵活使用display: none属性,操作结束后将其显示出来,因为display的属性为none的元素上进行的DOM操作不会引发回流和重绘。 4.获取各种会引起重绘/回流的属性,尽量将其缓存起来,不要频繁的去获取。 5.对复杂动画采用绝对定位,使其脱离文档流,否则它会频繁的引起父元素及其后续元素的回流。
一次请求大量数据怎么优化,数据多导致渲染慢怎么优化
首先大量数据的接收,那么肯定是用异步的方式进行接收,对数据进行一个分片处理,可以拆分成一个个的小单元数据,通过自定义的属性进行关联。这样数据分片完成。接下来渲染的话,由于是大量数据,如果是长列表的话,这里就可以使用虚拟列表(当前页面需要渲染的数据拿到进行渲染,然后对前面一段范围及后面一段范围,监听对应的滚动数据来切换需要渲染的数据,这样始终要渲染的就是三部分)。当然还有别的渲染情况,比如echarts图标大量点位数据优化等。
一、感知性能优化
1.loading
2.骨架屏
二、HTML优化
1.压缩 HTML
HTML代码压缩,将注释、空格和新行从生产文件中删除。删除所有不必要的空格、注释和中断行将减少HTML的大小,加快网站的页面加载时间,并显著减少用户的下载时间。
2.删除不必要的注释
注释对用户来说是没有用的,应该从生产环境文件中删除。可能需要保留注释的一种情况是:保留远端代码库(keep the origin for a library)。
我们可以使用HTML minify插件删除注释。(使用 remove-html-comments - npm)
3.删除不必要的属性
<!-- Before HTML5 --> <script type="text/javascript"></script> <!-- Today --> <script></script>
4.使用语义化标签
使用语义化标签可以提高代码的可读性和可维护性,并有助于搜索引擎优化。例如,使用 标签来定义页面头部,使用 标签来定义导航等。
5.减少iframe数量
尽量少用iframe标签,爬虫是不会读取iframe的内容的。
6..削减DOM数量和层级数量
HTML 中标签元素越多,标签的层级越深,浏览器解析 DOM 并制作到浏览器中所花的时间就越长,所以应尽或许坚持 DOM 元素简洁和扁平化的层级。
7.减少 HTTP 请求次数
将多个 CSS 和 JavaScript 文件合并为一个文件,可以减少 HTTP 请求次数,从而提高页面加载速度。同时,使用浏览器缓存可以避免每次请求相同的文件。
8.减少重排重绘
三、JavaScript优化
1.js脚本放到页面底部
将<script>标签尽量尽可能放到<body>标签的底部。
2.将js和css从外部引入
3.删除重复的脚本
4.减少DOM访问
5.节流与防抖
防抖
n秒后在执行该事件,若在n秒内被重复触发,则重新计时。 用户高频事件完了,再进行事件操作
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求 - 手机号、邮箱验证输入检测onchange /oninput事件 - 窗口大小计算。只需窗口调整完成后,计算窗口大小。防止重复渲染
function debounce(func, delay) { let timer; // 定义计时器变量 return function() { clearTimeout(timer); // 清除之前的计时器 timer = setTimeout(() => { func(); // 在指定的延迟时间内调用传入的函数 }, delay); }; } // 使用示例 const inputElement = document.getElementById('input'); const handleInputChange = () => { console.log("Input value changed"); }; inputElement.addEventListener('keyup', debounce(handleInputChange, 300));
节流
n秒内只运行一次,若在n 秒内重复触发,只有一次生效
- 懒加载、滚动加载、加载更多或监听滚动条位置 - 防止高频点击提交,防止表单重复提交
function throttle(func, delay) { let lastTime = null; // 记录上次执行的时间 return function() { const currentTime = Date.now(); // 当前时间的毫秒数 if (lastTime && currentTime - lastTime < delay) { return; // 若两次触发时间小于delay,则不执行函数 } else { func(); // 否则执行函数并更新上次执行时间 lastTime = currentTime; } }; } // 使用示例 const scrollHandler = () => { console.log("Scroll event triggered"); }; window.addEventListener('scroll', throttle(scrollHandler, 500));
6.合理的ajax恳求
7.长列表虚拟滚动优化
8 .代码结构的优化
1.设置Viewport:HTML的viewport可加快页面的渲染。 2.减少DOM结点:DOM结点太多会影响页面的渲染。 3.尽量使用css3动画:合理使用requestAnimationFrame动画代替setTimeout。 4.优化高频事件:scroll、touchmove等事件尽量使用函数防抖节流等进行限制。 5. 不滥用WEB字体:WEB字体需要下载、解析、重绘当前页面,尽量减少使用。 6. 文件命名规则须统一且要有意义,同类型文件归类到相同的文件夹中。 7. 删除无效注释。
四、CSS优化
1. 尽量少用@import
1.使用@import引入CSS会影响浏览器的并行下载。使用@import引用的CSS文件只有在引用它的那个css文件被下载、解析之后,浏览器才会知道还有另外一个css需要下载,这时才去下载,然后下载后开始解析、构建render tree等一系列操作。这就导致浏览器无法并行下载所需的样式文件。
2.多个@import会导致下载顺序紊乱。在IE中,@import会引发资源文件的下载顺序被打乱,即排列在@import后面的js文件先于@import下载,并且打乱甚至破坏@import自身的并行下载。
2.避免!important,可以选择其他选择器
3.不要在ID选择器前面进行嵌套其它选择器
4.CSS文件压缩
5.CSS层级嵌套最好不要超过3层
6.删除无用的css
7.慎用*通配符
8.删除不必要的单位和零
9.异步加载非首屏css
10.将样式表放到页面顶部
五、图片优化
1.压缩图片
2.小图片引入雪碧图
3.图片懒加载
4.img图片的alt属性要写
5.采用svg图片或者字体图标
6.Base64
六、webpack构建优化
指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。
1. 线程加载器
多线程可以提高程序的效率
2. 缓存加载器
在我们的项目开发过程中,Webpack 需要多次构建项目。为了加快后续构建,我们可以使用缓存,与缓存相关的加载器是缓存加载器。
3.Hot update
当我们在项目中修改一个文件时,Webpack 默认会重新构建整个项目,但这并不是必须的。我们只需要重新编译这个文件,效率更高,这种策略称为Hot update。Webpack 内置了Hot update插件,我们只需要在配置中开启Hot update即可。
4.exclude & include
一些文件和文件夹永远不需要参与构建。所以我们可以在配置文件中指定这些文件,防止Webpack取回它们,从而提高编译效率。
1.exclude : 不需要编译的文件。
2.include : 需要编译的文件。
5.缩小 CSS 代码
压缩和去重 CSS 代码
6.缩小 JavaScript 代码
压缩和去重 JavaScript 代码。
7.tree-shaking
作用是把js文件中无用的模块或者代码删掉。而这通常需要借助一些工具。在webpack中tree-shaking就是在打包时移除掉javascript上下文中无用的代码,从而优化打包的结果。在webpack5中已经自带tree-shaking功能,在打包模式为production时,默认开启 tree-shaking功能。
8.source-map
当我们的代码出现bug时,source-map可以帮助我们快速定位到源代码的位置。但是这个文件很大。因此根据不同的环境来配置。
开发模式:生成更准确(但更大)
module.exports = { mode: 'development', devtool: 'eval-cheap-module-source-map' }
生产方式: 生成更小(但不那么准确)
module.exports = { mode: 'production', devtool: 'nosources-source-map' }
9.Bundle Analyzer
我们可以使用 webpack-bundle-analyzer 来查看打包后的 bundle 文件的体积,然后进行相应的体积优化。
10.模块懒加载
如果模块没有延迟加载,整个项目的代码会被打包成一个js文件,导致单个js文件体积非常大。那么当用户请求网页时,首屏的加载时间会更长。
使用模块来加载后,大js文件会被分割成多个小js文件,加载时网页按需加载,大大提高了首屏的加载速度。
11.压缩包
Gzip是一种常用的文件压缩算法,可以提高传输效率。但是,此功能需要后端配合。
12.base64
对于一些小图片,可以转成base64编码,这样可以减少用户的HTTP请求次数,提升用户体验
13.正确配置哈希
我们可以将哈希添加到捆绑文件中,这样可以更轻松地处理缓存。
output: { path: path.resolve(__dirname, '../dist'), filename: 'js/chunk-[contenthash].js', clean: true, },
七、资源加载优化
1.使用 Web Workers
Web Worker 是一个独立的线程(独立的执行环境),这就意味着它可以完全和 UI 线程(主线程)并行的执行 js 代码,从而不会阻塞 UI,它和主线程是通过 onmessage 和 postMessage 接口进行通信的。
Web Worker 使得网页中进行多线程编程成为可能。当主线程在处理界面事件时,worker 可以在后台运行,帮你处理大量的数据计算,当计算完成,将计算结果返回给主线程,由主线程更新 DOM 元素。
2.DNS预解析
浏览器对网站第一次的域名DNS解析查找流程依次为:
浏览器缓存 ->系统缓存 ->路由器缓存 ->ISP DNS缓存 ->递归搜索
DNS预解析的实现:
用meta信息来告知浏览器, 当前页面要做DNS预解析: <meta http-equiv="x-dns-prefetch-control" content="on" />
在页面header中使用link标签来强制对DNS预解析: <link rel="dns-prefetch" href="https://code-nav.top" />
dns-prefetch最大的缺点就是使用它太多。过多的预获取会导致过量的DNS解析,对网络是一种负担
3.预加载 preload
1.遇到link标签时,立刻下载并放到内存中,不执行js。
2.遇到script标签时,将预加载的js执行。
3.对跨域的文件进行preload时,必须加上 crossorigin 属性
<link rel="preload" crossorigin href="./zone.js" as="script">
基于标记语言的异步加载:
<link rel="preload" as="style" href="asyncstyle.css" onload="this.rel='stylesheet'">
九、服务器优化
1.静态资源使用 CDN
用户与服务器的物理距离对响应时间也有影响。把内容部署在多个地理位置分散的服务器上能让用户更快地载入页面, CDN就是为了解决这一问题,在多个位置部署服务器,让用户离服务器更近,从而缩短请求时间。
2.添加Expires或者Cache-Control响应头
使用cach-control或expires这类强缓存时,缓存不过期的情况下,不向服务器发送请求。强缓存过期时,会使用last-modified或etag这类协商缓存,向服务器发送请求,如果资源没有变化,则服务器返回304响应,浏览器继续从本地缓存加载资源;如果资源更新了,则服务器将更新后的资源发送到浏览器,并返回200响应。
3.对组件使用Gzip压缩
4.配置ETag
Entity tags(ETags)(实体标签)是web服务器和浏览器用于判断浏览器缓存中的内容和服务器中的原始内容是否匹配的一种机制(“实体”就是所说的“内 容”,包括图片、脚本、样式表等)。增加ETag为实体的验证提供了一个比使用“last-modified date(上次编辑时间)”更加灵活的机制。Etag是一个识别内容版本号的唯一字符串。唯一的格式限制就是它必须包含在双引号内。原始服务器通过含有 ETag文件头的响应指定页面内容的ETag。
5.提供来自相同协议的文件
避免网站使用HTTPS同时使用HTTP来提供相同源地址的文件。
6.开启http2(多路复用,并行加载)
HTTP2带来了非常大的加载优化,所以在做优化上首先就想到了用HTTP2代替HTTP1。
7.服务端渲染
服务端返回 HTML 文件,客户端只需解析 HTML。首屏渲染快,SEO 好。
缺点:配置麻烦,增加了服务器的计算压力。
8.分域存放资源
由于浏览器同一域名并行下载数有限,利用多域名主机存放静态资源,增加并行下载数,缩短资源加载时间。
9.减少页面重定向
4. 前端工程化篇
1.git 和 svn 的区别
1.git 和 svn 最大的区别在于 git 是分布式的,而 svn 是集中式的。因此我们不能再离线的情况下使用 svn。如果服务器出现问题,就没有办法使用 svn 来提交代码。
2.svn 中的分支是整个版本库的复制的一份完整目录,而 git 的分支是指针指向某次提交,因此 git 的分支创建更加开销更小并且分支上的变化不会影响到其他人。svn 的分支变化会影响到所有的人。
3.svn 的指令相对于 git 来说要简单一些,比 git 更容易上手。
4.GIT把内容按元数据方式存储,而SVN是按文件:因为git目录是处于个人机器上的一个克隆版的版本库,它拥有中心版本库上所有的东西,例如标签,分支,版本记录等。
5.GIT分支和SVN的分支不同:svn会发生分支遗漏的情况,而git可以同一个工作目录下快速的在几个分支间切换,很容易发现未被合并的分支,简单而快捷的合并这些文件。
6.GIT没有一个全局的版本号,而SVN有
7.GIT的内容完整性要优于SVN:GIT的内容存储使用的是SHA-1哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题时降低对版本库的破坏
2.经常使用的 git 命令
git init // 新建 git 代码库 git add // 添加指定文件到暂存区 git rm // 删除工作区文件,并且将这次删除放入暂存区 git commit -m [message] // 提交暂存区到仓库区 git branch // 列出所有分支 git checkout -b [branch] // 新建一个分支,并切换到该分支 git status // 显示有变更文件的状态
3.git pull 和 git fetch
1.git fetch 只是将远程仓库的变化下载下来,并没有和本地分支合并。
2.git pull 会将远程仓库的变化下载下来,并和当前分支合并。
4. git rebase 和 git merge 的区别
1.git merge 会新建一个新的 commit 对象,然后两个分支以前的 commit 记录都指向这个新 commit 记录。这种方法会保留之前每个分支的 commit 历史。
2.git rebase 会先找到两个分支的第一个共同的 commit 祖先记录,然后将提取当前分支这之后的所有 commit 记录,然后将这个 commit 记录添加到目标分支的最新提交后面。经过这个合并后,两个分支合并后的 commit 记录就变为了线性的记录了
5. webpack与grunt、gulp的不同
Grunt、Gulp是基于任务运⾏的⼯具: 它们会⾃动执⾏指定的任务,就像流⽔线,把资源放上去然后通过不同插件进⾏加⼯,它们包含活跃的社区,丰富的插件,能⽅便的打造各种⼯作流。
Webpack是基于模块化打包的⼯具: ⾃动化处理模块,webpack把⼀切当成模块,当 webpack 处理应⽤程序时,它会递归地构建⼀个依赖关系图 (dependency graph),其中包含应⽤程序需要的每个模块,然后将所有这些模块打包成⼀个或多个 bundle。
6.webpack的构建流程
1.初始化参数:从配置⽂件和 Shell 语句中读取与合并参数,得出最终的参数;
2.开始编译:⽤上⼀步得到的参数初始化 Compiler 对象,加载所有配置的插件,执⾏对象的 run ⽅法开始执⾏编译;
3.确定⼊⼝:根据配置中的 entry 找出所有的⼊⼝⽂件;
4.编译模块:从⼊⼝⽂件出发,调⽤所有配置的 Loader 对模块进⾏翻译,再找出该模块依赖的模块,再递归本步骤直到所有⼊⼝依赖的⽂件都经过了本步骤的处理;
5.完成模块编译:在经过第4步使⽤ Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
6.输出资源:根据⼊⼝和模块之间的依赖关系,组装成⼀个个包含多个模块的 Chunk,再把每个 Chunk 转换成⼀个单独的⽂件加⼊到输出列表,这步是可以修改输出内容的最后机会;
7.输出完成:在确定好输出内容后,根据配置确定输出的路径和⽂件名,把⽂件内容写⼊到⽂件系统。
在以上过程中,Webpack 会在特定的时间点⼴播出特定的事件,插件在监听到感兴趣的事件后会执⾏特定的逻辑,并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。
7.⽤webpack来优化前端性能
⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。
1.压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css
2.利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径
3.Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现
4.Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存
5.提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码
8.提⾼webpack的打包速度
1.happypack: 利⽤进程并⾏编译loader,利⽤缓存来使得 rebuild 更快,遗憾的是作者表示已经不会继续开发此项⽬,类似的替代者是thread-loader
2.外部扩展(externals): 将不怎么需要更新的第三⽅库脱离webpack打包,不被打⼊bundle中,从⽽减少打包时间,⽐如jQuery⽤script标签引⼊
3.dll: 采⽤webpack的 DllPlugin 和 DllReferencePlugin 引⼊dll,让⼀些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
4.利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、 HappyPack.cache 都可以利⽤缓存提⾼rebuild效率缩⼩⽂件搜索范围: ⽐如babel-loader插件,如果你的⽂件仅存在于src中,那么可以 include: path.resolve(__dirname,'src') ,当然绝⼤多数情况下这种操作的提升有限,除⾮不⼩⼼build了node_modules⽂件
9.如何提⾼webpack的构建速度
1.多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
2.通过 externals 配置来提取常⽤库
3.利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
4.使⽤ Happypack 实现多线程加速编译
5.使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
6.使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码
对 WebSocket 的理解
是HTML5提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。
WebSocket 的出现就解决了半双工通信的弊端。它最大的特点是:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。
WebSocket原理:客户端向 WebSocket 服务器通知(notify)一个带有所有接收者ID(recipients IDs)的事件(event),服务器接收后立即通知所有活跃的(active)客户端,只有ID在接收者ID序列中的客户端才会处理这个事件。
// 在index.html中直接写WebSocket,设置服务端的端口号为 9999 let ws = new WebSocket('ws://localhost:9999'); // 在客户端与服务端建立连接后触发 ws.onopen = function() { console.log("Connection open."); ws.send('hello'); }; // 在服务端给客户端发来消息的时候触发 ws.onmessage = function(res) { console.log(res); // 打印的是MessageEvent对象 console.log(res.data); // 打印的是收到的消息 }; // 在客户端与服务端建立关闭后触发 ws.onclose = function(evt) { console.log("Connection closed."); };
5.浏览器原理篇
XSS 攻击
是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
CSRF 攻击
跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
中间人攻击
中间⼈ (Man-in-the-middle attack, MITM) 是指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。
有哪些可能引起前端安全的问题
1.跨站脚本 (Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式, 为了与 CSS 区分所以被称作 XSS。早期常⻅于⽹络论坛, 起因是⽹站没有对⽤户的输⼊进⾏严格的限制, 使得攻击者可以将脚本上传到帖⼦让其他⼈浏览到有恶意脚本的⻚⾯, 其注⼊⽅式很简单包括但不限于 JavaScript / CSS / Flash 等;
2.iframe的滥⽤: iframe中的内容是由第三⽅来提供的,默认情况下他们不受控制,他们可以在iframe中运⾏JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端⽤户体验;
3.跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已完成认证的⽤户进⾏⾮预期的个⼈信息或设定信息等某些状态更新,属于被动攻击
4.恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数时候都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被植⼊恶意代码很容易引起安全问题。
协商缓存和强缓存
强缓存(也被称为本地缓存)
浏览器在初次请求资源的时候,服务器会在响应头中去设置 Cache-Control 或者Expires这个字段,指示资源的有效期,下次请求同意资源前,浏览器会检查本地缓存,如果资源未超期并且有效,那直接从缓存中读取,不必再向服务器发起请求,节省了网络往返时间,他的特点就是零网络延迟/加载速度快,但是依赖过期管理;
协商缓存(也被称为条件请求)
当强缓存过期或者不存在的时候,浏览器向服务器发送请求,并且在请求头当中携带上一次缓存的资源信息,服务器根据这些信息来判断资源是否已经改变。
那如果资源没有改变,服务器返回304,指示浏览器使用本地缓存;
如果资源有更新,服务器则返回新的资源内容和新的缓存控制指令,
特点就是:减少数据传输量,
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。
在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。
浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。
如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别
1.点击刷新按钮或者按 F5:浏览器直接对本地的缓存文件过期,但是会带上If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是 304,也有可能是 200。
2.用户按 Ctrl+F5(强制刷新):浏览器不仅会对本地文件过期,而且不会带上 If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是 200。
3.地址栏回车: 浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
如何解决跨越问题
1. CORS
2、 通过jsonp跨域
3、 postMessage跨域
4、 nginx代理跨域
5、 nodejs中间件代理跨域
6、 document.domain + iframe跨域
7、 location.hash + iframe
8、 window.name + iframe跨域
9、 WebSocket协议跨域
6.手写代码
1. 手写 Object.create
function create(obj) { function F() {} F.prototype = obj return new F() }
2. 手写 instanceof 方法
实现步骤: 1.首先获取类型的原型 2.然后获得对象的原型 3.然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left), // 获取对象的原型 prototype = right.prototype; // 获取构造函数的 prototype 对象 // 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true; proto = Object.getPrototypeOf(proto); } }
防抖
函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时
// 函数防抖的实现 function debounce(fn, wait) { let timer = null; return function() { let context = this, args = arguments; // 如果此时存在定时器的话,则取消之前的定时器重新记时 if (timer) { clearTimeout(timer); timer = null; } // 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); }; }
节流函数
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
// 函数节流的实现; function throttle(fn, delay) { let curTime = Date.now(); return function() { let context = this, args = arguments, nowTime = Date.now(); // 如果两次时间间隔超过了指定时间,则执行函数。 if (nowTime - curTime >= delay) { curTime = Date.now(); return fn.apply(context, args); } }; }
手写一个深拷贝
// 手写一个深拷贝 function deepClone<T extends Array<T> | any>(obj: T): T { if (typeof obj !== "object" || obj === null) return obj; const result: T = obj instanceof Array ? ([] as T) : ({} as T); for (const key in obj) { if (obj.hasOwnProperty(key)) { result[key] = obj[key]; } } return result; } const obj = { a: 1, b: { bb: "hh", }, c() { console.log("cc"); }, }; const cloneObj = deepClone(obj); obj.a = 999; console.log("cloneObj :>> ", cloneObj); console.log("obj :>> ", obj); // cloneObj :>> { a: 1, b: { bb: 'hh' }, c: [Function: c] } // obj :>> { a: 999, b: { bb: 'hh' }, c: [Function: c] } const arr: Array<number | string> = [1, 2, 3, "6"]; const copyArr = deepClone(arr); arr[3] = 4; console.log("arr | copyArr :>> ", arr, copyArr); // arr | copyArr :>> [ 1, 2, 3, 4 ] [ 1, 2, 3, '6' ]
手写快速排序
function quickSort(arr: number[], startIndex = 0): number[] { if (arr.length <= 1) return arr; const right: number[] = [], left: number[] = [], startNum = arr.splice(startIndex, 1)[0]; for (let i = 0; i < arr.length; i++) { if (arr[i] < startNum) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...quickSort(left), startNum, ...quickSort(right)]; }
输入为两个一维数组,将这两个数组合并,去重,不要求排序,返回一维数组
function dealArr(arr1: any[], arr2: any[]): any[] { return Array.from(new Set([...arr1.flat(), ...arr2.flat()])); } const arr1 = ["a", 1, 2, 3, ["b", "c", 5, 6]]; const arr2 = [1, 2, 4, "d", ["e", "f", "5", 6, 7]]; console.log("dealArr(arr1, arr2 ); :>> ", dealArr(arr1, arr2)); // dealArr(arr1, arr2 ); :>> [ 'a', 1, 2, 3,'b', 'c', 5,6, 4, 'd', 'e', 'f','5', 7]
将数字每千分位用逗号隔开
function formatNumber(num) { // 确保输入是数字 if (typeof num !== 'number') { return '输入必须是数字'; } // 取绝对值并转为字符串 const isNegative = num < 0; const absNum = Math.abs(num).toString(); // 使用正则表达式在每三位数字前添加逗号 const parts = absNum.split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); // 整数部分格式化 // 如果有小数部分,拼接回来 const formattedNumber = parts.join('.'); // 如果原始数是负数,添加负号 return isNegative ? `-${formattedNumber}` : formattedNumber; } // 测试 console.log(formatNumber(1234567)); // 输出 "1,234,567" console.log(formatNumber(-1234567.89)); // 输出 "-1,234,567.89"
类型判断函数
function getType(value) { // 判断数据是 null 的情况 if (value === null) { return value + ""; } // 判断数据是引用类型的情况 if (typeof value === "object") { let valueClass = Object.prototype.toString.call(value), type = valueClass.split(" ")[1].split(""); type.pop(); return type.join("").toLowerCase(); } else { // 判断数据是基本数据类型的情况和函数的情况 return typeof value; } }
使用Promise封装AJAX请求
// promise 封装实现: function getJSON(url) { // 创建一个 promise 对象 let promise = new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest(); // 新建一个 http 请求 xhr.open("GET", url, true); // 设置状态的监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当请求成功或失败时,改变 promise 的状态 if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; // 设置错误监听函数 xhr.onerror = function() { reject(new Error(this.statusText)); }; // 设置响应的数据类型 xhr.responseType = "json"; // 设置请求头信息 xhr.setRequestHeader("Accept", "application/json"); // 发送 http 请求 xhr.send(null); }); return promise; }
实现日期格式化函数
dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01 dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01 dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日 const dateFormat = (dateInput, format)=>{ var day = dateInput.getDate() var month = dateInput.getMonth() + 1 var year = dateInput.getFullYear() format = format.replace(/yyyy/, year) format = format.replace(/MM/,month) format = format.replace(/dd/,day) return format }
数组去重
new Set
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]; Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
使用map存储不重复的数字
const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8]; uniqueArray(array); // [1, 2, 3, 5, 9, 8] function uniqueArray(array) { let map = {}; let res = []; for(var i = 0; i < array.length; i++) { if(!map.hasOwnProperty([array[i]])) { map[array[i]] = 1; res.push(array[i]); } } return res; }
将扁平结构转化为树形结构
function arrayToTree(array, parentId = null) { const result = []; // 遍历数组,查找当前层级的元素 for (let item of array) { // 检查是否是当前父级的子项 if (item.parentId === parentId) { // 递归查找该项的子项 const children = arrayToTree(array, item.id); if (children.length > 0) { // 将子项添加到当前项 item.children = children; } result.push(item); } } return result; } // 测试示例 const data = [ { id: 1, name: 'A', parentId: null }, { id: 2, name: 'B', parentId: 1 }, { id: 3, name: 'C', parentId: 1 }, { id: 4, name: 'D', parentId: 2 }, { id: 5, name: 'E', parentId: 2 }, { id: 6, name: 'F', parentId: null }, { id: 7, name: 'G', parentId: 6 }, ]; const tree = arrayToTree(data); console.log(JSON.stringify(tree, null, 2));
将js对象转化为树形结构
function jsonToTree(data) { // 初始化结果数组,并判断输入数据的格式 let result = [] if(!Array.isArray(data)) { return result } // 使用map,将当前对象的id与当前对象对应存储起来 let map = {}; data.forEach(item => { map[item.id] = item; }); // data.forEach(item => { let parent = map[item.pid]; if(parent) { (parent.children || (parent.children = [])).push(item); } else { result.push(item); } }); return result; }
每隔一秒打印 1,2,3,4
使用闭包实现
for (var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); }, i * 1000); })(i); }
使用 let 块级作用域
for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, i * 1000); }
用Promise实现图片的异步加载
let imageAsync=(url)=>{ return new Promise((resolve,reject)=>{ let img = new Image(); img.src = url; img.οnlοad=()=>{ console.log(`图片请求成功,此处进行通用操作`); resolve(image); } img.οnerrοr=(err)=>{ console.log(`失败,此处进行失败的通用操作`); reject(err); } }) } imageAsync("url").then(()=>{ console.log("加载成功"); }).catch((error)=>{ console.log("加载失败"); })
自执行函数,形参与实参
var test=(function(i){ return function(){ console.log(i+'=='+i*5); } })(5); test(2);//默认参数是5,所以test传值2根本就没接收,所以答案是25
var a="00"; (function(){ alert(a);//求a的输出是多少 var a="01"; })(); // undefined
var a="00"; function fun(){ console.log(this.a);//求a的输出是多少 console.log(a);//求a的输出是多少 var a="01"; } (fun)() // 00 undefined // 解题思路:自执行函数,全局变量a,局部变量a 加载自执行函数 // 的时候会遍历上下文这种方式叫执行上下文 // 执行上下文环境中包含a变量 但是未加载 程序开始执行console.log // 所以当前 a=undefined,如果当局部变量a被删除之后执行上下文 // 结果并没有变量声明程序索引到全局变量a这时候a输出"00",相当于程序正常加载, // 但并非如此(个人理解如有纰漏请指出)
原型链掌握程度
var A=function(name){ console.log('a-'+name) if(name) this.name=name; } var B=function(name){ console.log('B-'+name) this.name=name; } var C=function(name){ console.log('c-'+name) this.name=name||"jon" }; A.prototype.name="tomA"; B.prototype.name="tomB"; C.prototype.name="tomC"; //求下面三个输出值 console.log(new A().name);// a-undefined tomA // 首先A函数里面判断如果name有值的话 this.name=name // 当然name有值了!!是undefined ! 重新赋值name this只当前作用域 // 查询当前原型链name属性 tom 所以第一项是tom. console.log(new B().name);// B-undefined undefined console.log(new C().name);// c-undefined jon
考察变量的提升
function Foo(){ getName = function(){console.log(1);} return this; } Foo.getName = function(){console.log(2)} Foo.prototype.getName = function(){console.log(3)} var getName = function(){console.log(4)} function getName(){console.log(5)} Foo.getName(); // 2 getName(); // 4 Foo().getName(); // 1 getName(); // 1 new Foo.getName(); // 2 new Foo().getName(); // 3 new new Foo().getName();// 3
js的参数传递
function fun(n,o){ console.log(o); return { fun: function(m){ return fun(m,n); } } } let a = fun(0);// undefined a.fun(1); // 0 a.fun(2); // 0 a.fun(3); // 0 let b = fun(0).fun(1).fun(2).fun(3);// undefined 0 1 2 let c = fun(0).fun(1);// undefined 0 c.fun(2); // 1 c.fun(3); // 1
7.Vue
1. Vue的基本原理
当一个Vue实例创建时,Vue会遍历data中的属性,用 Object.defineProperty(vue3.0使用proxy )将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。
每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
2. 双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
第一步: 需要observer(数据劫持)对数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化;
第二步: compiler(订阅者)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;
第三步: Watcher(观察者)是Observer和Compiler之间通信的桥梁,主要做的事情是: 1、在自身实例化时往属性订阅器(dep)里面添加自己 2、自身必须有一个update()方法 3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步: MVVM作为数据绑定的入口,整合Observer、Compiler和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
3. computed和watch的区别
使用场景
computed适用于一个数据受多个数据影响使用; watch适合一个数据影响多个数据使用
区别
computed属性默认会走缓存,只有依赖数据发生变化,才会重新计算,不支持异步,有异步导致数据发生变化时,无法做出相应改变; watch不依赖缓存,一旦数据发生变化就直接触发响应操作,支持异步。
4. v-if和v-show的区别
1.手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
2.编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
3.编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
4.性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
5.使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。
5. v-model 是如何实现的
作用在表单元素上
动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值: <input v-model="sth" /> // 等同于 <input v-bind:value="message" v-on:input="message=$event.target.value" > //$event 指代当前触发的事件对象; //$event.target 指代当前触发的事件对象的dom; //$event.target.value 就是当前dom的value值; //在@input方法中,value => sth; //在:value中,sth => value;
作用在组件上
在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件 本质是一个父子组件通信的语法糖,通过prop和$.emit实现。因此父组件 v-model 语法糖本质上可以修改为: <child :value="message" @input="function(e){message = e}"></child> 在组件的实现中,可以通过 v-model属性来配置子组件接收的prop名称,以及派发的事件名称。
// 父组件 <aa-input v-model="aa"></aa-input> // 等价于 <aa-input v-bind:value="aa" v-on:input="aa=$event.target.value"></aa-input> // 子组件: <input v-bind:value="aa" v-on:input="onmessage"></aa-input> props:{value:aa,} methods:{ onmessage(e){ $emit('input',e.target.value) } }
6. data为什么是一个函数而不是对象
JavaScript中的对象是引用类型的数据,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化。
而在Vue中,更多的是想要复用组件,那就需要每个组件都有自己的数据,这样组件之间才不会相互干扰。
所以组件的数据不能写成对象的形式,而是要写成函数的形式。数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
7. $nextTick 原理及作用
在 Vue.js 中,当我们对数据进行修改时,Vue.js 会异步执行 DOM 更新。在某些情况下,我们需要在 DOM 更新完成后执行一些操作,这时就需要使用 Vue.nextTick() 方法。 Vue.nextTick() 方法的实现原理是基于浏览器的异步任务队列,采用微任务优先的方式。当我们修改数据时,Vue.js 会将 DOM 更新操作放到一个异步任务队列中,等待下一次事件循环时执行。而 Vue.nextTick() 方法则是将一个回调函数推入到异步任务队列中,等待 DOM 更新完成后执行。 具体实现方式有以下几种: 使用原生的 setTimeout 方法:在 Vue.js 2.x 中,如果浏览器支持 Promise,则会优先使用 Promise.then() 方法。如果不支持 Promise,则会使用原生的 setTimeout 方法模拟异步操作。 使用 MutationObserver:如果浏览器支持 MutationObserver,Vue.js 会使用 MutationObserver 监听 DOM 更新,并在 DOM 更新完成后执行回调函数。 使用 setImmediate:在 IE 中,setImmediate 方法可以用来延迟异步执行任务。在 Vue.js 2.x 中,如果浏览器支持 setImmediate,则会优先使用 setImmediate,否则会使用 setTimeout。
什么时候使用$nextTick
异步更新DOM:当你修改了Vue实例的数据后Vue会异步执行DOM更新。如果你想要在DOM更新完成后执行一些操作(例如获取更新后的DOM元素),可以使用 nextTick()来确在DOM更新完成后执行回调函数。
更新后的DOM操作:有时候,你可能需要更新后的DOM上进行一些操作,例如获取某个元素的尺寸、位置等信息。于Vue的DOM更新是异步,直接在数据变化后立即访问DOM可能无获取到最新的结果。通过将操作放在nextTick()的调函数中,可以保在DOM更新完成后再进行相关操作。
批量更新优化:Vue在更新DOM时会对多个数据变化进行批量处理,以高性能。而nextTick()会将回调函数入到下一个DOM更新循中执行,这样可以将多个nextTick()回调函数合并为一个,减少必要的DOM操作,提高性能。
总结起来,nextTick()的主要用是在下次DOM更新循环结束后执行回调函数,用处理DOM更新后的操作或者确获取到最新的DOM状态。
作用:
等待 DOM 更新后执行操作:有时候你需要在 Vue 更新 DOM 后执行一些操作,例如操作更新后的 DOM 元素。使用 $nextTick 可以确保你的操作在 DOM 更新完成后执行。
避免不同数据更新之间的竞态条件:如果你在数据更新后立即想要获取更新后的 DOM 信息或进行操作,直接在数据更新后使用 $nextTick 会更可靠,避免竞态条件。
8. Vue template 到 render 的过程
vue的模版编译过程主要如下:template -> ast -> render函数
export default { data() { return { message: 'Hello Vue!' }; }, render(h) { return h('div', { attrs: { id: 'app' } }, [ h('p', this.message) ]); } }
在这个render函数中,h是一个用来创建VNode的函数,类似于hyperscript,它是Vue的虚拟DOM创建函数。h('div', { attrs: { id: 'app' } }, [h('p', this.message)]) 创建了一个div元素,该div拥有一个id为'app'的属性,并包含一个文本节点this.message。
这个过程是自动的,Vue的编译器会在构建过程中把.vue文件中的template转换成render函数。开发者通常不需要直接处理这个过程,除非需要手动写render函数来取代template,这在高级用例中是允许的。
9. Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环tick中,Vue 刷新队列并执行实际(已去重的)工作。
10. React 和 Vue 的理解
相似之处
a.都将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库; b.都有自己的构建工具,能让你得到一个根据最佳实践设置的项目模板; c.都使用了Virtual DOM(虚拟DOM)提高重绘性能; d.都有props的概念,允许组件间的数据传递; e.都鼓励组件化应用,将应用分拆成一个个功能明确的模块,提高复用性。
不同之处
1)数据流
Vue默认支持数据双向绑定,而React一直提倡单向数据流
2)虚拟DOM
Vue2.x开始引入"Virtual DOM",消除了和React在这方面的差异,但是在具体的细节还是有各自的特点。
a.Vue宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
b.对于React而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate这个生命周期方法来进行控制,但Vue将此视为默认的优化。
3)组件化
React与Vue最大的不同是模板的编写。 a.Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属性。 b.React推荐你所有的模板通用JavaScript的语法扩展——JSX书写。 具体来讲:React中render函数是支持闭包特性的,所以import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以 import 一个组件完了之后,还需要在 components 中再声明下。
4)监听数据变化的实现原理不同
a.Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能
b.React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的vDOM的重新渲染。这是因为 Vue 使用的是可变数据,而React更强调数据的不可变。
5)高阶组件
react可以通过高阶组件(HOC)来扩展,而Vue需要通过mixins来扩展。
高阶组件就是高阶函数,而React的组件本身就是纯粹的函数,所以高阶函数对React来说易如反掌。相反Vue.js使用HTML模板创建视图组件,这时模板无法有效的编译,因此Vue不能采用HOC来实现。
6)构建工具
两者都有自己的构建工具: a.React ==> Create React APP b.Vue ==> vue-cli
7)跨平台
a.React ==> React Native b.Vue ==> Weex
11. Vue的优点
1.轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb;
2.简单易学:国人开发,中文文档,不存在语言障碍,易于理解和学习;
3.双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;
4.组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;
5.视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
6.虚拟DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;
7.运行速度更快:相比较于 react 而言,同样是操作虚拟 dom,就性能而言, vue 存在很大的优势。
12. assets和static的区别
相同点: assets 和 static 两个都是存放静态资源文件。图片,字体图标,样式文件等。
不相同点:在项目打包时,将 assets 中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static 文件中跟着 index.html 一同上传至服务器。static 中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static 中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets 中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议: 将项目中 template需要的样式文件js文件等都可以放置在 assets 中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css 等文件可以放置在 static 中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。
13. vue如何监听对象或者数组某个属性的变化
- this.$set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value) this.$set(this.arr, 0, "OBKoro1"); // 改变数组 this.$set(this.obj, "c", "OBKoro1"); // 改变对象 - 调用以下几个数组的方法 splice()、 push()、pop()、shift()、unshift()、sort()、reverse()
vm.$set 的实现原理是:
a.如果目标是数组,直接使用数组的 splice 方法触发相应式;
b.如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
14. vue 修改数据页面不重新渲染
vue2是用过Object.defineProperty实现数据响应式, 组件初始化时,对 data 中的 item 进行递归遍历,对 item 的每一个属性进行劫持,添加 set , get 方法。我们后来新加的属性 ,并没有通过Object.defineProperty设置成响应式数据,修改后不会视图更新 通过数组索引号修改了数组,界面会不会相应更新?为什么? 答:不会。vue 监听不到 vue 为什么没有提供 arr[下标] = val 变成响应式? 尤大:“因为性能问题,性能代价和获得的用户体验收益不成正比”
15. Vue的性能优化有哪些
(1)编码阶段
1.尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
2.v-if和v-for不能连用
3.如果需要使用v-for给每项元素绑定事件时使用事件代理
4.SPA 页面采用keep-alive缓存组件
5.在更多的情况下,使用v-if替代v-show
6.key保证唯一
7.使用路由懒加载、异步组件
8.防抖、节流
9.第三方模块按需导入
10.长列表滚动到可视区域动态加载
11.图片懒加载
(2)SEO优化
1.预渲染
2.服务端渲染SSR
(3)打包优化
1.压缩代码
2.Tree Shaking/Scope Hoisting
3.使用cdn加载第三方模块
4.多线程打包happypack
5.splitChunks抽离公共文件
6.sourceMap优化
(4)用户体验
1.骨架屏
2.PWA
3.还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
16. template和jsx的有什么分别?
对于 runtime 来说,只需要保证组件存在 render 函数即可,而有了预编译之后,只需要保证构建过程中生成 render 函数就可以。在 webpack 中,使用vue-loader编译.vue文件,内部依赖的vue-template-compiler模块,在 webpack 构建过程中,将template预编译成 render 函数。与 react 类似,在添加了jsx的语法糖解析器babel-plugin-transform-vue-jsx之后,就可以直接手写render函数。
所以,template和jsx的都是render的一种表现形式,不同的是:JSX相对于template而言,具有更高的灵活性,在复杂的组件中,更具有优势,而 template 虽然显得有些呆滞。但是 template 在代码结构上更符合视图与逻辑分离的习惯,更简单、更直观、更好维护。
17. vue生命周期
生命周期是指 Vue 实例从创建到销毁的整个过程。
beforecreate --创建前: 可以在这加个loading事件,在加载实例时触发; 组件实例更被创建,组件属性计算之前,数据对象data都为undefined,未初始化。 created --创建后: 初始化完成时的事件写在这里,如在这结束loading事件; 组件实例创建完成,属性已经绑定,数据对象data已存在,但dom未生成,$el未存在 beforeMount---挂载前:vue实例的$el和data都已初始化,挂载之前为虚拟的dom节点,data.message未替换 mounted--挂载后 : 挂载元素,获取到DOM节点;vue实例挂载完成,data.message成功渲染 beforeUpdate----更新前:当data变化时,会触发beforeUpdate方法 updated--更新后 : 当data变化时,会触发updated方法,如果对数据统一处理,在这里写上相应函数; beforeDestroy--销毁前 : 组件销毁之前调用,可以做一个确认停止事件的确认框; destoryed---销毁后:组件销毁之后调用,对data的改变不会再触发周期函数,vue实例已解除事件监听和dom绑定, 但dom结构依然存在 nextTick : 更新数据后立即操作dom;
keep-alive 独有的生命周期,分别为 activated 和 deactivated
18. created和mounted的区别
1.created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。
2.mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
19. 一般在哪个生命周期请求异步数据
我们可以在钩子函数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。 推荐在 created 中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点: a.能更快获取到服务端数据,减少页面加载时间,用户体验更好; b.SSR不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性。
20. 组件通信
(1) props / $emit
适用 父子组件通信
(2)eventBus事件总线($emit / $on)
适用于 父子、隔代、兄弟组件通信
(3)依赖注入(project / inject)
适用于 隔代组件通信
(4)ref / $refs
适用 父子组件通信
(5)$root/ $parent / $children
适用 父子组件通信
(6)$attrs / $listeners
适用于 隔代组件通信
(7)vue2的vuex ;vue3的 pinia
适用于 父子、隔代、兄弟组件通信
8. localtionStore
9.插槽
哪些其实是已经在 Vue3 中被废弃或者不推荐使用
$children
$listeners
$on
EventBus 不推荐使用
21. vuex
有哪几种属性
a.state => 基本数据(数据源存放地)
b.getters => 从基本数据派生出来的数据
c.mutations => 提交更改数据的方法,同步
d.actions => 像一个装饰器,包裹mutations,使之可以异步。
e.modules => 模块化Vuex
Mutation
目的:用于直接改变 Vuex 中的状态(state)。 同步性:必须是同步操作,用户不能在 mutation 中执行异步任务。 触发方式:通过 store.commit() 方法来触发。
const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { state.count++ // 变更状态 } } })
Action
目的:用于处理异步操作或复杂的业务逻辑,然后最终提交 mutation 来改变状态。 1.Action 可以包含任意异步操作。 2.Action 提交的是 mutation,而不是直接变更状态。
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') } } })
在视图更新时,先触发actions,actions再触发mutation
22. Redux 和 Vuex
区别
a.Vuex改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,无需switch,只需在对应的mutation函数里改变state值即可
b.Vuex由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可
c.Vuex数据流的顺序是∶View调用store.commit提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)
通俗点理解就是,vuex 弱化 dispatch,通过commit进行 store状态的一次更变;取消了action概念,不必传入特定的 action形式进行指定变更;弱化reducer,基于commit参数直接对数据进行转变,使得框架更加简易;
共同思想
a.单—的数据源
b.变化可以预测
本质上:redux与vuex都是对mvvm思想的服务,将数据从视图中抽离的一种方案;
形式上:vuex借鉴了redux,将store作为全局的数据中心,进行mode管理;
23. Vue3.0有什么更新
1)监测机制的改变
a.3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪。
b.消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制
2)只能监测属性,不能监测对象
a.检测属性的添加和删除;
b.检测数组索引和长度的变更;
c.支持 Map、Set、WeakMap 和 WeakSet。
3)模板
a.作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
b.同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
4)对象式的组件声明方式
a.vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
b.3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易
5)其它方面的更改
a.支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
b.支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
c.基于 tree shaking 优化,提供了更多的内置功能。
24. defineProperty和proxy的区别
Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。 但是这样做有以下问题: a.添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过$set 来调用Object.defineProperty()处理。 b.无法监控到数组下标和长度的变化。
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty(),其有以下特点: a.Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。 b.Proxy 可以监听数组的变化。
25.Vue3.0 为什么要用 proxy?
在 Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶ a.不需用使用 Vue.$set 或 Vue.$delete 触发响应式。 b.全方位的数组变化检测,消除了Vue2 无效的边界情况。 c.支持 Map,Set,WeakMap 和 WeakSet。
Proxy 实现的响应式原理与 Vue2的实现原理相同,实现方式大同小异∶ a.get 收集依赖 b.Set、delete 等触发依赖 c.对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑。
26.Vue 3.0 中的 Vue Composition API?
在 Vue2 中,代码是 Options API 风格的,也就是通过填充 (option) data、methods、computed 等属性来完成一个 Vue 组件。这种风格使得 Vue 相对于 React极为容易上手,同时也造成了几个问题: a.由于 Options API 不够灵活的开发方式,使得Vue开发缺乏优雅的方法来在组件间共用代码。 b.Vue 组件过于依赖this上下文,Vue 背后的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发原则相悖,比如在methods 中的this竟然指向组件实例来不指向methods所在的对象。这也使得 TypeScript 在Vue2 中很不好用。
于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API本质上是将 Options API 背后的机制暴露给用户直接使用,这样用户就拥有了更多的灵活性,也使得 Vue3 更适合于 TypeScript 结合。
Composition API 使得 Vue3 的开发风格更接近于原生 JavaScript,带给开发者更多地灵活性
27. Vue中key的作用
1.第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
2.第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
28. 为什么不建议用index作为key?
使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
29. vue2 和 vue3 的生命周期是有区别的
vue3
区别1
主要是针对最后两个生命周期:
beforeDestroy -> beforeUnmount
Destoryed -> Unmounted
区别2
composition API 提供了 setup 函数作为入口函数,替换了 beforeCreate 和 created 这两个生命周期钩子。
不能“简单的把 setup 理解为 created 进行使用”,setup里访问不了this,其次setup的执行时机比beforeCreate 还早
其他
其中在 created 中,因为组件实例已经处理好了所有与状态相关的操作,所以我们可以在这里 获取数据、调用方法、watch、computed 都可以。
而 mounted 主要是在 DOM 挂载之后被调用,所以如果我们想要获取 DOM 的话,那么需要在 mounted 之后进行。
其中 beforeUpdate 表示 数据变化后,视图改变前。updated 表示 数据变化后,视图改变后。 那么由这两个生命周期我们可以知道,vue 从数据变化到视图变化,其实是需要经历一定的时间的。原因是因为 vue 在内部通过 queue 队列的形式在更新视图(packages/runtime-core/src/scheduler.ts):
这个逻辑还被体现在了 nextTick 这个方法上(packages/runtime-core/src/scheduler.ts):
而这种更新本质上是一种异步的更新形式,因为这种异步更新形式(微任务)的存在,才导致出现 数据更新 -> 视图更新 出现延迟的原因。
因为它的异步更新是以微任务的形式呈现的,这也是为什么很多时候我们可以通过 setTimeout 代替 nextTick 的原因。 而如果从 vue 的源码中来看的话,整个组件的生命周期,其实是被分为两大部分的(packages/runtime-core/src/renderer.ts): isMounted 之前 isMounted 之后
isMounted 之前表示:视图被挂载之前。因为组件的渲染本质上是 render 渲染了所有的 subVNode,所以在 isMounted 之前,会得到一个 subTree 来进行渲染。
isMounted 之后表示:视图全部被渲染完成了,也就是 mounted 之后。着这个时候其实就是 beforeUpdate 和 updated 的活跃时期了。
30. 说说vue2和vue3的diff算法的区别
1.在vue2中使用的是双端diff算法:是一种同时比较新旧两组节点的两个端点的算法(比头、比尾、头尾比、尾头比)。一般情况下,先找出变更后的头部,再对剩下的进行双端diff。 2.在vue3中使用的是快速diff算法:它借鉴了文本diff算法的预处理思路,先处理新旧两组节点中相同的前置节点和后置节点。当前置节点和后置节点全部处理完毕后,如果无法通过简单的挂载新节点或者卸载已经不存在的节点来更新,则需要根据节点间的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
31. 关于vue3双向绑定的实现
vue3实现双向绑定的核心是Proxy(代理的使用),它会对需要响应式处理的对象进行一层代理,对象的所有操作(get、set等)都会被Prxoy代理到。在vue中,所有响应式对象相关的副作用函数会使用weakMap来存储。当执行对应的操作时,会去执行操作中所收集到的副作用函数。
// WeakMap常用于存储只有当key所引用的对象存在时(没有被回收)才有价值的消息,十分贴合双向绑定场景 const bucket = new WeakMap(); // 存储副作用函数 let activeEffect; // 用一个全局变量处理被注册的函数 const tempObj = {}; // 临时对象,用于操作 const data = { text: "hello world" }; // 响应数据源 // 用于清除依赖 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i]; deps.delete(effectFn); } effectFn.deps.length = 0; } // 处理依赖函数 function effect(fn) { const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; fn(); }; effectFn.deps = []; effectFn(); } // 在get时拦截函数调用track函数追踪变化 function track(target, key) { if (!activeEffect) return; // let depsMap = bucket.get(target); if (!depsMap) { bucket.set(target, (depsMap = new Map())); } let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } deps.add(activeEffect); activeEffect.deps.push(deps); } // 在set拦截函数内调用trigger来触发变化 function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); const effectsToRun = new Set(effects); effectsToRun.forEach(effectFn => effectFn()); // effects && effects.forEach(fn => fn()); } const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { if (!activeEffect) return; // console.log("get -> key", key); track(target, key); return target[key]; }, // 拦截设置操作 set(target, key, newValue) { console.log("set -> key: newValue", key, newValue); target[key] = newValue; trigger(target, key); }, }); effect(() => { tempObj.text = obj.text; console.log("tempObj.text :>> ", tempObj.text); }); setTimeout(() => { obj.text = "hi vue3"; }, 1000);
32.为什么要选vue? 与其它框架对比的优势和劣势?
Vue.js 为什么比较特别? Vue 最大优势就是它比较新颖,没历史包袱,它吸取了 React 和 Angular 的教训 Vue轻量级,易上手,易学习 Vue更加灵活,(比起 Angular)更少专制 组件(Component)是 Vue最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码.
Angularjs vs Vue.js
Vue.js 更加灵活
vue的双向邦定是基于ES5 中的 getter/setters来实现的,它的每个属性都有两个相对应的get和set方法, Angular是用的数据脏检测,当Model发生变化,会检测所有视图是否绑定了相关数据,再更改视图。 而Vue使用的发布订阅模式,是点对点的绑定数据。 因此,vue在性能上更高效,但是代价是对于ie8以下ie8的浏览器无法支持。 数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Reactjs vs Vue.js
Vue.js 更容易学习,而且可以快速形成生产力。它还提供了一条途径,使用新的工具和模式来简化大型代码库的管理工作。Vue.js 会随着你知识的日渐丰富而不断扩展,因此你可以利用它来学习最新的工具以及进行最佳的实践。reactjs代码量最多,因为它既要管理UI逻辑,又要操心dom的渲染。
低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xml代码。 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。
33.vue-router实现原理? Hash + History
更新视图但不重新请求页面
1、Hash模式:
hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中也不会不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;
原理: hash模式的主要原理就是onhashchange()事件:
window.onhashchange = function(event){ console.log(event.oldURL, event.newURL); let hash = location.hash.slice(1); }
使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。
2、History模式:
使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
当使用history模式时,URL就像这样:http://abc.com/user/id。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
小心也会出问题;比如: 但当用户直接在用户栏输入地址并带有参数时: Hash模式:xxx.com/#/id=5 请求地址为 xxx.com,没有问题; History模式: xxx.com/id=5 请求地址为 xxx.com/id=5,如果后端没有对应的路由处理,就会返回404错误; 为解决这一问题,vue-router提供的方法是: 在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。 给个警告,因为这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。或者,如果你使用 Node.js 服务器,你可以用服务端路由匹配到来的 URL,并在没有匹配到路由的时候返回 404,以实现回退。
history api可以分为两大部分,切换历史状态和修改历史状态:
1.修改历史状态:包括了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。
2.切换历史状态: 包括forward()、back()、go()三个方法,对应浏览器的前进,后退,跳转操作。
两种模式对比
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
1.pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
2.pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
3.pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
4.pushState() 可额外设置 title 属性供后续使用。
5.hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。
如何获取页面的hash变化
watch:监听$route的变化
window.location.hash读取#值
34.如何watch监听一个对象内部的变化
如果只是监听obj内的某一个属性变化,可以直接obj.key进行监听
如果对整个obj深层监听
immediate的作用:当值第一次进行绑定的时候并不会触发watch监听,使用immediate则可以在最初绑定的时候执行。
35.Vue 2 和 Vue 3 的 DOM DIFF 完整过程
Vue 2
Vue 2 使用了经典的 Diff 算法,也称为双指针算法。其原理可以概括为以下几个步骤:
1.生成新旧虚拟DOM树:在重新渲染组件时,Vue会生成新的虚拟DOM树,并将其与旧的虚拟DOM树进行比较。
2.深度优先遍历:Vue 2 使用深度优先遍历算法来遍历新旧虚拟DOM树的节点。同时,Vue 2 会为每个节点添加唯一的标识符(VNode Key)以提高性能。
3.Diff过程:在遍历的过程中,Vue 2 会对比新旧虚拟DOM节点的类型和属性,判断是否需要更新实际DOM。具体的对比逻辑如下: a.如果新旧虚拟DOM节点相同(类型相同且Key相同),则只需要更新节点的属性; b.如果新旧虚拟DOM节点不同,则直接替换整个节点及其子节点; c.如果节点存在子节点,则递归地对子节点进行Diff; d.如果节点在旧虚拟DOM树中存在但在新虚拟DOM树中不存在,则直接删除该节点及其子节点
4.更新实际DOM:根据Diff的结果,Vue 2 会将需要更新的部分进行批量更新,使实际DOM与新的虚拟DOM树保持一致。
Vue 3
采用了基于观察者的 Diff 算法,也称为静态分析算法。其主要原理如下:
1.生成新旧虚拟DOM树:与Vue 2 相同,Vue 3 在重新渲染组件时会生成新的虚拟DOM树,并将其与旧的虚拟DOM树进行比较。
2.标记静态节点:Vue 3 通过静态标记(Static Marking)技术,将那些不会发生变化的节点标记为静态节点,以减少对它们的Diff计算。
3.Patch 过程:在 Diff 过程中,Vue 3 采用了 Patch 策略来处理不同类型的节点:
4.对静态节点,Vue 3 将跳过其子节点的Diff过程,省略一些无谓的计算;
5.对动态节点,即有可能发生变化的节点,Vue 3 采用了优化的Heuristic算法,通过比较新旧虚拟DOM节点的选择器(Selector)信息,判断是否需要进行详细的Diff比较。
6.更新实际DOM:根据Patch的结果,Vue 3 将需要更新的部分进行批量更新,使实际DOM与新的虚拟DOM树保持一致。
36.谈谈你对vue的理解
Vue是一个渐进式JavaScript框架,它专注于构建用户界面。Vue的核心思想是数据驱动和组件化。通过将页面拆分成独立的组件,可以更好地管理代码,提高代码的复用性和可维护性。
Vue的优势在于其简单易用、灵活性高、性能卓越和扩展性强。Vue的模板语法易于理解和学习,可以快速构建交互式的Web应用程序。同时,Vue的生命周期钩子和自定义指令等功能,使得Vue可以满足各种复杂的需求。另外,Vue还提供了Vuex、Vue Router等官方插件,可以进一步扩展Vue的功能。
Vue的响应式数据绑定机制是Vue最核心的特性之一。通过对数据进行劫持和监听,可以实现数据的双向绑定,即数据变化会自动更新视图,同时视图的变化也会反映到数据上。这种机制使得Vue的数据流非常清晰和可预测,同时也减少了开发的工作量。
总之,我认为Vue是一个优秀的JavaScript框架,它简单易用、功能强大、扩展性好,并且有着极佳的性能表现。对于前端开发人员来说,Vue是一个值得深入学习和使用的框架。
37.vue的组件加载和渲染顺序
首先我们来说组件的加载顺序是自上而下的,也就是先加载父组件,再加载子组件。当父组件被加载时,它会递归地加载其所有子组件,并按照顺序依次渲染它们。
组件的渲染顺序是由组件的深度优先遍历决定的,也就是先渲染最深层的子组件,再依次向上渲染其父组件。
38.computed和watch的区别
Vue中的computed和watch都是用来监听数据变化并做出相应的操作的,但它们的使用场景和功能有所不同。
computed属性是通过计算已有的属性值得出的一个新值。computed属性可以依赖于其他的响应式数据,当这些数据发生变化时,computed属性会自动更新。computed属性的值会被缓存,只有在依赖数据发生变化时才会重新计算,这样可以避免重复计算和提高性能。computed属性可以看做是一个缓存的属性,它不会直接修改数据,只是对已有数据的计算和处理。
watch属性用于监听数据的变化,并在数据变化时执行一些逻辑。watch属性可以监听单个数据或者一个数据数组,当数据发生变化时,watch属性会执行对应的回调函数。watch属性可以用来处理一些异步操作或者需要对数据进行复杂处理的逻辑。与computed属性不同的是,watch属性不会缓存计算结果,它会在每次数据变化时都执行回调函数。
总的来说,computed属性适用于需要根据已有数据进行计算和处理的场景,而watch属性适用于需要对数据变化做出响应或者执行异步操作的场景。当需要根据已有数据计算一个新值时,使用computed属性可以提高性能。而当需要监听数据变化并执行一些逻辑时,使用watch属性可以更加灵活和方便。我们需要根据具体的业务场景选择合适的方式来监听数据变化并做出相应的处理。
39.对虚拟DOM的理解?
从本质上来说,Virtual Dom是一个JavaScript对象,来表示DOM结构。将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
虚拟DOM是对DOM的抽象,这个对象是更加轻量级的对 DOM的描述。它设计的最初目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR,那么一个方式就是借助虚拟DOM,因为虚拟DOM本身是js对象。 在代码渲染到页面之前,vue会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实DOM结构,最终渲染到页面。在每次数据发生变化前,虚拟DOM都会缓存一份,变化之时,现在的虚拟DOM会与缓存的虚拟DOM进行比较。在vue内部封装了diff算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率。
40.虚拟DOM的解析过程
1.首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
2.当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
3.最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
41.为什么要用虚拟DOM
保证性能下限,在不进行手动优化的情况下,提供过得去的性能
看一下页面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler 下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶ a.真实DOM∶ 生成HTML字符串+重建所有的DOM元素 b.虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新 Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。
跨平台
本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
42.虚拟DOM真的比真实DOM性能好吗
1.首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
2.正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。
43.DIFF算法的原理
在新老虚拟DOM对比时:
1.首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
2.如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
3.比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
4.匹配时,找到相同的子节点,递归比较子节点
在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
44.为什么避免 v-if 和 v-for 用在一起
v-for 和 v-if 存在优先级的问题,造成性能问题。
vue3中v-if的优先级高于v-for,在vue2.x中v-for的优先级高
8.React篇
子主题
9. ts
TypeScript中的类型有哪些
内置
数字(number),字符串(string),布尔值(boolean),无效(void),空值(null)和未定义(undefined)
用户定义的
枚举(enums),类(classes),接口(interfaces),数组(arrays)和元组(tuple)
说说你对 typescript 的理解? 与 javascript 的区别?
TypeScript 是 JavaScript 的类型的超集,支持ES6语法,支持面向对象编程的概念,如类、接口、继承、泛型等
typescript在编译阶段需要编译器编译成纯Javascript来运行
TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法 TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译 TypeScript 文件的后缀名 .ts (.ts,.tsx,.dts),JavaScript 文件是 .js 在编写 TypeScript 的文件的时候就会自动编译成 js 文件
type和interface的区别
1.interface可以重复声明,type不行; 2.继承方式不一样,type使用交叉类型方式,interface使用extends实现。 在对象扩展的情况下,使用接口继承要比交叉类型的性能更好。建议使用interface来描述对象对外暴露的借口,使用type将一组类型重命名(或对类型进行复杂编程)。
any、unkonwn、never
any和unkonwn在TS类型中属于最顶层的Top Type,即所有的类型都是它俩的子类型。而never则相反,它作为Bottom Type是所有类型的子类型。
微信小程序
1.微信小程序的主要文件有哪些
WXML——模板文件 JSON——配置/设置文件,如标题,tabbar,页面注册 WXSS——样式文件,样式可直接用import导入 JS——脚本逻辑文件,逻辑处理,网络请求 app.json——配置文件入口,整个小程序的全局配置,网络超时时间、底部tab、页面路径,window字段是小程序所有页面的顶部背景颜色、文字颜色 app.js——可以没有内容,可以在里边监听生命周期函数、声明全局变量 app.wxss——全局配置样式文件
2.小程序 WXSS 与 CSS 的区别
1.wxss 背景图片只能引入外链,不能使用本地图片 2.小程序样式使用 @import 引入 外联样式文件,地址为相对路径。 3.尺寸单位为 rpx , rpx 是响应式像素,可以根据屏幕宽度进行自适应。
3.微信小程序bindtap 和 catchtap 区别
相同点: 都是点击事件
不同点: bindtap 不会阻止冒泡,catchtap 可以阻止冒泡。
4.小程序页面间有哪些传递数据的方法
1.使用全局变量实现数据传递 2.页面跳转或重定向时,使用url带参数传递数据 3.使用组件模板 template传递参数 4.使用缓存传递参数 5.使用数据库传递数据
5.微信小程序的优劣势
优势
1、无需下载,通过搜索和扫一扫就可以打开。 2、良好的用户体验:打开速度快。 3、开发成本要比App要低。 4、安卓上可以添加到桌面,与原生App差不多。 5、为用户提供良好的安全保障。小程序的发布,微信拥有一套严格的审查流程,不能通过审查的小程序是无法发布到线上的。
劣势
1、限制较多。页面大小不能超过1M。不能打开超过5个层级的页面。 2、样式单一。小程序的部分组件已经是成型的了,样式不可以修改。例如:幻灯片、导航。 3、推广面窄,不能分享朋友圈,只能通过分享给朋友,附近小程序推广。其中附近小程序也受到微信的限制。 4、依托于微信,无法开发后台管理功能。
6.微信小程序原理
1.小程序本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口; 2.它的架构,是数据驱动的架构模式,它的UI和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现; 3.它从技术讲和现有的前端开发差不多,采用JavaScript、WXML、WXSS三种技术进行开发; 4.功能可分为webview和appService两个部分; 5.webview用来展现UI,appService有来处理业务逻辑、数据及接口调用; 6.两个部分在两个进程中运行,通过系统层JSBridge实现通信,实现UI的渲染、事件的处理等。
前端埋点的实现,说说看思路
对于埋点方案:一般分为手动埋点(侵入性强,和业务强关联,用于需要精确搜集并分析数据,不过该方式耗时耗力,且容易出现误差,后续要调整,成本较高)、可视化埋点(提供一个可视化的埋点控制台,只能在可视化平台已支持的页面进行埋点)、无埋点(就是全埋点,监控页面发生的一切行为,优点是前端只需要处理一次埋点脚本,不过数据量过大会产生大量的脏数据,需要后端进行数据清洗)。
埋点长传采用img方式来上传,首先所有浏览器都支持Image对象,并且记录的过程很少出错,同时不存在跨域问题,请求Image也不会阻塞页面的渲染。建议使用1*1像素的GIF,其体积小。
现在的浏览器如果支持Navigator.sendBeacon(url, data)方法,优先使用该方法来实现,它的主要作用就是用于统计数据发送到web服务器。当然如果不支持的话就继续使用图片的方式来上传数据。
说说封装组件的思路
要考虑组件的灵活性、易用性、复用性。
面试总结
一面: 重基础/懂道理,要思考/知进退/势不可挡 二面: 横向扩展/项目结合,做到有的放矢 三面: 有经验,懂合作,有担当,懂规矩,察言观色 终面:会沟通,要上进,好性格,有主见,强逻辑,无可挑剔 复盘: 胜不骄,败不馁,总结经验,步步为营,多拿几个offer 职业规划
自我介绍
做什么,角色,成绩 1.姓名,年龄,专业,特长,经历(有价值的) 2.在一分钟自我介绍,人士想了解什么。 讲出跟他招聘有关联性的东西。 必须做岗位和企业的一样的
面试官您好,我叫魏团兵,是从15年毕业开始从事前端开发的,有8年多的工作经验,主要做一些后台管理系统和移动端的项目,比如浙里办小程序。vue2/vue3/angular都有接触,我在上家公司,主要做政府项目(市场监管局+消防总队),陆陆续续开发了十几个项目,前端负责人,是负责消防业务线的项目,带领团队将消防从0到1,包含了移动端和PC端,angular + vue;我主要的工作就是带带新人、任务的分配、代码的 codeReview 和 复盘、团队技术分享、公共组件的提取等。
你对未来3-5年的职业规划
对于程序员来说,属于逐渐专精的一个过程,比如阿里的技术专家,是在某一特定领域下的一个技术方案专家,在未来我想主攻某一个细分领域,不一定是技术,也可能是某一个业务或者场景下的,比如积累足够多的技术组件,当一个项目来了之后,张三可能用1个月,李四有足够的积累,比如组件的积累,可能需要一个星期的开发,这样就节省了时间成本了。
三. 如何看待加班
1.对于紧急加班,表示这是每个公司都会遇到的情况,自己愿意牺牲时间帮助公司和团队
2.对于长期加班,如果是自己长期加班那么会磨练自己的技能,提高自己的效率,如果是团队长期加班,自己会帮助团队找到问题,利用自动化工具或者更高效的协作流程来提高整个团队的效率,帮助⼤家摆脱加班
四. 为什么离职
咱们公司的技术氛围好/公司业务前景好,我呢,工作了几年,自己在业务上有一定的积累,技术上也有了一定的沉淀,但是我想来咱们这样的公司继续锤炼和深造,我想挑战更大的困难,让自己有一个全方面的成长. (没有挑战我来你这干嘛?让他觉得你是有理智的,是经过多方面的衡量的)
前东家对我挺好的,工作也得心应手(进入舒适圈了),消防项目已经在5月份结束了,只是我不想一直呆在舒适圈,我不是那种混日子的人,所以希望跳出来,找一份更有挑战性,更有成就感的工作,贵公司的岗位很符合我的预期,同时家里面也有些事情,丈母娘乳腺癌,我就回去了几个月,9月底才回杭州
五. 面对大量超过自己承受能力且时间有限的工作时你会怎么办?
你的时间管理能力和沟通能力
1.将大量任务分解为紧急且重要、重要但不紧急、紧急但不重要、不重要且不紧急,依次完成上述任务,在这里体现出时间管理的能力
2.与自己的领导沟通将不重要的任务放缓执行或者砍掉,或者派给组内的新人处理,在这里体现出沟通能力
六. 你有什么要问的?
1.贵公司开发团队是多少?如果我去的话,我是处于什么岗位?主要负责什么事情?
项目中的难点 问题、行动、结果
后台管理项目
问题-Problem
1.复杂的权限控制和角色管理 2.大量的数据处理和展示 3.多样化的表单操作和验证 4.多模块的功能模块之间的协作与集成 5.高效的前端性能和页面渲染优化
行动(Action)
1.设计清晰合理的权限管理和角色控制方案 2.使用合适的前端框架和组件库,提高开发效率和用户体验 3.结合后端接口设计前端数据处理和展示逻辑 4.采用可扩展和可维护的代码架构,方便模块的添加和修改 5.优化前端性能和页面渲染速度,提高用户满意度
结果(Result)
1.简化和加强了权限控制和角色管理,提高了系统的安全性 2.缩短了开发周期,提高了开发效率 3.改善了数据处理和展示的质量,提高了用户体验 4.实现了功能模块的快速集成和协作,提高了系统的可扩展性 5.加快了页面渲染速度和响应速度,提高了用户满意度
前端性能和页面渲染优化
问题(Problem)
前端性能和页面渲染优化的问题主要体现在页面加载速度慢、交互体验差、性能瓶颈等方面,影响用户体验和网站的整体效率。
原因(Analysis)
造成前端性能和页面渲染优化问题的原因有很多,例如图片过大、CSS、JS文件过多、资源未压缩等问题。此外,浏览器的渲染机制、缓存机制、网络环境等因素也会影响页面的加载速度和性能。
解决方案(Resolution)
1.压缩和合并文件:通过压缩和合并CSS、JS文件,可以减少HTTP请求和文件大小,提高页面加载速度。 2.** 图片优化**:通过对图片进行压缩和裁剪等优化,可以减少图片大小,提高页面加载速度。 3.CDN加速:使用CDN可以将网站的静态资源分发到全球各地的服务器上,提高资源的加载速度。 4.懒加载:通过懒加载可以延迟页面中非关键内容的加载,减少首次加载的数据量,提高页面的加载速度。 5.缓存优化:合理利用浏览器缓存和服务端缓存,可以减少HTTP请求和数据传输,提高网站的响应速度。
消防综合监管平台 滚动条的问题
在一个长页面中,当 select 滚动到底部后,要让整个页面不再滚动
JavaScript 事件监听
可以通过监听 select 的滚动事件,来判断是否达到滚动底部,并防止页面的滚动
const selectElement = document.querySelector('select'); selectElement.addEventListener('scroll', () => { if (selectElement.scrollTop + selectElement.clientHeight >= selectElement.scrollHeight) { // 当到达底部 document.body.style.overflow = 'hidden'; // 禁止页面滚动 } else { document.body.style.overflow = ''; // 恢复页面滚动 } }); // 监测select失去焦点时恢复页面滚动 selectElement.addEventListener('blur', () => { document.body.style.overflow = ''; // 恢复页面滚动 });
通过聚焦和失焦进行控制
const selectElement = document.querySelector('select'); selectElement.addEventListener('focus', () => { // 当 select 被聚焦时,禁用页面滚动 document.body.style.overflowY = 'hidden'; }); selectElement.addEventListener('blur', () => { // 当 select 失去焦点时,恢复页面滚动 document.body.style.overflowY = 'auto'; });
谷歌浏览器会自动填充密码,填充的颜色不是白色
使用CSS 来覆盖浏览器的默认样式,以更改自动填充输入框的外观
echarts :要求数字右对齐
angular 跳转到详情页面时,第二次进来,不会重新刷新接口
方法:追加时间戳
水印
定期检查水印元素
如果水印是一个 DOM 节点,可以使用 MutationObserver 定期监控水印节点。如果发现其被删除,则可重新添加
const watermark = document.createElement('div'); watermark.innerText = '水印'; watermark.style.position = 'fixed'; watermark.style.opacity = '0.1'; document.body.appendChild(watermark); // 创建观察者实例 const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.removedNodes.length > 0) { mutation.removedNodes.forEach((node) => { if (node === watermark) { console.log('水印已被删除,正在重建...'); document.body.appendChild(watermark); } }); } }); }); // 配置并开始观察 observer.observe(document.body, { childList: true, subtree: true });
签名
1. 横屏签名,需要去除多余的空白位置,最后旋转180度保存给后端
上传图片太大失败
原本在转换为 Base64 之前,先对图片进行压缩,比例是0.8,但是苹果手机拍出了BWM格式的图片50多M,会卡住
分片上传
将大文件拆分成小块进行上传
采用浙里办的方案:使用其返回的https地址,但是有缺陷,就是图片是在人家数据库上的
上传图片格式校验
有些图片的后缀上大小写的英文,
在校验的时候统一转化为小些格式
浙里办退出程序的时候,tickitid失效
有一个启动页,去拿用户的信息来校验的,如果一直后退,会进入到启动页面,重新去拿用户信息
beforeRouteLeave 中,禁止页面回退,回退直接销毁应用,比如注册页面,岗位选择页面,首页等