HTML 和 CSS 篇
1. display:none 和 visiblity:hidden 的区别
- display:none;给一个元素设置后,该元素自身以及它内部的子元素都会被隐藏。
- visiblity:hidden:起到了隐藏的作用,但是隐藏之后还是会占据空间,元素虽然不可见,但是仍然会影响布局。具有继承性,子元素也会继承。但是如果给子元素再设置 visbility:hidden,子元素又会出现。
2.BFC 和清除浮动
- 浮动的定义: 非 IE 浏览器下,容器不设高度且子元素浮动时,容器高度不能被内容撑开。 此时,内容会溢出到容器外面而影响布局。这种现象被称为浮动(溢出)。
- 浮动:浮动的元素会脱离文档流,父元素就管不了这个叼毛了,就出现高度塌陷了
- 清除浮动:
- 我他妈直接给父元素也浮动(啥也不说了,一起开摆!爷爷级:tnnd,都浮动是吧!我咋办)
- 给父级元素添加 position 为 absolute,一样会脱离文档流
- 给父级设置 overflow:hidden(文本内容过长,会出现文本被隐藏的情况)
- 给父级设置定高,但是如果是自定义高,又出问题!
- 给末尾添加空元素,属性设置 clear,代表它左边或者右边,又或者左右都不可以有浮动(增加了新元素,会给 Dom 渲染增加负担。)
- 给父级加伪元素并且设置为 clear 属性。(元素盒子的边不能和前面的浮动元素相邻,对元素设置 clear 属性是为了避免浮动元素对该元素的影响,而不是清除掉浮动)
- BFC(块级格式上下文)
- BFC 元素的子元素会一个接着一个放置
- 内部的盒子上下的距离由 margin 决定,同一个 BFC 的元素上下 margin 会重叠
- 每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此
- BFC 的区域不会与 float 的元素区域重叠
- 计算 BFC 的高度时,浮动子元素也参与计算
- 如何触发?
- HTML 标签会触发
- 浮动:float
- overflow 的值不是 visible 会触发
- display 是 inline-block flex grid 都会触发
- position 为 fixed 和 absolute
3. CSS 怎么设置响应式布局
- 媒体查询 @media screen;根据浏览器窗口大小控制页面渲染
- 百分比布局,设置宽高百分号进行布局,随着浏览器宽高变化而变化的布局
- rem 布局;大小根据根元素 html 的 font-size 来决定的,动态改变根元素的 font-size 来实现响应式
- 视口布局 vm/vh 与视图窗口有关,vw 表示视图窗口的宽度,vh 表示视图窗口的高度
4. 选择器的优先级
- !important > 行内样式 > ID 选择器 > 类选择器 > 元素选择器 > 通配符
5. 行内元素设置 float 会发生什么?
- 元素性质会发生变化 类似于 inline-block 可以设置宽高了
6. px/rpx/em/rem/vw/vh 的区别
- px:绝对单位,按照页面像素显示元素
- rpx:相对单位,微信小程序根据屏幕尺寸的自适应大小。不同手机对应比例不一样。如某手机屏幕宽 400px,则 750rpx=400px,如屏幕宽 500px,则 750rpx=500px
- em:相对单位,按照父元素的字体大小算,如果设置了自己的字体大小就按照自己的来算,1em=(设置的父元素字体大小或者本元素字体大小)px
- rem:即 root em,根据 html 节点的字体大小计算 1rem=(html 标签字体大小)px
- vw:可视宽度 100vw = 100%页面宽度
- vh:可视高度 100vh = 100%页面高度
7. 伪类和伪元素的区别
-
伪元素:在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。它们只在外部显示可见,但不会在文档的源代码中找到它们,因此,称为“伪”元素
-
伪类:将特殊的效果添加到特定选择器上。它是已有元素上添加类别的,不会产生新的元素。例如:
8. 如何实现一个三角形
//一个边有颜色其他边设置为透明
.triangle {
width: 0px;
height: 0px;
border: 100px solid transparent;
border-bottom-color: red;
}
9. html 语义化标签
- 什么是语义化:就是说元素标签传达了关于标签所包含的一些信息
- 为什么要语义化
- 在页面没有 CSS 的情况下,也良好的代码结构
- 有利于 SEO(seo 后面会讲):爬虫依赖标签来确定关键字的权重,因此可以和搜索引擎建立良好的沟通,帮助爬虫抓取更多的有效信息
- 提升用户体验
- 便于团队开发和维护
- 方便设备解析
- 常见的语义化元素
- header
- h 类
- nav
- p
- ul ol li
10. 什么是 SEO(搜索引擎优化)
- SEO 的存在就是为了提升网页在搜索引擎自然搜索结果中的收录数量以及排序位置而做的优化行为。而优化的目的就是为了提升网站在搜索引擎中的权重,增加对搜索引擎的友好度,使得用户在访问网站时能排在前面。
- 分类,白帽 SEO 和黑帽 SEO
- 白帽:起到了改良和规范网站设计的作用,使网站对搜索引擎和用户更加友好,并且网站也能从搜索引擎中获取合理的流量,这是搜索引擎鼓励和支持的
- 黑帽:利用和放大搜索引擎政策缺陷来获取更多用户的访问量,这类行为大多是欺骗搜索引擎,一般搜索引擎公司是不支持与鼓励的
- 白帽 SEO 能干啥:
- 对网站标题,关键字,描述进行设置,反映网站定位,让搜索引擎知道网站是干什么的
- 网站内容优化:内容和关键字对应,增加关键字密度
- 生成针对搜索引擎友好的网站地图;
- 增加外部链接,到各个网站上宣传。
- 在网站上合理设置 Robots.txt 文件;(这个文件是啥)
- 作用:提高网站的权重,增强搜索引擎友好度,以达到提高排名,增加流量,改善(潜在)用户体验,促进销售的作用。
- 前端 SEO 优化:
- 网页代码的优化
- 控制首页链接数量:首页链接太少,没办法爬到内页,太多的话没有实际性的链接,影响用户体验,降低网站首页权重。收录效果不好
- 扁平化目录结构:尽量只跳转 3 次就能达到网站内任何页面
- 导航优化,可以采用关键性文字,如果是图片的加,加上 alt
- 利用布局,把重要 html 代码放前面,因为搜索引擎是从上而下检索的,可以让重要代码先被读取
11. 行内和块级元素的区别
- 行内元素不单独占一行,能设置左右的 margin padding,上下的无效,不可以设置 width height 属性,不可包含块级元素
- 常见的有 span button a
- 块级元素单独占一行,能设置宽高,具有宽高属性,可包含行内和块级元素
- 常见的有 div p h 标签 form canvas ol ul li
- img input 属于行内块级元素
12. DOCTYPE 是干嘛用的?
- 告诉浏览器应该用什么样的文档类型定义来解析文档
13. img 的 srcset 属性是干嘛的?
- 不同屏幕密度下,img 自动加载不同的图片。
14. 隐藏元素的方法有哪些?
- display: none:渲染树不会包含该渲染对象,因此该元素不会在页面中占据位置,也不会响应绑定的监听事件。
- visibility: hidden:元素在页面中仍占据空间,但是不会响应绑定的监听事件。
- opacity: 0:将元素的透明度设置为 0,以此来实现元素的隐藏。元素在页面中仍然占据空间,并且能够响应元素绑定的监听事件。
- position: absolute:通过使用绝对定位将元素移除可视区域内,以此来实现元素的隐藏。
- z-index: 负值:来使其他元素遮盖住该元素,以此来实现隐藏。
- clip/clip-path :使用元素裁剪的方法来实现元素的隐藏,这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件。
- transform: scale(0,0):将元素缩放为 0,来实现元素的隐藏。这种方法下,元素仍在页面中占据位置,但是不会响应绑定的监听事件。
15. link 和@import 的区别
- 都是外部引入 CSS 的方式
- link 引入在页面载入时间内同步加载 @import 是在页面完全引入后再加载的
- link 没有兼容性问题 @import 需要网页完全载入以后加载
- link 支持使用 Javascript 控制 DOM 去改变样式;而@import 不支持。
16. 为什么有点时候使用 translate 来改变位置而不是采用定位的方式?
- translate 是 transform 的一个属性,改变 transform 或者 optacity 不会导致浏览器发生重绘
- 改变布局的话会重新布局,同时触发回流重绘。
17. li 与 li 之间存在一个看不见的空白间隔是什么原因?怎么解决
- 浏览器会把 inline 内联元素间的空白字符(空格、换行、Tab 等)渲染成一个空格。为了美观,通常是一个
- 放在一行,这导致
- 换行后产生换行字符,它变成一个空格,占用了一个字符的宽度。
- 解决方法
- 设置浮动
- 所有 li 写在同一行,不换行
- 将
-
内的字符尺寸直接设为 0,即 font-size:0。
- 消除
-
的字符间隔 letter-spacing:-8px,不足:这也设置了
- 内的字符间隔,因此需要将
- 内的字符间隔设为默认 letter-spacing:normal。
18. line-height 的理解和赋值方式
- 文本高度,实际上是下一行到上一行的基线距离;
- 如果标签没有定义 height 属性,最终高度由 line-height 决定
- 一个容器没有设置高度,那么撑开容器高度的是 line-height,而不是容器内的文本内容;
- 把 line-height 值设置为 height 一样大小的值可以实现单行文字的垂直居中;
- line-height 和 height 都能撑开一个高度;
19. CSS 优化和提高性能的方式有哪些?
- CSS 压缩,将写好的 css 进行打包压缩,可以减小文件体积
- CSS 单一样式,用 margin-bottom,之类的来代替整个 margin 执行的效率会更高
- 减少使用@import 改为使用 link
20. z-index 在什么情况下会失效?
- 父元素的 position 为 relative 的时候 (可以换成 position)
- z-index 的同时还设置了浮动 float
- 缺少 position 属性
21. 实现一个宽高自适应的正方形
width: 20%; height: 20vw; 这边设置的是可视宽度 background: tomato;
22. 画一条宽度为 0.5px 的线
- 先画出一条 1px 的线
- transfrom 中的 scale 方法缩放 0.5
JS 基础篇
JS 数据类型
- 基本数据类型:null Number boolean undefined String symbol(es6) BigInt(es10)
- 引用数据类型 Object :Array Function Object Date()
具体讲一下 Symbol
- 用法 const symbol1 = Symbol() //没有两个 Symbol 值是相等的 而且可以作为对象的属性名,只有 字符串 和 Symbol 可以作为对象的属性名
- 创建 Symbol 的时候里面也可以传参 Symbol('a symbol') 但是即使都传了同样的参数 symbol 的值也是不相等的
1. 数据类型如何存储的
- 基本数据类型存储在栈中
- 引用数据类型存储在栈和堆中。栈中存放引用数据类型的指针,堆中存放它的实体。
- 堆和栈的区别:栈先进后出的数据结构,占据空间小,大小固定,回收的时候会被系统自动回收;堆:占据空间大,大小不固定。无序树状结构。
2. && || !!
- && 找到第一个虚值就直接返回它,没有找到一个虚值就返回最后一个真值,采用短路防止不必要操作
- || 找到第一个真值就返回它。
- !! 将右侧的值强制转换成布尔值
3. 数据类型的转换
- 转换成布尔值 Boolean()方法
- 转换成数字 Number() parseInt() parseFloat()
- 转换成字符串 调用.toString 或者 String() null 和 undefined 没有.toSring 方法
4. JS 中数据类型判断
- typeof 对于基本数据类型来说,出了 null 都能正确显示,对于引用数据类型的话只能显示 Function Array 和 Object 只会显示出 Object
- instanceof 可以正确判断引用数据类型,原理是通过原型链查找类型的 prototype,但是不能准确判断基本数据类型 使用方法 a instanceof Object
- constructor 但是如果创建一个对象改变了它的原型,constructor 会不可靠
- Object.prototype.toString.call()使用对象的原型方法.toString 返回值比较特殊 '[object Array]'字符串类型
5. JS 内置对象
- 数字和日期对象 Number Math Date
- 值属性 Null undefined NaN
- 函数属性 parseInt() parseFloat() eval()
- 字符串 String RegExp
6. undefined 和 null 区别
- undefined 代表含义未定义,null 代表空对象,定义来用于赋值给可能返回对象的变量
7. {}和[]的 valueOf 和 toString 的结果是什么?
- {}的 valueOf 结果是{} toString 结果是 "[object Object]"
- []的 valueOf 结果是[] toString 结果是 ""
8. JS 的作用域和作用域链
- 作用域:定义变量的区域,它有一套访问变量的规则
- 作用域链:作用域链的作用是保证对执行环境有权访问的所有变量和函数有序访问,通过作用域链,可以访问到外层环境的变量和函数
- 作用域链的本质是指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。全局执行上下文的对象始终是作用域链最后一个对象。
- 当我们查找一个变量时,如果当前执行环境没有找到就会沿着作用域链向后查找。
9. JS 创建对象的几种方式,一般采用字面量方式创建函数,但是创建大量相似对象的时候会产生重复代码。
1.Object构造函数创建 const Person = new Object() 2.工厂模式创建 function createPerson(name,age,sex){ const o = new Object() o.name = name o.age = age o.sex = sex o.sayName = ()=>{ console.log(this.name) return 0 } } 优点:解决创建多个相似对象时,代码复用的问题 缺点:无法解决对象识别问题,没法知道一个对象的类型是什么 3. 构造函数创建: funtion person(name,age,sex){ this.name = name this.age = age this.sex = sex } 优点:解决工厂模式下对象类型无法识别的问题。可以将它的实例标识为一种特定的实例 缺点:函数中的每个方法都会在实例中再创建一遍,造成内存浪费。 4. 原型创建 利用函数的prototype属性 function Person(){} Person.prototype.name ="a" Person.prototype.age = 20 Person.prototype.sex = "男" 优点:解决了构造函数模式多次创建相同函数对象的问题,所有的实例可以共享同一组属性和函数。 缺点:所有实例默认情况下都会取得默认的属性值。所有实例都共享某一个属性,对于包含基本值的属性没问题,对包含引用类型的值来说,会导致读写混乱 5.组合使用构造函数模式和原型模式 function Person(name,age,sex){ this.name = name this.age = age this.sex = sex } Person.prototype ={ constructor :Person sayName:function(){ console.log(this.name) } } 优点:采用构造器和原型模式; 缺点:对于代码的封装性不太好 6.动态原型模式 function Person(name,age,sex){ this.name = name this.age = age this.sex = sex if(typeof this.sayName !=="funciton"){ Person.prototype.sayName:function(){ console.log(this.name) } } } 7.寄生构造函数模式 function Person(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ console.log(this.name); }; return o; }
11. 前端继承实现方式
1.构造函数继承 function Father() { this.name = ["芜湖","起飞","大司马"] } function Child() { Father.call(this) } const child1 = new Child() child1.name.push("韩金龙!") const child2 = new Child() console.log(child1, child2); 优点:避免引用类型的属性被实例共享,可以从子向父级传递参数 例子 function Father(name) { this.name = name } function Child(name) { Father.call(this,name) } const child1 = new Child("卧槽!") // child1.name.push("韩金龙!") const child2 = new Child("挺牛的") console.log(child1, child2); 缺点:方法都在构造函数上定义,每次创建实例都会创建一个方法!浪费性能 2. 原型链继承 function Parent() { this.name = "父母"; } Parent.prototype.getName = function () { console.log(this.name); }; function Child(){ } Child.prototype = new Parent() const child1 = new Child() console.log(child1.getName()) 缺点:引用类型的属性会被所有实例共享啊!创建Child实例的时候,也不能向parent传参 3.组合继承的方式 融合二者的优点,常用的方式 function Parent(name) { this.name = name; this.colors = ["red", "blue"]; } Parent.prototype.getName = function () { return this.name; }; function Child(name, age) { Parent.call(this, name); this.age = age; } Child.prototype = new Parent(); const child1 = new Child("卧槽", 18); console.log(child1.colors()); 4.直接通过原型继承 function createObj(o){ function F(){} F.prototype = o return new F() } //和原型链继承一样,包含引用类型的属性值始终会共享相应的值 5.寄生式继承 function createObj(o) { const clone = Object.create(o); clone.sayName = function () { console.log("wdnmd"); }; return clone; } //和构造函数继承一样的问题,每次创建对象就会创建一次方法,性能浪费 6.寄生组合式继承 function Parent (name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function () { console.log(this.name) } function Child (name, age) { Parent.call(this, name); this.age = age; } Child.prototype = new Parent(); //第一次创建子类型原型的时候 var child1 = new Child('kevin', '18'); //创建子类型实例的的时候 console.log(child1) 缺点:会调用两次父构造函数
12. this,apply,call,bind
- 浏览器中,全局范围内的 this 指向 window 对象
- 函数中,this 永远指向最后调用它的那个对象
- 构造函数中,this 指向 new 出来的那个新对象
13. 箭头函数
- 本身没有 prototype,所以本身就是没有 this 的
- 箭头函数的 this 为父级作用域的 this,而且不是调用时候的 this,在箭头函数被创建的时候 this 指向就已经确认下来了
- 没有 arguments 对象(使用的是那个剩余运算符代替 )
- 不能作为构造函数使用
14. JS 原型和原型链,有何特点
- js 构造函数创建对象,每一个对象里头都有一个 prototype 属性值,属性值是个对象,对象包含了所有实例共享的属性和方法。 当我们使用构造函数新建一个对象的时候,对象的内部将包含一个指针,指向构造函数的 prototype,这个就是原型
- 如何访问原型 proto ,也可以 Object.getPrototypeOf()方法获取,p.constructor.prototype 方法
- 原型链 我们对象的一个属性的时候,如果这个对象内部不存在这个属性,它就会去它的原型里查找,原型内部又有原型,形成一种链式的结构
15. 什么是闭包?(原理就是作用域链)
- 有权访问另一个函数作用域内变量的函数,常用方式就是在一个函数内部创建另外一个函数呗。
- 用途:
- 让我们在函数外部就可以访问到函数内部存在的变量,在外部调用闭包函数,从外部访问到函数内部的变量,可以用这种方法创建私有变量
- 保存变量在内存中,不会被垃圾回收机制回收
16. 浏览器的三种事件模型
- DOM0 级模型,事件不会传播,没有事件流的概念,可以在网页直接监听函数,也可以通过 js 属性来指定监听函数
- IE 事件模型:两次过程 事件处理阶段以及事件冒泡阶段,处理阶段会首先执行元素绑定的监听事件。事件冒泡阶段,从目标元素冒泡到 document 依次检查经过的节点是否绑定了事件监听函数,如果有则执行。
- DOM2 级模型:一次事件共有三个过程,第一个过程是事件捕获阶段,事件处理阶段以及事件冒泡阶段
17. DOM 操作——怎么添加、移除、移动、复制、创建和查找节点
- 创建新节点
- createDocumentFragment() //创建一个 DOM 片段
- createElement() //创建一个具体的元素 * createTextNode() //创建一个文本节点
- 添加、移除、替换、插入
- appendChild(node)
- removeChild(node)
- replaceChild(new,old)
- insertBefore(new,old)
- 查找
- getElementById();
- getElementsByName();
- getElementsByTagName();
- getElementsByClassName();
- querySelector();
- querySelectorAll();
- 属性操作
- getAttribute(key);
- setAttribute(key, value);
- hasAttribute(key);
- removeAttribute(key);
18. Ajax 是什么,如何创建一个 Ajax
- ajax 是一种异步通信方式,通过直接由 js 脚本向服务器发起 http 通信,根据服务器返回的数据,更新网页相应的部分,而不是刷新整个页面的方法
- 步骤:创建 xhr 对象——>配置 ajax 请求地址,发送请求,监听请求,接收响应。
手写ajax const xhr = window.XMLHttpRequest(); xhr.open("get", "sss.xml", true); xhr.send(null); xhr.onreadystatechange = function(){ if(xhr.readyState ==4 && xhr.status==200){ console.log(xhr.responseXML) } }
19. JS 延迟加载有哪些方式?提高页面渲染速度
- js 放在文档底部,最后渲染执行
- 给 js 脚本添加 defer 属性,会让脚本的加载和文档同步解析,然后文档解析完成之后再执行这个脚本;多个设置 defer 属性按规范来说是顺序执行的,但是有些浏览器可能不是,这个得注意一下
- 给 js 脚本添加 async 属性,会让脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,如果这个时候文档还没有解析完的话可能会阻塞。多个 async 属性执行顺序是不可预测的。
- 动态创建 DOM 标签,我们可以对文档的加载事件进行监听,当文档加载完成后再动态创建 script 标签来引入 js 脚本。
20. JS 模块化开发的理解
- 一个模块用来实现一个特定功能的方法,最开始 js 实现的功能比较简单,没有模块的概念,后面随着程序越来越复杂,代码模块化就变得比较重要了。
- 由于函数具有独立作用域的特点,最原始的模块化写法就是用函数作为模块,几个函数一个模块,但是这种方式容易造成全局变量污染,并且哥各个模块之间没有什么关联,缺乏交互性
- 后面提出对象的写法,通过将函数作为一个对象来实现,这样的话解决了函数带来的问题,但是外部代码可以修改内部的属性。
- 最常用的是立即执行函数写法,利用闭包来实现模块私有作用域建立,同时不会污染全局作用域
21. JS 模块规范有几种
- CommonJS 方案,通过 require 来引入的,通过 module.exports 来定义模块的输出接口,但是这种方式是同步引入的,通常应用在服务端,因为服务端的数据都是存储在磁盘中的,读取的时候是非常快速的,但是在浏览器端模块加载是使用网络请求,因此需要用异步请求
- AMD 方案:采用的是异步加载方式,不影响后面引用,所有依赖这个模块的语句都定义在一个回调函数里面,等加载完再去执行回调函数。require.js 就是这种方案
- CMD 方案,也是异步加载方式,sea.js 实现了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
- ES6 方案,import 和 export 的方式导入导出模块
22. AMD 和 CMD 模块的区别
- 模块定义的时候对依赖的处理是不同的。AMD 在定义模块的时候就要求申明依赖,而 CMD 推崇就近依赖,用到某个模块的时候再去 require
- 第二个是对依赖模块的执行时机处理不同,都是异步加载,不同在于:AMD 是依赖加载完成之后就立即执行依赖模块,执行顺序而且和我们写上去的不一定会一致;CMD 则是在于模块加载完之后不立即执行,只是下载好放在那里,所有模块都下载好,才进入回调函数逻辑。遇到 require 的时候才会去执行对应模块,和我们书写的顺序是一致的
23. ES6 模块与 CommonJS 模块、AMD、CMD 的差异
- CommonJS 模块输出的是一个值的拷贝,es6 输出的是值的引用;Commonjs 输出的一个值,一旦模块内部发生变化之后,值是不会变的。
- CommonJS 模块在运行时加载,es6 模块是编译时输出接口。CommonJS 模块就是对象。
24. requireJS 原理时什么?
- 通过动态创建 script 脚本来引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。
25. js 运行机制
- JS 最大的特点是单线程语言。为什么会是单线程的,多线程不好吗?JS 主要作用就是和用户互动以及操作 DOM,假设如果 JS 多线程,一个线程你在操作某个 DOM,另一个线程在删除这个 DOM,那谁会先执行呢?会存在冲突。
- 事件轮询
- 同步和异步;一般页面渲染和页面结构的,耗时比较短的都是同步任务。一般加载音频,获取图片的都走异步。
- 任务进入执行栈之后,会去先判断是同步还是异步执行的 ——> 同步的话就会进入主线程执行 ——> 异步进入 Event Table 并且注册回调函数———>进入事件队列中 ——> 主线程任务执行完毕之后,才会去事件队列读取函数进入主线程执行。然后往后执行,不断重复这样一个过程。
- 主线程执行栈何时为空呢?js 引擎存在 monitoring process 进程,会连续不断的检查主线程中是否是空的,如果是空的就回去任务队列里检查是否有等待被调用的函数。
- 标准回答:
- JS 是单线程语言,执行代码的时候会将不同函数的执行上下文压入栈中,保证代码是有序执行的。
- 在执行同步代码的时候,如果遇到了异步事件,js 会继续执行执行栈中的代码,而将异步事件挂起
- 执行栈中的所有同步任务执行完毕,系统就会先执行微任务队列里的任务,然后执行宏任务队列里的一个宏任务,期间有可能会引发出新的微任务,所以会再次指向微任务队列里的任务,然后再回来执行下一个宏任务
26. arguments 对象是什么。
- 函数中传递参数值的集合。类似数组的对象,箭头函数是没有这个对象的。
27. 浏览器的垃圾回收机制
- 程序工作过程中会产生很多的垃圾,就是程序不用或者之前用过了的,以后不需要再占据内存空间,这个时候就需要进行垃圾回收,浏览器的垃圾回收机制是我们无法感知的。不是所有语言会有垃圾回收机制,C 和 C++就没有,需要我们自己管理内存。
- 浏览器是如何进行垃圾回收的呢?
- 垃圾回收策略:存在一个概念可达值,某些可以访问或者有用的值。如何进行垃圾回收实际上就是发现这些不可达值的对象并给予清理。
- 垃圾回收机制的原理就是 定期 找出不可达值对它们进行清理。疑问:?为什么不进行实时清理?主要是实时清理的开销比较庞大
- 垃圾回收机制中的两种方法:标记清除法,引用计数法。
- 标记清除法:JS 中最常用的一种方法。多数浏览器采用这种方式,有的浏览器厂商还对其算法进行了优化。不同浏览器垃圾回收机制频率上有所差异。分为标记和清除两个阶段呗,标记就是给所有可活动对象都加上标记,清除就是把没有标记上的非活动对象清除。执行过程大致就是
- 垃圾收集器运行时会给所有变量都标记,假设全部都是垃圾,标记为 0
- 然后各个根对象开始遍历,把不是垃圾的标记上 1
- 清理所有标记为 0 的对象,销毁回收他们占用的内存空间
- 最后再把所有内存对象都标记上 0,等待下一次的垃圾回收。
- 优点就是比较简单
- 缺点就是清除之后内存位置不变,导致空闲空间不连续(内存碎片问题);分配速度慢,每次都需要遍历
- 引用计数法:很少使用了,就是有没有引用指向这个对象,没有就垃圾回收这个对象。
- 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值的引用次数就为 1
- 如果同一个值又被赋给另一个变量,那么引用数加 1
- 如果该变量的值被其他的值覆盖了,则引用次数减 1
- 当这个值的引用次数变为 0 的时候,说明没有变量在使用,这个值没法被访问了,回收空间,垃圾回收器会在运行的时候清理掉引用次数为 0 的值占用的内存
- 标记清除法:JS 中最常用的一种方法。多数浏览器采用这种方式,有的浏览器厂商还对其算法进行了优化。不同浏览器垃圾回收机制频率上有所差异。分为标记和清除两个阶段呗,标记就是给所有可活动对象都加上标记,清除就是把没有标记上的非活动对象清除。执行过程大致就是
28. V8 对垃圾回收机制的优化(针对标记清楚法)
-
分代式垃圾回收:将堆内存分为新生代(存活时间短),老生代(存活时间长或者常驻,通常比较大),V8 采用两个垃圾回收器,新生代垃圾回收,老生代垃圾回收器
-
新生代垃圾回收器:主要采用 Scavenge 算法,主要是复制式方法 Cheney 算法。将新生代的堆内存一份为二,分成使用区和空闲区,新加入的对象会放到使用区,使用区快被写满时,就需要执行一次垃圾清理,执行过程如下:
- 当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记
- 标记完成之后将使用区的活动对象复制进空闲区并进行排序
- 随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。
- 最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区
-
注:当一个对象复制多次后依然存活,就会被认为时生命周期比较长的对象,之后被移动到老生代;又或者复制一个对象到空闲区的时候,空闲区占用超过 1/4,也会进入老生代。
-
老生代垃圾回收;老生代存活时间长,占用空间大,所以不会频繁的执行垃圾回收,采用的是标记清除。
-
采用并行回收:所谓并行,也就是同时的意思,它指的是垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作
29. es6 有哪些新特性?
- 块级作用域
- 类
- 箭头函数
- 模板字符串
- 解构对象
- Promise
- Symbol
- 函数默认参数
30. var,let,const 的区别?
- var 声明的变量会挂载在 window 上,let,const 则不会
- var 声明的变量存在变量提升,提升至 作用域 的顶部
- let const 有块级作用域
- 同一作用域下 let 和 const 不能声明同名变量,var 却可以
- const 定义常量,let 定义变量,const 定义完之后值无法修改,修改了就会报错,但是如果定义引用类型,只要引用类型的地址不变,就可以修改引用类型。
31. 为什么会出现变量提升?
- 变量提升的原理:js 的编译过程是一边编译一遍执行的,所有的 JS 代码在执行之前都会被编译,只是编译的时间非常短,紧接着代码就会执行。就是短暂编译的过程里,js 引擎会搜集所有的变量声明,并且提前让声明生效。
32. 全局声明 let 和 const 不挂载在 window 下,那挂载在哪里了?
- 挂载在 script 上和 Global 同级,window 则可看作是 global 的代理。
33. Set,Map 是啥?(新增的数据结构)
- 存储任何类型的唯一值。
- const arr = [1, 1]
const obj = {
a: 1
}
const set2 = new Set([1, 1, 2, 2, 3, 3, "a", "a", "d", "d", [],
[], {}, {},
arr, arr, obj, obj
])
console.log(set2) //Set(11) { 1, 2, 3, 'a', 'd', [], [], {}, {}, [ 1, 1 ], { a: 1 } }
- const arr = [1, 1]
- WeakSet 和 Set 类似,但是 WeakSet 的成员只能是对象;
- Map 类似于对象,也是键值对的集合,但是 Map 的键范围可以不限于是字符串,各种类型的值都可以是键。
- WeakMap 只接收对象作为键名:null 除外!!而且键名指向的对象不计入垃圾回收
34. 什么是高阶函数:将函数作为参数或者返回值的函数。
35. 手写一个 Map 方法
function myMap(arr,optionCB){ //做个简单判断,是否传进来的是数组,是否数组是空,是否存在回调函数 if(!Array.isArray(arr)||!arr.length||typeof optionCB !=='function'){ return [] }else{ let result for(let i =0;i<arr.length;i++){ result.push(optionCB(arr[i],i,arr)) } return result } }
36. 手写 filter 方法
function myFilter(arr, optionFunc) { if (!Array.isArray(arr) || !arr.length || typeof optionFunc !== "function") { return [] } else { let result = []; for (let i = 0; i < arr.length; i++) { if (optionFunc(arr[i], i, arr)) { result.push(arr[i]) } } return result } } function option(item, index, arr) { return item > 5 } console.log(myFilter([1,2,3,4,5,6,7],option))
37. 手写 reduce 方法
function myReduce(arr, callBack, initialValue) { if (!Array.isArray(arr) || !arr.length || typeof callBack !== "function") { return []; } else { let hasInitialValue = initialValue !== undefined; let value = hasInitialValue ? initialValue : arr[0]; for (let i = hasInitialValue ? 0 : 1; i < arr.length; i++) { value = callBack(value, arr[i], i, arr); } return value; } } function option(prev, nowCur, index, arr) { console.log(prev, nowCur, index, arr); return prev + nowCur; } console.log(myReduce([1, 2, 3, 4, 5], option));
38. 深浅拷贝
- 浅拷贝:创建一个新对象,对原来对象所有的属性值精确拷贝,如果属性时基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是引用类型的地址。如果一个对象引用类型改变了,那么其他的也会受到影响
- 实现方法:Object.assign();
- 拓展运算符(这个不能算深拷贝,也不能算浅拷贝,只能深拷贝一层而已)
- 深拷贝:将一个对象从内存中完整的拷贝出来,从堆中开辟一个新类型进行存储这个对象 *实现方法:JSON.parse(JSON.stringify(obj)) 会忽略 undefined;symbol,函数,也不能循环引用,也不能处理正则和日期。
- structuredClone(obj), 这个目前兼容性不好,但是 Chrome 和火狐还是支持的。
//浅拷贝手写方法 function clone(obj) { if (typeof obj === "object" && obj !== null) { const cloneObj = Array.isArray(obj) ? [] : {}; for (let item in obj) { if (obj.hasOwnProperty(item)) { cloneObj[item] = obj[item]; } } return cloneObj; } else { return -1; } } console.log(clone(obj)); //深拷贝 function deepClone(obj) { if (typeof obj === "object" && obj !== null) { let cloneObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { if (typeof obj[key] === "object" && obj[key] !== null) { cloneObj[key] = deepClone(obj[key]); } else { cloneObj[key] = obj[key]; } } } return cloneObj; } else { return -1; } } console.log(deepClone(obj));
40. 函数柯里化:柯里化就是将使用多个参数的一个函数转换成一系列使用使用一个函数参数的技术。
//简单写法 function curry(fn, currArgs) { return function () { let args = [...arguments]; //最开始调用的时候如果没有提供currArgs那么就不拼接参数 if (currArgs !== undefined) { args = args.concat(currArgs) } //通过fn.length,可以获取到函数参数的长度 if (args.length < fn.length) { return curry(fn, args) } //递归出口 参数个数相等就把参数传递给fn return fn(...args) } }
41. JS 实现 new 操作符
function person(name, age) { this.name = name; this.age = age; } function myNew() { const obj = {}; //获取第一项 const constructor = [].shift.call(arguments); obj.__proto__ = constructor.prototype; //apply接收一个数组 const result = constructor.apply(obj, arguments); return typeof result === "object" ? result : obj; } const p = myNew(person, "卧槽", 12); console.log(p)
42. 回调函数,以及其特点?
- 回调函数:函数作为参数传递给另一函数,缺点十分明显;回调函数过多,造成地狱回调(callback hell),不利于维护和阅读,而且也不能直接 return
43. Promise 是什么?(承诺)
- 存在三种状态:pending,起始状态;fulfilled 意味着操作完成;rejected 操作失败。
- 一旦承诺状态改变了就无法再变回去了,状态无法更改;
- 缺点也是有的;无法取消 Promise,出现错误后需要回调函数捕获。
new Promise((resolve, reject) => { setTimeout(() => { console.log(1) resolve() }, 1000) }).then(() => { setTimeout(() => { console.log(2) }, 1000) }).then(() => { setTimeout(() => { console.log(3) }, 1000) }).catch(() => { console.log("不执行") }).finally(() => { console.log(5) })
-
.then 方法的第二个参数
- 也是 reject 和 catch 是就近原则,如果 Promise 同时存在 catch 和 then 的第二个参数,reject 抛出错误之后,只有 then 的第二个参数可以捕获到,如果 then 的第二个参数不存在才会 catch 捕获
-
.all 和 .race 语法
- 都会返回一个 Promise
- .all 语法 :最终都转化为 fulfilled 态时,则会执行 resolve 回调,并将返回值是的所有的 Promise 的 resolve 的回调的 value 的数组。其中一个任何 Promise 为 reject 状态时,则返回的 Promise 的状态更改为 rejected
- race: 传入的所有 Promise 其中任何一个有状态转化为 fulfilled 或者 rejected,则将执行对应的回调。(谁先执行就执行谁)
-
如果.all 和 .race 传入的是空数组会出现什么情况呢?
- .all 会立刻决议,执行 resolve
- .race 返回的 promise 会一直保持 pending 状态。
44. 手动实现 Promise
//手写Promise function myPromise(constructor) { let self = this //这边为什么要定义这样一个参数呢,直接用this不好码? self.status = "pending" //初始状态 //之所以这么命名时为了满足Promises/A+的规范 self.value = undefined; self.reason = undefined function resolve(value) { //解释原因,这边如果直接用this指向的话myPromise是改变不了的,this指向的是resolve if (self.status === 'pending') { self.value = value self.status = "fulfilled" } } function reject(reason) { if (self.status === 'pending') { self.reason = reason self.status = "rejected" } } try { constructor(resolve, reject) } catch (err) { //捕获异常 reject(err) } } //链式调用 myPromise.prototype.then = function (fulfilled, rejected) { let self = this if (self.status === "fulfilled") { fulfilled(self.value) } if (self.status === "rejected") { rejected(self.reason) } } //验证 const p = new myPromise((resolve, reject) => { console.log(1) resolve() }).then(() => { console.log(2) })
45. async/await 如何工作以及优缺点(es7 的语法!)
-
async 写在一个函数前面,声明这个函数是异步函数
-
await 即 async wait 等待异步函数执行完成
-
一个函数前面加上 async,那么这个函数就会返回一个 Promise
-
优点在于处理 then 的链式调用的时候看起来非常简洁
-
怎么实现的 async/await?(用到的是 ES6 里的迭代函数——generator 函数)
- generator 是个什么函数 就是函数写法上多了个*号而已,内部有一个 yield,就是函数的中途暂停点
- yield 后面也可以接一个函数
//generator function* gen() { yield 1; yield 2; yield 3; return 4; } const g = gen(); console.log(g.next(), g.next(), g.next(), g.next()); //{ value: 1, done: false } { value: 2, done: false } { value: 3, done: false } { value: 4, done: true }
46. 手写 instanceof
const myInstanceof = (left, right) => { let prototype = right.prototype left = left.__proto__ while (1) { if (left === null || left === undefined) { return false } if (prototype === left) { return true } left = left.__proto__ } }
47. 显式原型 prototype 和隐式原型proto?
- 对象具有属性(** proto**)称为隐式原型,对象的隐式原型指向构造该对象的构造函数的显式原型 (prototype)
- 二者关系 隐式原型指向创建这个对象的构造函数的 prototype
- prototype 是专属于函数的一个属性的,类型是对象,为了给将来自身所在的构造函数被 new 出来的实例做父级使用的。
- proto专属于对象的一个属性,类型也是对象,查找对象父级的
48. 防抖和节流
- 防抖:在事件被触发 n 秒后执行回调,如果在这 n 秒内事件又被触发,则会重新计时。
- 节流:规定一个单位时间,在这个单位时间呢,只能有一次触发事件的回调函数,如果在同一时间触发多次,只生效一次。
//实现一个防抖函数 function debouce(fn, wait) { let timer = null; return function () { let args = arguments; clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, wait); }; } //实现一个节流函数 function throttle(fn, wait) { let timer = null; return function () { let args = arguments; if (!timer) { timer = setTimeout(() => { fn.apply(this, args); timer = null; }, wait); } }; }
49. JSON 字符串和 JS 对象互转
- JSON.stringfy:接收一个 JS 对象转换成 JSON 字符串
- JSON.parse:接收一个 JSON 字符串转化成 JS 对象
50. for in 和 for of 的区别
- for in 遍历的式索引
- for of 遍历的是值 但是 for of 没法遍历对象(可遍历数组,字符串,其实任何具有 Symbol.iterator 属性的值都可以用 for of 遍历)
- Symbol.iterator 迭代器
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列
- ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 使用
51. 合并对象的方法
- Object.assign(obj1,obj2)
- 拓展运算符
- hasOwn
52. 遍历对象的方法
- Object.keys 遍历的是对象的 key,返回的是一个数组
- Object.values 遍历的是对象的 value,返回的也是数组
- Object.entries 遍历对象的键值对,返回的还是数组!
53.合并数组的方法
- concat
- 拓展运算符
- for 循环
- push 和 apply 并用
54.setTimeout 和 setInterval
- setTimeout :传参:回调,时间(ms),params,传递给回调函数的参数,一般不常用,IE9 和更早版本是不兼容
- setInterval 同理;只不过执行完了还会多次执行。
- setTimeout 有个最短延迟时间:如果延迟时间短于 0,则将延迟时间设置为 0。如果嵌套级别大于 5,延迟时间短于 4ms,则将延迟时间设置为 4ms。
- 因为 javascript 是单线程编程,它把任务放到队列中,不会同步去执行,必须在完成一个任务后才开始另外一个任务。setTimeout 执行只是把任务放到代码中,没有立马去执行,所以说接下去的代码接着跑,直到 js 这个任务执行完成,才有可能往下执行。
55. ==操作符的强制转换规则?
- 双方同一类型,比较大小
- 判断双方类型,双方类型不一样就进行类型转换。
- 会先判断null和undefined,返回true
- 是否是字符串和Number,是的话把字符串转换成number
- 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
- 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
56. 哪些情况会导致内存泄漏
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
Vue篇
1. Vue 常见修饰符
- v-on 语法糖 @
- .stop 阻止事件冒泡
- .prevent 阻止默认事件
- .capture 事件捕获
- .native 监听组件根元素的原生事件
- .left 点击鼠标左键触发
- .right 点击鼠标右键触发
- .middle 鼠标中键触发
- .once 只触发一次
2. MVC 和 MVVM 模型
- MVC 模型 Model-View-Control;交互的 Control 将 model 的数据渲染到视图上面。
- MVVM 模型 Model-View-ViewModel;双向绑定的;就是将 View 转化成 modle,modle 转化成 View;实现了 View 和 Model 自动同步;就是当 Model 改变的时候,不用再自己手动操作改变视图。
- 区别:简化了用选择器操作 Dom 的操作,再 MVVM 中;View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性
- Vue 采用的是 MVVM 的框架:但是也不完全是,因为 MVVM 中 View 和 Model 不可以相互通信,但是 Vue 可以用 ref 来实现。
3. 为什么 Data 是一个函数
- 因为一个组件可能会被拿到多个地方去调用;每一次调用都会调用 Data 去执行;如果 data 是一个对象的话,多次调用组件的时候都会去改变 data,会导致数据污染,如果是函数的话每次返回的都是一个新的对象,data 的引用地址每次都是不一样的
4. Vue 的内部指令有哪些?
- v-if 和 v-show;相同点都是控制视图出现或者不出现;
- 不同点:v-if 有配套的 v-else v-else-if v-show 没有
- v-show 在首次渲染的时候,开销更大,v-if 首次的时候开销小;如果是频繁的进行切换的话 v-if 开销更大,推荐使用 v-show
- v-if 可以搭配 template 使用
- v-show 只是简单的控制元素的 display 进行切换,v-if 控制的是真正的元素渲染和销毁
- v-for;循环渲染,可以同时渲染多个相同元素。
- v-slot;插槽,比如说如果一个封装好的组件,在不改变原组件的情况下,加入插槽,可以个性化处理,在不同地方调用相同组件,但是可以在组件中额外加入别的新元素。
- v-bind,语法糖 ':',动态绑定各种数据,class,style 等等都可以
- v-model:双向绑定
5.组件之间传值方式有哪些?
- 父组件传给子组件,子组件用 props 接收
- 子组件传递给父组件
- 子:this.$emit('自定义事件名称', 数据) 子组件标签上绑定@自定义事件名称 = '回调函数'
- 父:methods: { 回调函数() { //逻辑处理 } }
- $refs 进行传值,前提先设置了 ref
- $parent和$children 进行传值
- Vuex 进行传值
- eventBus 公交车传值,知道,但没用过
- v-model 传值 利用名为 value 的 prop 和名为 input 的事件给子组件传值,子组件在 props 里接收 value(被考到过)
- provide/inject 传值 :祖先组件通过 provide 定义变量后,无论组件层次有多深,它的子孙后代组件都能够通过 inject 获取变量值。
- 作用域插槽
6.路由模式
- 先搞懂什么是 SPA 单页面应用,就是 Web 只有一个页面,但是却可能有不同的视图,页面加载好了之后,不会根据用户操作而进行页面重新跳转和加载。利用 JS 操作 HTML 来实现动态的切换
- 前端路由的由来:最开始网页是有多个页面的,之后出现了 ajax,就慢慢发展出今天的 SPA,但是 SPA 是通过 JS 操作 HTML 改变视图,页面变化了,但是 url 却变化不了,就带来了问题。
- 无法记录用户操作
- SPA 中虽然由于业务的不同会有多种页面展示形式,但只有一个 url,对 SEO 不友好,不方便搜索引擎进行收录
- 前端路由:保证一个 HTML 页面的情况下,和用户交互的时候不刷新页面,跳转页面的同时,为每个视图的展示形式提供一个匹配的 URL:需要做到的就是
- 改变 url 的时候不让服务器去发送请求
- 可以监听到 url 的变化。
- 路由的两种模式
- hash 模式
- 用#拼接在真实的 url 后面,#后面的路由发生变化,浏览器不会重新发送请求,而是触发 onhashchange 事件。
- 实现就是通过 location.hash 实现的,比如 location.hash = '#abc'
- 通过 window.onhashchange 来监听 hash 改变,从而无刷新页面进行跳转
- 不会提交到服务端,在前端创建前端销毁
- history 模式
- 允许开发之直接更改路由,更新 url 地址而不去发起请求,通过调用 window.history 对象上的一系列方法(pushState()和 replaceState)来实现无刷新跳转
- 在不进行页面刷新的情况下操作修改浏览器的历史记录,不同的是,pushState 是新增一个历史记录,replaceState 是直接替换当前的历史记录
- pushState 和 replaceState 无法通过对 popState 监听来查看变化。接收三个参数 state,title 和 url。
- history 其他的 api :window.history.back,回退,window.history.forward,前进,window.history.go(-1)前进或者后退多少步。window.history.length;可以查看历史堆栈中页面的数量。这些操作执行了之后会可以监听 popState 进行监听,或者点击浏览器前进后退,也可以监听;
- hash 模式
7.如何动态设置 class 和 style
:class="{"red":isRed,'is-active':active}" :class="["isActive",isRed:"red":""]" :style="{ color: textColor, fontSize: '18px' }" :style="[{ color: textColor, fontSize: '18px' }, { fontWeight: '300' }]"
8. computed 和 watch 有何区别?
- computed 是依赖一个变量去计算目标变量,而且有缓存的机制,如果依赖不变情况下直接读取缓存进行复用,而且不可以处理异步。多个变量在一起计算出一个变量
- watch 是监听某个值是否发生变化,如果值发生变化,就可以执行相应的某些方法,可以进行异步操作,可以接收新变化的值和原来的值。一个变量变化决定多个变量的变化。
9. Vue 的生命周期。
- beforeCreate:实例了 Vue 实例但是还没有进行数据初始化,以及响应式处理
- created:数据已经被初始化和响应式原理,可以访问到数据,也可以修改数据
- beforeMount:render 函数执行,生成虚拟 Dom,但是还没有转化成真实的 Dom
- Mounted:真实的 Dom 挂载完毕
- beforeUpdate:数据更新之后,新的虚拟 Dom 生成,但是还没有和旧的虚拟 Dom 对比
- Update:新旧虚拟 Dom 对比打补丁之后,进行真实的 Dom 更新。
- beforeDestory:实例销毁之前调用,但是仍旧可以访问到数据
- Destoryed:实例销毁后调用。
10. 为什么不建议 v-if 和 v-for 一起使用:
- v-for 的优先级是高于 v-if 的,如果这样操作,就是会先渲染 Dom,之后再去判断 v-if,这样会多渲染出无用的 Dom 操作,造成性能浪费。
11. Vuex 的相关知识点。
- 核心属性:state,getters,mutations,actions,modules - Vuex状态存储在state里,改变状态的唯一的方法,就是显式的提交mutation,$commit - 状态如果是对象的话,引用类型,需要进行深拷贝。 - Mutation:唯一更改store状态的方法,必须是同步代码 - actions:是用来提交Mutation的,不是直接变更状态,可以包含异步的操作 - Module:支持Store拆分成多个Store且同时保存单一状态树。
12. 不需要响应式的数据应该怎么处理。有一些数据从头到尾都是没有改变过的,就不需要做响应式处理了,不然会无意义的消耗性能
- 把数据放到data中的return外部,写死的数据不需要去改变 - 或者用Object.freeze()
13. watch 中的某些属性
- 监听基本数据类型的时候:value(){} - 监听引用数据类型的时候: watch:{ obj:{ handler(){ //内部执行回调 }, deep:true 是否进行深度监听 immediate:是否初始化就执行watch中的回调 } }
14. 父子生命周期的运行顺序
父beforeCreate ——> 父created——>父beforeMount———>子beforeCreate——>子 created ——>子父beforeMount———>子Mounted———> 父Mounted
15. Vue 中的插槽的使用原理。
给子组件内部加入slot标签,匿名插槽和具名插槽两种, - 匿名插槽可以不用设置名字 - 具名插槽的写法<slot slot ="name"></slot>旧语法;<slot v-slot:name></slot>或者#表示v-slot新语法。 - 作用域插槽,把子组件的值传给子组件的slot<slot name= 'name' :value1='child1'> 这就是默认值1</slot>,父组件在调用的时候<template v-slot:name='slotone'> {{ slotone.value1 }} </template>
16. 为什么不建议使用 index 作为 key 值(哈哈哈哈我特么每次都这么写!)
- 绑定了数组的 index,如果对数组进行改变了,很有可能会导致数组内部的 index 对应的原先的数组变化了,比如说在数组最前面加入一个值,渲染的时候每一项都会往后移动,每个 item 对应的 index 值都发生变化了,造成性能上的浪费。
- 当然也不能用随机树,同理。
17. this.$nextTick 是干啥用的?
- vue 用异步队列的方式来控制 Dom 更新和 nextTick 回调后执行
- microtask 因为其高优先级特性,能确保队列中的微任务在一次循环前被执行
- 因为兼容性问题,vue 不得不做了 microtask 向 macrotask 的降级方案
- 是怎么监听到 Dom 更新的呢?
- 调用监听 Dom 改动的 API :MutationObserver,这个可以监听到节点属性、文本内容、子节点等的改动。
18. keep-alive 相关
- keep-alive:是一个全局组件 包裹动态组件的时候,会缓存不活动的组件 ,不会被渲染在页面上(abstract 布尔值)。
- 接收三个参数 include 名字匹配成功的组件会被缓存 exclude 匹配成功的不会被缓存 max 限制缓存组件的最大数量
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount"> <component :is="currentComponent"></component> </keep-alive>
-
在生命周期中做了啥
- created 初始化一个 cache 和 keys,前者用来存缓存组件和虚拟 DOM 集合,后者用来缓存组件的 key 的集合
- mounted 实时的监听 include 和 exclude 的变化
- desytroyed 删除缓存相关内容(执行 pruneCacheEntry 函数)
-
pruneCacheEntry 函数具体操作了啥
- 遍历集合,执行所有缓存组件的$destory 方法
- 将 cache 对应的 key 内容设置为空
- 删除 keys 中对应的元素
- 代码如下
function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array<string>, current?: VNode ) { const cached = cache[key] if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroy() // 执行组件的destory钩子函数 } cache[key] = null // 设为null remove(keys, key) // 删除对应的元素 }
- render 函数
- 获取到 keep-alive 包裹的第一个组件以及它的组件名称
- 判断组件名称是否能被 include 或者 enclude 所匹配,不能被 include 匹配||能被 enclude 匹配直接返回 vNode,不往下执行,反之,则执行下一步
- 根据组件 ID、tag 生成缓存 key,并在缓存集合中查找是否已缓存过此组件。如果缓存过,则直接取出缓存组件,然后更新缓存 key 和 keys 中的位置,如果没缓存过,则下一步
- 分别在 cache,keys 中保存此组件以及他的缓存 key,并检查数量是否超过 max,超过则根据 LRU 算法删除
- 将此组件实例的 keepAlive 的 abstract 属性设置为 true
Vue 数据双向绑定原理
- 双向绑定是数据是双向的,数据改变驱动视图变化,视图变化也会修改数据。
- 使用数据劫持和发布订阅模式实现的
18. Vue 的响应式是怎么实现的?(数据劫持和观察者模式)
- 数据劫持核心是 defineProperty,主要使用 Object.defineProperty 来对对象访问器 getter 和 setter 进行劫持。数据变更时 set 函数里面通知视图更新
- 使用数据劫持时候,对象和数组分开处理,对于对象的话是遍历对象属性之后进行递归劫持,对于数组的话则是重写。其实 definePorperty 是可以获取到数组下标的,但是 Vue 在设计的时候考虑到性能问题弃用了这个方案。
- Vue3 使用的是 proxy 进行数据劫持的;(这玩意是 es6 的东西,兼容性可能有问题,但是都用 Vue 了,基本都谷歌浏览器了吧,IE? 狗都不用!)
- 可以直接去监听对象,不用再遍历;
- 可以监听对象新增的属性,defineProperty 只能监听一开始存在的属性,新增的属性则需要手动 Observer
- 可以直接监听数组的变化
- 返回一个新对象,只操作新的对象达到目的;defineProperty 只能遍历对象属性去直接修改。
19. 为什么只对对象劫持,而要去对数组进行重写呢?
- 对象上的属性数量是比较少的,但是数组的话,内部可能会有几百上千个项,拦截的时候是比较耗费性能的,直接他妈的对它重写的话,节省性能。
20. Vue 中的 diff 算法
-
当数据变化时,vue 是怎么更新节点的?
- 渲染真实的 Dom 开销是比较大的,直接渲染真实的 Dom 会引起整个 Dom 树的重排和重绘。
- 根据真实的 Dom 去生成虚拟的 Dom,当虚拟 Dom 的某个节点的数据变化会生成一个新节点,然后新旧节点进行对比,发现不一样的地方直接修改在真实的 Dom 上,使旧节点的值等于新节点的值。
-
虚拟 Dom 和真实 Dom 的区别
- 虚拟 Dom 就是将真实的 Dom 的数据抽离出来,以对象的形式模拟树形结构。
-
<div> <p>123</p> </div> const vNode = { tag:"div", children:[ {tag:"p",text:123} ] }
-
虚拟 Dom 转化成真实 Dom 的过程
-
diff 的比较方式
- diff 算法比较新旧节点,只会同级进行比较,不会跨级。遵从树节点的那种接口,每一层每一层的比较
-
root1 root2 同级对比,不会子和父对比 / \ / \ vNode vNode vNode vNode
-
diff 流程
- 数据变化之后,set 方法会让调用 Dep.notify 通知所有订阅者 Watcher,订阅者会调用 patch 给真实的 Dom 打补丁,更新相应视图。
- 那么 patch 具体是怎么打补丁的呢?
- patch 接收两个参数,新节点 Vnode 和旧节点 oldVnode
- 先进行判断哈,对比同层级的虚拟节点是否是同一类型的标签,是同一类型的话就深层次对比(执行 patchVnode);不是同一类型的就直接新节点整个替换旧节点(最关键的一步,isSameVnode 方法,前面说到同一类型,就是在这个方法里面判断,key 值是否是一样的,tagName,标签名是否一样,是否都定义了 data;是否都为注释节点;还有一个 sameInputType 方法,判断定义的 input 中的 type 是否一致)
- patchVnode 中的处理方式
- 找到对应真实的 Dom,el
- 判断 newVnode 和 oldVnode 是否指向同一个对象,是的话直接 return
- 如果都有文本节点并且不相等,将真实 Dom 的文本节点设置成 newVnode 的文本节点
- 如果 newVnode 没有子节点,旧的有,Dom 对应的子节点
- 如果 new 有子节点,old 没有,将 newVnode 真实的子节点真实化之后添加到 Dom 中去
- 都有子节点,执行 updateChildren 函数,比较子节点。
- updateChildren 方法(patchVnode 里面最重要的一个方法)
- 收尾指针法(5 种情况)sameVNode(x,y),新老 Vnode 节点,首位都做标记,遍历过程中,这几个变量都会向中间靠拢,当 oldS>oldE 或者 newS>newE 时候跳出循环。
- 新节点首部(newS)和旧节点首部(oldS)对比,满足相同条件,直接将这个节点执行 patchVnode
- 新节点尾部(newE)和旧节点尾部(oldE)对比,满足相同条件,也是直接执行 patchVnode
- 新节点首部(newS)和旧节点尾部(oldE)对比,相同的话,需要把真实 Dom 节点移动到 oldS 前面。
- 新节点尾部(newE)和旧节点首部(oldS)对比,相同的话,还需要把真实 Dom 的节点移动到 oldE 后面
- 如果以上情况都不存在,则在旧节点中找与新节点头部相同的节点,存在就执行 patchVnode 处理方法,上面说的替换 Dom 方法。
- 当然也存在新 Vnode 节点在旧节点中找不到一致的 sameVnode 的情况,会直接创建一个新的 Dom。
- 如果旧的 Vnode 中节点遍历完毕跳出循环,新的 Vnode 节点还没有;就是新 Vnode 中节点数量比旧的多,需要将剩下的 Vnode 对应的 Node 真实化根据下表插入真实 Dom 中
- 反之,新的 Vnode 中节点遍历完成,旧的还有剩余,就在真实 dom 中,将区间在 oldStart 和 oldEnd 之间多余的节点删除掉。
- 图解链接https://www.cnblogs.com/lilicat/p/13448827.html
- 收尾指针法(5 种情况)sameVNode(x,y),新老 Vnode 节点,首位都做标记,遍历过程中,这几个变量都会向中间靠拢,当 oldS>oldE 或者 newS>newE 时候跳出循环。
21. 子组件改变 props 里面的值会怎么样
- 如果是基本数据类型,会报错
- 如果是引用数据类型,可以修改,而且父组件的这个数据也会被修改。
22. Vue 异步请求放在哪个生命周期中?
- 最好是在 created 周期中触发,组件中的数据都已经存在了,可以发送 ajax 请求
- 当然,如果说不需要依赖数据去请求的话,也可以在 beforeCreate 发起请求
23. Vue 导航守卫?
- 全局守卫
- router.beforeEach 全局前置守卫,进入路由之前
- router.beforeResolve 全局解析守卫,在 beforeRouteEnter 调用之后调用
- router.afterEach 全局后置钩子,进入路由之后的
- to from next 三个参数
- to 和 from 是将要进入和将要离开的路由对象(这里的路由对象是指的是 this.$route 获取到的路由对象)
- next 是进入该路由;必须调用,否则不能进入路由
- next()进入该路由
- next(false) 取消进入,url 地址重置为 from 路由地址
- next 跳转新路由,当前的导航被中断,重新开始一个新导航
- 路由组件内的守卫
- beforeRouteEnter 进入路由前 ,组件还没创建时就调用,所以不能获取组件实例 this,可以通过传一个回调给 next 来访问组件实例
- beforeRouteUpdate 路由复用同一个组件
- beforeRouteLeave 离开当前路由时;导航离开该组件之前被调用,用来禁止用户离开
24. Vue.set()和$set()
- 在构造完实例更新数据的时候,有的时候并不会导致视图变化。没有触发视图更新,为什么呢?
- 受到 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化的时候将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换他,才能让他是响应的。
- 所以 Vue 可能不能检测变动的数组。
- 原理:每个组件实例都有相应的 watcher 实例对象,它会在组件渲染过程中把属性记录为依赖,之后依赖项的 setter 被调用时,会通知 watcher 重新计算,从而使关联组件更新。受现代 JS 限制,Vue 不能检测到对象属性的添加或者删除。
- Vue.set()和$set()都是使用了 set 函数,前者是将 set 绑定到 Vue 构造函数上,后者则是将构造函数绑定到 Vue 原型上。
25. Vue 的性能优化
- 对于不需要响应式的数据使用 Object.freeze()或者说是写在 data 部分的 return 外部。
- 虚拟滚动的效果,一次性渲染太多 DOM 会比较卡,虚拟滚动只渲染少部分数据在可视区域,滚动的时候不停的替换滚动,模拟滚动效果
- v-for 绑定上 key 加快提高 diff 计算
浏览器和计网篇
状态码
- 1XX 100 表示继续,
- 2XX
- 200 表示成功
- 201 表示成功,并且创建了新资源
- 202 服务端已经收到消息,但是还没有处理
- 204 表示请求成功,但是客户端不需要更新页面
- 3XX
- 301 表示永久的重定向
- 302 临时的重定向
- 304 表示不需要再去传输请求的内容;可以使用缓存的内容
- 4XX
- 400 无效语法,服务器无法识别
- 401 缺乏资源要求的身份凭证
- 403 表示浏览器有能力处理请求,但是禁用了
- 404 表示无法找到请求的资源
- 405 表示服务端禁止使用当前 HTTP 方法的请求
- 414 表示 get 请求 url 拼接的参数太长了,服务器拒绝接受请求
- 5XX
- 500 表示所请求的服务器遇到意外的情况并阻止其执行请求。
- 502 上游服务器接收的请求无效
- 503 表示服务端未处于可以接收请求的状态;服务器关了
- 504 表示服务端在规定的时间内没法获得想要的响应.
1.回流和重绘
- 回流:是指浏览器 Dom 发生几何变化时,浏览器重新计算元素几何属性,最后把计算结果计算出来,也叫重排
- 重绘:Dom 元素样式发生改变,却未影响几何属性,比如说颜色变化,直接绘制新的样式上去而已
- 区别:回流是一定会导致重绘的,重绘不一定会导致回流.
2.浏览器渲染过程
- 浏览器解析 HTML 代码生成 Dom 树
- 解析 CSS 代码生成 CSSOM 树
- 然后 Dom 树和 CSSOM 树结合,去除其中的不可见元素(计算每个可见元素的布局),生成 Render 树,也叫渲染树
- 然后浏览器会根据渲染树,去给它布局,获得几何信息
- 最后是绘制过程,根据 Render 树得到几何信息,将 Render 树的每个像素渲染到屏幕.
3.http0.9 1.0 1.1 2.0
4.http 和 https
5.前端优化(感知性能,客观性能)
- 客观性能
- 去除 console.log
- 预渲染:将 JS 动态渲染的工作,在打包的过程中就去完成。
- 浏览器缓存
- 异步加载 JS 文件:defer 和 async
- 使用图片 webp 格式,体积只有 JPG 文件大小的 2/3(可能存在浏览器兼容问题)
- 感知性能(页面加载也许没有那么快,但是可以通过一些方式让用户觉得并不慢)
- 设置 loading 加载图
6.浏览器缓存
- 强缓存(也称本地缓存)
- 不需要发送请求的缓存,保存在本地,使用字段 Expires(http/1.0)和 Cache-Control(http/1.1)
- Expires 即缓存时间,存在服务器返回的请求头中,告诉浏览器这个时间内可以直接从缓存里面拿取到数据。设置的是一个具体的日期,存在问题就是:服务器和浏览器的时间如果不一致,过期时间就不准确了,所以后面 1.1 版本才采用的是 Cache-control
- Cache-control,采用的是一个时长(Expires 和这个同时存在,优先考虑 Cache-control)
- max-age=3600(缓存一个小时)
- private 只有浏览器自己可以缓存,代理的服务器不能缓存。
- no-cache 跳过当前缓存,发送 HTTP 请求——>进入协商缓存阶段。
- no-store 直接他妈的不缓存!!
- s-maxage 和 max-age 很像,针对于代理服务器的缓存时间
- 啊!!资源缓存时间失效了呢?!怎么办啊?协商缓存呢?救一下啊!
- 协商缓存(也称 304 缓存)
- 强缓存失效后,浏览器请求头中响应的缓存 Tag 向服务器发请求,由服务器根据这个 Tag,来判断是否使用缓存:Tag 分类:Last-Modified 和 Etag.
- Last-Modified:最后修改时间,浏览器给服务器第一次发送请求后,会加上这个字段;如果浏览器再次发起请求,会在请求头加入 if-Modified-Since 字段,字段是服务器传来的最后修改时间;服务器拿到请求头中 if-modified-since 字段后,会和服务器中该资源最后修改时间进行对比;
- 如果这个时间小于最后修改时间,说明是时候更新了。返回新的资源。
- 否则返回 304,直接使用缓存。
- Etag:是服务器根据当前文件的内容,给文件生成唯一的标识,是要里面的内容有改动,这个值就会变。服务器通过响应头把这个值传给浏览器
- 浏览器接收到 Etag 值,会在下次请求时,把这个值作为 if-none-match 这个字段的内容,并放到请求头中,发送给服务器
- 服务器接收到 if-None-Match;会和服务器上的 Etag 进行对比,不一样就发送数据,一样就告诉浏览器用缓存。
- Last-Modified:最后修改时间,浏览器给服务器第一次发送请求后,会加上这个字段;如果浏览器再次发起请求,会在请求头加入 if-Modified-Since 字段,字段是服务器传来的最后修改时间;服务器拿到请求头中 if-modified-since 字段后,会和服务器中该资源最后修改时间进行对比;
- Last-Modified 和 Etag 对比
- 精确度上,Etag 优于 Last-Modified;Etag 是按照内容给资源打标识,能准确感知资源变化,last-modified 在一些特殊的情况下,无法感知(编辑了资源文件,文件内容没有发生改变;last-modified 能够感知的单位时间是秒,如果一秒内发生多次变化了,那么就无法具体体现出来了)
- 性能上,last-Modified 是优于 Etag 的,Last-Modified 只是记录时间点,而 Etag 则需要根据文本内容生成哈希值
- 强缓存失效后,浏览器请求头中响应的缓存 Tag 向服务器发请求,由服务器根据这个 Tag,来判断是否使用缓存:Tag 分类:Last-Modified 和 Etag.
- 缓存的位置
- Memory Cache 内存缓存,效率上最快,存活时间最短,渲染进程结束后,内存缓存也就不存在了
- Disk Cache 磁盘缓存,比内存缓存慢,优势是储量大和存储时间比较长。
- 浏览器的缓存策略:比较大的 CSS 和 JS 文件被丢进磁盘中,小的进内存;内存使用率比较高的时候会优先进磁盘。
- push Cache 推送缓存,HTTP/2 的内容了。
7. 浏览器的本地存储
-
Cookie:容量小,最开始是因为 http 无状态,所以被设计出来弥补 http 状态管理的不足。无法直接删除,根据时间失效。
- 缺点:
- 容量太少了,只能存少量信息;
- Cookie 后面紧跟域名,不管域名下面某一个地址是否需要这个 Cookie;都会携带完整的 Cookie,导致性能浪费;
- 安全性不好,容易被非法截取。
- 缺点:
-
localStorage:容量上限为 5M,只存在于客户端,不和服务端通信,解决性能和安全问题。
-
sessionStorate:容量上限也是 5M,存在客户端中,不和服务端通信;特点是,浏览器页面一旦关闭直接就不存在了。
-
IndexdDB 存储大小无限,手动更新和删除,否则一直存在 (考到,不知道)
-
- IndexedDB 有哪些特点?
- 键值对储存:IndexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括 JavaScript 对象。对象仓库中,数据以"键值对"的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
- 异步:IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。异步设计是为了防止大量数据的读写,拖慢网页的表现。
- 支持事务:IndexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
- 同源限制: IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
- 储存空间大:IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。
- 支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)
- IndexedDB 有哪些特点?
-
8.懒加载和预加载
- 懒加载
- 也叫延迟加载,指的是在长网页中延迟加载图像,是一种优化网页性能的方式。用户滚动到可视区域之前不会加载。
- 提升用户的体验:在刚打开页面的时候让图片之后加载不会有太长的等待时间,
- 减少无效资源的加载:减少服务器的压力,减小浏览器的负担
- 防止并发加载的资源过多阻塞 JS 加载,影响网站的正常使用。
- 原理:将网页上的 src 属性设置成空字符串,而图片的真实路径则设置为 data-original 属性中,当页面滚动的时候需要去监听 scroll 事件,在 scroll 事件的回调中,判断我们的懒加载图片是否进入可视区域,如果进入,则将图片在可视区域内将图片的 src 属性设置为 data-original 的值,以达到懒加载的目的
- 预加载
- 预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。
9. 浏览器跨域方法
- jsonp 方法: 利用的是 script 标签没有跨域限制的漏洞,网页可以得到其他来源产生的 JSON 数据。
- 原理:请求一个 JS 文件,这个 JS 文件会执行一个回调函数,数据存储在回调函数里边。
- 优点:实现起来简单,然后兼容性比较好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 get 方法,而且不安全
- CORS 跨域:两种请求:简单请求和复杂请求 不支持 IE8/9
- 简单请求:请求方式是 GET,POST 或 GET
- http 头信息不超出以下字段:Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type
- 就是在信息头部添加一个 Origin 字段,说明本次请求来自哪个源,服务器会根据 Origin 的值来判断是否接收本次请求。如果 Origin 表示的源被服务器接收,那么服务器就会响应,返回字段如下
- Access-Control-Allow-Origin 要么是一个请求时候的 Orgin 值,要么是一个*
- Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送 Cookie
- Access-Control-Expose-Headers:该字段可选。CORS 请求时,XMLHttpRequest 对象的 getResponseHeader()方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在 Access-Control-Expose-Headers 里面指定。
- 复杂请求:请求的方法是 PUT 或 DELETE 或 Content-Type 字段的类型是 application/json
- 简单请求:请求方式是 GET,POST 或 GET
- proxy 代理跨域
- 在 Vue 脚手架里面,vue.config.js 文件中添加配置,命名一个别名代理一下服务器。
10. 从 url 输入到返回请求的过程
- 解析 URL:首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。
- 如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。
- 如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程。
- 缓存判断: 浏览器会判断所请求的资源是否在缓存里
- 如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求。
- DNS 解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,
- 首先会在浏览器的缓存中查找对应的 IP 地址,如果查找到直接返回,若找不到继续下一步
- 将请求发送给本地 DNS 服务器,在本地域名服务器缓存中查询,如果查找到,就直接将查找结果返回,若找不到继续下一步
- 本地 DNS 服务器向根域名服务器发送请求,根域名服务器会返回一个所查询域的顶级域名服务器地址
- 本地 DNS 服务器向顶级域名服务器发送请求,接受请求的服务器查询自己的缓存,如果有记录,就返回查询结果,如果没有就返回相关的下一级的权威域名服务器的地址
- 本地 DNS 服务器向权威域名服务器发送请求,域名服务器返回对应的结果
- 本地 DNS 服务器将返回结果保存在缓存中,便于下次使用
- 本地 DNS 服务器将返回结果返回给浏览器
- 获取 MAC 地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。
- 网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。
- 通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。
- TCP 三次握手
- HTTPS 握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程
- 返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
- 页面渲染
- TCP 四次挥手
11. 渲染阻塞
- 当浏览器遇到一个 script 标记时,DOM 构建将会暂停,直到脚本执行完成,才会继续渲染 DOM。每次去执行 JavaScript 脚本都会严重地阻塞 DOM 树的构建,如果 JavaScript 脚本还操作了 CSSOM,而正好这个 CSSOM 还没有下载和构建,浏览器甚至会延迟脚本执行和构建 DOM,直至完成其 CSSOM 的下载和构建,所以要把 script 标签放到最后面引用
12. 有时候同步执行代码修改 DOM,但是却出现了异步的效果
- GUI 渲染引擎线程和 JS 引擎线程是互斥的,线程间同步造成了 DOM 的"异步"效果
- GUI 渲染线程在能够执行的情况下的优化策略,渲染出的是最终得到的样式结果(将多个同一 DOM 下的样式合并后渲染)。
13. 具体讲讲 Cookie(cookie 本身没有设置删除)
-
是用来保存状态的,第一次请求的时候没有这个 cookie,请求完了之后服务端把 cookie 发给客户端,当客户端再次去请求的时候,这个时候会携带 cookie,服务器就会通过这个 cookie 辨别用户,也可以修改这个 cookie 的内容
-
cookie 不可跨域
-
cookie 的属性
- name:cookie 的名字,一个域名下绑定的 cookie,name 不能相同,相同的 name 值会被覆盖掉。
- value:cookie 的值,cookie 规定是名称/值不允许包含分号,逗号,空格,所以为了不给用户带来麻烦,考虑道服务器兼容性,任何存储 cookie 的数据都应该被编码
- domain :代表 cookie 绑定的域名,如果没有设置,会自动绑定执行语句的当前域,还有值得注意的点,同一个域名下的二级域名也是不可以交换使用 cookie 的,比如,你设置www.baidu.com和image.baidu.com,依旧是不能公用的
- path :默认是"/" 匹配的是 web 的路由
- 存储事件,一般浏览器默认存储的,当关闭浏览器 cookie 其实删除了,但是如果想让 cookie 保存的时间长一点,可以通过 Expires 设置未来的一个时间点,不过已经被 Max-age 取代了
- secure: 这个是为了安全性,这个属性设置为 true 的时候,cookie 只会在 https 和 ssl 等安全协议下传输
- HttpOnly:如果设置为 true 就不能通过 JS 脚本来获取 cookie 的值,可以有效放置 XSS 攻击
-
JS 操作 cookie : document.cookie, 服务端通过 setCookie 去设置 cookie,设置多个 cookie 就多写几个 setCookie
14. 什么是 XSS 攻击
- xss 是代码注入攻击,攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用恶意脚本,攻击者可以用来窃取网站 cookie,sessionID 等等。
- 本质:代码未经过滤和网站正常的代码混在一起,浏览器无法分辨哪些脚本可信,哪些脚本不可信,导致恶意代码被执行。
- 分类:存储型,反射型,Dom 型
- 如何防范?
- 对于明确的输入型,数字,url 或者证件号,输入过滤还是比较有必要的,但是又很大的不确定性和乱码的可能
- 纯前端渲染,把代码和数据分隔,其实就是前后端分离
- 转义 HTML
- 严谨前端代码,减少使用 v-html(Vue)之类的代码
- HTTP-only:禁止 JS 读取某些敏感的 cookie
15. 页面如果有多张图片,http 是怎样的加载表现?
- 在 http 1 中,同一个域名下最大 TCP 连接时 6,所以会请求多次。可以使用多域名部署进行解决
- 在 http 2 中,可以一瞬间加载很多资源,因为 http2 具有多路复用的特点,可以在一个 TCP 连接中发送多个请求
16. http2 的头部压缩算法是什么样的?
- 在客户端和服务端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再每次发送请求和响应
- 首部表在 HTTP/2 的连接存续期始终存在,由客户端和服务器共同渐进更新
- 每个新的首部键值对要么被追加到当前表的末尾,要么替换表中之前的值。
17. DNS 同时使用 TCP 和 UDP 协议。
- 占用 53 号端口,同时使用 UDP 和 TCP 协议。
- 在传输区域使用 TCP 协议
- 域名解析的时候使用 UDP 协议:客户端向 DNS 服务器查询域名,一般返回的内容不超过 512 字节,通过 UDP 传输即可。
- 不用经过三次握手,这样的话 DNS 服务器的负载会更低,响应更快。
18. 迭代查询和递归查询(DNS 模块)
- 递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归 查询,用户只需要发出一次查询请求。
- 迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出 多次的查询请求。
- 我们向本地 DNS 服务器发送的请求是递归查询;而本地 DNS 向其他域名服务器请求的过程是迭代查询。
19. TCP 协议的重传机制了解吗?
-
网络层可能出现丢失、重复或失序的情况,为了确保数据的准确性,TCP 会重传自己认为已经丢失的包
- 基于时间重传
- 基于确认信息
- 发送一个数据之后,就开启一个定时器,如果这个时间没有收到发送数据的 ACK 确认报文,就会重传则对该报文进行重传,在达到一定次数还没有成功时放弃并发送一个复位信号
20. TCP 的拥塞控制
- 慢启动:开始发送的时候设置 cwnd(拥塞窗口)=1,开始的时候不发送大量数据,而是先测试一下网络的拥塞程度,由小到大增加拥塞窗口大小
- 拥塞避免: 让拥塞窗口 cwnd 缓慢的增大,即每经过一个返回时间 RTT 就把发送方的拥塞控制窗口加一;无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限设置为出现拥塞时的发送窗口大小的一半。然后把拥塞窗口设置为 1,执行慢开始算法
- 快速重传:
- 快速恢复
21. TCP 的流量控制:滑动窗口
- 为了让发送方传输数据的速度不要太快,接收方来得及接收。
22. 数据劫持有哪几种方式。
- DNS 劫持:302 跳转,通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起 302 跳转的回复,引导⽤户获取内容
- HTTP 劫持:运营商修改你 http 的响应内容
23.
- 消除