面试题(持续更新)
1、HTML
1.1、对 HTML语义化的理解:
- 语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化)。
通俗来讲,语义化就是用正确的标签做正确的事,并且语义化便于搜索引擎解析 - 语义化的优点:
- **对机器友好,带有语义化的文字表现力丰富,更适合搜索引擎的爬虫爬取有效信息,有利于 **
SEO:搜索引擎优化,是指通过优化网站内容、结构和外部链接等因素,提升网站在搜索引擎中的排名,从而增加网站的流量和曝光率的技术和策略。SEO 的核心目标是让网站更容易被搜索引擎抓取和理解,从而在用户搜索相关关键词时获得更高的自然排名。除此之外,语义类还支持读屏软件,根据文章可以自动生成目录; - 对开发者友好,使用语义类标签增强了可读性,结构更加清晰,开发者能清晰的看出网页结构,便于团队开发与维护。
- 常见的语义化标签:
<header></header> // 头部 <nav></nav> //导航 <section></section> // 区块(有语义化的div) <main></main> // 主要区域 <article></article> // 主要内容 <aside></aside> // 侧边栏 <footer></footer> // 底部
- **对机器友好,带有语义化的文字表现力丰富,更适合搜索引擎的爬虫爬取有效信息,有利于 **
1.2、DOCTYPE(文档类型)的作用
DOCTYPE是 HTML5中一种标准通用标记语言的文档类型声明,他的目的是告诉浏览器(解析器)应该是什么样(html或xhtml)的文档类型定义来解析文档,不同的渲染模式会影响浏览器对 CSS代码甚至 JavaScript脚本的解析。他必须声明在 HTML文档的第一行。
**浏览器渲染页面的两种模式(可通过 **document.compatMode获取,比如:语雀官网的文档类型是CSS1Compat):
CSS1Compat:标准模式(Strick mode),默认模式,浏览器使用W3C的标准解析渲染页面。在标准模式中,浏览器以其支持的最高标准呈现页面BackCompat:怪异模式(混杂模式)(Quick mode),浏览器使用自己的怪异模式解析渲染页面。在怪异模式中,页面以一种比较宽松的向后兼容的方式显示。- 标准模式和怪异模式的区别:标准模式的页面排版和
JS运作模式都是浏览器支持的最高标准,而怪异模式是向后兼容,模拟老浏览器模式行为,防止页面无法正常工作
用一句话总结:DOCTYPE是作用于文档最顶部的文档声明,是告诉浏览器是以标准模式还是以怪异模式展示该页面。DOCTYPE不存在或格式错误都会导致页面以怪异模式展示页面。
1.3、script 标签中 defer 和 async 的区别
**如果没有 **defer 或 async属性,浏览器会立即加载并执行相应的脚本。它不会等待后面加载的文档元素,读取到就会开始加载和执行,这样就阻碍了后续文档的加载。
下图可以直观的看出三者的区别:

- **图中蓝色代表 **
js脚本的网络加载时间,红色代表js脚本的执行时间,绿色代表html解析。 defer和async属性都是去异步加载外部的js脚本文件,他们都不会阻碍页面的解析,其区别如下:- 执行顺序:多个带
async属性的标签,不能保证加载的顺序;多个带defer属性的标签,按照加载顺序执行; - 脚本是否并行执行:
async属性,表示后续文档的加载和执行与js脚本的加载和执行是并行执行的,即异步执行;defer属性加载后续文档的过程和js脚本的加载(此时仅加载不执行)是并行进行的(异步),js脚本需要等到文档所有元素解析完成之后才执行,DOMContentLoaded事件触发执行之前。
- 执行顺序:多个带
1.4、行内元素有哪些?块级元素有哪些?空(void)元素有哪些?
- 行内元素:
a b span img input select strong ...; - 块级元素:
div ul ol li dl dt dd h1-h6 p ... - 空元素,即没有内容的
HTML元素。空元素是在开始标签中关闭的,也就是空元素没有闭合标签:- 常见的有:
<br> <hr> <img> <input> <link> <meta>; - 罕见的有:
<area>、<base>、<col>、<colgroup>、<command>、<embed>、 <keygen>、<param>、<source>、<track>、<wbr> ...。
- 常见的有:
1.5、浏览器是如何对 HTML5的离线存储资源进行管理和加载?
**在线的情况下,浏览器发现 **html头部有 manifest属性,它会请求 manifest文件,如果是第一次访问页面,那么浏览器就会根据 manifest文件的内容下载相应的资源并且进行离线存储。如果已经访问过页面并且资源已经进行离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的 manifest文件与旧的 manifest文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,就会重新下载文件中的资源并进行离线存储。
离线的情况下,浏览器会直接使用离线存储的资源。
1.6、 Canvas和 SVG的区别
SVG:
SVG可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言XML描述的2D图形的语言,SVG基于XML就意味着SVG DOM中的每个元素都是可用的,可以为某个元素附加JavaScript事件处理器。在SVG中,每个被绘制的图形均被视为对象。如果SVG对象的属性发生变化,那么浏览器能够自动重现图形。其特点如下:- 不依赖分辨率;
- 支持事件处理器;
- 最适合带有大型渲染区域的应用程序(比如谷歌地图);
- **复杂度高会减慢渲染速度(任何过度使用 **
DOM的应用都不快); - 不适合游戏应用。
Canvas:
**Canvas是画布,通过JavaScript来绘制2D图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。**其特点如下:- 依赖分辨率;
- 不支持事件处理器;
- 弱的文本渲染能力;
- 能够以
.png或.jpg格式保存结果图像; - 最适合图像密集型的游戏,其中的许多对象会被频繁重绘。
- **注:**矢量图,也被称为面向对象的的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。
1.7、说一下 HTML5 drag API
dragstart:事件主体是被拖放元素,在开始拖放被拖放元素时触发。 darg:事件主体是被拖放元素,在正在拖放被拖放元素时触发。 dragenter:事件主体是目标元素,在被拖放元素进入某元素时触发。 dragover:事件主体是目标元素,在被拖放在某元素内移动时触发。 dragleave:事件主体是目标元素,在被拖放元素移出目标元素是触 发。 drop:事件主体是目标元素,在目标元素完全接受被拖放元素时触发。 dragend:事件主体是被拖放元素,在整个拖放操作结束时触发。
1.8、HTML5新特性有哪些?如何处理 HTML5新标签的兼容性问题?如何区分 HTML和 HTML5?
HTML5新特性:- 绘图方面:加入了
Canvas和SVG绘图; - 媒体方面:加入了
video和audio标签; - 语义化标签;
- 本地存储:
localStorage和sessionStorage两种本地离线缓存:localStorage:长期存储数据,关闭浏览器后数据不会丢失;sessionStorage:关闭浏览器后数据自动删除。
- 表单控件:
calendar、date、time、email、url、search; - 以及一些新技术:
webwoker / websocket / GelolCation
- 绘图方面:加入了
- 如何处理
HTML5新标签的兼容性问题:-
使用
document.createElement()方法: 在旧版浏览器中,HTML5新标签不能被正确解析或应用样式。可以通过JavaScript动态创建这些新标签,来让浏览器识别并应用样式:document.createElement('header'); document.createElement('footer'); document.createElement('section'); document.createElement('article'); // 为所有其他 HTML5 新标签创建元素这种方式适用于 IE 8 及以下浏览器。
-
使用
HTML5shiv库:HTML5shiv是一个JavaScript库,专门用来让旧版浏览器(特别是 IE 8 及以下)支持HTML5新标签。可以在页面的<head>部分引入:<!--[if lt IE 9]> <script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script> <![endif]--> -
使用 CSS 设置新标签的
display样式: 由于某些旧版浏览器默认不认识HTML5标签,它们会被当作inline元素处理,可以手动设置这些新标签为block,以确保页面结构正确:header, footer, section, article, aside, nav, figure, figcaption { display: block; }
-
- 如何区分
HTML和HTML5:Doctype声明:HTML5的Doctype声明比之前版本的更简洁:HTML5:<!DOCTYPE html>HTML之前的版本(如HTML4.01):<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
- 新标签:
HTML5引入了许多新语义标签,而这些标签在之前版本的HTML中不存在。例如:- 新的结构标签:
<header>,<footer>,<section>,<article>,<aside>,<nav> - 多媒体标签:
<audio>,<video> - 表单增强:
<datalist>,<output>,<progress>,<meter>
- 新的结构标签:
- 浏览器兼容性:
HTML5在语义化标签和功能支持上比HTML4具有更广泛的兼容性。现代浏览器支持HTML5的新特性,而HTML4不具备这些新的功能和标签。
1.9、cookies/sessionStorage/localStorage的区别
- **
cookies:是网站为了表示用户身份而储存在用户本地终端上的数据,cookies的数据始终在同源的http请求中携带,会在浏览器和服务器中来回传递,大小不能超过4kb(通常经过加密,所以不用担心账号被盗,同源策略~[同源是指"协议+域名+端口"三者相同]~,可以防止 **XSS和CSRF攻击浏览器,XSS就是通过浏览器 的cookies截取用户数据,CSRF是模拟用户在网页上的操作,完成数据请求,异步策略牵扯到了JSONP) - **
sessionStorage和localStory**的数据都是在本地存储,不会把数据发给服务器,localStorage是关闭浏览器,数据还存在不会丢失,而sessionStorage是离开浏览器后,数据会自动删除。
1.10、Iframe有哪些缺点?
Iframe会阻碍页面的onload事件;- 浏览器的搜索引擎一般读无法解读
Iframe页面,不利于SEO的搜索; Iframe和主页面共享链接池,会影响页面的并行加载;- 使用
js动态添加iframe的src属性,可以避免以上1、3问题。
2、CSS
2.1、display 的 block、inline 和 inline-block 的区别
- **
block:**会独占一行,多个元素会另起一行,可以设置 width、height、margin 和 padding 属性; - **
inline:**元素不会独占一行,设置width、height属性无效。但可以设置水平方向的margin和padding属性,不能设置垂直方向的padding和margin; - **
inline-block:**将对象设置为inline对象,但对象的内容作为block对象呈现,之后的内联对象会被排列在同一行内。对于行内元素和块级元素,其特点如下:- 行内元素:
- 设置宽高无效;
- **可以设置水平方向的 **
margin和padding属性,不能设置垂直方向的padding和margin; - 不会自动换行;
- 块级元素:
- 可以设置宽高;
- **设置 **
margin和padding都有效; - 可以自动换行;
- 多个块状,默认排列从上到下。
- 行内元素:
2.2、页面导入样式时,使用link和@import有什么区别?
link属于XHTML标签,除了加载CSS之外还能用于定义RSS等其他事务,@import是CSS提供的,只能用于加载CSS;link加载的文件,在页面加载的时候,link文件会同时加载,而@import引入的CSS文件,是页面在加载完成后再加载的;@import有兼容性问题,IE5以下的浏览器是无法识别的,而link无兼容性问题。link支持使用JavaScript控制DOM去改变样式;而@import不支持。
2.3、CSS3 中有哪些新特性
- 新增各种
CSS选择器::not(.input):所有class不是input的节点) - 圆角:
border-radius:8px - 多列布局:
multi-column layout - 阴影和反射:
Shadoweflect - 文字特效:
text-shadow - 文字渲染:
Text-decoration - 线性渐变:
gradient - 旋转:
transform - **增加:**旋转,缩放,定位,倾斜,动画,多背景
2.4、对 CSSSprites(精灵图) 的理解
CSSSprites(精灵图),将一个页面涉及到的所有图片都包含到一张大图中去,然后利用 CSS 的 background-image,background-repeat,background-position 属性的组合进行背景定位。
- 优点:
- **利用
CSSSprites能很好地减少网页的http请求,从而大大提高了 **页面的性能,这是CSSSprites最大的优点; CSSSprites能减少图片的字节,把 3 张图片合并成 1 张图片的字节** **总是小于这 3 张图片的字节总和。
- **利用
- 缺点:
- 在图片合并时,要把多张图片有序的、合理的合并成一张图片,还要留好足够的空间,防止板块内出现不必要的背景。在宽屏及高分辨率下的自适应页面,如果背景不够宽,很容易出现背景断裂;
CSSSprites在开发的时候相对来说有点麻烦,需要借助photoshop或其他工具来对每个背景单元测量其准确的位置。
- 维护方面:
CSSSprites在维护的时候比较麻烦,页面背景有少许改动时,就要改这张合并的图片,无需改的地方尽量不要动,这样避免改动更多的CSS,如果在原来的地方放不下,又只能(最好)往下加图片,这样图片的字节就增加了,还要改动CSS。
2.5、CSS 优化和提高性能的方法有哪些?
- 加载性能:
css压缩:将写好的css进行打包压缩,可以减小文件体积。- **
css单一样式:**当需要下边距和左边距的时候,很多时候会选择使用margin-top 0; margin-bottom 0;但margin-bottom:bottom;margin-left:left;执行效率会更高。 - 减少使用
@import:建议使用link,因为后者在页面加载时一起加载,前者是等待页面加载完成之后再进行加载。
- 选择器性能:
- 关键选择器(
key selector),选择器的最后面的部分为关键选择器(即用来匹配目标元素的部分)。CSS选择符是从右到左进行匹配的。当使用后代选择器的时候,浏览器会遍历所有子元素来确定是否是指定的元素等等; - **如果规则拥有 **
ID选择器作为其关键选择器,则不要为规则增加标签。过滤掉无关的规则(这样样式系统就不会浪费时间去匹配它们了)。 - 避免使用通配规则,如
*{}计算次数惊人,只对需要用到的元素进行选择。 - **尽量少的去对标签进行选择,而是用 **
class。 - 尽量少的去使用后代选择器,降低选择器的权重值。后代选择器的开销是最高的,尽量将选择器的深度降到最低,最高不要超过三层,更多的使用类来关联每一个标签元素。
- 了解哪些属性是可以通过继承而来的,然后避免对这些属性重复指定规则。
- 关键选择器(
- 渲染性能:
- 慎重使用高性能属性:浮动、定位。
- 尽量减少页面重排、重绘。
- 去除空规则:
{}。空规则的产生原因一般来说是为了预留样式。去除这些空规则无疑能减少css文档体积。 - 属性值为 0 时,不加单位。
- **属性值为浮动小数 **
0.**,可以省略小数点之前的0。 - 标准化各种浏览器前缀:带浏览器前缀的在前。标准属性在后。
- 不使用
@import前缀,它会影响css的加载速度。 - 选择器优化嵌套,尽量避免层级过深。
css雪碧图,同一页面相近部分的小图标,方便使用,减少页面的请求次数,但是同时图片本身会变大,使用时,优劣考虑清楚,再使用。- **正确使用 **
display的属性,由于display的作用,某些样式组合会无效,徒增样式体积的同时也影响解析性能。 - **不滥用 **
web字体。对于中文网站来说WebFonts可能很陌生,国外却很流行。WebFonts通常体积庞大,而且一些浏览器在下载WebFonts时会阻塞页面渲染损伤性能。
- 可维护性、健壮性:
- **将具有相同属性的样式抽离出来,整合并通过 **
class在页面中进行使用,提高css的可维护性。 - **样式与内容分离:将 **
css代码定义到外部css中。
- **将具有相同属性的样式抽离出来,整合并通过 **
2.6、对 CSS 工程化的理解
CSS工程化是为了解决以下问题:- 宏观设计:
CSS代码如何组织、如何拆分、模块结构怎样设计? - **编码优化:**怎样写出更好的
CSS? - **构建:**如何处理我的
CSS,才能让它的打包结果最优? - **可维护性:**代码写完了,如何最小化它后续的变更成本?如何确保
- 任何一个同事都能轻松接手?
- 宏观设计:
- 以下三个方向都是时下比较流行的、普适性非常好的
CSS工程化实践:-
预处理器:
Less、 Sass等;- 为什么要用预处理器?它的出现是为了解决什么问题?
==预处理器,其实就是CSS世界的“轮子”。预处理器支持我们写一种类似CSS、但实际并不是CSS的语言,然后把它编译成CSS代码:==

- 那为什么写
CSS代码写得好好的,偏偏要转去写“类 CSS”呢?这就和本来用JS也可以实现所有功能,但最后却写React的jsx或者Vue的模板语法一样——为了爽!要想知道有了预处理器有多爽,首先要知道的是传统CSS有多不爽。随着前端业务复杂度的提高,前端工程中对CSS提出了以下的诉求:- **宏观设计上:**我们希望能优化
CSS文件的目录结构,对现有的CSS文件实现复用; - **编码优化上:**我们希望能写出结构清晰、简明易懂的
CSS,需要它具有一目了然的嵌套层级关系,而不是无差别的一铺到底写法;我们希望它具有变量特征、计算能力、循环能力等等更强的可编程性,这样我们可以少写一些无用的代码; - **可维护性上:更强的可编程性意味着更优质的代码结构,实现复用 意味着更简单的目录结构和更强的拓展能力,这两点如果能做到,自 **然会带来更强的可维护性。
- 这三点是传统
CSS所做不到的,也正是预处理器所解决掉的问题。
- **宏观设计上:**我们希望能优化
- 预处理器普遍会具备这样的特性:
- **嵌套代码的能力,通过嵌套来反映不同 **
css属性之间的层级关系 ; - **支持定义 **
css变量; - 提供计算函数;
- **允许对代码片段进行 **
extend和mixin; - 支持循环语句的使用;
- **支持将 **
CSS文件模块化,实现复用。
- **嵌套代码的能力,通过嵌套来反映不同 **
- 为什么要用预处理器?它的出现是为了解决什么问题?
-
**重要的工程化插件: **
PostCss;PostCss是如何工作的?我们在什么场景下会使用PostCss?
PostCss仍然是一个对CSS进行解析和处理的工具,它会对CSS做这样的事情:

**它和预处理器的不同就在于,预处理器处理的是 **类 CSS,而PostCss处理的就是CSS本身。Babel可以将高版本的JS代码转换为低版本的JS代码。PostCss做的是类似的事情:它可以编译尚未被浏览器广泛支持的先进的CSS语法,还可以自动为一些需要额外兼容的语法增加前缀。更强的是,由于PostCss有着强大的插件机制,支持各种各样的扩展,极大地强化了CSS的能力。PostCss在业务中的使用场景非常多:- **提高 **
CSS代码的可读性:PostCss其实可以做类似预处理器能做的工作; - **当 我 们 的 **
CSS代 码 需 要 适 配 低 版 本 浏 览 器 时 ,PostCss的Autoprefixer插件可以帮助我们自动增加浏览器前缀; - **允许我们编写面向未来的 **
CSS:PostCss能够帮助我们编译CSS next代码;
- **提高 **
-
Webpack loader等 。Webpack能处理CSS吗:Webpack在裸奔的状态下,是不能处理CSS的,Webpack本身是一个面向JavaScript且只能处理JavaScript代码的模块化打包工具;、Webpack在loader的辅助下,是可以处理CSS的。
- 如何用
Webpack实现对CSS的处理:-
Webpack中操作CSS需要使用的两个关键的loader:css-loader和style-loadercss-loader:导入CSS模块,对CSS代码进行编译处理;style-loader:创建style标签,把CSS内容写入标签。
在实际使用中,
css-loader的执行顺序一定要安排在style-loader的前面。因为只有完成了编译过程,才可以对css代码进行插入;若提前插入了未编译的代码,那么webpack是无法理解这坨东西的,它会无情报错。
-
-
2.7、常见的 CSS 布局单位
常用的布局单位包括像素(px),百分比(%),em,rem,vw/vh。
- 像素(
px)是页面布局的基础,一个像素表示终端(电脑、手机、平板等)屏幕所能显示的最小的区域,像素分为两种类型:CSS像素和物理像素:CSS像素:为web开发者提供,在CSS中使用的一个抽象单位;- 物理像素:只与设备的硬件密度有关,任何设备的物理像素都是固定的。
- 百分比(
%),当浏览器的宽度或者高度发生变化时,通过百分比单位可以使得浏览器中的组件的宽和高随着浏览器的变化而变化,从而实现响应式的效果。一般认为子元素的百分比相对于直接父元素。 em和rem相对于px更具灵活性,它们都是相对长度单位,它们之间的区别:em相对于父元素,rem相对于根元素。em: 文本相对长度单位。相对于当前对象内文本的字体尺寸。如果当前行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(默认16px)。(相对父元素的字体大小倍数)。rem:rem是CSS3新增的一个相对单位,相对于根元素(html元素)的font-size的倍数。作用:利用rem可以实现简单的响应式布局,可以利用html元素中字体的大小与屏幕间的比值来设置font-size的值,以此实现当屏幕分辨率变化时让元素也随之变化。
vw/vh是与视图窗口有关的单位,vw表示相对于视图窗口的宽度,vh表示相对于视图窗口高度,除了vw和vh外,还有vmin和vmax两个相关的单位。- **
vw:**相对于视窗的宽度,视窗宽度是100vw; - **
vh:**相对于视窗的高度,视窗高度是100vh; vmin:vw和vh中的较小值;vmax:vw和vh中的较大值;vw/vh和百分比很类似,两者的区别:- 百分比(
%):大部分相对于祖先元素,也有相对于自身的情况比如(border-radius、translate等) vw/vm:相对于视窗的尺寸
- 百分比(
- **
2.8、水平垂直居中的实现
- **利用绝对定位,先将元素的左上角通过 **
top:50%和left:50%定位到页面的中心,然后再通过translate来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。

- 利用绝对定位,设置四个方向的值都为 0,然后使用
margin: auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况:

- **利用绝对定位,先将元素的左上角通过 **
top:50%和left:50%定位到页面的中心,然后再通过margin负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况

- **使 用 **
flex布 局 , 通 过align-items: center和justify-content:center设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要考虑兼容的问题,该方法在移动端用的较多:

2.9、如何解决 1px 问题?
1px 问题指的是:在一些 Retina 屏幕 的机型上,移动端页面的 1px会变得很粗,呈现出不止 1px 的效果。原因很简单——CSS 中的 1px并不能和移动设备上的 1px 划等号。它们之间的比例关系有一个专门的属性来描述:
window.devicePixelRatio= 设备的物理像素 /css像素
**打开 **Chrome 浏览器,启动移动端调试模式,在控制台去输出这个 devicePixelRatio 的值。这里选中 iPhone6/7/8 这系列的机型,输出的结果就是 2:
![]()
**这就意味着设置的 **1px CSS 像素,在这个设备上实际会用 2 个物理像素单元来进行渲染,所以实际看到的一定会比 1px 粗一些。
解决 1px 问题的三种思路:
-
直接写
0.5px
**如果之前 **1px的样式这样写:border: 1px solid #000
**可以先在 **JS中拿到window.devicePixelRatio的值,然后把这个值通过JSX或者模板语法给到CSS的data里,达到这样的效果这里用Vue语法做示范):<div id="container" :data-device={{ window.devicePixelRatio }}></div>**然后就可以在 **
CSS中用属性选择器来命中devicePixelRatio为某一值的情况,比如说这里尝试命中devicePixelRatio为2的情况:#container[data-device="2"]{ border: .5px solid #000 }直接把
1px改成1/devicePixelRatio后的值,这是目前为止最简单的一种方法。这种方法的缺陷在于兼容性不行,IOS系统需要IOS8及以上的版本,安卓系统则直接不兼容。 -
伪元素先放大后缩小
**这个方法的可行性会更高,兼容性也更好。唯一的缺点是代码会变多。思路是先放大、后缩小:在目标元素的后面追加一个 **::after伪元素,让这个元素布局为absolute之后、整个伸展开铺在目标元素上,然后把它的宽和高都设置为目标元素的两倍,设置样式border: 1px。接着借助CSS动画特效中的放缩能力,把整个伪元素缩小为原来的50%。此时,伪元素的宽高刚好可以和原有的目标元素对齐,而border也缩小为了1px的二分之一,间接地实现了0.5px的效果。#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 #000; }
2.10、Display和 Position有哪些哪些值?说明他们的作用
Display:- **
block:**元素转化为块级元素; - **
inline:**元素转化为行内元素; - **
inline-block:**元素转化为行内块元素; - **
none:**隐藏元素,脱离文档流 - **
List-item:**元素转化为行内样式,并添加列表样式(如li) - **
table:**元素会以块级表格来显示 - **
Inherit:**继承父元素display属性
- **
Position:- **
Relative:**相对定位(相对于原来位置定位,不脱离文档流); - **
Absolute:**绝对定位(相对于他最近的定位父元素定位,脱离文档流); - **
Fixed:**窗口定位(相对于浏览器窗口进行定位,脱离文档流); - **
Static:**默认值,不定位; - **
Inherit:**继承父元素的position属性;
- **
2.11、flex布局以及常用属性


2.12、CSS打造三角形?
宽度0,高度0,边框设置宽度,给某一条边加颜色,其余三边使用
transparent
3、JavaScript
javascript是运行在客户端的弱类型或者说动态类型的脚本语言,是嵌套在html中,能够被浏览器直接解析(与浏览器内核有关);可简称为js。
js作用:主用于写网页特效,表单验证,增加用户与浏览器之间的交互效果。
js组成:ECMAScript(组织,欧洲计算机制造商协会,用于规定js语法规范;ES5即是说ECMAScript的第五版)、BOM(浏览器对象模型)、DOM(文档对象模型)
3.1、JavaScript 有哪些数据类型?栈与堆的区别?
JavaScript 共有八种数据类型,分别是 Undefined、Null、Boolean、 Number、String、Object、Symbol、BigInt。
Symbol和BigInt是ES6中新增的数据类型:Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。BigInt是一种数字类型的数据,它可以表示任意精度格式的整数,使用BigInt可以安全地存储和操作大整数,即使这个数已经超出了Number能够表示的安全整数范围。
- 这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(
Undefined、Null、Boolean、Number、String) - 堆:引用数据类型(对象、数组和函数)
- 栈:原始数据类型(
- 两种类型的区别在于存储位置的不同:
- 原始数据类型直接存储在栈(
stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储; - 引用数据类型存储在堆(
heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:- 在数据结构中,栈中数据的存取方式为先进后出。
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。
- 原始数据类型直接存储在栈(
- 在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
3.2、数据类型检测的方式有哪些?
typeof:

其中数组、对象、null都会被判断为object,其他判断都正确。instanceof:
instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。

可以看到,instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。constructor:

constructor有两个作用,一是判断数据的类型,二是对象实例通过constrcutor对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

Object.prototype.toString.call():
Object.prototype.toString.call()使用Object对象的原型方法toString来判断数据类型:
**同样是检测对象 **
obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?**这是因为 **
toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
3.3、null 和 undefined 区别
**首先 **Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
**当对这两种类型使用 **typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
3.4、Object.is() 与比较操作符 “ ==”、“ ===” 的区别?
- 使用双等号(
==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。 - 使用三等号(
===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回false。 - **使用 **
Object.is来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如-0和+0不再相等,两个NaN是相等的。
3.5、如果 new 一个箭头函数的会怎么样
箭头函数是 ES6中的提出来的,它没有 prototype,也没有自己的 this指向,更不可以使用 arguments 参数,所以不能 New 一个箭头函数。** **new 操作符的实现步骤如下:
- 创建一个对象;
- 将构造函数的作用域赋给新对象(将对象的
__proto__属性指向构造函数的prototype属性),也就是设置原型链; - 让实例化对象中的
this指向对象,并执行函数体(也就是为这个对象添加属性和方法); - 返回新的对象。
3.6、什么是 DOM 和 BOM?
DOM指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。BOM指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM的核心是window,而window对象具有双重角色,它既是通过js访问浏览器窗口的一个接口,又是一个Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window对象含有location对象、navigator对象、screen对象等子对象,并且DOM的最根本的对象document对象也是BOM**的 **window对象的子对象。
3.7、for...in 和 for...of 的区别
for…of 是 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值,和 ES3 中的 for…in 的区别如下:
for…of遍历获取的是对象的键值,for…in获取的是对象的键名;for… in会遍历对象的整个原型链,性能非常差不推荐使用,而for … of只遍历当前对象不会遍历原型链;- 对于数组的遍历,
for…in会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of只返回数组的下标对应的属性值;const arr = [1,2,3] for(const key in arr){ console.log(key) // 0,1,2 } for(const item of arr){ console.log(item) // 1,2,3 }
总结:
for...in循环主要是为了遍历对象而生,不适用于遍历数组;for...of循环可以用来遍历数组、类数组对象,字符串、Set、Map、Generator对象。
3.8、ajax、axios、fetch 的区别
AJAX:
Ajax即“AsynchronousJavascriptAndXML”(异步JavaScript和XML),是指一种创建交互式网页应用的网页开发技术。它是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用Ajax)如果需要更新内容,必须重载整个网页页面。其缺点如下:- **本身是针对 **
MVC编程,不符合前端MVVM的浪潮; - **基于原生 **
XHR开发,XHR本身的架构不清晰 - 不符合关注分离(
Separation of Concerns)的原则 - 配置和调用方式非常混乱,而且基于事件的异步模型不友好。
- **本身是针对 **
Fetch:
fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch 是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。fetch的优点:- 语法简洁,更加语义化;
- **基于标准 **
Promise实现,支持async/await; - **更加底层,提供的 **
API丰富(request, response); - **脱离了 **
XHR,是ES规范里新的实现方式。
fetch的缺点:fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回400,500错误码时并不会reject,只有网络错误这些导致请求不能完成时,fetch才会被reject。fetch默 认 不 会 带cookie, 需 要 添 加 配 置 项 :fetch(url,{credentials: 'include'});fetch不 支 持abort, 不 支 持 超 时 控 制 , 使 用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费;fetch没有办法原生监测请求的进度,而XHR可以。
Axios:
Axios 是一种基于 Promise 封装的 HTTP 客户端,其特点如下:- **浏览器端发起 **
XMLHttpRequests请求; node端发起http请求;- **支持 **
Promise API; - 监听请求和返回;
- 对请求和返回进行转化;
- 取消请求;
- **自动转换 **
json数据; - **客户端支持抵御 **
XSRF攻击。
- **浏览器端发起 **
3.9、对原型、原型链的理解
- 构造函数:
构造函数和普通函数本质上没什么区别,只不过使用了new关键字创建对象的函数,被叫做了构造函数。构造函数的首字母一般是大写,用以区分普通函数,当然不大写也不会有什么错误。function Person(name, age) { this.name = name; this.age = age; this.species = '人类'; this.say = function () { console.log("Hello"); } } let per1 = new Person('xiaoming', 20); - 原型对象:
-
在
js中,每一个函数类型的数据,都有一个叫做prototype的属性,这个属性指向的是一个对象,就是所谓的原型对象。

-
对于原型对象来说,它有个
constructor属性,指向它的构造函数。

-
原型对象的作用就是用来存放实例对象的公有属性和公有方法。
在上面那个例子里species属性和say方法对于所有实例来说都一样,放在构造函数里,那每创建一个实例,就会重复创建一次相同的属性和方法,显得有些浪费。这时候,如果把这些公有的属性和方法放在原型对象里共享,就会好很多。function Person(name, age) { this.name = name; this.age = age; } Person.prototype.species = '人类'; Person.prototype.say = function () { console.log("Hello"); } let per1 = new Person('xiaoming', 20); let per2 = new Person('xiaohong', 19); console.log(per1.species); // 人类 console.log(per2.species); // 人类 per1.say(); // Hello per2.say(); // Hello可是这里的
species属性和say方法不是实例对象自己的,为什么可以直接用点运算符访问?这是因为在js中,对象如果在自己的这里找不到对应的属性或者方法,就会查看构造函数的原型对象,如果上面有这个属性或方法,就会返回属性值或调用方法。所以有时候,我们会用per1.constructor查看对象的构造函数:console.log(per1.constructor); // Person()这个
constructor是原型对象的属性,在这里能被实例对象使用,原因就是上面所说的。那如果原型对象上也没有找到想要的属性呢?这就要说到原型链了。
-
- 原型链:
-
显式原型:
显示原型就是利用prototype属性查找原型,只是这个是函数类型数据的属性。 -
隐式原型:
隐式原型是利用__proto__属性查找原型,这个属性指向当前对象的构造函数的原型对象,这个属性是对象类型数据的属性,所以可以在实例对象上面使用console.log(per1.__proto__ === Person.prototype); // true console.log(per2.__proto__ === Person.prototype); // true根据上面,就可以得出
constructor、prototype和__proto__之间的关系了:
-
原型链:
既然这个是对象类型的属性,而原型对象也是对象,那么原型对象就也有这个属性,但是原型对象的__proto__又是指向哪呢?
我们来分析一下,既然原型对象也是对象,那我们只要找到对象的构造函数就能知道****proto的指向了。而js中,对象的构造函数就是Object(),所以对象的原型对象,就是Object.prototype。既然原型对象也是对象,那原型对象的原型对象,就也是Object.prototype。不过Object.prototype这个比较特殊,它没有上一层的原型对象,或者说是它的__proto__指向的是null。
所以上面的关系图可以拓展成下面这种:

到这里,就可以回答前面那个问题了,如果某个对象查找属性,自己和原型对象上都没有,那就会继续往原型对象的原型对象上去找,这个例子里就是
Object.prototype,这里就是查找的终点站了,在这里找不到,就没有更上一层了(null里面啥也没有),直接返回undefined。可以看出,整个查找过程都是顺着
__proto__属性,一步一步往上查找,形成了像链条一样的结构,这个结构,就是原型链。所以,原型链也叫作隐式原型链。正是因为这个原因,我们在创建对象、数组、函数等等数据的时候,都自带一些属性和方法,这些属性和方法是在它们的原型上面保存着,所以它们自创建起就可以直接使用那些属性和方法。
-
3.10、对作用域、作用域链的理解
- 全局作用域和函数作用域:
- 全局作用域:
- 最外层函数和最外层函数外面定义的变量拥有全局作用域;
- 所有未定义直接赋值的变量自动声明为全局作用域;
- 所有 window 对象的属性拥有全局作用域;
- 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
- **函数作用域:**函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到。
作用域是分层的,内层作用域可以访问外层作用域,反之不行
- 全局作用域:
- 块级作用域:
**使用 **ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)let和const声明的变量不会有变量提升,也不可以重复声明在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
- 作用域链:
**在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到 **
window对象就被终止,这一层层的关系就是作用域链。
3.11、对 this 对象的理解
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断:
- **函数调用模式:**当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
- **方法调用模式:**如果一个函数作为一个对象的方法来调用时,
this指向这个对象。 - **构造器调用模式:**如果一个函数用
new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象。 - **
apply 、 call和bind调用模式:**这三个方法都可以显示的指定调用函数的this指向。- **
apply:**接收两个参数:一个是this绑定的对象,一个是参数数组; - **
call:**接收多个参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数,也就是说,在使用call()方法时,传递给函数的参数必须逐个列举出来。 - **
bind:**传入一个对象返回一个this绑定了传入对象的新函数。这个函数的this指向除了使用 new 时会被改变,其他情况下都不会改变。
- **
**这四种方式,使用构造器调用模式的优先级最高,然后是 **
apply、call和bind调用模式,其次是方法调用模式,最后是函数调用模式。
3.12、call、apply 以及 bind 的区别和用法
-
call和apply的共同点:
它们的共同点是,都能够****改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的。另外,它们的写法也很类似,调用 call 和 apply 的对象,必须是一个函数 Function。 -
call和apply的区别:
它们的区别,主要体现在参数的写法上-
call的写法:Function.call(obj,[param1[,param2[,…[,paramN]]]])需要注意以下几点:
- **调用 **
call的对象,必须是个函数Function。 call的第一个参数,是一个对象。Function的调用者,将会指向这个对象。如果不传,则默认为全局对象window。- **第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 **
Function的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到Function对应的第一个参数上,之后参数都为空。function func (a,b,c) {} func.call(obj, 1,2,3) // func 接收到的参数实际上是 1,2,3 func.call(obj, [1,2,3]) // func 接收到的参数实际上是 [1,2,3],undefined,undefined
- **调用 **
-
apply的写法:// 数组/类数组(伪数组) Function.apply(obj, argArray);需要注意以下几点:
- **它的调用者必须是函数 **
Function,并且只接收两个参数,第一个参数的规则与call一致。 - **第二个参数,必须是数组或者类数组(伪数组),它们会被转换成类数组(伪数组),传入 **
Function中,并且会被映射到Function对应的参数上。这也是call和apply之间,很重要的一个区别。function func (a,b,c) {} func.apply(obj, [1,2,3]) // func 接收到的参数实际上是 1,2,3 func.apply(obj, { 0: 1, 1: 2, 2: 3, length: 3 }) // func 接收到的参数实际上是 1,2,3
- **它的调用者必须是函数 **
-
-
bind的使用:
bind()方法创建一个新的函数,在调用时设置this关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
它的语法如下:Function.bind(thisArg[, arg1[, arg2[, ...]]])bind方法 与apply和call比较类似,也能改变函数体内的this指向。不同的是,bind方法的返回值是函数,并且需要稍后调用,才会执行。而apply和call则是立即调用。**如果 **
bind的第一个参数是null或者undefined,this就指向全局对象window -
总结:
call和apply的主要作用,是改变对象的执行上下文,并且是立即执行的。它们在参数上的写法略有区别。
bind也能改变对象的执行上下文,它与call和apply不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。
3.13、什么是类数组(伪数组)
**先说数组,这我们都熟悉。它的特征有:可以通过角标调用,如 **array[0];具有长度属性 length;可以通过 for 循环或 forEach方法,进行遍历。
那么,类数组(伪数组)是什么呢?顾名思义,就是****具备与数组特征类似的对象。比如,下面的这个对象,就是一个类数组(伪数组)。
let arrayLike = {
0: 1,
1: 2,
2: 3,
length: 3
};
**类数组(伪数组) **arrayLike 可以通过角标进行调用,具有 length属性,同时也可以通过 for 循环进行遍历。
**类数组(伪数组),还是比较常用的,只是我们平时可能没注意到。比如,我们获取 **DOM 节点的方法,返回的就是一个类数组(伪数组)。再比如,在一个方法中使用 arguments 获取到的所有参数,也是一个类数组(伪数组)。
**但是需要注意的是:**类数组(伪数组)无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。
3.14、哪些情况会导致内存泄漏
- **意外的全局变量:**由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- **被遗忘的计时器或回调函数:**设置了
setInterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 - **脱离 DOM 的引用:**获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- **闭包:**不合理的使用闭包,从而导致某些变量一直被留在内存当中。
3.15、继承的方法有哪些?
原型链继承、构造继承、实例继承、拷贝继承、组合继承、寄生组合继承
定义一个父类:
//先定义一个父类
function Animal(name){
//属性
this.name = name || 'Animal';
//实例方法
this.sleep = function(){
console.log(this.name + "正在睡觉!")
}
}
//原型方法
Animal.prototype.eat = function(food){
console.log(this.name + "正在吃" + food);
}
-
原型链继承:
- **核心:**将父类的实例作为子类的原型
/原型链继承 function Cat(){ } Cat.prototype = new Animal(); Cat.prototype.name = "cat"; //Test Code var cat = new Cat(); console.log(cat.name); //cat console.log(cat.eat("fish")); //cat正在吃fish console.log(cat.sleep()); //cat正在睡觉 console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //true - 特点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例;
- 父类新增原型方法、原型属性,子类都能够访问到;
- 简单,易于实现。
- 缺点:
- 要想实现子类新增属性的方法,必须要在
new Animal( )这样的语句之后执行,不能放在构造器中; - 无法实现多继承;
- 来自原型对象的引用属性是所有实例共享的;
- 创建子类实例时, 无法向父类构造函数传参。
- 要想实现子类新增属性的方法,必须要在
- **核心:**将父类的实例作为子类的原型
-
构造函数:
- **核心:**使用父类的构造函数来增强子类实例,等于是赋值父类的实例属性给子类(没用的原型)
//构造函数 function Cat(name){ Animal.call(this); this.name = name || "Tom" } //Test Code var cat = new Cat(); console.log(cat.name); //Tom console.log(cat.sleep()); //Tom正在睡觉 console.log(cat instanceof Animal); //false console.log(cat instanceof Cat); //true - 特点:
- 解决了1中,子类实例共享父类引用属性的问题;
- 创建子类实例时,可以向父类传递参数;
- 可以实现多继承(call多个父类对象)。
- **核心:**使用父类的构造函数来增强子类实例,等于是赋值父类的实例属性给子类(没用的原型)
-
实例继承:
- 核心: 为父类实例添加新特性,作为子类实例返回
//实例继承 function Cat(name){ var instance = new Animal(); instance.name = name || "Tom"; return instance; } //Test Code var cat = new Cat(); console.log(cat.name); //Tom console.log(cat.sleep()); //Tom正在睡觉! console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //false - **特点:**不限制调用方法,不管是
new子类()还是调用子类,返回的对象具有相同的效果 - 缺点:
- 实例是父类的实例,,不是子类的实例;
- 不支持多继承。
- 核心: 为父类实例添加新特性,作为子类实例返回
-
拷贝继承:
//拷贝继承 function Cat(name){ var animal = new Animal(); for(var p in animal){ Cat.prototype[p] = animal[p]; } Cat.prototype.name = name || "Tom" } //Test Code var cat = new Cat(); console.log(cat.name); //Tom console.log(cat.sleep()); //Tom正在睡觉! console.log(cat instanceof Animal); //false console.log(cat instanceof Cat); //true- **特点:**支持多继承
- 缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性)
- 无法获取父类不可枚举的方法(不可枚举方法,不能使用
for in访问到)
-
组合继承:
- **核心:**通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
//组合继承 function Cat(name){ Animal.call(this); this.name = name || "Tom"; } Cat.prototype = new Animal(); //组合继承也需要修复构造函数的指向问题 Cat.prototype.constructor = Cat; //Test Code var cat = new Cat(); console.log(cat.name); //Tom console.log(cat.sleep()); //Tom正在睡觉! console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //true - 特点:
- 弥补了方法2的缺陷,可以继承实例属性、方法,也可以继承原型属性和方法;
- 既是子类的实例,也是父类的实例;
- 不存在引用属性共享的问题;
- 可传参;
- 函数可复用。
- **缺点:**调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
- **核心:**通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
-
寄生组合继承:
- **核心:**通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法、属性,避免了组合继承的缺点
//寄生组合继承 function Cat(name){ Animal.call(this); this.name = name || "Tom" } (function(){ //创建一个没有实例方法的类 var Super = function(){}; Super.prototype = Animal.prototype; //将实例作为子类的原型 Cat.prototype = new Super(); })(); //Test Code var cat = new Cat(); console.log(cat.name); //Tom console.log(cat.sleep()); //Tom正在睡觉! console.log(cat instanceof Animal); //true console.log(cat instanceof Cat); //true //该实现没有修复constructoe - **特点:**堪称完美
- 缺点: 实现较为复杂
- **核心:**通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法、属性,避免了组合继承的缺点
3.16、什么是闭包?闭包有什么作用?
由于在
js中,变量到的作用域属于函数作用域,在函数执行后作用域会被清除、内存也会随之被回收,但是由于闭包是建立在一个函数内部的子函数,由于其可访问上级作用域的原因,即使上级函数执行完,作用域也不会随之销毁,这时的子函数---也就是闭包,便拥有了访问上级作用域中的变量权限,即使上级函数执行完后,作用域内的值也不会被销毁。
闭包解决了什么:在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
由于闭包可以缓存上级作用域,那么就使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量。以平时使用的
Ajax成功回调为例,这里其实就是个闭包,由于上述的特性,回调就拥有了整个上级作用域的访问和操作能力,提高了几大的便利。开发者不用去写钩子函数来操作审计函数作用域内部的变量了。
闭包有哪些应用场景:
闭包随处可见,一个 Ajax请求的成功回调,一个事件绑定的回调函数,一个 setTimeout的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。简而言之,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都有闭包的身影.
闭包的缺陷:由于闭包打破了函数作用域的束缚,导致里面的数据无法清除销毁,当数据过大时会导致数据溢出
3.17、深拷贝和浅拷贝
- 深拷贝和浅拷贝值针对
Object和Array这样的引用类型 a和b指向了同一块内存,所以修改其中任意一个值,另外一个值也会随之变化,这是浅拷贝a和b指向同一块内存,但是修改其中任意一个值,另外一个调用的变量,不会受到影响,这是深拷贝- 浅拷贝:“
Object.assign()”方法用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象,它将返回目标对象 - 深拷贝:
JSON.parse( )和JSON.stringify( )给了我们一个基本的解决办法。但是函数不能被正确处理
3.18、显示转换与隐式转换
JS中有5种简单的数据类型(也称之为基本数据类型):undefined、Null、Boolean、Number、String。还有一种复杂的数据(也称之为引用数据类型)--------Object,Object本质是一组无序的名值对组成的。
对一个值使用 typeof操作符可以返回该值的数据类型,typeof操作符会返回一些令人迷惑但技术上却正确的值,比如调用 typeof null会返回“object”,应为特殊值 null被认为是一个空的对象引用。
- **显式转换:**主要通过
JS定义的数据转换方法
各种数据类型及其对应的转化规则:| 数据类型 | 转换为true的值 | 转换为false的值 |
| ------------------ | ------------------------------------ | ------------------------- |
|Boolean|true|false|
|String| 任何非空字符串 |“”(空字符串) |
|Number| 任何非零数字值(包括无穷大) |0和NaN|
|Object| 任何对象 |null|
|Underfined|n/a|undefined| - **隐式转换:**是系统默认的,不需要加以声明就可以进行的转换。一般情况下,数据的类型转换通常是由编译系统自动进行的,不需要人工干预
大致规则如下:- **对象和布尔值比较:**对象和布尔值比较时,对象先转换为字符串,然后再转换为数字,布尔值直接转换为数字;
- **对象和字符串比较:**对象和字符串进行比较时,对象转换为字符串,然后两者进行比较;
- **对象和数字比较:**对象和数字进行比较时,字符串转换为数字,二者再比较
- **字符串和数字比较:**字符串和数字进行比较时,字符串转换成数字,二者再比较,
true=1,false=0 - **字符串和布尔值比较:**字符串和布尔值进行比较时,二者全部转换成数值再比较
- **布尔值和数字比较:**布尔值和数字进行比较时,布尔转换为数字,二者比较
3.19、== 与 === 的区别?
==为等值符,用来判断值是否相同,不会判断类型是否相同===为等同符,当左边与右边的值与类型都完全相等时,会返回true;
3.20、ES6新特性
常用的ES6特性:
- let、const
- 箭头函数
- 类的支持
- 字符串模块
- symbols
- Promises
- 参数默认值
- 解构赋值
- for of
4、性能优化
4.1、懒加载
-
懒加载的概念:
懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。
如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。
-
懒加载的特点:
- 减少无用资源的加载:使用懒加载明显减少了服务器的压力和流量,同时也减小了浏览器的负担。
- 提升用户体验: 如果同时加载较多图片,可能需要等待的时间较长,这样影响了用户体验,而使用懒加载就能大大的提高用户体验。
- 防止加载过多图片而影响其他资源文件的加载 ,会影响网站应用的正常使用。
-
懒加载的实现原理:
**图片的加载是由 **
src引起的,当对src赋值时,浏览器就会请求图片资源。根据这个原理,我们使用HTML5的data-xxx(自定义属性)来储存图片的路径,在需要加载图片的时候,将data-xxx中图片的路径赋值给src,这样就实现了图片的按需加载,即懒加载。注意:
data-xxx中的xxx可以自定义,这里我们使用data-src来定义。懒加载的实现重点在于确定用户需要加载哪张图片,在浏览器中,可视区域内的资源就是用户需要的资源。所以当图片出现在可视区域时,获取图片的真实地址并赋值给图片即可。
使用原生 JavaScript 实现懒加载:- 知识点:
- **
window.innerHeight:**是浏览器可视区的高度; - **
document.body.scrollTop || document.documentElement.scrollTop:**是浏览器滚动的过的距离; - **
imgs.offsetTop:**是元素顶部距离文档顶部的高度(包括滚动条的距离) - 图片加载条件 :
img.offsetTop<window.innerHeight+document.body.scrollTop;
- **
- 图示:

- 代码实现:

- 知识点:
4.2、回流(重排)与重绘的概念及触发条件
-
回流(重排):
当渲染树中部分或者全部元素的尺寸、结构或者属性发生变化时,浏览器会重新渲染部分或者全部文档的过程就称为回流(重排)。
在触发回流(重排)的时候,由于浏览器渲染页面是基于流式布局的,所以当触发回流时,会导致周围的 DOM 元素重新排列,它的影响范围有两种:
- **全局范围:**从根节点开始,对整个渲染树进行重新布局;
- **局部范围:**对渲染树的某部分或者一个渲染对象进行重新布局。
下面这些操作会导致回流:
- 页面的首次渲染;
- 浏览器的窗口大小发生变化;
- 元素的内容发生变化;
- 元素的尺寸或者位置发生变化;
- 元素的字体大小发生变化;
- **激活 **
CSS伪类; - 查询某些属性或者调用某些方法;
- 添加或者删除可见的 DOM 元素。
-
重绘:
当页面中某些元素的样式发生变化,但是不会影响其在文档流中的位置时,浏览器就会对元素进行重新绘制,这个过程就是重绘。
下面这些操作会导致重绘:
color、background相关属性:background-color、background-image等outline相关属性:outline-color 、 outline-width 、text-decoration、border-radius、visibility、box-shadow
**注意: ****==当触发回流(重排)时,一定会触发重绘,但是重绘不一定会引发回流。==**
4.3、如何避免回流与重绘?
减少回流与重绘的措施:
- 尽量在低层级的 DOM 节点进行操作:操作 DOM 时,选择较低层级的节点进行修改,避免影响大量的元素,减少回流和重绘的范围。
- 避免使用 table 布局:
table布局在页面布局发生变化时,可能会引发整个表格的重新布局,导致性能开销增加。建议使用div等替代方案。 - 不要使用 CSS 表达式:CSS 表达式(如
calc())在某些情况下会频繁重新计算,影响性能。 - 避免频繁操作样式:如果需要对元素样式进行频繁的修改,最好通过修改
class来进行批量样式更新,而不是直接修改单个样式属性。 - 使用
absolute或fixed定位:将元素设置为absolute或fixed定位,使其脱离文档流,避免其变化影响其他元素,减少不必要的回流和重绘。 - 使用
documentFragment批量操作 DOM:避免频繁操作 DOM,可以使用documentFragment来集中操作 DOM,最后一次性将其添加到页面,减少回流和重绘。 - 隐藏元素再操作:将要操作的元素先设置为
display: none,操作完成后再显示。display: none状态下的元素进行 DOM 操作不会触发回流和重绘,性能更高。 - 将多个读操作或写操作合并在一起:将 DOM 的多个读操作(如获取
offsetWidth)或者写操作(如修改style)放在一起,减少浏览器频繁交替读写操作的回流与重绘。 - 利用浏览器的渲染队列优化:浏览器会将多次的回流和重绘操作放入渲染队列中,当达到一定数量或时间时批量执行。通过合理安排操作顺序,可以充分利用渲染队列的优化机制,减少多次回流重绘的发生。
4.4、如何优化动画?
**一般情况下,动画需要频繁的操作DOM,就就会导致页面的性能问题,可以将动画的 position 属性 **设置为 absolute 或者 fixed,将动画脱离文档流,这样他的回流就不会影响到页面了。
4.5、documentFragment 是什么?用它跟直接操作 DOM 的区别是什么?
DocumentFragment(文档片段接口),一个没有父对象的最小文档对象。它被作为一个轻量版的Document使用,就像标准的document一样,存储由节点(nodes)组成的文档结构。与document相比,最大的区别是DocumentFragment不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染,且不会导致性能等问题。**当我们把一个 **
DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点。在频繁的 DOM 操作时,我们就可以将 DOM 元素插入DocumentFragment,之后一次性的将所有的子孙节点插入文档中。和直接操作 DOM 相比,将DocumentFragment节点插入 DOM 树时,不会触发页面的重绘,这样就大大提高了页面的性能。
4.6、 对节流与防抖的理解
防抖就像回城,打断就得重来;节流就像技能冷却,间隔指定时间才能再次使用
- 防抖:
- **概念:**函数防抖是指在事件被触发
n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。 - 应用场景:
- **按钮提交场景:**防⽌多次提交按钮,只执⾏最后提交的⼀次;
- **服务端验证场景:**表单验证需要服务端配合,只执⾏⼀段连续的输⼊事 件 的 最 后 ⼀ 次 , 还 有 搜 索 联 想 词 功 能 类 似 ⽣ 存 环 境 请 ⽤
lodash.debounce。
- 代码实现:
function debounce(fn, wait) { var timer = null; return function() { var context = this, args = [...arguments]; // 如果此时存在定时器的话,则取消之前的定时器重新计时 if (timer) { clearTimeout(timer); timer = null; } // 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); } }
- **概念:**函数防抖是指在事件被触发
- 节流:
- **概念:**函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在
scroll函数的事件监听上,通过事件节流来降低事件调用的频率。 - 应用场景:
- **拖拽场景:**固定时间内只执⾏⼀次,防⽌超⾼频次触发位置变动;
- **缩放场景:**监控浏览器
resize; - **动画场景:**避免短时间内多次触发动画引起性能问题。
- 代码实现:
- 定时器版节流:
function throttle(fun, wait) { let timeout = null; return function() { let context = this; let args = [...arguments]; if (!timeout) { timeout = setTimeout(() => { fun.apply(context, args); timeout = null; }, wait); } } } - 时间戳版节流:
function throttle(fn, delay) { var preTime = Date.now(); return function() { var context = this, args = [...arguments], nowTime = Date.now(); // 如果两次时间间隔超过了指定时间,则执行函数 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } } }
- 定时器版节流:
- **概念:**函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在
4.7、如何对项目中的图片进行优化?
对项目中的图片进行优化可以有效提升页面的加载速度和性能。以下是具体的优化措施:
- 避免使用不必要的图片:对于装饰性的图片,尽量使用
CSS代替。许多修饰类效果可以通过CSS实现,比如渐变、阴影等,减少对图片资源的依赖。 - 使用适应屏幕大小的图片:尤其是在移动端,由于屏幕尺寸有限,加载原始大图只会浪费带宽。通过
CDN服务器提供自适应屏幕的图片,按需加载适合屏幕宽度的裁剪图,从而减少加载时间。 - 小图片使用
Base64格式:将小图(如图标)转为Base64格式并嵌入到CSS文件或HTML中,可以减少HTTP请求次数。不过,Base64编码的图片较大时会增加页面体积,适用于体积很小的图片。 - 使用雪碧图(
CSS Sprites):将多个小图标整合到一张大图中,通过background-position来显示不同的图标,减少HTTP请求次数,从而提高页面性能。 - 选择合适的图片格式:
WebP:在支持WebP格式的浏览器中,优先使用WebP格式。WebP比传统的JPEG和PNG格式压缩率更高,能在保证图像质量的情况下大幅减少图片体积。PNG:适用于小图标或透明背景的图片,具有较高的质量。SVG:如果是矢量图,尽量使用SVG格式。SVG体积小且可无限缩放,不会出现失真,尤其适合图标、简单的形状和图案。JPEG:适用于压缩后的照片和大图,可以在质量与文件大小之间取得平衡。
4.8、常见的图片格式及使用场景
关于不同图片格式的特点和适用场景总结如下:
- BMP(位图图像格式):
- 特点:无损压缩,支持索引色和直接色的点阵图。
- 优点:图像质量高。
- 缺点:由于几乎没有压缩,文件体积较大。
- 适用场景:适合对图像质量要求极高的场景,但由于体积较大,较少用于Web环境。
- GIF(图形交换格式):
- 特点:无损压缩,采用索引色,使用LZW算法。
- 优点:文件体积小,支持动画和透明。
- 缺点:仅支持8位索引色,色彩表现有限。
- 适用场景:适合简单图像、动画、低色彩需求的场景,不适合复杂图像和照片。
- JPEG(联合图像专家组格式):
- 特点:有损压缩,采用直接色。
- 优点:色彩丰富,适合照片类图片,体积相对较小。
- 缺点:有损压缩导致细节损失,不适合需要高精度的图像如Logo、线图等。
- 适用场景:非常适合照片和色彩丰富的图像,不适合需要高精度细节的图像。
- PNG-8:
- 特点:无损压缩,使用索引色。
- 优点:支持透明度,文件体积较小,是GIF的替代方案。
- 缺点:支持的颜色数量有限(256色)。
- 适用场景:适用于需要透明效果的小图标和简单图像,尤其适合替代GIF。
- PNG-24:
- 特点:无损压缩,使用直接色。
- 优点:文件质量高,支持透明度。
- 缺点:文件体积较大,尤其是与JPEG相比。
- 适用场景:适合需要高质量图像且对体积要求不高的场景。
- SVG(可缩放矢量图形格式):
- 特点:无损压缩,矢量图(矢量图并不是由像素点组成的,而是通过线条、曲线、形状等几何元素以及它们的绘制方式来表示)。
- 优点:可无限放大而不失真,非常适合Logo和Icon设计。
- 缺点:不适合复杂的照片类图像。
- 适用场景:用于Logo、图标等需要高可缩放性的图像。
- WebP:
- 特点:谷歌开发的新格式,支持有损和无损压缩,使用直接色。
- 优点:在相同质量下,文件体积比PNG和JPEG小得多,支持透明度。
- 缺点:兼容性有限,主要支持Chrome和Opera浏览器。
- 适用场景:非常适合Web使用,尤其在对传输效率有要求的场景。
4.9、如何⽤webpack 来优化前端性能?
使用 Webpack 优化前端性能的目的是为了让打包后的最终结果在浏览器中运行更快、更高效。以下是几种常用的 Webpack 优化技术:
- 压缩代码:
- 目标:减少文件体积,加快浏览器的加载和解析速度。
- 实现方法:
JS文件压缩:使用UglifyJsPlugin或ParallelUglifyPlugin来压缩和混淆JavaScript文件,删除多余代码和注释。CSS文件压缩:使用cssnano或者在css-loader中添加minimize参数来压缩CSS文件,减少样式文件体积。
- 使用
CDN加速:- 目标:通过
CDN(内容分发网络)加速静态资源的加载速度。 - **实现方法:**在
Webpack构建时,可以将静态资源的路径指向CDN,通过设置Webpack的output参数和各个loader的publicPath参数,将资源路径调整为CDN地址,这样可以利用CDN的地理优势加快文件下载速度。
- 目标:通过
Tree Shaking:- 目标:删除代码中永远不会执行的部分,减小打包体积。
- 实现方法:
Webpack内置支持Tree Shaking功能。通过启动Webpack时追加--optimize-minimize参数,或者在production模式下打包时自动启用,可以删除无用代码(仅适用于ES6模块导出语法)。
Code Splitting(代码分割):- 目标:将代码按需加载,提高页面初次加载的速度。
- **实现方法:**使用
Webpack的 动态导入 或者按路由拆分代码,将不同页面或者组件的代码拆成独立的文件(chunk),这样可以做到按需加载,而不是将所有代码一次性加载。可以使用Webpack的import()语法,或者借助插件进行代码分割。
- 提取公共第三方库:
- 目标:减少重复加载相同的库,利用浏览器缓存提高性能。
- 实现方法:
Webpack提供了SplitChunksPlugin插件,可以用于将项目中公共的依赖库(如react,lodash等)单独打包成一个文件,避免每次变动都重新加载这些不常变化的代码。浏览器可以对这些公共代码进行长期缓存,从而减少重复加载,提升加载速度。
Lazy Loading(懒加载):- 目标:在用户真正需要某个资源时才去加载该资源。
- **实现方法:**通过
Webpack实现懒加载可以使用import()动态导入模块。这样可以将不需要立即执行的代码延迟加载,减少初次加载的压力。
- 图片和资源文件优化:
- 目标:减少图片和资源文件体积,加快页面加载速度。
- 实现方法:
- **使用 **
Webpack的file-loader或url-loader对图片进行优化,尽量使用WebP等更高压缩率的格式,或者将小图片内联成Base64格式。 - **使用 **
image-webpack-loader进一步压缩图片体积。
- **使用 **
- 开发模式和生产模式分离:
- 目标:在开发和生产环境中使用不同的配置,优化各自的性能。
- **实现方法:**在
Webpack配置中,设置mode为development或production,生产模式下会自动进行代码压缩、Tree Shaking等优化,而开发模式下则保留详细的源代码映射、错误提示等方便调试。
4.10、如何提⾼webpack 的构建速度?
- 多入口情况下,使用
CommonsChunkPlugin来提取公共代码- 目标:减少重复的代码,优化加载速度。
- 实现方法:
CommonsChunkPlugin是Webpack 3.x及之前版本用来提取公共代码的插件,可以将多个入口文件中的公共部分提取出来,减少重复加载。然而在Webpack 4.x及之后的版本中,该插件被废弃,取而代之的是内置的SplitChunksPlugin。通过SplitChunksPlugin,可以更灵活地控制公共代码提取,提升性能。
- 通过
externals配置来提取常用库- 目标:避免将一些大而不变的库(如
jQuery、React等)打包进最终的bundle文件,减小打包体积。 - **实现方法:**使用
externals配置,告诉Webpack不要将这些库打包,而是在运行时通过外部CDN或其他方式加载这些库。这样可以减少项目的bundle大小,从而加快页面加载速度。
- 目标:避免将一些大而不变的库(如
- 利用
DllPlugin和DllReferencePlugin预编译资源模块- 目标:加快项目的构建速度,尤其是在频繁开发和调试的场景下。
- 实现方法:
DllPlugin可以将项目中的一些不会经常变动的第三方库(如React、Lodash等)单独打包为一个静态的动态链接库(DLL),下次构建时不会重新打包这些库。DllReferencePlugin则用来在构建时引入这些预编译的模块,从而加快编译速度。对于频繁变动的业务代码,则只需要重新编译,避免了重复打包第三方库。
- 使用
Happypack实现多线程加速编译- 目标:加快代码的构建速度,特别是在大型项目中。
- 实现方法:
Happypack可以将Webpack的加载器工作分布到多个线程中并行执行。因为JavaScript是单线程运行的,通常在构建大型项目时可能会变得缓慢,而Happypack能充分利用多核CPU来加速构建。不过目前Webpack 5中已经内置了类似功能,可以通过thread-loader来实现多线程编译,因此Happypack不再是必需。
- 使用
webpack-uglify-parallel来提升uglifyPlugin的压缩速度- 目标:提升代码压缩速度,尤其是针对大项目的情况。
- 实现方法:
webpack-uglify-parallel是一个利用多核CPU并行压缩JavaScript文件的插件,极大提高了压缩的速度。原理上,它通过将UglifyJSPlugin压缩任务分配到多个CPU核心上同时运行,来提升压缩效率。Webpack 5中的TerserPlugin也已经内置了多线程支持,因此不再需要依赖这个插件。
- 使用 Tree Shaking 和 Scope Hoisting 剔除多余代码
Tree Shaking:- 目标:通过剔除无用代码,减小打包体积。
- 实现方法:
Tree Shaking是Webpack内置的功能,通过分析ES6的模块依赖关系,将那些永远不会被使用的代码移除。使用 ES6 的import和export语法,可以确保Tree Shaking能够正常工作。
Scope Hoisting:- 目标:减少函数声明和闭包,提升执行性能。
- 实现方法:
Scope Hoisting是Webpack的优化技术,能够将各个模块的函数合并到一个作用域内,从而减少函数声明带来的性能开销。开启optimization.concatenateModules选项可以实现这个功能。
5、前端工程化
5.1、webpack 与 grunt、gulp 的不同?
Grunt、Gulp:基于任务运行的工具,使用任务链处理文件。这些工具主要通过插件链式调用来完成文件操作,如压缩、转码等任务。这种方式更像是流水线,针对每个任务操作文件。
Webpack:基于模块化的打包工具,注重模块依赖,构建依赖图,将模块按依赖关系打包。Webpack 的独特之处在于它把所有资源文件(JS、CSS、图片等)都视为模块,适用于复杂的前端项目。
区别:Grunt、Gulp 是面向任务的,Webpack 是面向模块化构建的,它们解决的痛点不同。Webpack 更适合处理现代模块化的前端项目,而 Grunt、Gulp 更适合任务驱动的工作流。
5.2、Webpack、Rollup、Parcel 的优劣
Webpack:适合大型、复杂的前端项目,支持模块打包、按需加载、Tree Shaking、代码分割等功能,拥有强大的生态系统。
Rollup:适合库的打包,如 Vue.js 或 D3.js,重点是对代码进行 Tree Shaking 以减小体积。它的代码压缩效果好,但不如 Webpack 在项目依赖管理、按需加载等方面强大。
Parcel:适用于小型、实验性的项目,易于上手,零配置,但生态较弱,适合快速原型开发。
5.3、webpack的构建流程?
webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数开始编译:用上一步得到的参数初始化Compiler对象,加载所有配置的插件,执行对象的run方法开始执行编译确定入口:根据配置中的entry找出所有的入口文件编译模块:从入口文件出发,调用所有配置的loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理完成模块编译:在经过上一步使用loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
在以上过程中,webpack会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack提供的 API 改变 webpack的运行结果。
简单说:
- **初始化:**启动构建,读取与合并配置参数,加载
Plugin,实例化Compiler- **编译:**从
entry出发,针对每个Module串行调用对应的loader去翻译文件的内容,再找到该Module依赖的Module,递归地进行编译处理- **输出:**将编译后的
Module组合成Chunk,将Chunk转换成文件,输出到文件系统中
5.4、常见的 Loader
默认情况下,
webpack只支持对js和json文件进行打包,但是像css、html、png等其他类型的文件,webpack则无能为力。因此,就需要配置相应的loader进行文件内容的解析转换。
image-loader:加载并且压缩图片文件。
less-loader: 加载并编译 LESS 文件。
sass-loader:加载并编译 SASS/SCSS 文件。
css-loader:加载 CSS,支持模块化、压缩、文件导入等特性,使用 css-loader必须要配合使用 style-loader。
style-loader:用于将 CSS 编译完成的样式,挂载到页面的 style 标签上。需要注意 loader 执行顺序,style-loader 要放在第一位,loader 都是从后往前执行。
babel-loader:把 ES6 转换成 ES5
postcss-loader:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀。
eslint-loader:通过 ESLint 检查 JavaScript 代码。
vue-loader:加载并编译 Vue 组件。
file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)
5.5、 常见的 Plugin
webpack中的plugin赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在webpack的不同阶段(钩子 / 生命周期),贯穿了webpack整个编译周期。目的在于解决loader无法实现的其他事。
常用的 plugin如下:
- **
HtmlWebpackPlugin:**简化HTML文件创建 (依赖于html-loader) mini-css-extract-plugin: 分离样式文件,CSS提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)clean-webpack-plugin: 目录清理
5.6、Bundle、Chunk、Module 的定义
Bundle:Webpack 打包输出的文件。
Chunk:代码块,一个 Chunk 通常由多个模块组成,用于代码分割和合并。
Module:开发中的单个模块,Webpack 将所有文件视为模块,并根据依赖关系打包。
5.7、Loader 和 Plugin 的不同
Loader:用于转换不同类型的模块,如 CSS、图片、JSX 等文件,Loader 通过指定规则(module.rules)来处理文件。执行顺序是从右向左、从下往上执行的。
Plugin:扩展 Webpack 功能,可以处理更复杂的任务,如文件生成、环境变量定义等。Plugin 通常是在 Webpack 生命周期中的特定阶段执行。
总结来说,
Loader主要用于处理文件内容的转换,Plugin则用于扩展Webpack构建过程中的功能。这两者相辅相成,共同完成项目的构建和优化。
5.8、webpack 热更新的实现原理?
模块热替换(HMR - hot module replacement),又叫做 热更新,在不需要刷新整个页面的同时更新模块,能够提升开发的效率和体验。热更新时只会局部刷新页面上发生了变化的模块,同时可以保留当前页面的状态,比如复选框的选中状态等。
**热更新的核心就是客户端从服务端拉去更新后的文件,准确的说是 **chunk diff (chunk 需要更新的部分),实际上 webpack-dev-server与浏览器之间维护了一个 websocket,当本地资源发生变化时,webpack-dev-server会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 webpack-dev-server发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 webpack-dev-server发起 jsonp 请求获取该 chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像 react-hot-loader和 vue-loader都是借助这些 API 实现热更新。
5.9、如何提高 webpack的构建速度?
- 代码压缩
JS压缩:webpack 4.0默认在生产环境的时候是支持代码压缩的,即mode=production模式下。 实际上webpack 4.0默认是使用terser-webpack-plugin这个压缩插件,在此之前是使用uglifyjs-webpack-plugin,两者的区别是后者对ES6的压缩不是很好,同时我们可以开启parallel参数,使用多进程压缩,加快压缩。CSS压缩:CSS压缩通常是去除无用的空格等,因为很难去修改选择器、属性的名称、值等。可以使用另外一个插件:css-minimizer-webpack-plugin。- **
HTML压缩:**使用HtmlWebpackPlugin插件来生成HTML的模板时候,通过配置属性minify进行html优化。module.exports = { plugin:[ new HtmlwebpackPlugin({ minify:{ minifyCSS: false, // 是否压缩css collapseWhitespace: false, // 是否折叠空格 removeComments: true // 是否移除注释 } }) ] }
- 图片压缩 配置
image-webpack-loader Tree Shaking:Tree Shaking是一个术语,在计算机中表示消除死代码,依赖于ES Module的静态语法分析(不执行任何的代码,可以明确知道模块的依赖关系)。 在webpack实现Tree shaking有两种方案:-
**
usedExports:**通过标记某些函数是否被使用,之后通过Terser来进行优化的module.exports = { ... optimization:{ usedExports } }**使用之后,没被用上的代码在 **
webpack打包中会加入unused harmony export mul注释,用来告知Terser在优化时,可以删除掉这段代码。 -
**
sideEffects:**跳过整个模块/文件,直接查看该文件是否有副作用sideEffects用于告知webpack compiler哪些模块时有副作用,配置方法是在package.json中设置sideEffects属性。如果sideEffects设置为false,就是告知webpack可以安全的删除未用到的exports。如果有些文件需要保留,可以设置为数组的形式,如:"sideEffecis":[ "./src/util/format.js", "*.css" // 所有的css文件 ]
-
- 缩小打包域 排除
webpack不需要解析的模块,即在使用loader的时候,在尽量少的模块中去使用。可以借助include和exclude这两个参数,规定loader只在那些模块应用和在哪些模块不应用。 - 减少
ES6转为ES5的冗余代码 使用bable-plugin-transform-runtime插件 - 提取公共代码 通过配置
CommonsChunkPlugin插件,将多个页面的公共代码抽离成单独的文件
5.10、Babel 的原理是什么?
babel 的转译过程也分为三个阶段,这三步具体是:
- 解析
Parse: 将代码解析⽣成抽象语法树(AST),即词法分析与语法分析的过程; - 转换
Transform: 对于AST进⾏变换⼀系列的操作,babel接受得到AST并通过babel-traverse对其进⾏遍历,在此过程中进⾏添加、更新及移除等操作; - ⽣成
Generate: 将变换后的AST再转换为JS代码, 使⽤到的模块是babel-generator。

5.11、git 和 svn 的区别
git和svn最大的区别在于git是分布式的,而svn是集中式,因此我们不能再离线的情况下使用svn。如果服务器出现问题,就没有办法使用svn来提交代码;svn中的分支是整个版本库的复制的一份完整目录,而git的分支是指针指向某次提交,因此git的分支创建更加开销更小并且分支上的变化不会影响到其他人。svn的分支变化会影响到所有的人;svn的指令相对于git来说要简单一些,比git更容易上手;GIT把内容按元数据方式存储,而SVN是按文件:因为git目录是处于个人机器上的一个克隆版的版本库,它拥有中心版本库上所有的东西,例如标签,分支,版本记录等;GIT分支和SVN的分支不同:svn会发生分支遗漏的情况,而git可以同一个工作目录下快速的在几个分支间切换,很容易发现未被合并的分支,简单而快捷的合并这些文件;GIT没有一个全局的版本号,而SVN有;GIT的内容完整性要优于SVN:GIT的内容存储使用的是SHA-1哈希算法。这能确保代码内容的完整性,确保在遇到磁盘故障和网络问题时降低对版本库的破坏。
5.12、经常使用的 git 命令?
- 初始化一个仓库:
git init; - 查看分支:
git branch; - 将已修改或未跟踪的文件添加到暂存区:
git add [file]或git add .; - 提交至本地仓库:
git commit -m"提及记录xxxx"; - 本地分支推送至远程分支:
git push; - 查看当前工作目录和暂存区的状态:
git status; - 查看提交的日志记录:
git log; - 从远程分支拉取代码:
git pull; - 合并某分支(
xxx)到当前分支:git merge xxx; - 切换到分支
xxx:git checkout xxx; - 创建分支
xxx并切换到该分支:git checkout -b xxx; - 删除分支
xxx:git branch -d xxx; - 将当前分支到改动保存到堆栈中:
git stash; - 恢复堆栈中缓存的改动内容:
git stash pop...
6、浏览器
6.1、XSS
- 什么是
XSS:- 概念:
XSS攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如cookie等。** **XSS的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
攻击者可以通过这种攻击方式可以进行以下操作:- **获取页面的数据,如 **
DOM、cookie、localStorage; DOS攻击,发送合理请求,占用服务器资源,从而使用户无法访问服务器;- 破坏页面结构;
- 流量劫持(将链接指向某网站);
- **获取页面的数据,如 **
- 攻击类型:
XSS可以分为存储型、反射型和DOM型:-
存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,** **脚本从服务器传回并执行。
存储型XSS的攻击步骤:- 攻击者将恶意代码提交到⽬标⽹站的数据库中。
- **⽤户打开⽬标⽹站时,⽹站服务端将恶意代码从数据库取出,拼接在 **
HTML中返回给浏览器。 - ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
- 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
这种攻击常⻅于带有⽤户保存数据的⽹站功能,如论坛发帖、商品评论、⽤户私信等。
-
反射型指的是攻击者诱导用户访问一个带有恶意代码的
URL后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有XSS代码的数据后当做脚本执行,最终完成XSS攻击。
反射型XSS的攻击步骤:- **攻击者构造出特殊的 **
URL,其中包含恶意代码。 - **⽤户打开带有恶意代码的 **
URL时,⽹站服务端将恶意代码从URL中取出,拼接在HTML中返回给浏览器。 - ⽤户浏览器接收到响应后解析执⾏,混在其中的恶意代码也被执⾏。
- 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
**反射型 **
XSS跟存储型XSS的区别是:存储型XSS的恶意代码存在数据库⾥,反射型XSS的恶意代码存在URL⾥。**反射型 **
XSS漏洞常⻅于通过URL传递参数的功能,如⽹站搜索、跳转等。 由于需要⽤户主动打开恶意的URL才能⽣效,攻击者往往会结合多种⼿段诱导⽤户点击。 - **攻击者构造出特殊的 **
-
DOM型指的通过修改页面的DOM节点形成的XSS
DOM型XSS的攻击步骤:- **攻击者构造出特殊的 **
URL,其中包含恶意代码。 - **⽤户打开带有恶意代码的 **
URL。 - **⽤户浏览器接收到响应后解析执⾏,前端 **
JavaScript取出URL中的恶意代码并执⾏。 - 恶意代码窃取⽤户数据并发送到攻击者的⽹站,或者冒充⽤户的⾏为,调⽤⽬标⽹站接⼝执⾏攻击者指定的操作。
DOM型XSS跟前两种XSS的区别:DOM型XSS攻击中,取出和执** ⾏恶意代码由浏览器端完成,属于前端JavaScript⾃身的安全漏洞, **⽽其他两种XSS都属于服务端的安全漏洞。 - **攻击者构造出特殊的 **
-
- 概念:
- 如何防御
XSS攻击?- 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到
HTML中的代码做好充分的转义。对于DOM型的攻击,主要是前端脚本的不可靠而造成的,对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码情况进行判断。 - **使用 **
CSP,CSP的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。CSP指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。- **通常有两种方式来开启 **
CSP,一种是设置HTTP首部中的Content-Security-Policy,一种是设置meta标签的方式<meta http-equiv="Content-Security-Policy">
- 可以从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回(不使用服务端渲染)。另一种是对需要插入到
6.2、CSRF
- 什么是
CSRF攻击?- 概念:
CSRF攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。** **CSRF攻击的本质是利用cookie会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。 - 攻击类型:
常见的CSRF攻击有三种:GET类型的CSRF攻击,比如在网站中的一个img标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。POST类型的CSRF攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。- **链接类型的 **
CSRF攻击,比如在a标签的href属性里构建一个请求,然后诱导用户去点击。
- 概念:
- 如何防御
CSRF攻击?
CSRF攻击可以使用以下方法来防护:- **进行同源检测,服务器根据 **
http请求头中origin或者referer信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当origin或者referer信息都不存在的时候,直接阻止请求。这种方式的缺点是有些情况下referer可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。(Referer字段会告诉服务器该网页是从哪个页面链接过来的) - **使用 **
CSRF Token进行验证,服务器向用户返回一个随机数Token,当网站再次发起请求时,在请求参数中加入服务器端返回的token,然后服务器对这个token进行验证。这种方法解决了使用cookie单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点就是,我们需要给网站中的所有请求都添加上这个token,操作比较繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的session中没有保留这个token的话,就没有办法验证了。这种情况可以通过改变token的构建方式来解决。 - **对 **
Cookie进行双重验证,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从cookie中取出这个字符串,添加到URL参数中,然后服务器通过对cookie中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用cookie,但是不能访问获取cookie的特点。并且这种方法比CSRF Token的方法更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果网站存在XSS漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。 - **在设置 **
cookie属性的时候设置Samesite,限制cookie不能作为被第三方使用,从而可以避免被攻击者利用。Samesite一共有两种模式,一种是严格模式,在严格模式下cookie在任何情况下都不可能作为第三方Cookie使用,在宽松模式下,cookie可以被请求是GET请求,且会发生页面跳转的请求所使用。
- **进行同源检测,服务器根据 **
6.3、有哪些可能引起前端安全的问题?
- 跨站脚本 (
Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式, 为了与CSS区分所以被称作XSS。早期常⻅于⽹络论坛, 起因是⽹站没有对⽤户的输⼊进⾏严格的限制, 使得攻击者可以将脚本上传到帖⼦让其他⼈浏览到有恶意脚本的⻚⾯, 其注⼊⽅式很简单包括但不限于JavaScript / CSS / Flash等; iframe的滥⽤:iframe中的内容是由第三⽅来提供的,默认情况下他们不受控制,他们可以在iframe中运⾏JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端⽤户体验;- 跨站点请求伪造(
Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已完成认证的⽤户进⾏⾮预期的个⼈信息或设定信息等某些状态更新,属于被动攻击恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数时候都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被植⼊恶意代码很容易引起安全问题。
6.4、网络劫持有哪几种,如何防范?
⽹络劫持分为两种:
-
DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)DNS- 强制解析: 通过修改运营商的本地
DNS记录,来引导⽤户流量到缓存服务器 - **
302跳转的⽅式:**通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起302跳转的回复,引导⽤户获取内容
DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。 - 强制解析: 通过修改运营商的本地
-
HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)
6.5、浏览器渲染进程的线程有哪些
浏览器的渲染进程的线程总共有五种:

GUI渲染线程:
**负责渲染浏览器页面,解析 **HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。注意:
GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。JS引擎线程:
JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序;注意:
GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。- 时间触发线程:
**时间触发线程属于浏览器而不是 **JS引擎,用来控制事件循环;当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;**注意:由于 **
JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行); - 定时器触发进程:
**定时器触发进程即 **setInterval与setTimeout所在线程;浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;注意:
W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms。 - 异步
http请求线程:
XMLHttpRequest连接后通过浏览器新开一个线程请求;
**检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待 **JS引擎空闲后执行;
6.6、僵尸进程和孤儿进程是什么?
- **孤儿进程:父进程退出了,而它的一个或多个进程还在运行,那这些子进程都会成为孤儿进程。孤儿进程将被 **
init进程(进程号为 1)所收养,并由init进程对它们完成状态收集工作。 - 僵尸进程:子进程比父进程先结束,而父进程又没有释放子进程占用的资源,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵死进程。
6.7、如何实现浏览器内多个标签页之间的通信?
实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。通信方法如下:
- **使用 **
websocket协议,因为websocket协议可以实现服务器推送,所以服务器就可以用来当做这个中介者。标签页通过向服务器发送数据,然后由服务器向其他标签页推送转发。 - **使用 **
ShareWorker的方式,shareWorker会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。 - 使用
localStorage的方式,我们可以在一个标签页对localStorage的变化事件进行监听,然后当另一个标签页修改数据的时候,我们就可以通过这个监听事件来获取到数据。这个时候localStorage对象就是充当的中介者的角色。 - **使用 **
postMessage方法,如果我们能够获得对应标签页的引用,就可以使用postMessage方法,进行通信。
6.8、对浏览器的缓存机制的理解
浏览器缓存的全过程:
- **浏览器第一次加载资源,服务器返回 **
200,浏览器从服务器下载资源文件,并缓存资源文件与response header,以供下次加载时对比使用; - **下一次加载资源时,由于强制缓存优先级较高,先比较当前时间与上一次返回 **
200时的时间差,如果没有超过cache-control设置的max-age,则没有过期,并命中强缓存,直接从本地读取资源。如果浏览器不支持HTTP1.1,则使用expires头判断是否过期; - **如果资源已过期,则表明强制缓存没有被命中,则开始协商缓存,向服务器发送带有 **
If-None-Match和If-Modified-Since的请求; - **服务器收到请求后,优先根据 **
Etag的值判断被请求的文件有没有做修改,Etag值一致则没有修改,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200; - **如果服务器收到的请求没有 **
Etag值,则将If-Modified-Since和被请求文件的最后修改时间做比对,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
**很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 **
JS或CSS文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的JS或CSS文件 ,以保证用户能够及时获得网站的最新更新。
6.9、协商缓存和强缓存的区别
-
强缓存:
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。
强缓存策略可以通过两种方式来设置,分别是
http头信息中的Expires属性和Cache-Control属性:-
**服务器通过在响应头中添加 **
Expires属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。 -
Expires是http1.0中的方式,因为它的一些缺点,在HTTP1.1中提出了一个新的头部属性就是Cache-Control属性,它提供了对资源的缓存的更精确的控制。它有很多不同的值,Cache-Control可设置的字段:- **
public:**设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,一般还是使用max-age=来精确控制; - **
private:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,通常都要设置这个字段值,避免代理服务器(CDN)**缓存; - **
no-cache:**设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源; - **
no-store:**设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源; - **
max-age=:**设置缓存的最大有效期,单位为秒; - **
s-maxage=:**优先级高于max-age=,仅适用于共享缓存(CDN),优先级高于max-age或者Expires头; - **
max-stale[=]:**设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制。
一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,
Cache-Control的优先级要高于Expires。no-cache 和 no-store 很容易混淆:
no-cache是指先要和服务器确认是否有资源更新,在进行判断。也就是说没有强缓存,但是会有协商缓存;no-store是指不使用任何缓存,每次请求都直接从服务器获取资源。
- **
-
-
协商缓存:
如果命中强制缓存,我们无需发起新的请求,直接使用缓存内容,如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。
上面已经说到了,命中协商缓存的条件有两个:
max-age=xxx过期了- **值为 **
no-store
**使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 **
304状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。协商缓存也可以通过两种方式来设置,分别是
http头信息中的Etag和Last-Modified属性:- **服务器通过在响应头中添加 **
Last-Modified属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个If-Modified-Since的属性,属性值为上一次资源返回时的Last-Modified的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回304状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是Last-Modified标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是Last-Modified却没有改变,这样会造成缓存命中的不准确。 - **因为 **
Last-Modified的这种可能发生的不准确性,http中提供了另外一种方式,那就是Etag属性。服务器在返回资源的时候,在头信息中添加了Etag属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个If-None-Match属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的Etag的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比Last-Modified的方式更加精确。**当 **
Last-Modified和Etag属性同时出现的时候,Etag的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的Last-Modified应该保持一致,因为每个服务器上Etag的值都不一样,因此在考虑负载平衡时,最好不要设置Etag属性。
-
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
6.10、点击刷新按钮或者按 F5、按 Ctrl+F5 (强制刷新)、地址栏回车有什么区别?
- 点击刷新按钮或者按
F5:
**浏览器直接对本地的缓存文件过期,但是会带上 **If-Modifed-Since,If-None-Match,这就意味着服务器会对文件检查新鲜度,返回结果可能是304,也有可能是200。 - 用户按
Ctrl+F5(强制刷新):
**浏览器不仅会对本地文件过期,而且不会带上 **If-Modifed-Since,If-None-Match,相当于之前从来没有请求过,返回结果是200。 - 地址栏回车:
浏览器发起请求,按照正常流程,本地检查是否过期,然后服务器检查新鲜度,最后返回内容。
6.11、常见的浏览器内核比较
- **
Trident:**这种浏览器内核是IE浏览器用的内核,因为在早期IE占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于IE的高市场占有率,微软也很长时间没有更新Trident内核,就导致了Trident内核和W3C标准脱节。还有就是Trident内核的大量Bug等安全问题没有得到解决,加上一些专家学者公开自己认为IE浏览器不安全的观点,使很多用户开始转向其他浏览器。 - **
Gecko:**这是Firefox和Flock所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存。 Presto:Opera曾经采用的就是Presto内核,Presto内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理JS脚本等脚本语言时,会比其他的内核快3倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性。Webkit:Webkit是Safari采用的内核,它的优点就是网页浏览速度较快,虽然不及Presto但是也胜于Gecko和Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit前身是KDE小组的KHTML引擎,可以说WebKit是KHTML的一个开源的分支。- **
Blink:**谷歌在Chromium Blog上发表博客,称将与苹果的开源浏览器核心Webkit分道扬镳,在Chromium项目中研发Blink渲染引擎(即浏览器核心),内置于Chrome浏览器之中。其实Blink引擎就是Webkit的一个分支,就像webkit是KHTML的分支一样。Blink引擎现在是谷歌公司与Opera Software共同研发,上面提到过的,Opera弃用了自己的Presto内核,加入Google阵营,跟随谷歌一起研发Blink。
6.12、浏览器的渲染过程
浏览器渲染主要有以下步骤:
- **首先解析收到的文档,根据文档定义构建一棵 **
DOM树,DOM树是由DOM元素及属性节点组成的。 - **然后对 **
CSS进行解析,生成CSSOM规则树。 - **根据 **
DOM树和CSSOM规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和DOM元素相对应,但这种对应关系不是一对一的,不可见的DOM元素不会被插入渲染树。还有一些DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。 - 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
- **布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 **
paint方法将它们的内容显示在屏幕上,绘制使用UI基础组件。 - 大致过程如图所示:
**注意:这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 **
html都解析完成之后再去构建和布局render树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
6.13、渲染过程中遇到 JS 文件如何处理?
JavaScript的加载、解析与执行会阻塞文档的解析,也就是说,在构建DOM时,HTML解析器若遇到了JavaScript,那么它会暂停文档的解析,将控制权移交给JavaScript引擎,等JavaScript引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载JS文件,这也是都建议将script标签放在body标签底部的原因。当然在当下,并不是说script标签必须放在底部,因为你可以给script标签添加defer或者async属性。
6.14、事件是什么?事件模型?
**事件是用户操作网页时发生的交互动作,比如 **
click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个event对象,包含了该事件发生时的所有相关信息(event的属性)以及可以对事件进行的操作(event的方法)。
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:
DOM0级事件模型,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过js属性来指定监听函数。所有浏览器都兼容这种方式。直接在dom对象上注册事件名称,就是DOM0写法。IE事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过attachEvent来添加监听函数,可以添加多个监听函数,会按顺序依次执行。DOM2级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从document一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
6.15、对事件循环的理解
**因为 **
js是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码时,如果遇到异步事件,js引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。

Event Loop执行顺序如下所示:
- 首先执行同步代码,这属于宏任务;
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行;
- 执行所有微任务;
- 当执行完所有微任务后,如有必要会渲染页面;
- **然后开始下一轮 **
Event Loop,执行宏任务中的异步代码。
6.16、浏览器输入域名后的执行全过程
- 解析域名:
首先,浏览器需要解析你输入的域名www.baidu.com。这个过程称为DNS查询。- 浏览器缓存:浏览器首先检查其缓存,看是否已经有这个域名的解析记录。
- 系统缓存:如果浏览器缓存中没有,它会检查操作系统的
hosts文件和DNS缓存。 - 路由器缓存:接着是本地网络中的
DNS缓存,比如路由器。 - 递归查询:如果以上都没有找到,浏览器会向设置的
DNS服务器发起查询请求。
- 建立
TCP/IP连接:
一旦浏览器获得了目标IP地址,它需要与该地址建立TCP连接。这通常被称为TCP三次握手。SYN:浏览器发送一个SYN包到服务器,询问是否可以建立连接。SYN-ACK:服务器回应一个SYN-ACK包,表示同意建立连接。ACK:浏览器再回应一个ACK包,确认连接。
- 发送
HTTP请求:
建立了TCP/IP连接之后,浏览器会向服务器发送一个HTTP请求,通常是GET请求,来请求网页内容。 - 服务器处理请求: 服务器接收到请求后,会进行以下操作:
- 处理请求:服务器软件(如
Apache, Nginx等)会解析请求,并确定要返回给客户端的文件或资源。 - 查找资源:服务器查找请求的资源,可能是
HTML文件、图片、CSS、JavaScript等。 - 构建响应:一旦找到资源,服务器会构建一个
HTTP响应。
- 处理请求:服务器软件(如
- 返回响应:
服务器将构建好的HTTP响应通过之前建立的TCP连接发送回浏览器。这个响应包含了状态码(如200 OK)、响应头和响应体(实际的网页内容)。 - 浏览器解析
HTML:
浏览器接收到响应后,开始解析HTML内容。- 构建
DOM树:浏览器根据HTML标记构建文档对象模型(DOM)。 - 下载资源:解析过程中,发现有其他资源(如图片、
CSS、JavaScript文件)需要下载,浏览器会发起新的请求来获取这些资源。 - 渲染页面:浏览器引擎根据
DOM树和资源内容来渲染页面。
- 构建
- 断开连接:
一旦浏览器完成了所有资源的下载和页面的渲染,它可能会关闭与服务器的TCP连接,这个过程称为TCP四次挥手。FIN:浏览器发送一个FIN包,告诉服务器它已经完成数据传输。ACK:服务器回应一个ACK包,确认收到了关闭通知。FIN:服务器也发送一个FIN包,告诉浏览器它也准备好关闭连接了。ACK:浏览器再次回应一个ACK包,确认连接关闭。
- 显示页面:
最后,浏览器显示完整的网页给用户,完成整个请求过程。
7、网络
7.1、GET 和 POST 的请求的区别
Post 和 Get 是 HTTP 请求的两种方法,其区别如下:
- 应用场景:
GET请求是一个幂等的请求,一般Get请求用于对服务器资源不会产生影响的场景,比如说请求一个网页的资源。而Post不是一个幂等的请求,一般用于对服务器资源会产生影响的情景,比如注册用户这一类的操作。 - **是否缓存:**因为两者应用场景不同,浏览器一般会对
Get请求缓存,但很少对Post请求缓存。 - 发送的报文格式:
Get请求的报文中实体部分为空,Post请求的报文中实体部分一般为向服务器发送的数据。 - 安全性:
Get请求可以将请求的参数放入url中向服务器发送,这样的做法相对于 Post 请求来说是不太安全的,因为请求的url会被保留在历史记录中。 - **请求长度:**浏览器由于对
url长度的限制,所以会影响get请求发送数据时的长度。这个限制是浏览器规定的,并不是RFC规定的。 - 参数类型:
post的参数传递支持更多的数据类型。
7.2、POST 和 PUT 请求的区别
PUT请求是向服务器端发送数据,从而修改数据的内容,但是不会增加数据的种类等,也就是说无论进行多少次PUT操作,其结果并没有不同。(可以理解为时更新数据)POST请求是向服务器端发送数据,该请求会改变数据的种类等资源,它会创建新的内容。(可以理解为是创建数据)
7.3、常见的 HTTP 请求头和响应头
HTTP Request Header常见的请求头:- **
Accept:**浏览器能够处理的内容类型; - **
Accept-Charset:**浏览器能够显示的字符集; - **
Accept-Encoding:**浏览器能够处理的压缩编码; - **
Accept-Language:**浏览器当前设置的语言; - **
Connection:**浏览器与服务器之间连接的类型; - **
Cookie:**当前页面设置的任何 Cookie; - **
Host:**发出请求的页面所在的域; - **
Referer:**发出请求的页面的URL; - **
User-Agent:**浏览器的用户代理字符串;
- **
HTTP Responses Header常见的响应头:- **
Date:**表示消息发送的时间,时间的描述格式由rfc822定义; - **
server:**服务器名称; - **
Connection:**浏览器与服务器之间连接的类型; - **
Cache-Control:**控制HTTP缓存; - **
content-type:**表示后面的文档属于什么MIME类型。
常见的Content-Type属性值有以下四种:- **
application/x-www-form-urlencoded:**浏览器的原生form表单 , 如 果 不 设 置enctype属 性 , 那 么 最 终 就 会 以application/x-www-form-urlencoded方式提交数据。该种方式提交的数据放在body里面,数据按照key1=val1&key2=val2的方式进行编码,key和val都进行了URL转码。 - **
multipart/form-data:**该种方式也是一个常见的POST提交方式,通常表单上传文件时使用该种方式。 - **
application/json:**服务器消息主体是序列化后的JSON字符串。 - **
text/xml:**该种方式主要用来提交XML格式的数据。
- **
- **
7.4、常见的 HTTP 请求方法
- **
GET:**向服务器获取数据; - **
POST:**将实体提交到指定的资源,通常会造成服务器资源的修改; - **
PUT:**上传文件,更新数据; - **
DELETE:**删除服务器上的对象; - **
HEAD:**获取报文首部,与GET相比,不返回报文主体部分; - **
OPTIONS:**询问支持的请求方法,用来跨域请求; - **
CONNECT:**要求在与代理服务器通信时建立隧道,使用隧道进行TCP通信; TRACE: 回显服务器收到的请求,主要⽤于测试或诊断。
7.5、HTTP 1.1 和 HTTP 2.0 的区别
- 二进制协议:
HTTP/2是一个二进制协议。在HTTP/1.1版中,报文的头信息必须是文本(ASCII编码),数据体可以是文本,也可以是二进制。HTTP/2则是一个彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。 - 多路复用:
HTTP/2实现了多路复用,HTTP/2仍然复用TCP连接,但是在一个连接里,客户端和服务器都可以同时发送多个请求或回应,而且不用按照顺序一一发送,这样就避免了"队头堵塞"的问题。 - 数据流:
HTTP/2使用了数据流的概念,因为HTTP/2的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的请求。因此,必须要对数据包做标记,指出它属于哪个请求。HTTP/2将每个请求或回应的所有数据包,称为一个数据流。每个数据流都有一个独一无二的编号。数据包发送时,都必须标记数据流ID,用来区分它属于哪个数据流。 - 头信息压缩:
HTTP/2实现了头信息压缩,由于HTTP 1.1协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如Cookie和User Agent,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用gzip或compress压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。 - 服务器推送:
HTTP/2允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是http2下服务器主动推送的是静态资源,和WebSocket以及使用SSE等方式向客户端发送即时数据的推送是不同的。
7.6、HTTP 和 HTTPS 协议的区别
HTTP 和 HTTPS 协议的主要区别如下:
HTTPS协议需要CA证书,费用较高;而HTTP协议不需要;HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;- 使用不同的连接方式,端口也不同,
HTTP协议端口是80,HTTPS协议端口是443; HTTP协议连接很简单,是无状态的;HTTPS协议是有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全。
7.7、HTTP2 的头部压缩算法是怎样的?
HTTP2的头部压缩是HPACK算法。在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。
具体来说:
在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送;
**首部表在 **HTTP/2 的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值。
例如下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销。

7.8、说一下 HTTP 3.0
HTTP/3基于UDP协议实现了类似于TCP的多路复用数据流、传输可靠性等功能,这套功能被称为QUIC协议。

- 流量控制、传输可靠性功能:
QUIC在UDP的基础上增加了一层来保证数据传输可靠性,它提供了数据包重传、拥塞控制、以及其他一些TCP中的特性。 - **集成
TLS加密功能:**目前QUIC使用TLS1.3,减少了握手所花费的RTT数。 - **多路复用:**同一物理连接上可以有多个独立的逻辑数据流,实现了数据流的单独传输,解决了
TCP的队头阻塞问题。

- **快速握手:**由于基于
UDP,可以实现使用0 ~ 1个RTT来建立连接。
7.9、 什么是 HTTPS 协议?
超文本传输安全协议(
Hypertext Transfer Protocol Secure,简称:HTTPS)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用SSL/TLS来加密数据包。HTTPS的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。

HTTP 协议采用明文传输信息,存在信息窃听、信息篡改和信息劫持的风险,而协议 TLS/SSL 具有身份验证、信息加密和完整性校验的功能,可以避免此类问题发生。
**安全层的主要职责就是对发起的 **HTTP 请求的数据进行加密操作 和对接收到的 HTTP 的内容进行解密操作。
7.10、HTTPS 通信(握手)过程
HTTPS 的通信过程如下:
- 客户端向服务器发起请求,请求中包含使用的协议版本号、生成的一个随机数、以及客户端支持的加密方法。
- 服务器端接收到请求后,确认双方使用的加密方法、并给出服务器的证书、以及一个服务器生成的随机数。
- **客户端确认服务器证书有效后,生成一个新的随机数,并使用数字证书中的公钥,加密这个随机数,然后发给服 务器。并且还会提供一个前面所有内容的 **
hash的值,用来供服务器检验。 - **服务器使用自己的私钥,来解密客户端发送过来的随机数。并提供前面所有内容的 **
hash值来供客户端检验。 - 客户端和服务器端根据约定的加密方法使用前面的三个随机数,生成对话秘钥,以后的对话过程都使用这个秘钥来加密信息。
7.11、OSI 七层模型
ISO 为了更好的使网络应用更为普及,推出了 OSI 参考模型。

- 应用层:
OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。
在客户端与服务器中经常会有数据的请求,这个时候就是会用到http(hyper text transfer protocol)(超文本传输协议)或者https. 在后端设计数据接口时,我们常常使用到这个协议。
FTP是文件传输协议,在开发过程中,个人并没有涉及到,但是我想,在一些资源网站,比如百度网盘``迅雷应该是基于此协议的。
SMTP是simple mail transfer protocol(简单邮件传输协议)。在一个项目中,在用户邮箱验证码登录的功能时,使用到了这个协议。 - 表示层:
表示层提供各种用于应用层数据的编码和转换功能,确保一个系统的应用层发送的数据能被另一个系统的应用层识别。如果必要,该层可提供一种标准表示形式,用于将计算机内部的多种数据格式转换成通信中采用的标准表示形式。数据压缩和加密也是表示层可提供的转换功能之一。
**在项目开发中,为了方便数据传输,可以使用 **base64对数据进行编解码。如果按功能来划分,base64应该是工作在表示层。 - 会话层:
会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。 - 传输层:
传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。 - 网络层:
**本层通过 **IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。我们可以这样理解,网络层规定了数据包的传输路线,而传输层则规定了数据包的传输方式。 - 数据链路层:
**将比特组合成字节,再将字节组合成帧,使用链路层地址 (以太网使用 **MAC地址)来访问介质,并进行差错检测。
网络层与数据链路层的对比,通过上面的描述,我们或许可以这样理解,网络层是规划了数据包的传输路线,而数据链路层就是传输路线。不过,在数据链路层上还增加了差错控制的功能。 - 物理层:
实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。常用设备有(各种物理设备)集线器、中继器、调制解调器、网线、双绞线、同轴电缆。这些都是物理层的传输介质。
OSI七层模型通信特点:对等通信对等通信,为了使数据分组从源传送到目的地,源端 OSI 模型的每一层都必须与目的端的对等层进行通信,这种通信方式称为对等层通信。在每一层通信过程中,使用本层自己协议进行通信。
8、VUE
8.1、Vue 的基本原理
**当 一 个 **Vue 实 例 创 建 时 , Vue 会 遍 历 data 中 的 属 性 , 用 Object.defineProperty ( vue3.0 使 用 proxy ) 将 它 们 转 为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

8.2、双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- **需要 **
observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。 compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:- 在自身实例化时往属性订阅器(
dep)里面添加自己 ; - **自身必须有一个 **
update()方法 ; - **待属性变动 **
dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
- 在自身实例化时往属性订阅器(
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input)-> 数据model变更的双向绑定效果。

8.3、MVVM、MVC、MVP 的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。** **在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。
-
MVC:
MVC通过分离Model、View和Controller的方式来组织代码结构。其中View负责页面的显示逻辑,Model负责存储页面的业务数据,以及对相应数据的操作。并且View和Model应用了观察者模式,当Model层发生改变的时候它会通知有关View层更新页面。
Controller层是View层和Model层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller中的事件触发器就开始工作了,通过调用Model层,来完成对Model的修改,然后Model层再去通知View层更新。

-
MVVM:
MVVM分为Model、View、ViewModel:Model代表数据模型,数据和业务逻辑都在Model层中定义;View代表UI视图,负责数据的展示;ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。**这种模式实现了 **
Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
-
MVP:
MVP模式与MVC唯一不同的在于Presenter和Controller。在MVC模式中使用观察者模式,来实现当Model层数据发生变化的时候,通知View层的更新。这样View层和Model层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP的模式通过使用Presenter来实现对View层和Model层的解耦。MVC中的Controller只知道Model的接口,因此它没有办法控制View层的更新,MVP模式中,View层的接口暴露给了Presenter因此可以在Presenter中将Model的变化和View的变化绑定在一起,以此来实现View和Model的同步更新。这样就实现了对View和Model的解耦,Presenter还包含了其他的响应逻辑。
8.4、slot 是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
- **默认插槽:**又名匿名插槽,当
slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。 - **具名插槽:**带有具体名字的插槽,也就是带有
name属性的slot,一个组件可以出现多个具名插槽。 - **作用域插槽:**默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
- **实现原理:**当子组件
vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.slot中,默认插槽为vm.slot.default,具名插槽为vm.slot.xxx,xxx为插槽名,当组件执行渲染函数时候,遇到slot标签,使用slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
8.5、$nextTick 原理及作用
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
nextTick 的核心是利用了如 Promise、MutationObserver、setImmediate、setTimeout 的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时机的后续逻辑处理。
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中的示例,引入异步更新队列机制的原因:
- **如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 **
UI/DOM的渲染,可以减少一些无用渲染。 - **同时由于 **
VirtualDOM的引入,每一次状态发生变化后,状态变化的信号会发送给组件,组件内部使用VirtualDOM进行计算得出需要更新的具体的DOM节点,然后对DOM进行更新操作。每次更新状态后的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所以异步渲染变得更加至关重要。
Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作 DOM。有时候,可能遇到这样的情况,DOM1 的数据发生了变化,而 DOM2 需要从 DOM1 中获取数据,那这时就会发现 DOM2 的视图并没有更新,这时就需要用到了 nextTick。
**由于 **Vue 的 DOM 操作是异步的,所以,在上面的情况中,就要将 DOM2 获取数据的操作写在 $nextTick 中。
所以,在以下情况下,会用到 nextTick:
- **在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的 DOM 结构的时候,这个操作就需要放在 **
nextTick()的回调函数中。 - **在 **
Vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。
8.6、Vue 单页应用与多页应用的区别
- 概念:
SPA单页面应用(SinglePage Web Application),指只有一个主页面的应用,一开始只需要加载一次js、css等相关资源。所有内容都包含在主页面,对每一个功能模块组件化。单页应用跳转,就是切换相关组件,仅仅刷新局部资源。
MPA多页面应用 (MultiPage Application),指有多个独立页面的应用,每个页面必须重复加载js、css等相关资源。多页应用跳转,需要整页资源刷新。 - 区别:

8.7、Vue 中封装的数组方法有哪些,其如何实现页面更新
**在 **Vue 中,对响应式处理利用的是 Object.defineProperty 对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行 hack,让 Vue 能监听到其中的变化。

那 Vue 是如何实现让这些数组方法实现元素的实时更新的呢,下面是 Vue 中对这些方法的封装:

简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的 __ob__,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 继续对新的值观察变化(也就是通过 target__proto__== arrayMethods 来改变了数组实例的型),然后手动调用 notify,通知渲染 watcher,执行 update。
8.8、Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会立即同步执行重新渲染。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
**如果同一个 **watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。
8.9、简述 mixin、extends 的覆盖逻辑
mixin和extends
mixin和extends均是用于合并、拓展组件的,两者均通过mergeOptions方法实现合并。
mixins接收一个混入对象的数组,其中混入对象可以像正常的实例对象一样包含实例选项,这些选项会被合并到最终的选项中。Mixin钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
extends主要是为了便于扩展单文件组件,接收一个对象或构造函数。

mergeOptions的执行过程
**规范化选项( **normalizeProps 、 normalizelnject 、normalizeDirectives)
对未合并的选项,进行判断

8.10、子组件可以直接改变父组件的数据吗?
**子组件不可以直接改变父组件的数据。这样做主要是为了维护父子组件的单向数据流。每次父级组件发生更新时,子组件中所有的 **prop都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
Vue 提倡单向数据流,即父级 props 的更新会流向子组件,但是反过来则不行。这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解,导致数据流混乱。如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
**只能通过 **$emit 派发一个自定义事件,父组件接收到后,由父组件修改。
8.11、Vue 的优点
- **轻量级框架:**只关注视图层,是一个构建数据的视图集合,大小只有几十
kb; - **简单易学:**国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
- **双向数据绑定:**保留了
angular的特点,在数据操作方面更为简单;组件化:保留了react的优点,实现了html的封装和重用,在构建单页面应用方面有着独特的优势; - **视图,数据,结构分离:**使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
- 虚拟
DOM:dom操作是非常耗费性能的,不再使用原生的dom操作节点,极大解放dom操作,但具体操作的还是dom不过是换了另一种方式; - **运行速度更快:**相比较于
react而言,同样是操作虚拟dom,就性能而言,vue存在很大的优势。
8.12、assets 和 static 的区别
- 相同点:
assets和static两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点; - 不同点:
assets中存放的静态资源文件在项目打包时,也就是运行npm run build时会将assets中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在static文件中跟着index.html一同上传至服务器。static中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是static中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于assets中打包后的文件提交较大点。在服务器中就会占据更大的空间。 - **建议:**将项目中
template需要的样式文件js文件等都可以放置在assets中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css等文件可以放置在static中,因为这些引入的第三方文件已经经过处理,不再需要处理,直接上传。
8.13、Vue 模版编译原理
vue 中的模板 template 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所有需要将 template 转化成一个 JavaScript 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。模板编译又分三个阶段,解析 parse,优化 optimize,生成 generate,最终生成可执行函数 render。
- **解析阶段:**使用大量的正则表达式对
template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。 - **优化阶段:**遍历
AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化runtime的性能。 - **生成阶段:**将最终的
AST转化为render函数字符串。
8.14、v-if 和 v-for 哪个优先级更高?如果同时出现,应如何优化?
v-for 优先于 v-if 被解析,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何循环都不可避免,浪费了性能。
**要避免出现这种情况,则在外层嵌套 **template,在这一层进行 v-if 判断,然后在内部进行 v-for 循环。如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项。
8.15、对 Vue 组件化的理解
- **组件是独立和可复用的代码组织单元。组件系统是 **
Vue核心特性之一,它使开发者使用小型、独立和通常可复用的组件构建大型应用; - 组件化开发能大幅提高应用开发效率、测试性、复用性等;
- 组件使用按分类有:页面组件、业务组件、通用组件;
vue的组件是基于配置的,我们通常编写的组件是组件配置而非组件,框架后续会生成其构造函数,它们基于VueComponent,扩展于Vue;- **
vue中常见组件化技术有:**属性prop,自定义事件,插槽等,它们主要用于组件通信、扩展等; - 合理的划分组件,有助于提升应用性能;
- 组件应该是高内聚、低耦合的;
- 遵循单向数据流的原则。
8.16、对 vue 设计原则的理解
- **渐进式
JavaScript框架:**与其它大型框架不同的是,Vue被设计为可以自底向上逐层应用。Vue的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue也完全能够为复杂的单页应用提供驱动。 - 易用性:
vue提供数据响应式、声明式模板语法和基于配置的组件系统等核心特性。这些使我们只需要关注应用的核心业务即可,只要会写js、html和css就能轻松编写vue应用。 - **灵活性:**渐进式框架的最大优点就是灵活性,如果应用足够小,我们可能仅需要
vue核心特性即可完成功能;随着应用规模不断扩大,我们才可能逐渐引入路由、状态管理、vue-cli等库和工具,不管是应用体积还是学习难度都是一个逐渐增加的平和曲线。 - **高效性:**超快的虚拟
DOM和diff算法使我们的应用拥有最佳的性能表现。追求高效的过程还在继续,vue3中引入Proxy对数据响应式改进以及编译器中对于静态内容编译的改进都会让vue更加高效。
8.17、说一下 Vue 的生命周期
Vue实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom-> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
- **
beforeCreate(创建前):**数据观测和初始化事件还未开始,此时data的响应式追踪、event/watcher都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。 - **
created(创建后) :**实例创建完成,实例上配置的options包括data、computed、watch、methods等都配置完成,但是此时渲染得节点还未挂载到DOM,所以不能访问到$el属性。 - **
beforeMount(挂载前):**在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。 - **
mounted(挂载后):**在el被新创建的vm.$el替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。 - **
beforeUpdate(更新前):**响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实DOM还没有被渲染。 - **
updated(更新后) :**在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时DOM已经根据响应式数据的变化更新了。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。 - **
beforeDestroy(销毁前):**实例销毁之前调用。这一步,实例仍然完全可用,this仍能获取到实例。 - **
destroyed(销毁后):**实例销毁后调用,调用后,Vue实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。 - **另外还有 **
keep-alive独有的生命周期,分别为activated和deactivated。用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated钩子函数,命中缓存渲染后会执行activated钩子函数。
8.18、Vue 子组件和父组件执行顺序
- 加载渲染过程:
- **父组件 **
beforeCreate - **父组件 **
created - **父组件 **
beforeMount - **子组件 **
beforeCreate - **子组件 **
created - **子组件 **
beforeMount - **子组件 **
mounted - **父组件 **
mounted
- **父组件 **
- 更新过程:
- **父组件 **
beforeUpdate - **子组件 **
beforeUpdate - **子组件 **
updated - **父组件 **
updated
- **父组件 **
- 销毁过程:
- **父组件 **
beforeDestroy - **子组件 **
beforeDestroy - **子组件 **
destroyed - **父组件 **
destoryed
- **父组件 **
8.19、created 和 mounted 的区别
- **
created:**在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。 - **
mounted:**在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。
8.20、一般在哪个生命周期请求异步数据
**我们可以在钩子函数 **created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
**推荐在 **created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好;
SSR不支持beforeMount 、mounted钩子函数,放在created中有助于一致性。
8.21、keep-alive 中的生命周期哪些
keep-alive 是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染 DOM。
**如果为一个组件包裹了 **keep-alive,那么它会多出两个生命周期:deactivated、activated。同时 beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
**当组件被换掉时,会被缓存到内存中、触发 **deactivated 生命周期;当组件被切回来时,再去缓存里找这个组件、触发 activated 钩子函数。
8.22、路由的 hash 和 history 模式的区别
Vue-Router有两种模式:hash模式和history模式。默认的路由模式是hash模式。
-
hash模式:
简介:hash模式是开发中默认的模式,它的URL带着一个#,例如:http://www.abc.com/#/vue,它的hash值就是#/vue。
特点:hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。这种模式的浏览器支持度很好,低版本的IE浏览器也支持这种模式。hash路由被称为是前端路由,已经成为SPA(单页面应用)的标配。
原理:hash模式的主要原理就是onhashchange()事件:
.jpg)
**使用 **onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现页面的前进和后退。虽然是没有请求后端服务器,但是页面的hash值和对应的URL关联起来了。 -
history模式:
简介:history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
特点 : 当使用history模式时,URL就像这样 :http://abc.com/user/id。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
API:history api可以分为两大部分,切换历史状态和修改历史状态:- **修改历史状态:**包括了
HTML5 History Interface中新增的pushState()和replaceState()方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。 - 切换历史状态: 包括
forward()、back()、go()三个方法,对应浏览器的前进,后退,跳转操作。
**虽然 **
history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。如果想要切换到history模式,就要进行以下配置(后端也要进行配置):

- **修改历史状态:**包括了
-
两种模式对比:
调用history.pushState()相比于直接修改hash,存在以下优势:pushState()设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,因此只能设置与当前URL同文档的URL;pushState()设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发动作将记录添加到栈中;pushState()通过stateObject参数可以添加任意类型的数据到记录中;而hash只可添加短字符串;pushState()可额外设置title属性供后续使用。
hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。
hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。
8.23、Vue-router 跳转和 location.href 有什么区别
**使用 **location.href= /url 来跳转,简单方便,但是刷新了页面;
**使用 **history.pushState( /url ) ,无刷新页面,静态跳转;
**引进 **router ,然后使用 router.push( /url ) 来跳转,使用了 diff 算法,实现了按需加载,减少了 dom 的消耗。其实使用 router 跳转和使用 history.pushState() 没什么差别的,因为 vue-router 就是用了 history.pushState() ,尤其是在 history 模式下。
8.24、Vuex 的原理
Vuex是一个专为Vue.js应用程序开发的状态管理模式。每一个Vuex应用的核心就是store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
**改变 **store 中的状态的唯一途径就是显式地提交 (commit)mutation。这样可以方便地跟踪每一个状态的变化。

Vuex 为 Vue Components 建立起了一个完整的生态圈,包括开发中的 API 调用一环。
- 核心流程中的主要功能:
Vue Components是vue组件,组件会触发(dispatch)一些事件或动作,也就是图中的Actions;- **在组件中发出的动作,肯定是想获取或者改变数据的,但是在 **
vuex中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到Mutations中; - **然后 **
Mutations就去改变(Mutate)State中的数据; - **当 **
State中的数据被改变之后,就会重新渲染(Render)到Vue Components中去,组件展示更新后的数据,完成一个流程。
- 各模块在核心流程中的主要功能:
Vue Components∶Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。- **
dispatch:**操作行为触发方法,是唯一能执行action的方法。 - **
actions:**操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。 - **
commi:**状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。 - **
mutations:**状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。 state: 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。getters:state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,VueComponents通过该方法读取全局state对象。-
总结:
** **
Vuex实现了一个单向数据流,在全局拥有一个State存放数据,当组件要更改State中的数据时,必须通过Mutation提交修改信息,Mutation同时提供了订阅者模式供外部插件调用获取State数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走Action,但Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。
8.25、Vuex 和 localStorage 的区别
- 最重要的区别:
vuex存储在内存中;localstorage则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要JSON的stringify和parse方法进行处理。 读取内存比读取硬盘速度要快。
- 应用场景:
Vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex用于组件之间的传值。localstorage是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用。Vuex能做到数据的响应式,localstorage不能
- 永久性:
- **刷新页面时 **
vuex存储的值会丢失,localstorage不会。 - 注意:对于不变的数据确实可以用
localstorage可以代替vuex,但是当两个组件共用一个数据源(对象或数组)时,如果其中一个组件改变了该数据源,希望另一个组件响应该变化时,localstorage无法做到,原因就是区别 1。
- **刷新页面时 **
8.26、Vuex 有哪几种属性?
有五种,分别是 State、 Getter、Mutation 、Action、 Module
state=> 基本数据(数据源存放地)getters=> 从基本数据派生出来的数据mutations=> 提交更改数据的方法,同步actions=> 像一个装饰器,包裹 mutations,使之可以异步。modules=> 模块化 Vuex
8.27、Vuex 和单纯的全局对象有什么区别?
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
**不能直接改变 **store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。
8.28、为什么 Vuex 的 mutation 中不能做异步操作?
Vuex 中所有的状态更新的唯一途径都是 mutation,异步操作通过 Action 来提交 mutation 实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。
**每个 **mutation 执行完成后都会对应到一个新的状态变更,这样 devtools就可以打个快照存下来,然后就可以实现 time-travel 了。
**如果 **mutation 支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
8.29、Vue3.0 有什么更新
- 监测机制的改变:
3.0将带来基于代理Proxy的observer实现,提供全语言覆盖的反应性跟踪。** **消除了Vue 2当中基于Object.defineProperty的实现所存在的很多限制; - 只能监测属性,不能监测对象:
- 检测属性的添加和删除;
- 检测数组索引和长度的变更;
- **支持 **
Map、Set、WeakMap和WeakSet。
- 模板:
作用域插槽,2.x的机制导致作用域插槽变了,父组件会重新渲染,而3.0把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
**同时,对于 **render函数的方面,vue3.0也会进行一系列更改来方便习惯直接使用api来生成vdom。 - 对象式的组件声明方式:
vue2.x中 的 组 件 是 通 过 声 明 的 方 式 传 入 一 系 列option, 和TypeScript的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
3.0修改了组件的声明方式,改成了类式的写法,这样使得和TypeScript的结合变得很容易 - 其它方面的更改:
**支持自定义渲染器,从而使得 **weex可以通过自定义渲染器的方式来扩展,而不是直接fork源码来改的方式。支持Fragment(多个根节点)和Protal(在dom其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。基于tree shaking优化,提供了更多的内置功能。
8.30、defineProperty 和 proxy 的区别
Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。
Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
但是这样做有以下问题:
- 添加或删除对象的属性时,
Vue检测不到。因为添加或删除的对象没有在初始化进行响应式处理, 只能通过$set来调用Object.defineProperty()处理; - 无法监控到数组下标和长度的变化;
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于 Object.defineProperty(),其有以下特点:
Proxy直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。Proxy可以监听数组的变化。
8.31、Vue3.0 为什么要用 proxy?
**在 **Vue2 中, 0bject.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点:
- **不需用使用 **
Vue.$set或Vue.delete触发响应式。 - **全方位的数组变化检测,消除了 **
Vue2无效的边界情况。 - **支持 **
Map,Set,WeakMap和WeakSet。 Proxy实现的响应式原理与Vue2的实现原理相同,实现方式大同小异∶get收集依赖;Set、delete等触发依赖;- 对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑。
8.32、虚拟 DOM 的解析过程
虚拟 DOM 的解析过程:
**首先对将要插入到文档中的 **DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
**当页面的状态发生改变,需要对页面的 **DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差异。
**最后将记录的有差异的地方应用到真正的 **DOM 树中去,这样视图就更新了。
8.33、DIFF 算法的原理
在新老虚拟 DOM 对比时:
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换;
- **如果为相同节点,进行 **
patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除); - **比较如果都有子节点,则进行 **
updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心); - 匹配时,找到相同的子节点,递归比较子节点;
- **在 **
diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n3)降低值O(n),也就是说,只有当新旧children; - **都为多个子节点时才需要用核心的 **
Diff算法进行同层级比较。
8.34、Vue 中 key 的作用
vue 中 key 值的作用可以分为两种情况来考虑:
**第一种情况是 **v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
**第二种情况是 **v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速:
- **更准确:**因为带
key就不是就地复用了,在sameNode函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。 - **更快速:**利用
key的唯一性生成map对象来获取对应节点,比遍历方式更快。