导图社区 001.html_JS
整理了前端面试中可能涉及的知识点,从基础的浏览器原理、JavaScript和ES6知识,到代码手写能力、TypeScript应用,再到性能优化等方面,为前端求职者提供了一个清晰的复习要点,有助于求职者有针对性地准备面试,提升面试通过率。
编辑于2025-06-16 23:14:42前端面试 讲概念;说用途; 优缺点;延伸
1. HTML+CSS
1.盒模型了解吗?
a. 标准盒模型; b. 怪异盒模型; c. flex弹性伸缩盒模型;d. column多列布局盒模型 标准盒模型和IE盒模型的区别在于设置width和height时,所对应的范围不同; 在W3C 标准盒子模型中,盒子的总宽度和总高度 = content内容区的宽/高 + padding + border + margin,就说是,元素的content内容区宽高不包含padding 和 border值。 在IE 怪异盒子模型中,盒子的总宽度和总高度 = content内容区的宽/高+margin,就是说,元素的content内容区宽高包含了padding 和 border值。
● 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.对媒体查询的理解
web⽹⻚应对不同型号的设备⽽做出对应的响应适配。
<!-- link元素中的CSS媒体查询 --> <link rel="stylesheet" media="(max-width: 800px)" href="example.css" /> <!-- 样式表中的CSS媒体查询 --> <style> @media (max-width: 600px) { .facet_sidebar { display: none; } } </style>
简单来说,使用 @media 查询,可以针对不同的屏幕尺寸设置不同的样式。当重置浏览器大小的过程中,页面也会根据浏览器的宽度和高度重新渲染页面。
12. H5 如何解决移动端适配问题
移动端适配问题是指如何让⽹面在不同的移动设备上显⽰效果相同。
⽅案
1.使⽤ viewport 标签
通过设置 viewport 标签的 meta 属性,来控制⻚⾯的缩放⽐例和宽度,以适配不同的设备 <meta name="viewport" content="width=device-width, initial-scale=1.0">
2.使⽤ CSS3 的媒体查询@media
3.直接使⽤ rem 单位
html {font-size:0pt; color:#303030;} @media screen and (max-width: 640px) { html {font-size:0pt; color:#303030;">} div {width: 10rem;}
间接使用rem
4. 使⽤ flexible 布局⽅案
<script src="https://cdn.bootcdn.net/ajax/libs/lib-flexible/0.3.4/flexible.js"</script> import 'lib-flexible/flexible.js'
flexible.js 会在⻚⾯加载时动态计算根节点的字体⼤⼩,并将 px 转化为 rem 单位。在样式中可以直接使⽤ px 单位
5. post-css-pxtorem将PX转为rem:根据屏幕宽度动态设置html的font-size
function resizeRootFontSize() { const designWidth = 750; // 设计稿宽度,例如iPhone 6/7/8的设计稿宽度 const maxWidth = 1080; // 最大适配宽度 const minFontSize = 10; // 最小字体大小 const maxFontSize = 20; // 最大字体大小 const screenWidth = Math.min(window.innerWidth, maxWidth); const fontSize = Math.max(minFontSize, (screenWidth / designWidth) * 16); document.documentElement.style.fontSize = `${fontSize}px`; } window.addEventListener('resize', resizeRootFontSize); window.addEventListener('orientationchange', resizeRootFontSize); resizeRootFontSize();
13.两栏布局的方法
(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; }
14.三栏布局:如何实现双飞翼(圣杯)布局?
(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
效果
15.盒子水平垂直居中?
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; }
16. 如何垂直居中一个 img?
{ display:table-cell; text-align:center; vertical-align:middle; }
17. 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 接口使用更方便
18.HTTP协议
HTTP报文的组成部分
HTTP响应报文
响应⾏ / 响应头 / 空⾏ / 响应体
- 响应⾏:由网络协议版本,状态码和状态码的原因短语组成,例如 HTTP/1.1 200 OK 。
- 响应头:响应部⾸组成
- 响应体:服务器响应的数据
1. 从用户输入url地址,会发生什么?
用户输入URL并回车
1. 检测输入内容:纠错或补全
用户在浏览器的地址栏中输入内容后,浏览器会首先判断输入的是搜索内容还是URL地址。 如果是搜索内容,浏览器会使用默认搜索引擎加上搜索内容合成URL; 如果是符合URL规则的域名,则会加上协议(如http或https)合成完整的URL。
2. 发送URL请求
用户按下回车后,浏览器进程会通过进程间通信(IPC)把URL请求发送到网络进程。 网络进程接收到URL请求后,才会发起真正的URL请求流程。
浏览器处理URL请求
3. 查找缓存
浏览器会先检查自身缓存中是否有该URL的DNS解析结果或页面内容。
如果存在且未过期,则直接使用缓存中的数据,跳过后续步骤。
如果不存在或已过期,则继续后续步骤。
4. DNS解析
浏览器会尝试将URL中的域名解析为IP地址。
首先,浏览器会在本地DNS缓存中查找域名对应的IP地址。
如果本地缓存中不存在,则浏览器会向配置的DNS服务器发送解析请求。DNS服务器可能递归或迭代地查询根域服务器、顶级域服务器等,直到找到对应的IP地址。
5.准备IP地址和端口
获取到IP地址后,浏览器会选择一个本地端口,并向目标IP地址的对应端口(HTTP协议默认80端口,HTTPS协议默认443端口)发起TCP连接请求。
建立TCP连接
6. 三次握手
客户端(浏览器)发送一个带有SYN标志的数据包给服务器,请求建立连接。
服务器收到请求后,回复一个带有SYN/ACK标志的数据包,确认收到客户端的请求,并告知客户端自己的序列号。
客户端收发送一个带有ACK标志的数据包,表示连接建立成功,并告知服务器自己的序列号。
注:如果是https的话,会有一个 SSL握手,确保了客户端和服务器之间的通信是加密的、完整的和经过身份验证的
发送HTTP请求
7. 构建HTTP请求报文
HTTP请求报文包括请求行、请求报头和请求正文(如果有的话)。
请求行包含请求方法(如GET、POST)、请求URL和HTTP协议版本。
请求报头包含请求的附加信息和客户端自身的信息。
8. 发送请求
浏览器通过TCP连接将HTTP请求报文发送给服务器。
服务器处理请求并响应
9. 接收HTTP请求
会根据请求的内容进行处理。
10. 构建HTTP响应报文
HTTP响应报文包括状态行、响应头部和响应数据。
状态行包含状态码和HTTP协议版本,状态码用于指示请求的成功或失败。
11. 发送响应
服务器通过TCP连接将HTTP响应报文发送给浏览器
浏览器解析渲染页面
12.接收响应数据
网络进程接收到响应数据后进行解析。
根据响应头中的Content-type来判断响应数据的类型。
(set-cookie/content-type/缓存的处理/状态码/connection/keep-alive)
13. 处理响应数据
如果Content-type是text/html类型,则通知浏览器进程将获取的文档进行渲染
渲染过程包括构建DOM树、解析CSS样式、执行JavaScript代码等。
构建DOM树
浏览器会先将HTML字节流转换为字符,然后通过分词器将字符转换为Token,每个Token都有自己独特的含义和规则集。之后进行词法分析,将Token转为对象(nodes),这些对象分别定义他们的属性和规则之后进行DOM构建。
解析CSS并构建CSSOM树
与HTML解析并行进行的是CSS的加载和解析。浏览器会加载外部CSS文件或内部样式,并解析它们以构建CSSOM(CSS Object Model)树。CSSOM树表示了文档中所有元素的样式信息。
构建渲染树
一旦DOM树和CSSOM树都构建完成,浏览器就会将这两个树合并成一个渲染树。渲染树只包含需要显示的节点和它们的样式信息。不可见的元素(如设置了display:none的元素)不会被包含在渲染树中。
布局
浏览器会根据渲染树中的信息计算每个元素的几何属性,如位置和大小。这个过程涉及到复杂的算法,以确保页面上的元素能够按照CSS规则正确地排列。
绘制
浏览器会遍历渲染树,并使用图形库将每个节点的内容绘制到屏幕上。这个过程将渲染树中的节点转换为屏幕上的像素。现代浏览器还会进行复合操作,以优化渲染性能。 复合是将页面上的多个图层合并成一个图层的过程。浏览器会将渲染树分成多个图层,并在需要时只重绘和重新复合发生变化的图层,而不是整个页面。
显示页面
经过上述步骤后,一个完整的页面最终形成并显示在用户的屏幕上。
关闭TCP连接
14.四次挥手
如果HTTP请求头中未指定Connection: keep-alive,则浏览器和服务器在完成数据传输后会通过四次挥手关闭TCP连接。
浏览器发送一个带有FIN标志的数据包给服务器,表示想要关闭连接。
服务器收到FIN包后,回复一个ACK包确认收到。
服务器也发送一个FIN包给浏览器,表示自己也想要关闭连接。
浏览器收到服务器的FIN包后,回复一个ACK包确认收到,并关闭TCP连接。
阻塞问题
解析HTML文档时,会构建DOM树
遇到如<link>
DOM树的构建会继续进行,不会因为等待CSS的下载而暂停。
script标签
浏览器的解析过程会暂停,直到JavaScript代码被下载、解析和执行完毕。这是因为JavaScript可能会修改DOM树,所以浏览器需要等待JavaScript执行完毕后才能继续解析DOM树。
CSSOM的构建
虽然CSS的加载不会阻塞DOM树的解析,但会阻塞页面的渲染。
阻塞JS执行
由于JavaScript可能会访问CSSOM(例如,通过Element.getBoundingClientRect等方法获取元素的尺寸和位置),所以浏览器通常会等待CSSOM构建完毕后才开始执行后续的JavaScript代码。
2. http协议中,缓存机制有哪些? 强缓存/弱缓存
强缓存:浏览器直接从本地缓存中获取资源,而不和服务器进行交互,针对静态资源 (css/img/js) 常见的强缓存控制头有:Expires和Cache-Control,其中,Expires会指定资源的过期时间,Cache-Control中的max-age会指定资源的有效时间;如果同时设置了Expires和Cache-Control,浏览器会优先使用Cache-Control。
协商缓存:指浏览器与服务器通信,由服务器决定是否使用缓存。 常见的协商缓存控制头有:Last-Modified、If-Modified-Since、ETag和If-None-Match 如果同时存在Last-Modified和ETag,浏览器会优先使用ETag。 开启协商缓存:将catch-control 设置为no-catch,还有 Etag (16进制的标志位)+Last-Modified (最后修改时间)
或者问:浏览器有哪几种缓存
强缓存
弱缓存
LocalStorage、SessionStorage和indexdb
综合来说,优先级从高到低排列为:强缓存 > 协商缓存 > 浏览器存储
3. DNS完整的查询过程
1.首先会在浏览器的缓存中查找对应的IP地址,如果查找到直接返回,若找不到继续下一步
2.将请求发送给本地DNS服务器,在本地域名服务器缓存中查询,如果查找到,就直接将查找结果返回,若找不到继续下一步
3.本地DNS服务器向根域名服务器发送请求,根域名服务器会返回一个所查询域的顶级域名服务器 ip 地址
4.本地DNS服务器向顶级域名服务器发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的 ip 地址
5.本地DNS服务器向权威域名服务器发送请求,域名服务器返回对应的结果
6.本地DNS服务器将返回结果保存在缓存中,便于下次使用
7.本地DNS服务器将返回结果返回给浏览器
比如要查询 www.baidu.com 的 IP 地址,首先会在浏览器的缓存中查找是否有该域名的缓存,如果不存在就将请求发送到本地的 DNS 服务器中,本地DNS服务器会判断是否存在该域名的缓存,如果不存在,则向根域名服务器发送一个请求,根域名服务器返回负责 .com 的顶级域名服务器的 IP 地址的列表。然后本地 DNS 服务器再向其中一个负责 .com 的顶级域名服务器发送一个请求,负责 .com 的顶级域名服务器返回负责 .baidu 的权威域名服务器的 IP 地址列表。然后本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,最后权威域名服务器返回一个对应的主机名的 IP 地址列表。
5. http协议中,常见的状态码有哪些?
1xx: 指示信息 - 表示请求已接收,继续处理 2xx: 成功 - 表示请求已被成功接收 3xx: 重定向 - 要完成请求必须进行更进一步的操作 4xx: 客户端错误 - 请求有语法错误或请求无法实现 5xx: 服务器错误 - 服务器未能实现合法的请求
200 请求成功。 301 永久重定向,请求的网页已永久移动到新位置,浏览器会自动重定向到新的 url 地址。 302 临时重定向,服务器目前从不同位置的网页响应请求,可使用原有 url 地址。 303 查看其它位置,重定向。 304 Not Modified,资源未作修改。协商缓存。 305 所访问资源必须通过代理访问。 400:请求报文语法有误,服务器无法识别 401:请求需要认证 403:请求的对应资源禁止被访问 404:服务器无法找到对应资源 500 服务器内部错误。 501 服务器不支持请求的功能。 502 网关错误,通常需要后端找原因。 503 服务器超载或系统维护。
6. http协议 与 https协议 有什么区别?
http协议是明文传输,https协议是http协议+ssl协议构建的,加密传输协议。 1. http 的连接很简单,是无状态的。 2. http 传输的数据都是未加密的,是明文传输。 3. https 协议是由 http和ssl 协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。可防止数据在传输过程中不被窃取、改变,确保数据的完整性。 4. https 协议需要ca证书,费用较高。ssl 证书也需要钱,功能越强大的证书费用越高。
7. 浏览器是怎么渲染的呢? 重绘/重排
1. 通过网络获取到字节流和字符,然后需要进行分词,得到一个一个字组序列(token),根据token分析语法,得到node节点,然后再构建DOM树; 接下来就是构建CSSOM:为元素添加样式,浏览器从父节点开始,递归向下添加,直到每个元素都添加上样式为止; 构建DOM树期间,如果遇到JS会产生阻塞,优先加载 js ,加载完毕再继续构建;
3. 将DOM和CSSOM合并,生成渲染树(Render-Tree);
4. 根据渲染树计算可见元素的布局, 将布局渲染到屏幕上。
5. 最后如果 js 对DOM 节点进行了操作,那么根据实际情况,对页面进行重绘或重排
重绘/重排
重绘
当元素的外观发生变化,但不影响布局时,浏览器重新绘制元素的过程
重排 || 回流
当元素的位置、大小等布局属性发生变化时,浏览器重新计算页面布局的过程。
回流必将导致重绘,重绘不一定会引起回流。回流比重绘的代价更高。
常见改善方案
1. 多个样式尽量合并 2. 利用DocumentFragment,进行缓存操作,引发一次重排 3. 对复杂动画采用绝对定位,使其脱离文档流
8. 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是不可靠的。
9. 网站性能优化
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 实践可以帮助提升站点的访问速度与排名。确保网站结构友好,重视可达性也有助于改善整体用户体验。
10. post 和 get 的区别
1. 请求用途
get主要用于请求数据; post: 用于提交数据或创建资源, 常用于表单提交和上传文件
2.数据传输方式
GET:参数附加在 URL 的查询字符串中(即 URL 后面的部分)。 受限于 URL 长度,通常不适合发送大量数据。 POST:数据通过请求体(body)发送,通常不显示在 URL 中。 可以发送大量数据,包括文本和文件,不受 URL 长度限制。
3. 安全性
GET:相对不安全,因为参数暴露在 URL 中,容易被截取、缓存或者保存到浏览器历史记录中。 POST:较为安全,数据不在 URL 中,但并不意味着绝对安全。还需配合 HTTPS 等技术提升安全性。
4. 缓存
GET:默认可以被缓存,浏览器可能会保存 GET 请求的响应以提高性能。 POST:通常不会被缓存,请求的数据是一次性的。
SEO 优化 搜索引擎优化
1.页面结构优化
语义化标签,(实际上很难,除非重构,因为大部分都是div)
比如页面的标题,描述,header/nav/main/article/footer
2.内容优化
保证页面中关键词的覆盖
3.技术上 SEO 优化
1.站点地图:爬虫来了,告诉他哪些可以爬,哪些不可以爬
2.结构化数据:强行加戏
3.移动端兼容
19.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代码,可以应用到老项目中
20.Webpack 能处理 CSS 吗?如何实现?
Webpack 能处理 CSS 吗: 1.Webpack 在裸奔的状态下,是不能处理 CSS 的,Webpack 本身是一个面向 JavaScript 且只能处理 JavaScript 代码的模块化打包工具; 2.Webpack 在 loader 的辅助下,是可以处理 CSS 的。 如何用 Webpack 实现对 CSS 的处理:需要使用的 css-loader 和 style-loader a.css-loader:导入 CSS 模块,对 CSS 代码进行编译处理; b.style-loader:创建style标签,把 CSS 内容写入标签。 在实际使用中,css-loader 的执行顺序一定要安排在 style-loader 的前面。因为只有完成了编译过程,才可以对 css 代码进行插入;若提前插入了未编译的代码,那么 webpack 是无法理解这坨东西的,它会无情报错。
21.如何判断元素是否到达可视区域
1.window.innerHeight 是浏览器可视区的高度; 2.document.body.scrollTop: 浏览器已滚动到高度 3.imgs.offsetTop 是元素顶部距离文档顶部的高度(包括滚动条的距离); 4.内容达到显示区域的:img.offsetTop < window.innerHeight + document.body.scrollTop
22.画一条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只针对于移动端,只在移动端上才能看到效果
23.设置小于12px的字体
(1)使用Webkit的内核的-webkit-text-size-adjust: none。 但是chrome更新到27版本之后就不可以用了。所以高版本chrome谷歌浏览器已经不再支持,所以要使用时候慎用 (2)使用css3的transform缩放属性:-webkit-transform:scale(0.5); (3)使用图片:如果是内容固定不变情况下,使用将小于12px文字内容切出做图片,这样不影响兼容也不影响美观。
24.如何解决 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 }
缺陷:兼容性不行,ios8+,以及部分安卓系统可以正常展示。
思路二:伪元素+transform 最优
#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"> 整个页面被缩放
使用 border-image 实现
我们可以让设计同学出一个1px的图片,然后我们使用border-image属性来实现。缺点是后期样式调整需要找设计同学重新出图,而且还不支持边框圆角的场景。
使用box-shadow实现
通过box-shadow设置1px的阴影,缺点是边框有阴影,颜色浅,同样也有兼容性问题,Safari 不支持 1px 以下的 box-shadow。
25.rem和em的区别
rem,系统处理时(将rem转化为px)只会看 html 的font-size
em,系统处理时(将em转化为px)只会看 父元素 的font-size
26.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的内容会被读取。
CDN的原理是什么?
目前的互联网应用中都包含大量的静态内容,如果不做任何处理,所有的请求都指向源站服务器的话,不仅会耗费大量的带宽,还会拖累页面加载速度,影响用户体验。
CDN服务的出现可以解决上述问题。CDN的本质仍然是一个缓存,通过在现有网络中增加一层新的缓存节点,可以将数据缓存在里用户最近的地方,使用户以最快的速度获取数据,不需要每个用户的请求都去源站获取,避免网络拥塞、缓解源站压力。
比如,你住在东北,某网站源站服务器在深圳,如果没有CDN服务,那么每次数据请求都要长途跋涉到深圳的服务器,如果有CDN服务,就会在东北的CDN服务器上缓存一份数据,每次的数据请求就直接从东北的CDN服务器返回了,不用再大老远跑去深圳了。
当用户第一次发起内容请求时,不同地区的用户访问同一个域名,CDN服务商的智能DNS服务会返回不同CDN节点的IP地址。浏览器发起域名查询时,CDN 全局负载均衡设备根据用户 IP 地址,以及用户请求的内容URL,计算并返回离用户最近的相同网络运营商的CDN节点IP;
然后向边缘节点服务器发起请求,浏览器来请求内容数据,边缘节点会检测当前节点是否有数据,如果没有就去父级节点要数据,父级可能还会有父级节点,一直往上获取数据,如果还找不到就去源站服务器拿,并依次序返回,一个地区内只要有一个用户先加载资源,就会在CDN中建立缓存,该地区的其他后续用户都能直接读取缓存数据。
如果某个边缘节点可以找到,会先校验内容有效期,当确定有效期之后返回给用户。
用户后续再次发起请求时,会先去 CDN 缓存服务器获取。如果获取到数据,那么就直接返回。否则就重走一遍上面的流程
子主题
2. JS+ES
~~~ 数据类型 ~~~
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 中比较两个对象?
1. 通过JSON.stringify(obj)来判断两个对象转后的字符串是否相等 缺点:顺序要一致;无法比较日期和函数
2. Object.is(xxx1, xxx2)
类似于 == ===
顺序不一致就失败
3. 递归遍历
长度是否一致; for 循环中,key是否一致?value是否一致? 是否还有二级对象,有的化继续递归遍历
4.插件
“ lodash ”的JavaScript库: _.isEqual(obj1, obj2)
9.深拷贝和浅拷贝
浅拷贝:只复制一层,引用类型仍然共享。 在栈内存 开辟一个新空间,且将对象数据完全栲贝 但是2个对象用的是同一片内存空间,一个改变,另一个也改变
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
注意:这种方式不适用于包含函数、日期对象、正则等复杂类型,且会丢失原型链的信息。会忽略symbol 和 undeined
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
~~~ 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. 创建一个空对象 obj 2. 将对象的proto属性指向构造函数的prototype属性 3. 利用call方法,将原本指向window的绑定对象this指向obj 4. 函数执行完毕返回this,返回新的对象
所以,上面的第二、三步,箭头函数都是没有办法执行的。
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关键字
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 可以完美监听到任何方式的数据改变,唯一缺陷就是浏览器的兼容性不好。
15. Promise
是异步编程解决方案,他是一个对象,可以处理异步操作信息、避免回调地狱。
简单来说,他是一个容器,保存着某个未来才会结束的事件
有三种状态
Pending(等待):初始状态,既不是成功,也不是失败。
Fulfilled(成功):操作成功完成。
Rejected(失败):操作失败。
只有异步操作的结果可以决定当前是哪种状态,
用法:使用 new Promise() 来创建新的 Promise 实例。 通过 .then() 方法处理成功回调, 使用 .catch() 方法处理失败回调。 finally():无论结果是成功还是失败,都会执行的操作。
const fetchData = new Promise((resolve, reject) => { // 模拟异步操作 setTimeout(() => { const success = true; // 假设这是一个操作的结果 if (success) { resolve("数据加载成功!"); } else { reject("数据加载失败。"); } }, 1000); }); fetchData .then(result => { console.log(result); // 输出: 数据加载成功! }) .catch(error => { console.error(error); // 如果失败,将输出错误信息 }) .finally(() => { console.log("操作完成,无论成功或失败。"); // 最终都会执行 });
myPromise .then(result => { console.log(result); // "操作成功" }) .catch(error => { console.error(error); }) .finally(() => { console.log("操作结束"); });
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); });
缺点
1.无法取消,一旦新建他就立即执行
2.如果不设置回调函数,Promise内部抛出的错误,不回映射到外部
3.如果处于 padding状态,无法确定进展到哪一阶段,是刚开始还是即将完成
16.async/await 和 Promise 有什么关系
async/await 是 Promise 的一种语法糖,使得基于 Promise 的异步操作更容易编写和维护。
在使用 await 时,必须等待的值应该是一个 Promise。当 await 所等待的 Promise 被 resolved 时,它将返回值;如果 Promise 被 rejected,则会抛出错误,可以通过 try/catch 块捕获。
总结
Promise 提供了一种管理异步操作的机制,而 async/await 则提供了一种更加直观的方式来处理这些异步操作,使得代码结构更清晰。
await等待的就是 promise 的一个结果
17. generator:es6的异步编程解决方案
18. common.js和es6中模块引入的区别?
Common]S是一种模块规范,最初被应用于Nodejs,成为Nodejs 的模块规范。 在ES6之前,前端也实现了一套相同的模块规范(例如: AMD),用来对前端模块进行管理。 自ES6起,引入了一套新的ES6 Module规范,在语言标准的层面上实现了模块功能。
都提供了模块化的功能,但它们在模块引入方面存在一些关键差异。
一、加载方式
CommonJS:同步加载,在加载模块时,会阻塞代码的执行,直到模块加载完成。这是因为它最初是为服务器端设计的。 在运行时进行的,这意味着它无法在静态阶段进行优化。
ES6:是异步加载的,意味着在加载模块时,不会阻塞代码的执行。这是因为它主要是为浏览器设计的,浏览器中的网络请求是异步的,因此异步加载更符合Web开发的需求。 静态解析:ES6模块的import和export语句在编译时(静态)解析
二、引入和导出语法
CommonJS:
使用require函数来引入模块。例如:const moduleA = require('./moduleA');
使用module.exports或exports对象来导出模块中的函数、对象或变量。例如:module.exports = someFunction; 或 exports.myFunction = function() {};
ES6
使用import关键字来引入模块。例如:import moduleA from './moduleA';
使用export关键字来导出模块中的函数、对象或变量。例如:export default someFunction; 或 export const myVar = 'hello'; export function anotherFunction() {};
导出数量
CommonJS是单个值导出。
ES6 Module可以导出多个值。
应用环境
CommonJS主要应用在Node.js环境中。
ES6模块是ECMAScript标准的一部分,适用于现代浏览器和一些构建工具(如Webpack、Rollup等)。
19. 什么是 let 的临时性死区?
let 会产生临时性死区,在当前的执行上下文中,会进行变量提升,但是未被初始化,所以在执行上下文执行阶段,执行代码如果还没有执行到变量赋值,就引用此变量就会报错,此变量未初始化。
20.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 针正对性的优化,有助于性能提升
~~~ JavaScript基础 ~~~
21.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)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
22.正则表达式
// (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}$/;
23.遍历数组的方法
for
for...in
拿到的是数组下标
能遍历对象,拿到的是对象的key
for...of
拿到的是数组的值
不能遍历对象,会报错
forEach
会改变原数组
map
返回一个新的数组
some
如果有其中一项满足,就返回true。只有全部都不满足,才会返回false
every
只有全部都满足,才会返回 ** true **, 一旦其中某一项不符合条件,就中断循环,直接返回 false
find
到第一个满足条件的元素时,则直接返回该元素。如果都不满足条件, 则返回 undefined
filter
返回符合条件的新数组
不会影响原数组
reduce
一般用于求和
不会影响原数组。
findIndex
找到符合条件的返回当前项的下标,没找到返回 - 1
24.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]
25.JS的事件循环(EventLoop)
首先 js 是单线程,不能同时处理多个任务。
同步:是栈,先执行
发出功能,没有得到结果前,该调用就不返回;
过程:提交请求 ---> 等待服务器处理 --》处理完毕返回结果(浏览器是静止状态,不能干任何事情)
异步:是队列,后执行
异步调用发出后,不能立刻得到结果,需要等到完成后,通过状态值或回调来通知
过程:提交请求 ---> 服务器处理(浏览器可以做其他处理任务) --》得到状态或通知 --》处理完毕
Event Loop
JS的执行顺序
逐行执行,遇到报错就停止,先同步,后异步
同步的进入主线程,异步的进入任务队列
主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
执行步骤
遇到同步任务按照顺序放入js主线程的执行栈中,依次执行,完成后清空执行栈;
遇到异步代码,就放到 Event-table 中,等同步执行完毕,把异步代码放入回调队列;
通过 event loop 轮询机制,把回调队列中的代码放入执行栈中执行;
Event loop 继续轮询回调队列,直到回调队列为空;
任务队列task
微任务(microtask)队列:先执行
如Promise.then、Promise.catch、Promise.finally、process.nextTick(Node.js环境)等
宏任务(macrotask)队列:后执行
如setTimeout、setInterval、I/O、UI rendering等
先后顺序:同步任务(console.log)--->微任务(promise)---->宏任务(定时器等)
26.如何判断对象具有某属性?
如: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 方法
27. ajax、axios、fetch的区别
AJAX
异步 JavaScript 和 XML
一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新
Fetch
号称是AJAX的替代品
Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。
Axios
是一种基于Promise封装的HTTP客户端
28.原型与原型链
原型
每当定义一个对象时,对象中都会包含一些预定义的属性。
显式原型
函数的prototype 属性,只有函数有
prototype:指向函数的原型对象,属于一个地址的引用
隐式原型
实例对象都有__proto__属性,指向构造函数的原型对象
使用原型的目的
共享内存,节约资源
26.原型链
每个对象都可以有一个__proto__,__proto__指向构造函数的prototype,prototype也有一个__proto__,一层一层往上,直到找到Object.prototype,这样的查找过程就叫原型链
是实现继承的一种方式
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.父类新增原型方法/原型属性,子类都能访问到 缺点: 1.原型的实例方法是共享的,修改一个属性,所有的属性都会变化 2.创建子类实例时,无法向父类构造函数传参
2.构造函数继承 (借助 call将父类的构造函数绑定在子类对象上)
特点: 1.解决了1中,子类实例共享父类引用属性的问题 2.创建子类实例时,可以向父类传递参数 缺点: 每个子类对象,都有一个自己的继承函数,如果对象多了,内存消耗大 let child2 = new Child('nan') child1 和 child2 是没有关系的
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
这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销. 总体看下来,这六种继承方式中,寄生组合式继承是这六种里面最优的继承方式
29.闭包
概念
两个函数嵌套,内部函数引用外部函数变量
function fn1(){ let a = "1" let fn2 =function (){ console.log(a) } return fn2 } let fn3 = fn1()
作用
读取函数内部的变量;变量始终保存在内存中,不会被自动清除。
优点
1.减少全局变量的定义,减少全局污染
2.变量终保持在内存中,可以当做缓存使用
内存泄漏
在闭包中引用了大量不必要的外部变量,并且这些闭包长时间存在
避免的方法是在不需要闭包中的变量时,解除对外部变量的引用,例如将内部函数设置为null。
闭包的实际应用
封装成高阶函数
代码
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
30.对作用域、作用域链的理解
一个变量或者函数在其内能够被访问的“可见区域”; 作用域的作用:隔离变量
全局作用域
任何地方都可访问; ● 最外层函数和最外层函数外面定义的变量拥有全局作用域 ● 所有未定义直接赋值的变量自动声明为全局作用域 ● 所有window对象的属性拥有全局作用域 ● 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
函数作用域
声明在函数内部; ● 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到 ● 作用域是分层的,内层作用域可以访问外层作用域,反之不行
块级作用域
● 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段) ● let和const声明的变量不会有变量提升,也不可以重复声明 ● 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
作用域链
在当前作用域中查找所需变量,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。 当在某个作用域中尝试访问一个变量时,JS引擎会从当前作用域开始,沿着作用域链向上逐级查找,直到找到该变量为止,如果全局未找到就抛出错误。 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。 作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。 当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。
全局变量容易造成污染,造成命名冲突
31.对执行上下文的理解
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
32.说一下this、call、apply、bind区别
this
this 是一个指针变量,他动态指向当前函数的运行环境,指向最后一次调用这个方法的对象。
● 全局下的this -> window ● 函数内的 this -> window ● 对象中的 this:指向调用这些方法的对象 一层对象:谁调用,指向谁; 多层对象:谁离this近,就指向谁 ● 箭头函数,没有this和arguments,继承该外层的this ; ● 构造函数中的this指向实例,函数执行前会新创建一个对象,this 指向这个新创建的对象。
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 函数体。
两者的区别
async/await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。
33.垃圾回收与内存泄漏
垃圾回收
概念
JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。
回收机制
● Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。 ● JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。 ● 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
回收的方式
标记清除
标记阶段:对所有活动的变量做标记 清除阶段:把没有标记的销毁,并回收他们所占用的内存空间。
引用计数
● 这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。 ● 这种方法会引起循环引用的问题:例如:obj1和obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1和obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。
减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
● 对数组进行优化:在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。 ● 对**object**进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 ● 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
内存泄漏
无效引用,无法被程序使用,又没有被系统回收,直到浏览器进程结束
场景
全局变量过多
原因:全局变量,不会被回收。 解决:使用严格模式,减少全局变量。
闭包
原因:闭包内局部变量,得不到释放 解决:将事件处理函数定义在外部,解除闭包或者在定义事件处理
setInterval定时器引用了变量和对象
clearInterval
dom被移除了,但是事件还存在
对象循环引用
注意:进行删除和更新操作后,可能忘记释放内存,需要设置null
34. 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
35. 谈谈对面向对象的理解
答1: 面向对象编程,即OOP,是一种编程范式,满足面向对象编程的语言, 一般会提供类、封装、继承等语法和概念来辅助我们进行面向对象编程。 类型被设计为将数据和行为捆绑在一起的一种东西,数据和行为被称之 为类型的成员。我们可以创建类型的实例,不同的实例包含不同的数据, 从而其表现出来的行为也会不同,尽管其代码是一样的。 封装使得类的成员得以有选择性的暴露,一些成员只在类型的内部使用, 被称之为私有的(private),一些成员可以被派生类型使用,称之为 受保护的(protected),一些成员可以被任何东西使用,称之为公开 的(public)。而某些语言还提供了内部的(internal)这样的访问 修饰符来标识一些只能被同一个程序集或者包使用的成员。 继承可以从一个现有类型派生出新的类型来,派生类继承了基类的所有 成员,也可以新增只属于自己的成员。在任何情况下,派生类类型的实 例可以被当做基类类型的实例来使用。 虚方法为派生类修改基类的行为提供了一个途径,通过重写(override) 虚方法可以修改基类某些方法的行为。当派生类实例被当做基类实例来使 用时,这一行为的区别将会被体现出来,这种在运行时不同类型的实例在 同样的代码中呈现出完全不同行为的现象被称之为多态。 面向对象编程最初是为了解决GUI程序设计问题所提出的,后来面向对象 编程被发现也比较适合用于许多特定领域的开发。面向对象编程是目前运 用最为广泛的一种编程范式,从而也产生了非常多的解决代码复用的技巧 ,其中相当一部分技巧在程序中反复出现而被提炼为设计模式。
答2: 他问你面向对象编程,面试官想知道的是你的理解。 不是概念! 不是概念! 不是概念! 你那样回答没有错,但是不是面试官想要的,概念,特征都会讲,看你 怎么描述了: 网上讲的详细的很多,我讲个我当时面试的回答,一个瞎扯淡的例子: “假设我是女娲,我准备捏一些人, 首先,人应该有哪些基本特征: 1.有四肢 2.有大脑 3.有器官 4.有思想 我们就有了第一个模型,这就是抽象。 其次,我和西方上帝是好友,我想我的这个想法能够提供给他用,但是我不想 让他知道里面细节是怎么捏出来的,用的什么材料,他也不用考虑那么多,只 要告诉我他要捏什么样的人就可以了。这就是封装。 然后,我之后创造的人都以刚才的模型做为模板,我创造的人都有我模型的特征 这就是继承。 最后,我觉得为了让人更丰富多彩,暗合阴阳之原理,可以根据模型进行删减, 某些人上半身器官多突起那么一丢丢,下面少那么一丢丢。某些人,下半身多 突起那么一丢丢。这就是多态。 嘿嘿,当然为了,更丰富多彩,那么一丢丢大小也是可以有区别的。。。” 此时,面试官要是男的你可以露出你懂得的表情! 程序员面试都很枯燥,你可以适当弄点笑点,是加分项。
答3: 通过封装 继承 多态 组合等手段把N 变为1的方法,, 封装:主要是暴露接口,你不用关心内部实现。 继承:主要是让你不用重复造轮子了。 多态:让代码可读性更强,让编译器做更多的事。 这个问题很简单嘛,直接回答如下: 面向对象隐藏了面向过程具体实现的细节, 把属性和行为封装成一个抽象模型,即对象,以便用专业的方法做专业的事情。 就好比,面向过程是部门员工,他们具体怎么完成工作内容我不关心了,我只 关心是谁来做这些事,谁叫我是部门经理呢?——这就是面向对象。
答4: “人”是类。 “人”有姓名、出生日期、身份证号等属性。 “人”有约会、么么哒、啪啪啪等功能(方法)。 “男人”、“女人”是“人”的子类。继承“人”的属性和功能。但也有 自己特有的属性和功能。你、我是对象。
答5: 是一种编程的思想,主要是为了解决代码复用的问题。 封装:把属性值、红蓝条、攻击、走位、放技能、清兵、游走等行为都塞在一个英雄里。 继承:攻击+10 的装备可以升级到攻击+20,以后还可能升级到攻击+30 并带有吸血效 果。不管升级成什么,都携带着攻击+10 这部分属性。 多态:一个团队需要一个辅助,我们只需要一个辅助英雄,并不关心来的是哪个辅助 英雄,能加血就行。 具备这三种特性的编程思想,叫做面向对象。
36. 节流与防抖
防抖
防抖只执行最后一次函数
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求 - 窗口大小计算。只需窗口调整完成后,计算窗口大小。防止重复渲染
代码
function debounce(func, delay) { let timer = null; // 定义计时器变量 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));
节流
截流只执行一次函数
- 懒加载、滚动加载、加载更多或监听滚动条位置 - 防止高频点击提交,防止表单重复提交
代码
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));
37. 扩张运算符
仅对一维数组是深拷贝
如果有二位数组,就是浅拷贝
3.性能优化
1.第一次访问时的优化(把第一次的加载速度变快)
1.代码压缩和合并
压缩JavaScript、CSS文件,减少文件大小和HTTP请求数量
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.图片优化
使用合适的图片格式(如WebP),对图片进行压缩,在不影响视觉效果的前提下减小图片大小。
3.懒加载
对于图片、视频等资源,采用懒加载技术,只有当元素进入视口时才加载资源。
4.服务端渲染(SSR)或预渲染
SSR可以在服务器端生成HTML内容直接发送给客户端,预渲染则是预先生成静态HTML文件,这两种方式都可以加快首次加载速度。
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的执行。
首屏加载速度慢怎么解决?
一、什么是首屏加载
指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容 首屏加载可以说是用户体验中最重要的环节
二、加载慢的原因
1.网络延时问题
cdn,用户节点就近
preload 预加载
prereder 预渲染
2. 资源太大
a.包分chunk
b.懒加载
c.公共资源 vender
d.缓存(不变动的使用强缓存;变动的使用协商缓存; 离线环境下使用策略缓存,service-worker)
e.服务端渲染(DOM树在服务端生成,然后返回给前端。)
f. 局部SSR,(广告页/营销活动页等)
3.资源是否重复发送请求去加载了
4.加载脚本的时候,渲染内容堵塞了
三、解决方案
减小入口文件积
常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加; 在vue-router配置路由的时候,采用动态加载路由的形式: routes:[ path: 'Blogs', name: 'ShowBlogs', component: () => import('./components/ShowBlogs.vue') ] 以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件
静态资源本地缓存
采用HTTP缓存
采用Service Worker离线缓存
前端合理利用localStorage
UI框架按需加载
图片资源的压缩
图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素 对于所有的图片资源,我们可以进行适当的压缩 对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。
组件重复打包
假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载 解决方案:在webpack的config文件中,修改CommonsChunkPlugin的配置 minChunks: 3 表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件
开启GZip压缩
使用SSR,也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器
指标
First Contentful Paint (FCP) - 首次内容绘制时间
衡量从开始加载页面到页面上的第一个内容元素(如文本、图像、SVG 等)绘制到屏幕上的时间。这是用户感知到的第一个信号,表明页面开始加载内容。
Largest Contentful Paint (LCP) - 最大内容绘制时间:
衡量页面上最大的内容元素(通常是一个大的图片或视频背景)从开始加载到渲染完成的时间。LCP 是一个关键的用户体验指标,因为它反映了页面主要内容的加载速度。
First Meaningful Paint (FMP) - 首次有意义绘制时间
这一指标虽然不是官方标准的一部分,但它是评估页面何时变得有用的一个度量。它衡量的是页面首次呈现有意义的内容给用户的时间,即页面的主要内容已加载完毕并且视觉上稳定的时间。
Time to Interactive (TTI) - 交互就绪时间
衡量页面从开始加载到完全可用、响应用户输入所需的时间。在这段时间之后,页面应该可以快速响应用户的点击、触摸等交互。
Total Blocking Time (TBT) - 总阻塞时间
衡量从 FCP 到 TTI 之间,页面被认为是不可响应的时间总和。这个指标帮助识别那些使得页面无法立即响应用户输入的长时间任务。
Speed Index (SI) - 速度指数
速度指数是一种量化指标,它衡量页面内容在多快的时间内出现在可视区域。它记录了页面在一定时间内渲染了多少像素。
Cumulative Layout Shift (CLS) - 累积布局偏移
这是一个用户体验指标,用于测量页面在加载过程中发生的意外布局变化的程度。良好的 CLS 分数表示页面在加载期间保持了视觉稳定性。
First Byte Time (TTFB) - 第一个字节时间
衡量从浏览器发送请求到服务器返回第一个字节数据的时间。TTFB 反映了服务器端处理请求的速度以及网络延迟。
一、感知性能优化
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访问
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.图片懒加载:loading="lazy"
2.检测是否到达视窗 需要:旧浏览器需要降级处理
3.滚动事件监听
4.使用库:lazyize/lazyload
常规
1.压缩图片
2.小图片引入雪碧图
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.减少页面重定向
如何对大型前端项目进行性能剖析(profiling)?
1.使用Chrome DevTools中的Performance面板。可以记录页面加载和交互过程中的各种性能指标,如脚本执行时间、渲染时间、重绘和回流次数等。
2.利用Lighthouse工具,它可以对网页进行全面的性能评估,包括加载性能、可访问性、最佳实践等方面,并给出优化建议
3.在代码中手动插入性能测量点,例如使用console.time和console.timeEnd来测量特定代码块的执行时间。
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 记录就变为了线性的记录了
如何构建一个适合大型团队的前端代码规范和构建流程
代码规范方面
使用ESLint结合Prettier来统一JavaScript和CSS(包括预处理器如Sass或Less)的语法风格。例如,规定变量命名采用驼峰命名法,函数名要有明确含义等。
对于HTML结构,制定语义化标签的使用规范,如导航栏使用<nav>标签,页脚使用<footer>标签等。
确定组件化的规范,包括组件的命名(采用大驼峰命名法,如UserProfile)、组件的输入输出(props和events的定义规范)等。
构建流程
采用模块打包工具如Webpack或Rollup。在Webpack中配置不同的环境(开发、测试、生产),例如在生产环境下进行代码压缩、混淆,提取CSS到单独文件等操作。
使用自动化构建工具如Gulp或Grunt来处理一些重复性任务,如图片压缩、文件合并等。
集成版本控制系统(如Git)与持续集成/持续部署(CI/CD)工具(如Jenkins、Travis CI或GitHub Actions),实现代码的自动构建、测试和部署。
5. webpack与grunt、gulp的不同
Grunt、Gulp是基于任务运⾏的⼯具: 它们会⾃动执⾏指定的任务,就像流⽔线,把资源放上去然后通过不同插件进⾏加⼯,它们包含活跃的社区,丰富的插件,能⽅便的打造各种⼯作流。
Webpack是基于模块化打包的⼯具: ⾃动化处理模块,webpack把⼀切当成模块,当 webpack 处理应⽤程序时,它会递归地构建⼀个依赖关系图 (dependency graph),其中包含应⽤程序需要的每个模块,然后将所有这些模块打包成⼀个或多个 bundle。
6. 对 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. Webpack+vite
说说你对webpack的理解
一种打包工具,实现前端模块化,将多个 js,打包成一个 bundle.js [ˈbʌndl] 其他类型文件交由各自的 loader 处理
loader 了解吗? 常见的 loader 有哪些?
Loader(加载器)是webpack的核心之一。Loader用于将不同类型的文件转换为webpack可识别的模块。
webpack只能直接处理JavaScript格式的代码,任何非JS文件都必须被预先处理转换为JS代码,才可以参与打包。 每个 loader 都是一个函数,它接收原始文件的内容作为输入,并返回处理后的结果。
使用方式
配置方式(推荐)
在webpack.config.js文件中指定loader。
内联方式
在每个import语句中显式指定loader。但请注意,在webpack v4版本可以通过CLI使用loader,在webpack v5中已被弃用
loader的执行顺序
是从后往前执行,谁先定义后执行。
当为某个文件类型指定了多个loader时,这些loader会形成一个链式调用,每个loader都会接收上一个loader的输出作为输入,并返回自己的处理结果给下一个loader。这个链式调用的执行顺序就是按照配置数组中loader的顺序从后往前执行的。
例如,如果你有一个TypeScript文件,并希望先使用eslint-loader来检查代码质量, 然后使用ts-loader将其转换为JavaScript, 最后再使用babel-loader将ES6+的代码转换为ES5, 你可以按照以下方式配置webpack:
module.exports = { module: { rules: [ { test: /\.ts$/, use: [ 'babel-loader', 'ts-loader', 'eslint-loader' ] } ] } };
常见Loader
babel-loader
实现对 es6 语法向 es5 的兼容,以便在旧浏览器中运行。
css-loader
解析CSS文件,处理 css 文件中,url, @import 这样的语法,转换成 js 可以处理的形式;
style-loader
将 CSS 代码以内联的方式注入到 HTML 中。
file-loader
处理文件资源(如图片/字体等),将文件复制到输出目录,并返回文件路径。
url-loader
与 file-loader 类似,但可以根据文件大小将文件转换成Data-URL(base64格式)或文件路径。
sass-loader
解析Sass/Scss文件,并将其转为css代码。
less-loader
解析less文件,并将其转为css代码。
postcss-loader
处理css,可以自动添加前缀/压缩/css modules等操作。
ts-loader
将 TypeScript 代码转换为 JavaScript 代码。
eslint-loader
在构建过程中使用 Eslint 进行代码检查。
tslint-loader
在构建过程中使用 TSLint 进行代码检查。
stylelint-webpack-plugin
在构建过程中使用 Stykelint 进行CSS/Scss代码检查。
vue-loader
解析 vue 单文件,并转换为JavaScript 代码。
image-webpack-loader
优化图片资源,包括压缩/转换格式等操作。
html-loader
解析 HTML 文件,处理其中的引用资源(如图片/字体等),并返回处理后的 HTML 代码。
markdown-loader
将 Markdown 文件转换成 HTML 代码。
json-loader
解析 json 文件,并返回解析后的 JavaScript 对象。
prettier-loader
在构建过程中使用 Prettier 进行代码格式化。
webpack核心概念
1、entry:指定了模块的入口 js 文件,一个或多个
2、output:定义输出文件的格式和目录。
3、module:配置各种类型文件的解析规则,比如说.vue文件、.js文件、.css文件的loader等。
4、rosolve:配置alias(别名),或者定义寻找模块的规则。
5、plugins:配置扩展插件,扩展webpack的更多功能。
html-webpack-plugin, 在 html 文件中自动引入 js 文件 clean-webpack-plugin,在下次构建前清理上一次的打包产物
6、devServer:实现本地http服务等。
以 entry 作为入口,对代码静态分析 import 了哪些文件,通过不同的 loader 对不同后缀的文件处理,最终打包到一起。在最后的打包过程中,插件可以在适当的时机进行额外的操作。
webpack打包流程
webpack.config.js,用于配置Webpack的打包行为
1、从入口(entry) 开始,递归转换入口文件所依赖的module
2、每找到一个module,构建依赖关系图,根据依赖关系图,Webpack会逐个加载模块。 根据对应的loader去转换这个module。
3、组装成一个个包含多个模块的 Chunk
4、最后,Webpack会把所有Chunk转换成文件输出, 在整个流程中Webpack会在恰当的时机执行plugin里定义的扩展插件。
Loader(加载器) 和 Plugin(插件) 的区别
功能不同
1. Loader是在模块加载时对文件进行转换的机制,它主要处理特定类型的文件,将非JavaScript文件转换为浏览器能够理解的JavaScript模块。 例如babel - loader将ES6+代码转换为ES5代码,css - loader解析CSS文件中的@import和url()语句并将CSS转换为JavaScript模块。 加载器的作用是扩展Webpack对不同类型文件的处理能力,使得各种资源(如样式表、图片、字体等)能够被正确地整合到最终的JavaScript包中。
2. Plugin 是在构建流程的特定阶段(如编译完成后、打包开始前等)执行特定任务的工具。它可以对整个构建过程进行操作,而不仅仅是针对单个模块。 插件的作用包括代码压缩(如TerserPlugin)、优化构建结果(如SplitChunksPlugin用于分割代码)、添加版权信息、生成自定义插件输出等。
运行时机不同
loader运行在打包文件之前,对文件进行预处理;
plugins 运行在loader结束后,webpack打包的整个过程中,它是基于事件机制,监听webpack打包过程中的某些节点,从而执行相应任务,进而改变输出。
有哪些常见的Plugin(插件)
plugin是对webpack现有功能的扩展,可以用于打包优化、文件压缩等多种目的。
webpack内置的插件
1、ProvidePlugin:将指定模块暴露到全局,使用的时候就不需要再import和require,比如说jquery;
2、DefinePlugin:允许在编译时将你代码中的变量替换为其他值或表达式,比如说Vue中生产、开发环境的判断,所用的process.env;(项目)
3、HotModuleReplacementPlugin:热更新功能(项目)
4、CommonsChunkPlugin : 主要是用来提取第三方库和公共模块,避免首屏加载的bundle文件体积过大,从而导致加载时间过长。
webpack的第三方插件
1、copy-webpack-plugin: 可以将指定目录下的文件直接复制到指定路径。一般用于处理静态资源的移动,比如图片,视频等
2、clean-webpack-plugin: 可以在打包前,清除上一次打包的产物
3、html-webpack-plugin:该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。(项目)
4、uglifyjs-webpack-plugin:压缩代码,同时可以去掉代码中的debugger、console.log
5、compression-webpack-plugin : 压缩指定类型的文件为gzip 文件,以部署在服务器上时,加载页面时,传输时使用gzip文件,浏览器自行解析。(gzip文件的体积比正常文件体积小3-5倍)
6、hard-source-webpack-plugin : 利用缓存,提高打包与启动项目的速度;
7、extract-text-webpack-plugin : 抽离css样式,防止将样式打包在js中引起页面样式加载错乱的现象。
8、mini-css-extract-plugin : 将项目中引入的 css 文件合并提取到一个 css 文件中,将 css 单独独立出来,可以更好的和 js 文件并行加载,提升页面性能。实际配置中,可以在开发环境使用 style-loader, 加快编译速度,而生产环境中使用 mini-css-extract-plugin,提高页面加载速度。
webpack 的热更新原理
又称为热替换,这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
热更新的核心就是客户端与服务端建立的websocket连接
服务端监听文件变化,编译后再推送给客户端告之其哪些地方改变了,最终客户端发送ajax请求,获取最新资源,使用文件系统替换修改的内容实现局部更新。
webpack 打包比较慢,为什么? 有什么优化方法吗?
为什么打包慢?
webpack 的工作,会根据 entry 中指定的 js,一个一个加载并调用 loader 进行处理后,最终生成一个 bundle 文件。
打包慢,大概率就在于要处理的文件太多。不过现在 webpack 在文件修改后,本身就有优化,所以打包慢,一般发生在冷启动,和生产环境打包时。
优化方法?
如何提高 webpack 的打包速度
1、使用高版本的 Webpack 和 Node.js
2、通过 externals 配置提取页面公共资源, 将基础包通过 CDN 引入,不打入 bundle 中
3. 调整 loader 配置,增加 exclude,不对 node_modules 中文件进行处理,通过减少要处理的文件来减少构建时间;
8. 拆分项目,采用微前端的方案进行加载(把一个项目的多个页面进行拆分,通过微前端的技术将一个一个项目合并起来)。
4. 增加 cache-loader,缓存上次打包结果;
5. 使用 Terser 开启多进程,减少压缩代码时间;
6、使用 uglifyjs-webpack-plugin 对js进行代码压缩。
7、使用 tree-shaking 和 Scope hoisting 来剔除多余代码。
webpack 有什么方法,可以使打包出来的页面性能更好?
(1)js, css 文件压缩,减少文件体积
(2)js, css 打包文件名增加 hash 值(output 中 filename 使用 [hash] 即可插入文件 hash 值),借助 http 强缓存减少加载耗时
(3)图片压缩,借助 image-webpack-loader 或者手动压缩图片后引入,减少页面资源体积
(4)tree-shaking:webpack 中开启 usedExports: true。引入的 npm 包如果支持,也会自动 shaking,无需配置
(5) 通过懒加载拆分不同路由的文件,按需加载 js
懒加载:不是在最顶部放入import,会返回一个promise。当文件被加载完成后,才会执行它的.then
webpack 和 gulp 区别
webpack 是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源(图 片、js 文件、css 文件等)都看成模块,通过loader(加载器)和 plugins(插件)对资源 进行处理,打包成符合生产环境部署的前端资源。
gulp 强调的是前端开发的工作流程,我们可以通过配置一系列的 task,定义 task 处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序, 来让 gulp 执行这些 task,从而构建项目的整个前端开发流程。
tree shaking 原理
是一种通过清除多余代码方式来优化项目打包体积的技术, 清除一些被 import 了但其实没有被使用的代码。
实现原理
一是先标记出模块导出值中哪些没有被用过, 二是使用 Terser 删掉这些没被用到的导出语句。 标记过程大致可划分为三个步骤:
1、Make 阶段,收集模块导出变量并记录到模块依赖关系图 ModuleGraph 变量中
2、Seal 阶段,遍历 ModuleGraph 标记模块导出变量有没有被使用
3、生成产物时,若变量没有被其它模块使用则删除对应的导出语句
标记功能只会影响到模块的导出语句,真正执行“「Shaking」”操作的是 Terser 插件。只需要用 Terser 提供的 DCE 功能就可以删除这一段定义语句(不可能被执行到的代码),以此实现完整的 Tree Shaking 效果。
要注意,开启 tree-shaking 的条件是使用 import 和 export。如果是一些旧的 commonjs 等,是不支持 tree-shaking 的。
source map 是什么?生产环境怎么用?
source map 是将编译、打包、压缩后的代码映射会源代码的过程。 打包压缩后的代码不具备良好的可读性,想要调试源码就需要 source map。
map 文件只要不打开开发者工具,浏览器是不会加载的。
文件指纹是什么
文件指纹是指打包后输出文件的名的后缀。
用途
1、版本管理:在发布版本时,通过文件指纹来区分 修改的文件 和 未修改的文件。
2、使用缓存:未修改的文件,文件指纹保持不变,浏览器继续使用缓存访问。
常见的文件指纹
1、hash:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 就会变化。
2、chunkhash:和webpack打包的 chunk 有关,不同的 chunk、不同的 entry 会生成不同的 chunkhash。
3、contenthash:根据文件内容来定义 hash,文件内容不发生变化,则contenthash就不会变化。直接在输出文件名添加对应的 hash值即可。
[webpack] 打包时 hash 码是如何⽣成的
Webpack 在打包过程中⽣成 hash 码主要⽤于缓存和版本管理。主要有三种类型的 hash 码:
1. hash:是和整个项⽬的构建相关,只要项⽬⽂件有修改,整个项⽬构建的 hash 值就会更改。这意味着任何⼀个⽂件的改动都会影响到整体的 hash 值。 2. chunkhash:与 webpack 打包的 chunk 有关,不同的 entry 会⽣成不同的 chunkhash 值。例如,如果你的配置⽣成了多个 chunk(例如使⽤了 code splitting),每个 chunk 的更新只会影响到它⾃⾝的 chunkhash。 3. contenthash:根据⽂件内容来定义 hash,内容不变,则 contenthash 不变。这在使⽤诸如 CSS提取到单独⽂件的插件时特别有⽤,因此只有当⽂件的内容实际改变时,浏览器才会重新下载⽂件。
说说Vite
一个现代化的前端构建工具,基于浏览器原生ES模块提供快速开发服务器启动和即时模块热更新的能力。
核心原理
原生ES模块加载(ESM)
在开发环境中,Vite利用现代浏览器支持的ES模块特性来处理文件。 它不预先打包文件,而是直接将源文件作为模块发送给浏览器。 因此启动速度非常快
当浏览器请求一个文件(如JavaScript模块或Vue组件)时,Vite仅处理该特定文件,并实时地将其转换成浏览器可理解的格式。
Vite启动一个开发服务器(devServer),当代码执行到模块加载时再请求对应模块的文件,本质上实现了动态加载。
优化的生产构建流程
虽然Vite在开发阶段不进行打包,但它在生产环境中使用Rollup作为打包工具。 相比于Webpack,Rollup要小巧的多
在生产模式下,Vite对项目进行打包和压缩,包括代码分割、懒加载和其他常见的优化措施。
其作用类似webpack+ webpack-dev-server,其特点如下
1、快速的冷启动
2、即时的模块热更新
3、真正的按需编译
Vite 原理
原生 ES 模块
Vite 利用浏览器对原生 ES 模块(ESM)的支持,实现按需加载和编译。 开发服务器启动时,Vite 只需解析入口文件,并将导入的模块路径记录下来。
按需编译
当浏览器请求某个模块时,Vite 会实时编译该模块及其依赖。 使用 esbuild 或 Vite 自带的编译器快速处理 JavaScript 和 CSS。
热模块替换(HMR)
Vite 的 HMR 实现非常高效,因为它只更新变更的部分,而不是整个页面。 通过 WebSocket 实时推送更新到浏览器。
插件系统
Vite 的插件系统允许开发者扩展其功能,如处理非 JavaScript 文件、优化构建等。 插件可以在不同的生命周期阶段执行特定的任务。
webpack和vite的区别
1.构建速度:Vite通常比Webpack更快。Webpack在开发过程中需要对整个项目进行全量构建,而Vite则利用现代浏览器支持的ES Module特性,按需编译模块,仅构建正在编辑的文件,从而显著缩短启动时间。
2.开发体验:Vite在开发模式下启动速度快,支持热更新,当模块内容改变时,Vite会直接向浏览器请求更新模块,而不需要重新编译整个项目。相比之下,Webpack的热更新需要重新编译和打包整个模块链,过程较为耗时。
3.插件生态:Webpack拥有丰富的插件生态系统,能够满足各种前端工程需求。尽管Vite的插件生态在不断发展,但与Webpack相比仍显得较为有限。这意味着在特定需求下,Webpack可能提供更多的选择和灵活性。
4.配置复杂度: Webpack的配置相对复杂,适合需要高度定制的项目。 Vite则设计为开箱即用,减少了配置需求,适合快速开发和原型制作。
5. 适用场景:由于Vite的快速开发体验和按需编译的优势,它更适合中小型项目和快速开发需求。而Webpack由于其丰富的插件生态和强大的功能,更适合大型、复杂的项目。
为什么 Vite 速度⽐ Webpack 快?
1、开发模式的差异
当使⽤ Webpack 时,会在项目启动时就把所有的代码和资源都打包好,这样虽然最终的打包文件很完美,但是这会增加启动时间和构建时间 而 Vite 会在请求模块时再进⾏实时编译,不需要一开始就把所有代码都打包好,所以它的启动速度和热更新速度都非常快。
2、对ES Modules的⽀持
什么是ES Modules?
通过使⽤ export 和 import 语句,ES Modules 允许在浏览器端导⼊和导出模块。 当使⽤ ES Modules 进⾏开发时,开发者实际上是在构建⼀个 依赖关系图 ,不同依赖项之间通过导⼊语句进⾏关联。 主流浏览器(除IE外)均⽀持ES Modules,并且可以通过在 script 标签中设置 type="module" 来 加载模块。默认情况下,模块会延迟加载,执⾏时机在⽂档解析之后,触发DOMContentLoaded事件前
现代浏览器本⾝就⽀持 ES Modules ,会 主动发起 请求去获取所需⽂件。Vite充分利⽤了这⼀点,将开发环境下的模块⽂件直接作为浏览器要执⾏的⽂件,⽽不是像 Webpack 那样 先打包 ,再交给浏览器执⾏。这种⽅式减少了中间环节,提⾼了效率。
3、底层语⾔的差异
Webpack 是基于 Node.js 构建的, ⽽ Vite 则是基于 esbuild 进⾏预构建依赖。esbuild 是采⽤ Go 语⾔编写的,Go 语⾔是 纳秒 级别的,⽽ Node.js 是 毫秒 级别的。因此,Vite 在打包速度上相⽐Webpack 有 10-100 倍的提升。
什么是预构建依赖?
预构建依赖通常指的是在项⽬ 启动或构建 之前,对项⽬中所需的依赖项进⾏预先的 处理或构建 。这样做的好处在于,当项⽬实际运⾏时,可以 直接使⽤ 这些已经预构建好的依赖,⽽⽆需再进⾏实时的编译或构建,从⽽提⾼了应⽤程序的运⾏速度和效率。
4、热更新的处理
在 Webpack 中,当⼀个模块或其依赖的模块内容改变时,需要 重新编译 这些模块。 ⽽在 Vite 中,当某个模块内容改变时,只需要让浏览器 重新请求 该模块即可,这⼤⼤减少了热更新的时间。
总结
总的来说,Vite 之所以⽐ Webpack 快,主要是因为它采⽤了 不同的开发模式、 充分利⽤了现代浏览器的 ES Modules ⽀持 、 使⽤了更⾼效的底层语⾔ , 并优化了热更新的处理。这些特点使得Vite在⼤型项⽬中具有显著的优势,能够快速启动和构建,提⾼开发效率。
简单来说,Vite 利用了现代浏览器对 ES 模块的支持,直接使用源代码而不是预打包的代码,这样就减少了启动和打包的时间。同时,Vite 还使用了一种叫做 esbuild 的工具来快速处理代码,进一步提高了效率。所以,Vite 在开发速度上比 Webpack 有优势,特别适合那些需要快速迭代和开发的项目。
vite 和 webpack 在热更新上有啥区别
1. 模块级别的热更新:Vite 使⽤浏览器原⽣的 ES 模块系统,可以实现模块级别的热更新,即只更新修改的模块,⽽不需要刷新整个⻚⾯。这样可以提供更快的开发迭代速度。 ⽽在 Webpack中,热更新是基于⽂件级别的,需要重新构建并刷新整个⻚⾯。
2. 开发环境下的⽆构建:Vite 在开发环境下不会对代码进⾏打包构建,⽽是直接利⽤浏览器原⽣的模块导⼊功能,通过 HTTP 服务器提供模块的即时响应。这样可以避免了构建和重新编译的时间,更快地反映出代码的修改。 ⽽在 Webpack 中,每次修改代码都需要重新构建和编译,耗费⼀定的时间。
3. 构建环境下的优化:尽管 Vite 在开发环境下不进⾏打包构建,但在⽣产环境下,它会通过预构建的⽅式⽣成⾼性能的静态资源,以提⾼⻚⾯加载速度。 ⽽ Webpack 则通过将所有模块打包成bundle ⽂件,进⾏代码压缩和优化,以及使⽤各种插件和配置来优化构建结果。
总的来说,Vite 在热更新上⽐ Webpack 更加快速和精细化,能够在开发过程中提供更好的开发体验和更快的反馈速度。但是,Webpack 在构建环境下有更多的优化和功能,适⽤于更复杂的项⽬需求。
对比
6.浏览器原理篇
前端加密
HTTPS采用SSL/TLS协议进行加密。在SSL/TLS握手过程中,客户端和服务器通过交换公钥、私钥等信息来协商一个对称加密密钥。
然后,客户端和服务器之间传输的数据使用这个对称密钥进行加密和解密。
重要性:
保护数据的机密性,防止数据在传输过程中被窃取或篡改。
确保数据的完整性,使得客户端和服务器能够验证数据是否被恶意修改。
建立用户与服务器之间的信任关系,对于涉及敏感信息(如登录凭证、支付信息等)的前端应用至关重要。
防范跨站脚本攻击(XSS)
是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
1.对用户输入进行严格的过滤和转义。在前端,可以使用库如DOMPurify来净化HTML内容,防止恶意脚本注入。
2.在服务器端,对接收到的数据进行验证和编码,确保输出到页面的数据是安全的。
3.设置合适的HTTP头部,如Content - Security - Policy(CSP),限制页面可以加载的资源来源。
跨站请求伪造(CSRF)
跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
在服务器端生成并验证随机令牌(token)。在用户提交表单时,将令牌包含在表单数据中,服务器端验证该令牌是否匹配。
使用SameSite属性的Cookie,设置SameSite = Strict或SameSite = Lax,限制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.恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数时候都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被植⼊恶意代码很容易引起安全问题。
点击刷新按钮或者按 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
原理:CORS通过服务端设置HTTP响应头,声明允许的跨域请求来源与方法。
服务端配置示例(Node.js)
app.use((req, res, next) => { res.setHeader('Access-Control-Allow-Origin', 'https://www.my-domain.com'); // 指定允许的域名 res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); // 允许的HTTP方法 res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // 允许的请求头 res.setHeader('Access-Control-Allow-Credentials', 'true'); // 允许携带Cookie next(); });
操作流程
关键要点
简单请求:
如GET、HEAD、POST(且Content-Type为text/plain、multipart/form-data、application/x-www-form-urlencoded),直接发送。
复杂请求
如PUT、DELETE或自定义头,需触发预检请求(OPTIONS)
带Cookie请求
需在前端设置withCredentials为true
2、通过jsonp跨域
原理:JSONP利用<script>标签不受同源策略限制的特性,通过动态创建脚本实现跨域数据获取。
实现示例
// 前端定义回调函数 function handleResponse(data) { console.log('Received:', data); } // 动态添加script标签 const script = document.createElement('script'); script.src = 'https://api.other-domain.com/data?callback=handleResponse'; document.body.appendChild(script); // 服务端返回数据包装为函数调用 handleResponse({ "status": "success", "data": [...] });
限制
仅支持GET请求
存在XSS安全风险
无法处理HTTP错误状态码
然JSONP在较早的Web应用中广泛使用,但由于其局限性,建议在现代开发中优先考虑其他解决方案
3、postMessage API
原理
postMessage允许跨窗口之间的安全通信
跨窗口通信示例
// 父窗口(https://parent.com) const iframe = document.getElementById('child-iframe'); iframe.contentWindow.postMessage('Secret data', 'https://child.com'); // 子窗口(https://child.com) window.addEventListener('message', (event) => { if (event.origin !== 'https://parent.com') return; console.log('Received:', event.data); });
这种方法非常适合在同一页面中嵌入多个域的内容时使用。
4、Nginx反向代理
原理
通过服务端代理转发请求,使浏览器认为所有请求源自同一域。
Nginx配置示例
server { listen 80; server_name my-domain.com; location /api/ { proxy_pass https://api.other-domain.com/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }
操作流程
在这种情况下,Nginx充当了中间层,浏览器只需与自己的域进行交互,避免了跨域限制。
5、WebSocket协议
原理
WebSocket是基于TCP的全双工通信协议,默认支持跨域。
客户端实现
const socket = new WebSocket('wss://chat.other-domain.com'); socket.onmessage = (event) => { console.log('Message:', event.data); };
WebSocket非常适合实时应用,比如在线聊天 或实时数据更新 ,能够有效减少请求延迟。
对比
7.手写代码
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
8. 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是所有类型的子类型。
微前端
定义
是一种将前端应用拆分成多个小型的前端应用,每个应用运行在独立的环境中,但能够被组合成一个整体应用的技术。
实现方式
iframe方案
通过iframe加载不同的应用,实现应用的隔离和独立部署。
JS模块联邦
通过Webpack的Module Federation插件,实现模块之间的共享和通信。
qiankun方案
基于single-spa的qiankun库,提供了一套简单的API来实现微前端架构。
YY EMP方案
基于EMP(Enterprise Microservices Platform)实现微前端架构,支持多语言和多种技术栈。
技术挑战和解决方案
状态管理:如何保证各个微服务之间的状态同步和共享。
资源管理:如何高效地加载和卸载微服务,减少资源浪费。
安全性和隔离性:确保每个微服务在独立的环境中运行,避免相互影响。
调试和维护:提供便捷的调试工具和维护手段,确保系统的稳定性和可维护性。
为什么不用iframe做微前端?
其实,如果你不考虑用户体验问题的话,iframe 几乎是最完美的微前端解决方案,什么样式隔离,JS隔离,这些问题在iframe中通通不存在,因为浏览器提供了原生的硬隔离方案,但也正是这种硬隔离方案,导致了无法在应用之间进行数据共享,随之而来的,就是一些列的开发和产品体验问题。比如
第一个问题,每次刷新页面,iframe的url都会丢失,而且前进后退按钮没办法用,当然,这个问题,你可以通过提前缓存url来解决。
第二个问题,就是慢,因为每次加载子应用,都是一次资源重新加载的过程,每次都要重新渲染页面,当然了,如果你睁一只眼闭一只眼,这个问题也是可以忍受的。
第三个问题,因为硬隔离导致的主应用和子应用之间数据完全隔离,不共享,导致你需要做很多额外工作来处理,比如每个子应用的免登陆问题,数据实时同步问题等等
第四个问题,就是主应用和子应用之间因为DOM结构不共享,而导致的UI不同步问题。比如这个场景,我们要求子应用中的弹窗要在浏览器中居中显示,用户拖动浏览器大小时候,也要自适应居中,其实做起来就很麻烦,增加了很多额外的工作量。
如何在微前端项目中实现路由的管理?
路由管理通常采用主应用统一管理的方式,主应用控制微应用的加载和切换。
示例
我们可以使用 qiankun 作为微前端框架,并通过主应用定义路由,加载各个微应用。以下是主应用中的简单路由配置示例:
import { registerMicroApps, start } from 'qiankun'; registerMicroApps([ { name: 'app1', entry: '//localhost:3001', container: '#micro-container', activeRule: '/app1', }, { name: 'app2', entry: '//localhost:3002', container: '#micro-container', activeRule: '/app2', }, ]); start();
在这个配置中,qiankun 会根据 URL 路径来加载对应的微应用 app1 或 app2,从而实现主应用对微应用的路由控制。
微前端如何实现跨应用通信?
1. 全局状态管理工具:在主应用中使用 Redux、Vuex 等状态管理工具,通过上下文来共享数据。
2. 事件总线:通过事件总线在微应用间传递事件和数据。
3. URL 参数传递:通过 URL 参数传递信息。
4. 全局变量:在主应用中定义全局变量,子应用可以访问和修改这些变量。
示例:
下面是使用事件总线的示例,主应用可以使用 window 的 CustomEvent 发出事件,各个微应用可以监听事件。
// 主应用 function sendMessage(data) { window.dispatchEvent(new CustomEvent('microAppEvent', { detail: data })); } // 微应用 window.addEventListener('microAppEvent', (event) => { console.log('收到主应用消息:', event.detail); });
微前端架构的优缺点是什么
优点
提升开发效率,方便独立开发与部署。
代码更易维护,减少单体应用的代码量。
团队协作更加灵活,各个团队可以自由选择技术栈。
缺点
资源加载和性能问题需要优化。
跨应用的共享和通信复杂度较高。
架构和部署复杂,学习成本较高。
如何在项目中实践微前端架构?
拆分应用:将单体应用拆分成多个独立的功能模块。
选择微前端框架:选择合适的微前端框架(如 qiankun)。
路由管理:配置主应用的路由管理,加载各微应用。
通信机制:实现主应用与微应用的通信机制。
资源优化:通过懒加载、缓存等手段优化资源加载。
微前端架构如何管理共享依赖?如何避免依赖冲突?
1. 主应用统一管理依赖:在主应用中定义和加载所有通用的依赖包,如 React 或 Vue,然后在微应用中直接引用,避免重复加载。
2. 使用模块联邦(Module Federation):通过 Webpack 5 的模块联邦功能,使多个微应用可以共享同一个依赖包,减少加载重复依赖。
3. 版本隔离:如果微应用需要特定版本的依赖,可以通过 namespace 隔离不同版本,防止冲突。
// webpack.config.js 配置示例 new ModuleFederationPlugin({ name: 'app1', shared: { react: { singleton: true, requiredVersion: '17.0.2' }, }, });
4. 控制依赖范围:微应用尽量避免加载过多的依赖,仅在主应用中引入通用的库,以减少微应用的体积和依赖冲突的风险。
微前端如何实现子应用的独立部署?部署时需要注意哪些问题?
1. 独立构建与发布:每个微应用需要拥有独立的构建配置和部署流程,如通过 CI/CD 工具进行自动化部署。
2. 主应用动态加载:主应用通过动态 URL 配置,加载各个微应用的资源。在部署时,只需更新对应微应用的 URL,无需修改主应用代码。
3. 跨域配置:不同微应用通常会部署在不同域名下,跨域访问时需要在服务器上配置 CORS 或通过代理解决跨域问题。
注意事项:
确保版本一致性,避免不同版本的依赖出现冲突。
部署过程中若需要重新编译,需要确保各微应用的路由路径配置不冲突。
关注加载速度和缓存策略,尽可能减少对用户的影响。
在微前端架构中如何保证样式的隔离?
微前端中各微应用可能使用不同的 CSS 框架或预处理器,样式隔离至关重要。常用的样式隔离方法包括:
1. CSS Modules:在组件级别启用 CSS Modules,限制样式的作用域。
2. Scoped CSS:通过 Vue 或 React 的 Scoped CSS 或内联样式确保样式不影响其他微应用。
3. Shadow DOM:使用 Web Component 和 Shadow DOM,形成独立的样式作用域,确保隔离效果(需考虑浏览器兼容性)。
4. 命名空间:给每个微应用添加独特的类前缀或命名空间,防止样式冲突。
示例
/* 主应用 */ .app-main { ... } /* 微应用 */ .app-micro { ... }
如何在微前端架构中处理公共状态的共享?有哪些常用方案?
1. 全局状态管理工具:如 Redux 或 Vuex,将公共状态放置在主应用中,通过上下文或事件传递给各个微应用。
2. 基于事件的通信:使用 EventEmitter、postMessage 或 CustomEvent 传递数据,实现微应用间的状态共享和同步。
3. 自定义的通信协议:使用 WebSocket、localStorage 等实现更复杂的跨应用通信和数据同步。
示例:基于事件的通信
// 主应用 function updateGlobalState(state) { window.dispatchEvent(new CustomEvent('updateState', { detail: state })); } // 微应用监听全局状态 window.addEventListener('updateState', (event) => { const sharedState = event.detail; console.log('Received shared state:', sharedState); });
微前端的性能优化有哪些手段?
微前端架构引入了多个微应用,性能优化尤为重要,常用手段包括:
1. 懒加载微应用:仅在需要时才加载微应用,减少初始加载的资源占用。
2. 缓存资源:对静态资源进行缓存或使用 Service Worker 缓存,提升加载效率。
3. 树状结构的路由加载:通过按需加载子模块,减少无用代码的加载。
4. CDN 加速:将微应用资源部署到 CDN,加快资源加载速度。
qiankun
qiankun 微前端框架的工作原理
qiankun 是一个基于 single-spa 的微前端实现框架。它的工作原理主要涉及到以下几个方面:
1. 应用加载:qiankun 通过动态创建 script 标签的方式加载子应用的入口文件。加载完成后,会执行子应用暴露出的生命周期函数。
2. 生命周期管理:qiankun 要求每个子应用都需要暴露出 bootstrap、mount 和 unmount 三个生命周期函数。bootstrap 函数在应用加载时被调用,mount 函数在应用启动时被调用,unmount 函数在应用卸载时被调用。
3. 沙箱隔离:qiankun 通过 Proxy 对象创建了一个 JavaScript 沙箱,用于隔离子应用的全局变量,防止子应用之间的全局变量污染。
4. 样式隔离:qiankun 通过动态添加和移除样式标签的方式实现了样式隔离。当子应用启动时,会动态添加子应用的样式标签,当子应用卸载时,会移除子应用的样式标签。
5. 通信机制:qiankun 提供了一个全局的通信机制,允许子应用之间进行通信。
在使用 qiankun 时,如果子应用是基于 jQuery 的多页应用, 你会如何处理静态资源的加载问题?
方案一:使用公共路径
在子应用的静态资源路径前添加公共路径前缀。例如,如果子应用的静态资源存放在 http://localhost:8080/static/,那么可以在所有的静态资源路径前添加这个前缀。
方案二:劫持标签插入函数
这个方案分为两步:
1. 对于 HTML 中已有的 img/audio/video 等标签,qiankun 支持重写 getTemplate 函数,可以将入口文件 index.html 中的静态资源路径替换掉。
2. 对于动态插入的 img/audio/video 等标签,劫持 appendChild、innerHTML、insertBefore 等事件,将资源的相对路径替换成绝对路径。
方案三:给 jQuery 项目加上 webpack 打包
这个方案的可行性不高,都是陈年老项目了,没必要这样折腾。
在使用 qiankun 时,如果子应用动态插入了一些标签, 你会如何处理?
我们可以通过劫持 DOM 的一些方法来处理。例如,我们可以劫持 appendChild、innerHTML 和 insertBefore 等方法,将资源的相对路径替换为绝对路径。
例子
假设我们有一个子应用,它使用 jQuery 动态插入了一张图片:
const render = $ => { $('#app-container').html('<p>Hello, render with jQuery</p><img src="./img/my-image.png">'); return Promise.resolve(); };
我们可以在主应用中劫持 jQuery 的 html 方法,将图片的相对路径替换为绝对路径:
beforeMount: app => { if(app.name === 'my-app'){ // jQuery 的 html 方法是一个复杂的函数,这里为了简化,我们只处理 img 标签 $.prototype.html = function(value){ const str = value.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">') this[0].innerHTML = str; } } }
在这个例子中,我们劫持了 jQuery 的 html 方法,将图片的相对路径 ./img/my-image.png 替换为了绝对路径 http://localhost:8080/img/my-image.png。这样,无论子应用在哪里运行,图片都可以正确地加载。
在使用 qiankun 时,你如何处理老项目的资源加载问题? 你能给出一些具体的解决方案吗?
1. 使用 qiankun 的 getTemplate 函数重写静态资源路径:对于 HTML 中已有的 img/audio/video 等标签,qiankun 支持重写 getTemplate 函数,可以将入口文件 index.html 中的静态资源路径替换掉。
例如:
start({ getTemplate(tpl,...rest) { // 为了直接看到效果,所以写死了,实际中需要用正则匹配 return tpl.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">'); } });
2. 劫持标签插入函数:对于动态插入的 img/audio/video 等标签,我们可以劫持 appendChild 、 innerHTML 、insertBefore 等事件,将资源的相对路径替换成绝对路径。例如,我们可以劫持 jQuery 的 html 方法,将图片的相对路径替换为绝对路径:
beforeMount: app => { if(app.name === 'my-app'){ $.prototype.html = function(value){ const str = value.replace('<img src="./img/my-image.png">', '<img src="http://localhost:8080/img/my-image.png">') this[0].innerHTML = str; } } }
3. 给老项目加上 webpack 打包
4. 使用 iframe 嵌入老项目
qiankun 的 start 函数的作用和参数吗?如果只有一个子项目,你会如何启用预加载?
start 函数是用来启动微前端应用的。在注册完所有的子应用之后,我们需要调用 start 函数来启动微前端应用。
参数
prefetch:预加载模式
sandbox:沙箱模式
singular:是否为单例模式
fetch:自定义的 fetch 方法,用于加载子应用的静态资源。
如果只有一个子项目,要想启用预加载,可以这样使用 start 函数: start({ prefetch: 'all' }); 这样,主应用 start 之后会预加载子应用的所有静态资源,无论子应用是否激活。
在使用 qiankun 时,你如何处理 js 沙箱不能解决的 js 污染问题?
qiankun 的 js 沙箱机制主要是通过代理 window 对象来实现的,它可以有效地隔离子应用的全局变量,防止子应用之间的全局变量污染。然而,这种机制并不能解决所有的 js 污染问题。例如,如果我们使用 onclick 或 addEventListener 给 添加了一个点击事件,js 沙箱并不能消除它的影响。
对于这种情况,我们需要依赖于良好的代码规范和开发者的自觉。在开发子应用时,我们需要避免直接操作全局对象,如 window 和 document。如果必须要操作,我们应该在子应用卸载时,清理掉这些全局事件和全局变量,以防止对其他子应用或主应用造成影响。 例如,如果我们在子应用中添加了一个全局的点击事件,我们可以在子应用的 unmount 生命周期函数中移除这个事件:
export async function mount(props) { // 添加全局点击事件 window.addEventListener('click', handleClick); } export async function unmount() { // 移除全局点击事件 window.removeEventListener('click', handleClick); } function handleClick() { // 处理点击事件 }
这样,当子应用卸载时,全局的点击事件也会被移除,不会影响到其他的子应用。
解释一下 qiankun 如何实现 keep-alive 的需求吗?
在 qiankun 中,实现 keep-alive 的需求有一定的挑战性。这是因为 qiankun 的设计理念是在子应用卸载时,将环境还原到子应用加载前的状态,以防止子应用对全局环境造成污染。这种设计理念与 keep-alive 的需求是相悖的,因为 keep-alive 需要保留子应用的状态,而不是在子应用卸载时将其状态清除。
然而,我们可以通过一些技巧来实现 keep-alive 的效果。一种可能的方法是在子应用的生命周期函数中保存和恢复子应用的状态。例如,我们可以在子应用的 unmount 函数中保存子应用的状态,然后在 mount 函数中恢复这个状态:
代码
// 伪代码 let savedState; export async function mount(props) { // 恢复子应用的状态 if (savedState) { restoreState(savedState); } } export async function unmount() { // 保存子应用的状态 savedState = saveState(); } function saveState() { // 保存子应用的状态 // 这个函数的实现取决于你的应用 } function restoreState(state) { // 恢复子应用的状态 // 这个函数的实现取决于你的应用 }
这种方法的缺点是需要手动保存和恢复子应用的状态,这可能会增加开发的复杂性。此外,这种方法也不能保留子应用的 DOM 状态,只能保留 JavaScript 的状态。
还有一种就是手动*loadMicroApp*+display:none,直接隐藏Dom
另一种可能的方法是使用 single-spa 的 Parcel 功能。Parcel 是 single-spa 的一个功能,它允许你在一个应用中挂载另一个应用,并且可以控制这个应用的生命周期。通过 Parcel,我们可以将子应用挂载到一个隐藏的 DOM 元素上,从而实现 keep-alive 的效果。然而,这种方法需要对 qiankun 的源码进行修改,因为 qiankun 目前并不支持 Parcel。
在使用 qiankun 时,你如何处理多个子项目的调试问题?
通常的方式是将每个子项目作为一个独立的应用进行开发和调试。每个子项目都可以在本地启动,并通过修改主应用的配置,让主应用去加载本地正在运行的子应用,这样就可以对子应用进行调试了。这种方式的好处是,子应用与主应用解耦,可以独立进行开发和调试,不会相互影响。
对于如何同时启动多个子应用,你可以使用npm-run-all这个工具。npm-run-all是一个CLI工具,可以并行或者串行执行多个npm脚本。这个工具对于同时启动多个子应用非常有用。
qiankun是如何实现CSS隔离的,该方案有什么缺点,还有其它方案么
通过使用Shadow DOM来实现CSS隔离。
1. Shadow DOM
是一种浏览器内置的Web标准技术,它可以创建一个封闭的DOM结构,这个DOM结构对外部是隔离的,包括其CSS样式。qiankun在挂载子应用时,会将子应用的HTML元素挂载到Shadow DOM上,从而实现CSS的隔离。
// qiankun使用Shadow DOM挂载子应用 const container = document.getElementById('container'); const shadowRoot = container.attachShadow({mode: 'open'}); shadowRoot.innerHTML = '<div id="subapp-container"></div>';
2. 使用CSS模块
CSS模块是一种将CSS类名局部化的方式,可以避免全局样式冲突。在使用CSS模块时,每个模块的类名都会被转换成一个唯一的名字,从而实现样式的隔离。
/* Button.module.css */ .button { background-color: blue; } 在你的JavaScript文件中,你可以这样引入并使用这个模块: import styles from './Button.module.css'; function Button() { return <button className={styles.button}>Click me</button>; } 在这个例子中,button类名会被转换成一个唯一的名字,如Button_button__xxx,这样就可以避免全局样式冲突了。
3.BEM命名规范隔离
qiankun中如何实现父子项目间的通信?
Actions 通信
qiankun 官方提供的通信方式,适合业务划分清晰,较简单的微前端应用。这种通信方式主要通过 setGlobalState 设置 globalState,并通过 onGlobalStateChange 和 offGlobalStateChange 来注册和取消 观察者 函数,从而实现通信。
在主项目中使用qiankun注册子项目时,如何解决子项目路由的hash与history模式之争?
1. 如果主项目使用 history 模式,并且子项目可以使用 history 或 hash 模式,这是 qiankun 推荐的一种形式。在这种情况下,子项目可以选择适合自己的路由模式,而且对于已有的子项目不需要做太多修改。但是子项目之间的跳转需要通过父项目的 router 对象或原生的 history 对象进行。
2. 如果主项目和所有子项目都采用 hash 模式,可以有两种做法:
使用 path 来区分子项目:这种方式不需要对子项目进行修改,但所有项目之间的跳转需要借助原生的 history 对象。
使用 hash 来区分子项目:这种方式可以通过自定义 activeRule 来实现,但需要对子项目进行一定的修改,将子项目的路由加上前缀。这样的话,项目之间的跳转可以直接使用各自的 router 对象或 。
3. 如果主项目采用 hash 模式,而子项目中有些采用 history 模式,这种情况下,子项目间的跳转只能借助原生的 history 对象,而不使用子项目自己的 router 对象。对于子项目,可以选择使用 path 或 hash 来区分不同的子项目。
qiankun中,如果实现组件在不同项目间的共享,有哪些解决方案?
1. 父子项目间的组件共享:主项目加载时,将组件挂载到全局对象(如window)上,在子项目中直接注册使用该组件。
2. 子项目间的组件共享(弱依赖):通过主项目提供的全局变量,子项目挂载到全局对象上。子项目中的共享组件可以使用异步组件来实现,在加载组件前先检查全局对象中是否存在,存在则复用,否则加载组件。
3. 子项目间的组件共享(强依赖):在主项目中通过loadMicroApp手动加载提供组件的子项目,确保先加载该子项目。在加载时,将组件挂载到全局对象上,并将loadMicroApp函数传递给子项目。子项目在需要使用共享组件的地方,手动加载提供组件的子项目,等待加载完成后即可获取组件。
需要注意的是,在使用异步组件或手动加载子项目时,可能会遇到样式加载的问题,可以尝试解决该问题。另外,如果共享的组件依赖全局插件(如store和i18n),需要进行特殊处理以确保插件的正确初始化。
前端架构设计
如何设计一个可扩展的前端架构来应对不断增长的业务需求?
采用微前端架构。
将大型前端应用拆分成多个小的、独立的子应用,每个子应用可以独立开发、部署和运行。例如使用single - spa框架来实现微前端。
遵循模块化和组件化的设计理念。
将功能封装成独立的模块和组件,方便复用和维护。可以使用Vue.js或React等框架的组件化特性,并且制定统一的组件规范。
建立分层架构
如将表示层(UI组件)、业务逻辑层和数据访问层分开。这样可以使得各层之间的职责更加清晰,便于修改和扩展。
在大型前端项目中,如何管理状态(state)以提高性能和可维护性?
对于局部状态,可以使用组件内部的状态管理(如Vue.js中的data属性或React中的state)。
对于全局状态,可以采用集中式状态管理工具,如Redux(适用于React项目)或Vuex(适用于Vue.js项目)。这些工具提供了统一的状态存储和管理机制,方便在不同组件之间共享状态,并且可以通过中间件等方式处理异步操作。
还可以使用状态管理模式的一些优化技巧,如状态的不可变性(在Redux中通过immutable.js或类似的库来实现),避免不必要的重新渲染;采用选择器(selector)来获取状态的部分内容,提高数据获取的效率。