汇总篇
HTML篇
1、HTML5语义化
(1)说一说你对HTML5语义化的理解
语义化意味着顾名思义,HTML5的语义化指的是合理正确的使用语义化的标签来创建页面结构,如 header,footer,nav,从标签上即可以直观的知道这个标签的作用,而不是滥用div。
(2)语义化的优点
- 代码结构清晰,易于阅读,利于开发和维护
- 方便其他设备解析(如屏幕阅读器)根据语义渲染网页。
- 有利于搜索引擎优化(SEO),搜索引擎爬虫会根据不同的标签来赋予不同的权重
(3)语义化标签种类
title、header、、nav、main、article、h1~h6、address、canvas、ul、ol、aside、section等
CSS篇
摘录:
一、CSS常见面试题
二、能不能讲一讲Flex布局,以及常用的属性?
-
采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。
-
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框的交叉点)叫做
main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。 -
项目默认沿主轴排列。单个项目占据的主轴空间叫做
main size,占据的交叉轴空间叫做cross size。
常用属性:
- flex-direction:
row | row-reverse | column | column-reverse flex-wrap:nowrap | wrap | wrap-reverse- flex-flow:
flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap - justify-content:
flex-start | flex-end | center | space-between | space-around - align-items:
flex-start | flex-end | center | baseline | stretch; - align-content:
flex-start | flex-end | center | space-between | space-around | stretch
三、BFC是什么?能解决什么问题?
1、概念
BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,并且与这个区域外部毫不相干。
2、BFC的创建方法(触发条件)
- 根元素
- float的值不为none
- overflow的值不为visible
- display的值为inline-block、table、table-cell、table-caption
- position的值为absolute或fixed
3、BFC渲染规则
BFC内,盒子依次垂直排列。BFC内,两个盒子的垂直距离由margin属性决定。属于同一个BFC的两个相邻Box的margin会发生重叠【符合合并原则的margin合并后是使用大的margin】BFC内,每个盒子的左外边缘接触内部盒子的左边缘(对于从右到左的格式,右边缘接触)。即使在存在浮动的情况下也是如此。除非创建新的BFC。- BFC的区域不会与浮动元素的盒子重叠
- BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。
- 计算BFC的高度时,浮动元素也参与计算。
4、BFC的应用场景
(1)防止浮动导致父元素高度塌陷
元素设置浮动,float:left之后,父元素产生塌陷的效果,找到父元素,添加overflow:hidden即可,同时这也是清除浮动的一种方式
<style>
.a{
height: 100px;
width: 100px;
margin: 10px;
background: pink;
float: left;
}
.container{
width: 120px;
border: 2px solid black;
}
</style>
<body>
<div class="container">
<div class="a"></div>
</div>
</body>
container 的高度没有被撑开,如果我们希望 container 的高度能够包含浮动元素,那么可以创建一个新的 BFC,因为根据 BFC 的规则,计算 BFC 的高度时,浮动元素也参与计算。
<style>
.a{
height: 100px;
width: 100px;
margin: 10px;
background: pink;
float: left;
}
.container{
width: 120px;
display: inline-block;/*触发生成BFC*/
border: 2px solid black;
}
</style>
(2)防止 margin 重叠
<style>
.a{
height: 100px;
width: 100px;
margin: 50px;
background: pink;
}
</style>
<body>
<div class="a"></div>
<div class="a"></div>
</body>
两个div直接的 margin 是50px,发生了 margin 的重叠。
根据BFC规则,同一个BFC内的两个两个相邻Box的
margin会发生重叠,因此我们可以在div外面再嵌套一层容器,并且触发该容器生成一个 BFC,这样<div class="a"></div>就会属于两个 BFC,自然也就不会再发生margin重叠
<style>
.a{
height: 100px;
width: 100px;
margin: 50px;
background: pink;
}
.container{
/*不设置margin会重叠*/
overflow: auto; /*触发生成BFC*/
}
</style>
<body>
<div class="container">
<div class="a"></div>
</div>
<div class="a"></div>
</body>
(3)自适应多栏布局
<style>
body{
width: 500px;
}
.a{
height: 150px;
width: 100px;
background: pink;
float: left;
}
.b{
height: 200px;
background: blue;
}
</style>
<body>
<div class="a"></div>
<div class="b"></div>
</body>
根据规则,BFC的区域不会与float box重叠。因此,可以触发生成一个新的BFC,如下:
<style>
.b{
height: 200px;
overflow: hidden; /*触发生成BFC*/
background: blue;
}
</style>
四、实现水平居中布局(定宽高+不定宽高)
面试官:你能实现多少种水平垂直居中的布局(定宽高和不定宽高 定宽高:
- 1、绝对定位 +
margin负值
margin设置百分比,是相对于父元素
- 2、绝对定位 +
transform -
- 3、绝对定位 +
top/bottom/left/right
- 3、绝对定位 +
- 4、flex
- 5、grid
- 6、table-cell
不定宽高
- 1、绝对定位 +
transform - 2、table-cell
- 3、flex
- 4、flex变异布局
- 5、grid + flex布局
五、css盒模型
css盒模型就是用来装页面上的元素的矩形区域。
- 低版本IE盒子模型:宽度=内容宽度(content+border+padding)+ margin
- 标准盒子模型:宽度=内容的宽度(content)+ border + padding + margin
box-sizing属性?
用来控制元素的盒子模型的解析模式,默认为content-box
context-box:W3C的标准盒子模型,设置元素的 height/width 属性指的是content部分的高/宽
border-box:IE传统盒子模型。设置元素的height/width属性指的是border + padding + content部分的高/宽
六、画一条0.5px的线
- 1、移动端,采用meta viewport的方式
/*可以直接设置视口*/
/**
*initial-scale [0,10] 初始缩放比例
*maximum-scale [0,10] 最大缩放比例
*minimum-scale [0,10] 最小缩放比例
*user-scalable yes/no 是否允许手动缩放页面,默认yes
**/
<meta name="viewport"
content="width=device-width,
initial-scale=0.5, minimum-scale=0.5, maximum-scale=0.5"/>
- 2、采用 transform: scale()的方式
/*用一个伪元素画条1px的线缩放为一半。定位在父亲中*/
.transform{
width:200px;
height:200px;
position:relative;
}
.transform::after{
//伪元素内容为空要加content:""
content:"";
position:absolute;
left:0px;
botton:0px;
width:100%;
height:1px;
backgroud-color:black;
transform:scaleY(0.5);
}
- 3、使用boxshadow:设置box-shadow的第二个参数为0.5px,表示阴影垂直方向的偏移为0.5px
<style>
.boxshadow {
height: 1px;
background: none;
box-shadow: 0 0.5px 0 #000;
}
</style>
<p>box-shadow: 0 0.5px 0 #000</p>
<div class="boxshadow"></div>
- 4、使用background-image结合SVG的方式
/*设置一边为透明*/
/*添加border-image,设置为50%渐变,一半透明一半有颜色*/
.border{
width:200px;
height:200px;
border-right:1px solid transparent;
border-image:linear-gradient(
to right,transparent 50%,blue 50%)0 100% 0 0;
}
七、visibility、opacity 和 display 的差别
- visibility 设置 hidden 会隐藏元素,但是其位置还存在与页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘
- display 设置了 none 属性会隐藏元素,且其位置也不会被保留下来,所以会触发浏览器渲染引擎的回流和重绘。
- opacity 会将元素设置为透明,但是其位置也在页面文档流中,不会被删除,所以会触发浏览器渲染引擎的重绘
八、重绘与回流
浏览器渲染过程:浏览器把html解析成DOM树,CSS解析成CSS规则树,两棵树合并后变成渲染树(RenderTree),有了渲染树,我们就知道了节点的样式,浏览器就会计算节点的大小和位置,最后渲染在页面上。
回流必将引起重绘,重绘不一定会引起回流
重绘:当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘
回流:当RenderTree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流
会导致回流的操作:
- 页面首次渲染
- 浏览器窗口大小发生改变
- 元素尺寸或位置发生改变
- 元素内容变化(文字数量或图片大小等等)
- 元素字体大小变化
- 添加或者删除可见的DOM元素
- 激活CSS伪类(例如::hover)
- 查询某些属性或调用某些方法(offsetWidth、offsetHeight、scrollTo()等)
九、移动端适配方法?
视口(viewport)代表当前可见的计算机图形区域。在Web浏览器术语中,通常与浏览器窗口相同,但不包括浏览器的UI, 菜单栏等——即指你正在浏览的文档的那一部分。
那么在移动端如何配置视口呢? 简单的一个meta标签即可!
<meta name="viewport" content="width=device-width; initial-scale=1; maximum-scale=1; minimum-scale=1; user-scalable=no;">
适配方法:
1、rem
rem是CSS3新增的一个相对单位,相对于html节点的font-size来做计算的。所以在页面初始话的时候给根元素设置一个font-size,接下来的元素就根据rem来布局,这样就可以保证在页面大小变化时,布局可以自适应。
举个例子:
//假设我给根元素的大小设置为14px
html{
font-size:14px
}
//那么我底下的p标签如果想要也是14像素
p{
font-size:1rem
}
//如此即可
他的原理非常简单
// set 1rem = viewWidth / 10
function setRemUnit () {
var rem = docEl.clientWidth / 10
docEl.style.fontSize = rem + 'px'
}
setRemUnit();
2、vw/vh
vh、vw方案即将视觉视口宽度 window.innerWidth和视觉视口高度 window.innerHeight 等分为 100 份。
工程化解决方案:webpack解析css 的时候用postcss-loader 有个postcss-px-to-viewport能自动实现px到vw的转化
3、px为主,vx和vxxx(vw/vh/vmax/vmin)为辅,搭配一些flex(推荐)
十、如何解决移动端 Retina 屏 1px 像素问题 ?
- 0.5px实现
- box-shadow模拟边框实现
- border-image实现
- viewport + rem 实现(推荐)
- 伪元素 + transform实现(推荐)
- svg 实现(推荐)
十一、如何隐藏页面中的某个元素
完全隐藏:
- display:none(不占据空间)
- HTML5 新增属性,相当于
display: none(不占据空间)<div hidden> </div>
视觉上的隐藏:(移出可视区)
- 1、设置
posoition为absolute或fixed,通过设置top、left等值- 可视区域不占位
position:absolute; left: -99999px;
- 可视区域不占位
- 2、设置
position为relative,通过设置top、left等值,- 可视区域占位
- 如希望其在可视区域不占位置,需同时设置
height: 0;position: relative; left: -99999px;
-3、 设置 margin 值
- 可视区域占位
- 如果希望其在可视区域不占位,需同时设置 height: 0;
margin-left: -99999px;
- 4、利用 transfrom
- 缩放(占据空间):
transform: scale(0); - 移动
translateX(占据空间):transform: translateX(-99999px);如果希望不占据空间,需同时设置height: 0 - 旋转
rotate(占据空间):transform: rotateY(90deg);
- 缩放(占据空间):
- 5、设置其大小为0
- 宽高为0,字体大小为0:
height: 0; width: 0; font-size: 0; - 宽高为0,超出隐藏:
height: 0; width: 0; overflow: hidden;
- 宽高为0,字体大小为0:
- 6、设置透明度为0 (占据空间):
opacity: 0; - 7、
visibility属性 (占据空间):visibility: hidden - 8、层级覆盖,
z-index属性 (占据空间):position: relative; z-index: -999; - 9、clip-path 裁剪 (占据空间):
clip-path: polygon(0 0, 0 0, 0 0, 0 0);
语义上的隐藏
<div aria-hidden="true">
</div>
使用JS将元素从页面中移除
JS篇
摘录:
- 「硬核JS」深入了解异步解决方案
- 万字长文,重学JavaScript异步编程
- 一次性搞懂JavaScript异步编程
- web前端面试总结
- 记一次大厂的面试过程
- 【面试篇】寒冬求职季之你必须要懂的原生JS(上)
- 【面试篇】寒冬求职季之你必须要懂的原生JS(中)
1. 异步编程方案有哪些?
异步编程方案:
- 回调函数(callback)
- Promise
- Generator
- Async/Await
1、回调函数(callback)
简单理解就是一个函数被作为参数传递给另一个函数。
优点:简单、容易理解和部署
缺点:不利于代码的阅读和维护,各个部分之间高度耦合
2、Promise
-
Promise本意是承诺,在程序中的意思就是承诺我过一段时间后会给你一个结果。
-
Promise的三种状态
- Pending----Promise对象实例创建时候的初始状态
- Fulfilled----可以理解为成功的状态
- Rejected----可以理解为失败的状态
-
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了。
-
链式调用
- 每次调用返回的都是一个新的Promise实例(这就是then可用链式调用的原因)
- 如果then中返回的是一个结果的话会把这个结果传递下一次then中的成功回调
- 如果then中出现异常,会走下一个then的失败回调
- 在 then中使用了return,那么 return 的值会被Promise.resolve() 包装
- then中可以不传递参数,如果不传递会透到下一个then中
- catch 会捕获到没有捕获的异常
-
优点:
- 解决了回调地狱
- 能够通过回调函数捕获错误
-
缺点:
- 无法取消Promise,一旦新建它就会执行,无法中途取消
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
- 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
3、Generator
- Generator 是一个带星号的函数(它并不是真正的函数),可以配合 yield 关键字来暂停或者执行函数。
- yield 关键词返回一个迭代器对象,该对象有 value 和 done 两个属性,属性分别代表返回值以及是否完成
生成器原理
在生成器内部,如果遇到 yield 关键字,那么 V8 引擎将返回关键字后面的内容给外部,并暂停该生成器函数的执行。生成器暂停执行后,外部的代码便开始执行,外部代码如果想要恢复生成器的执行,可以使用 result.next 方法。
那 V8 是怎么实现生成器函数的暂停执行和恢复执行的呢?
它用到的就是协程,协程是—种比线程更加轻量级的存在。我们可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行; 同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
4、Async/Await
Async/Await,它是 Generator 的语法糖;
Async/Await = Generator + Promise
1、async 函数
函数的返回值为 promise 对象
promise 对象的结果由 async 函数执行的返回值决定
2、await 表达式
await 右侧的表达式一般为 promise 对象, 但也可以是其它的值
如果表达式是 promise 对象, await 返回的是 promise 成功的值
如果表达式是其它值, 直接将此值作为 await 的返回值
注意:
await 必须写在 async 函数中, 但 async 函数中可以没有 await
如果 await 的 promise 失败了, 就会抛出异常, 需要通过 try...catch 捕获处理
2. 原型链相关
- 每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。
- 每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型(prototype),每一个对象都会从原型"继承"属性。
- 每个实例的
__proto__属性都指向构造函数的原型对象prototype。person.__proto__ === Person.prototype - 每个原型都有一个 constructor 属性指向关联的构造函数。
Person.prototype.constractor === Person - 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
- 原型对象就是通过 Object 构造函数生成的。
Person.prototype.__proto__ === Object.prototype - Object.prototype 的原型是null。
Object.prototype.__proto__ === null
(1)谈谈你对原型的理解?
在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。使用原型对象的好处是所有对象实例共享它所包含的属性和方法。
(2)简单描述原型链?
每个对象拥有一个原型对象,通过 __proto__ (读音: dunder proto) 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null(Object.prototype.__proto__ 指向的是null)。这种关系被称为原型链 (prototype chain),通过原型链一个对象可以拥有定义在其他对象中的属性和方法。
(3)原型链解决了什么问题?
原型链解决的主要是继承问题。
(4)prototype 和 __proto__ 区别?
prototype是构造函数的属性。__proto__是每个实例都有的属性,可以访问[[prototype]]属性。- 实例的
__proto__与其构造函数的prototype指向的是同一个对象。
3. 作用域?
- 作用域是指程序源代码中定义变量的区域。
- JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
- 因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了
4. es6与es6+常用的哪些?
es6 及 es6+ 的能力集,你最常用的,这其中最有用的,都解决了什么问题
1、ES6(ES2015)
- 类
- 模块化
- 箭头函数
- 函数参数默认值
- 模板字符串
- 解构赋值
- 扩展操作符
- 对象属性简写
- Promise
- let 与 const
2、ES7(ES2016)
- Array.prototype.includes()
- 指数操作符
3、ES8(ES2017)
- async/await
- Object.values()
- Object.entries()
- String padding:
padStart()和padEnd(),填充字符串达到当前长度
4、ES9(ES2018)
- 异步迭代(for await of)
- Promise.finally()
- Rest/Spread 属性
- 新的正则表达式特性
- 正则表达式反向断言(lookbehind)
- 正则表达式dotAll模式
- 正则表达式命名捕获组(Regular Expression Named Capture Groups)
- 正则表达式 Unicode 转义
- 非转义序列的模板字符串
5、ES10(ES2019)
- 新增了Array的
flat()方法和flatMap()方法 - 新增了String的
trimStart()方法和trimEnd()方法
6、ES11(ES2020)
Promise.allSettled()- 可选链(Optional chaining)
- 空值合并运算符(Nullish coalescing Operator)
import()
ES6新的特性有哪些?
-
新增了块级作用域(let,const)
-
提供了定义类的语法糖(class)
-
新增了一种基本数据类型(Symbol)
-
新增了变量的解构赋值
-
函数参数允许设置默认值,引入了rest参数,新增了箭头函数
-
数组新增了一些API,如 isArray / from / of 方法;数组实例新增了 entries(),keys() 和 values() 等方法
-
对象和数组新增了扩展运算符
-
ES6 新增了模块化(import/export)
-
ES6 新增了 Set 和 Map 数据结构
-
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例
-
ES6 新增了生成器(Generator)和遍历器(Iterator)
5. typeof与instanceof异同?
typeof 可以判断哪些类型?instanceof 做了什么?null为什么被typeof错误的判断为了'object'
相同点:都可以用于检测数据类型 不同点:
- typeof 返回值是一个字符串
typeof来判断引用类型变量时,无论是什么类型的变量,它都会返回Objectinstanceof用于判断引用类型属于哪个构造函数的方法。instanceof操作符检测过程中也会将继承关系考虑在内,即instanceof可以用于判断多层继承关系。- instanceof 的内部实现机制是:通过判断对象的
原型链上是否能找到对象的prototype,来确定instanceof返回值
6. instanceof 的实现原理是什么?
instanceof 是通过原型链判断的,A instanceof B, 在 A 的原型链中层层查找,是否有原型等于 B.prototype ,如果一直找到 A 的原型链的顶端(null;即Object.proptotype.__proto__), 仍然不等于 B.prototype,那么返回false,否则返回true。
instanceof的实现代码:
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
let prototype = R.prototype;
while (true) {
if(L === null) {//已经找到原型链的顶端
return false;
} else if(L.__proto__ === prototype) {
return true;
}
L = L.__proto__;//继续向上一层原型链查找
}
}
7. 事件循环机制,结合事件渲染说说
1、进程与线程
进程是CPU资源分配的最小单位(系统分配资源的最小单位)进程包括运行中的程序和程序所使用到的内存和系统资源线程是CPU调度的最小单位(程序执行的最小单位),线程是建立在进程的基础上的一次程序运行单位线程就是程序中的一个执行流,一个进程可以有多个线程- 一个
进程中只有一个执行流称作单线程,有多个执行流称作多线程
作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
2、浏览器的进程有哪些
浏览器是多进程的,包括:
- Browser进程:浏览器界面显示,与用户交互;网络资源的管理,下载等
- 第三方插件进程
- GPU进程
- 渲染进程(重):页面的渲染,JS的执行,事件的循环
3、浏览器的渲染进程
渲染进程Renderer的主要线程有:
- GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树,CSSOM(CSS规则树)和RenderObject树,布局和绘制等
- 页面的重绘(颜色、背景等改变)与回流(尺寸等改变)
- GUI渲染线程与JS引擎线程是互斥的
- JS引擎线程
- JS引擎线程就是JS内核,负责处理Javascript脚本程序(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码
- JS引擎一直等待着任务队列中任务的到来,然后加以处理
- js引擎线程会阻塞GUI渲染线程
- 事件触发线程
- 用来控制事件循环,并且管理着一个事件队列(task queue);(属于浏览器而不是JS引擎)
- 当js执行碰到事件绑定和一些异步操作,会走事件触发线程将对应的事件添加到对应的线程中(比如定时器操作,便把定时器事件添加到定时器线程),等异步事件有了结果,便把他们的回调操作添加到事件队列,等待js引擎线程空闲时来处理。
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
- 定时触发器线程
setInterval与setTimeout所在线程- 通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待JS引擎空闲后执行),这个线程就是定时触发器线程,也叫定时器线程
- 异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行
4、宏任务(macrotask) & 微任务(microtask)
常见的宏任务
- 主代码块
- setTimeout
- setInterval
- setImmediate ()-Node
- requestAnimationFrame ()-浏览器
常见微任务
- process.nextTick ()-Node
- Promise.then()
- catch
- finally
- Object.observe
- MutationObserver
当一个宏任务执行完,会在渲染前,将执行期间所产生的所有微任务都执行完
宏任务 -> 微任务 -> GUI渲染 -> 宏任务 -> ...
5、事件循环流程
- 首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分
- 同步任务会直接进入主线程依次执行
- 异步任务会再分为宏任务和微任务
- 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中(事件队列)
- 微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中
- 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行
- 检查是否有
Web Worker任务,有则执行,执行下一个宏任务,直到宏任务和微任务队列都为空 - 上述过程会不断重复,这就是Event Loop
8. 由一道bilibili面试题看Promise异步执行机制
总结:
Promise构造函数是同步执行的,then方法是异步执行的.then或者.catch的参数期望是函数,传入非函数则会直接执行Promise的状态一经改变就不能再改变,构造函数中的resolve或reject只有第一次执行有效,多次调用没有任何作用.then方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,在某些时候你可以认为.catch是.then第二个参数的简便写法- 当遇到
promise.then时, 如果当前的Promise还处于pending状态,我们并不能确定调用resolved还是rejected,只有等待promise的状态确定后,再做处理,所以我们需要把我们的两种情况的处理逻辑做成callback放入promise的回调数组内,当promise状态翻转为resolved时,才将之前的promise.then推入微任务队列
9. 请说出以下打印结果
let a = {a: 10};
let b = {b: 10};
let obj = {
a: 10
};
obj[b] = 20;
console.log(obj[a]);
我的回答是:20。这道题目主要考对JS数据类型的熟练度以及对ES6中属性名表达式的理解。在上题中obj[b] = 20的赋值操作后,obj其实已经变成了{a: 10, [object Object]: 20},这是因为如果属性名表达式是一个对象的话,那么默认情况下会自动将对象转为字符串[object Object],最后一步获取obj[a]时,a本身也是一个对象,所以会被转换为获取obj['[object Object]']也就是上一步赋值的20。
10. 说出几种数组去重的方式
- 1.利用
new Set()
function uniq(arry) {
return [...new Set(arry)];
}
- 2.利用
indexOf
function uniq(arry) {
var result = [];
for (var i = 0; i < arry.length; i++) {
if (result.indexOf(arry[i]) === -1) {
//如 result 中没有 arry[i],则添加到数组中
result.push(arry[i])
}
}
return result;
}
- 3.利用
includes
function uniq(arry) {
var result = [];
for (var i = 0; i < arry.length; i++) {
if (!result.includes(arry[i])) {
//如 result 中没有 arry[i],则添加到数组中
result.push(arry[i])
}
}
return result;
}
- 4.利用
reduce
function uniq(arry) {
return arry.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []);
}
- 5.利用
Map
function uniq(arry) {
const result = [];
const map = new Map();
for (let v of arry) {
if (!map.has(v)) {
map.set(v, true);
result.push(v);
}
}
return result
}
- 6.利用双层for循环
function uniq(arry) {
for (let i = 0; i < arry.length; i++) {
for (let j = i + 1; j < arry.length; j++) {
if (arry[i] === arry[j]) {
arry.splice(j, 1);
j--;
}
}
}
return array
}
11. 对象数组如何去重?
根据每个对象的某一个具体属性来进行去重
const responseList = [
{ id: 1, a: 1 },
{ id: 2, a: 2 },
{ id: 3, a: 3 },
{ id: 1, a: 4 },
];
const result = responseList.reduce((acc, cur) => {
const ids = acc.map(item => item.id);
return ids.includes(cur.id) ? acc : [...acc, cur];
}, []);
console.log(result); // -> [ { id: 1, a: 1}, {id: 2, a: 2}, {id: 3, a: 3} ]
12. 判断一个变量是数组?
- 使用
Array.isArray判断,如果返回 true, 说明是数组 - 使用
instanceof Array判断,如果返回true, 说明是数组 - 使用
Object.prototype.toString.call判断,如果值是[object Array], 说明是数组 - 通过 constructor 来判断,如果是数组,那么
arr.constructor === Array. (不准确,因为我们可以指定obj.constructor = Array)
13. 理解深拷贝和浅拷贝吗?
浅拷贝:
- 创建一个对象,这个对象有着原始对象属性值的一份精确拷贝
- 如果属性是基本类型,那么拷贝的就是基本类型的值
- 如果属性是引用类型,那么拷贝的就是内存地址,所以如果其中一个对象修改了某些属性,那么另一个对象就会受到影响。
深拷贝:
- 指从内存中完整地拷贝一个对象出来
- 并在堆内存中为其分配一个新的内存区域来存放
- 深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。
- 修改该对象的属性不会影响到原来的对象
14. 深拷贝和浅拷贝的实现方式分别有哪些?
浅拷贝:
Object.assign的方式- 通过对象
扩展运算符 - 通过数组的
slice方法 - 通过数组的
concat方法。
深拷贝:
- ①通过
JSON.stringify来序列化对象
缺陷:- 对象的属性值是函数时,无法拷贝
- 原型链上的属性无法获取
- 不能正确的处理 Date/RegExp 类型的数据
- 会忽略 symbol/undefined
- 如果是函数,序列化会变成
undefined - 如果是数组,数组的属性是
undefined、任意的函数以及symbol,转换成字符串"null" - 如果是
RegExp对象。 返回{}(类型是 string) - 如果是
Date对象,返回Date的toJSON字符串值
- ②手动实现
递归的方式
function deepClone(obj) { //递归拷贝
if(obj === null) return null; //null 的情况
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
if(typeof obj !== 'object') {
//如果不是复杂数据类型,直接返回
return obj;
}
/**
* 如果obj是数组,那么 obj.constructor 是 [Function: Array]
* 如果obj是对象,那么 obj.constructor 是 [Function: Object]
*/
let t = new obj.constructor();
for(let key in obj) {
//如果 obj[key] 是复杂数据类型,递归
t[key] = deepClone(obj[key]);
}
return t;
}
15. 说出以下代码的执行结果
var a = 10;
var obj = {
a: 20,
say: function () {
console.log(this.a);
}
};
obj.say();
// 20
扩展:如何才能打印出10
// 方式1
var a = 10;
var obj = {
a: 20,
say: () => { // 此处改为箭头函数
console.log(this.a);
}
};
obj.say(); // -> 10
// 方式2
var a = 10;
var obj = {
a: 20,
say: function () {
console.log(this.a);
}
};
obj.say.call(this); // 此处显示绑定this为全局window对象
// 方式3
var a = 10;
var obj = {
a: 20,
say: function () {
console.log(this.a);
}
};
var say = obj.say; // 此处先创建一个临时变量存放函数定义,然后单独调用
say();
16. parseInt执行结果
parseInt('2017-07-01') // -> 2017
parseInt('2017abcdef') // -> 2017
parseInt('abcdef2017') // -> NaN
17. js中跨域方法
js同源策略(协议+端口号+域名要相同)
那么同源策略的作用是什么呢?同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。
那么我们又为什么需要跨域呢?一是前端和服务器分开部署,接口请求需要跨域,二是我们可能会加载其它网站的页面作为iframe内嵌。
- 1、jsonp跨域(只能解决get)
- 原理:利用script标签的src属性不受同源策略限制,动态创建一个script标签。
- ①去创建一个script标签
- ②script的src属性设置接口地址
- ③接口参数,必须要带一个自定义函数名,要不然后台无法返回数据
- ④通过定义函数名去接受返回的数据
- 2、服务器设置对CORS的支持(支持get,head或者post)
- CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。
- 浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。
- 服务器设置
Access-Control-Allow-OriginHTTP响应头之后,浏览器将会允许跨域请求 - Content-Type是application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一个值,或者不设置也可以,一般默认就是application/x-www-form-urlencoded。
- 3、利用h5一个持久化的协议Websocket
- WebSocket和HTTP都是应用层协议,都基于 TCP 协议
- WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据
- WebSocket 在建立连接时需要借助 HTTP 协议
- 4、Node中间件代理(两次跨域)
- 实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略
- 代理服务器需要做的:
- ①接受客户端请求 。
- ②将请求 转发给服务器。
- ③拿到服务器 响应 数据。
- ④将响应转发给客户端。
- 5、nginx反向代理
- 实现原理与node中间件代理类似,搭建一个中转nginx服务器,用于转发请求。
- 使用nginx反向代理实现跨域,是最简单的跨域方式
- 实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
- 6、利用h5新特性window.postMessage()
- postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer])- message:传递的数据
- targetOrigin:指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)、URI
- 解决的问题:
- ①页面和其打开的新窗口的数据传递
- ②多窗口之间消息传递
- ③页面与嵌套的iframe消息传递
- ④上面三个场景的跨域数据传递
- 7、document.domain 基础域名相同 子域名不同
- 8、window.name + iframe 利用在一个浏览器窗口内,载入所有的域名都是共享一个window.name
- 9、location.hash + iframe
- 实现原理:
- a.html欲与c.html跨域相互通信,通过中间页b.html来实现。
- 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
- a.html和b.html是同域的,c.html不同域
- 实现原理:
18. this指向?箭头函数的this?
- 全局作用域下的this指向window
- 如果给元素的事件行为绑定函数,那么函数中的this指向当前被绑定的那个元素
- 函数中的this,要看函数执行前有没有
., 有.的话,点前面是谁,this就指向谁,如果没有点,指向window - 自执行函数中的this永远指向window
- 定时器中函数的this指向window
- 构造函数中的this指向当前的实例
- call、apply、bind可以改变函数的this指向
- 箭头函数中没有this,如果输出this,就会输出箭头函数定义时所在的作用域中的this
(1)this的四个绑定规则
-
1、默认绑定
规则:非严格模式默认指向window,严格模式指向undefined。
- 不带任何修饰的函数引用进行调用(独立函数调用),指向window。
- 函数调用链(一个函数又调用另外一个函数)
- 将函数作为参数,传入到另一个函数中,作为函数的参数,指向window
-
2、隐式绑定
隐式绑定的 this,指向调用函数的上下文对象
- ①一般的对象调用。
规则:会把函数调用中的this绑定到这个上下文对象。 - ②对象属性引用链
规则:对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。 - ③隐式绑定丢失问题:
- 将对象里的函数赋值给一个变量 :应用默认绑定
- 传入回调函数时
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。
- ①一般的对象调用。
-
3、显式绑定
直接指定 this 的绑定对象,因此我们称之为显式绑定- ①使用 call(...) 和 apply(...)
- ②硬绑定-bind
由于硬绑定是一种非常常用的模式,所以 ES5 提供了内置的方法Function.prototype.bind
调用 bind() 方法,返回一个函数,那么这个新函数的this,永远指向我们传入的obj对象 - ③API调用的 “上下文(内置函数)
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和bind(..)一样,确保你的回调函数使用指定的this。
例如:- 数组方法
forEach()调用 foo(..) 时把 this 绑定到 obj 身上[1, 2, 3].forEach(foo, obj) setTimeout()的回调函数中的this一般指向window
- 数组方法
-
4、new绑定:new 绑定的 this,都指向通过 new 调用的函数的实例对象。
(2)绑定规则的优先级
它们之间的优先级关系为:
默认绑定 < 隐式绑定 < 显示绑定(bind) < new绑定
1、同时使用隐式绑定和显示绑定
function foo() {
console.log(this.a)
}
var obj1 = {
a: 1,
foo: foo
}
var obj2 = {
a: 2,
foo: foo
}
obj1.foo.call(obj2) // 2
(3)this绑定对象的判断总结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 如果以上都不是,那么使用默认绑定。默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
- 如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。
- 箭头函数没有自己的 this, 它的this继承于上一层代码块的this。
词法作用域和this的区别?
- 词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的
- this 是在调用时被绑定的,this 指向什么,完全取决于函数的调用位置(关于this的指向问题,本文已经有说明)
JS执行上下文栈和作用域链的理解
(1)JS执行上下文
定义
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则。
- JavaScript执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行-完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的JS执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
类型
- 全局执行上下文
- 函数执行上下文
- eval函数执行上下文(不被推荐)
创建过程
执行上下文创建过程中,需要做以下几件事:
- 创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
- 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
- 确定this的值,即 ResolveThisBinding
(2)作用域
定义
作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
作用域的两种工作模式
作用域有两种工作模型:词法作用域和动态作用域,JS采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。(with 和 eval 能够修改词法作用域,但是不推荐使用,对此不做特别说明)
作用域的种类
- 全局作用域
- 函数作用域
- 块级作用域
(3)JS执行上下文栈
JS执行上下文栈的理解
执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。
规则
- 首次运行JavaScript代码的时候,会创建一个全局执行的上下文并Push到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push当前执行栈的栈顶。
- 当栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文的控制权将移动到当前执行栈的下一个执行上下文。
以一段代码具体说明:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
Global Execution Context (即全局执行上下文)首先入栈,过程如下:
伪代码:
//全局执行上下文首先入栈
ECStack.push(globalContext);
//执行fun1();
ECStack.push(<fun1> functionContext);
//fun1中又调用了fun2;
ECStack.push(<fun2> functionContext);
//fun2中又调用了fun3;
ECStack.push(<fun3> functionContext);
//fun3执行完毕
ECStack.pop();
//fun2执行完毕
ECStack.pop();
//fun1执行完毕
ECStack.pop();
//javascript继续顺序执行下面的代码,但ECStack底部始终有一个 全局上下文(globalContext);
(4)作用域链
作用域链: 无论是 LHS 还是 RHS 查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。
var a = 10;
function fn1() {
var b = 20;
console.log(fn2)
function fn2() {
a = 20
}
return fn2;
}
fn1()();
fn2作用域链 = [fn2作用域, fn1作用域,全局作用域]
19. 浏览器事件代理机制的原理是什么?
(1)事件流
早期浏览器,IE采用的是事件冒泡事件流,而Netscape采用的则是事件捕获。"DOM2级事件"把事件流分为三个阶段,捕获阶段、目标阶段、冒泡阶段。现代浏览器也都遵循此规范。
(2)事件代理(事件委托)
事件代理又称为事件委托,在祖先级DOM元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件冒泡的原理来触发绑定在祖先级DOM的事件。因为事件会从目标元素一层层冒泡至document对象。
(3)为什么要事件代理?
- 添加到页面上的事件数量会影响页面的运行性能,如果添加的事件过多,会导致网页的性能下降。采用事件代理的方式,可以大大减少注册事件的个数。
- 事件代理的当时,某个子孙元素是动态增加的,不需要再次对其进行事件绑定。
- 不用担心某个注册了事件的DOM元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题。
(4)如将页面中的所有click事件都代理到document上
document.addEventListener('click', function (e) {
console.log(e.target);
/**
* 捕获阶段调用调用事件处理程序,eventPhase是 1;
* 处于目标,eventPhase是2
* 冒泡阶段调用事件处理程序,eventPhase是 1;
*/
console.log(e.eventPhase);
});
(5)事件冒泡与事件捕获区别?
事件冒泡和事件捕获分别由微软和网景公司提出,这两个概念都是为了解决页面中事件流(事件发生顺序)的问题。
区别:
- 事件冒泡:事件会从最内层的元素开始发生,一直向上传播,直到document对象。
- 事件捕获:件会从最外层开始发生,直到最具体的元素。
DOM2级事件”中规定的事件流同时支持了事件捕获阶段和事件冒泡阶段,而作为开发者,我们可以选择事件处理函数在哪一个阶段被调用。
element.addEventListener(event, function, useCapture)
- event:事件名
- function:事件处理程序
- useCapture:可选。布尔值,指定事件是否在捕获或冒泡阶段执行
- true:事件句柄在捕获阶段执行(即在事件捕获阶段调用处理函数)
- false(默认):事件句柄在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)
阻止事件冒泡:
- 给子级加 event.stopPropagation( )
- 在事件处理函数中返回 false
addEventListener兼容ie9+ 要兼容旧版本的IE浏览器,可以使用IE的attachEvent函数:object.attachEvent(event, function)
20. 闭包?
1、什么是闭包?
闭包是指有权访问另一个函数作用域中的变量的函数。
创建闭包最常用的方式就是在一个函数内部创建另一个函数。
2、闭包的产生
《你不知道的JavaScript》这样描述:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 也就是说,有权访问另一个函数作用域中的变量的函数。
看一段代码
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
fn2();
}
fn1();
上面的代码已经产生了闭包。
解析:
- fn2访问到了fn1的变量
- fn2本身是个函数
- 满足了条件“有权访问另一个函数作用域中的变量的函数”
下一段代码
function fn1() {
var name = 'iceman';
function fn2() {
console.log(name);
}
return fn2;
}
var fn3 = fn1();
fn3();
这样就清晰地展示了闭包:
- fn2的词法作用域能访问fn1的作用域
- 将fn2当做一个值返回
- fn1执行后,将fn2的引用赋值给fn3
- 执行fn3,输出了变量name
我们知道通过引用的关系,fn3就是fn2函数本身。执行fn3能正常输出name,这不就是fn2能记住并访问它所在的词法作用域,而且fn2函数的运行还是在当前词法作用域之外了。
3、闭包的作用
- 保护函数的私有变量不受外部的干扰,模拟私有属性(封装私有变量)
- 模仿块级作用域(ES5中没有块级作用域)
- 实现JS的模块
- 柯里化(柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术)
闭包的副作用:内存泄漏。解决方法是避免过度使用闭包
21. 立即执行函数
立即执行函数(自执行函数) :定义和调用合为一体。我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象
立即执行函数的作用:创建独立的作用域,让外部无法访问作用域内部的变量,从而避免变量污染。
22. JS 堆栈内存释放
堆内存:存储引用类型值,对象类型就是键值对,函数就是代码字符串
堆内存释放:将引用类型的空间地址变量赋值成 null,或没有变量占用堆内存了浏览器就会释放掉这个地址
栈内存:提供代码执行的环境和存储基本类型值
栈内存释放:一般当函数执行完后函数的私有作用域就会被释放掉
但栈内存的释放也有特殊情况:
① 函数执行完,但是函数的私有作用域内有内容被栈外的变量还在使用的,栈内存就不能释放里面的基本值也就不会被释放。(闭包)
② 全局下的栈内存只有页面被关闭的时候才会被释放
23. js继承方式有哪些
-
原型链继承
- 原理:子类的原型对象属性是父类的一个实例。
Son.prototype = new Parent() - 本质是
改变子类构造函数的原型对象变量(Son.prototype)的指向。 Son.prototype对象缺少属性constructor,因此需要加上:Son.prototype.constructor = Son- 不足:不能通过子类构造函数向父类构造函数传递参数。
- 原理:子类的原型对象属性是父类的一个实例。
-
借用构造函数继承
- 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类
- 原理:子类的内部借助
apply()和call ()方法调用并传递参数给父类 - 不足:实现了子类构造函数向父类构造函数
传递参数,但没有继承父类原型的属性和方法,无法访问父类原型上的属性和方法。
-
组合继承
- 组合继承 = 借用构造函数继承+原型链继承
- 具备借用构造函数的优点:子类可以向父类传递参数;具备原型链继承的优点:继承父类原型的属性和方法
- 不足:调用了两次父类构造函数,一次是在创建子类型的时候,一次是在子类型的构造函数内部- 进入
父类构造函数为属性赋值,分配内存空间,浪费内存;- 进入
父类构造函数为属性赋值,分配内存空间,浪费内存; - 赋值导致效率下降一些,关键是
new 父类()赋的值无意义,出现代码冗余
- 进入
-
原型式继承
- 利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型
-
寄生式继承
- 在原型式继承的基础上,增强对象,返回构造函数
-
寄生组合式继承
- 寄生组合继承模式=借用构造函数继承+寄生继承
24. ES5继承和ES6继承的区别
ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。(Parent.call(this))
ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。
super这个关键字,既可以当作函数使用,也可以当作对象使用:
super作为函数调用时,代表父类的构造函数super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
使用ES5实现一个继承?
寄生组合式继承(ES5继承的最佳方式)
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
只调用了一次超类构造函数,效率更高。避免在Children.prototype上面创建不必要的、多余的属性,与其同时,原型链还能保持不变。
因此寄生组合继承是引用类型最理性的继承范式。
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
return this.name;
}
function Children(name, age) {
Parent.call(this, name);
this.age = age;
}
//寄生组合式继承
Children.prototype = Object.create(Parent.prototype);
Children.prototype.constructor = Children;
Children.prototype.getAge = function () {
return this.age;
}
let girl = new Children('Yvette', 18);
girl.getName();
ES6 中 extends class 的方式使用 babel 编译到ES5,使用的也是寄生组合式继承。
25. js设计模式有哪些?
观察者模式、发布订阅模式、策略模式、适配器模式、发布订阅模式、工厂模式、组合模式、代理模式、门面模式等等
26. for of , for in 和 forEach,map 的区别
- for...of循环
- 具有 iterator 接口,就可以用for...of循环遍历它的成员(属性值)
- 数组、Set 和 Map 结构、某些类似数组的对象、Generator 对象,以及字符串都可以使用
- for...of循环调用遍历器接口
- 数组的遍历器接口只返回具有数字索引的属性
- 于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用
- 可以中断循环。
- for...in
- 遍历对象
自身的和继承的可枚举的属性 - 不能直接获取属性值
- 可以中断循环。
- 遍历对象
- forEach
- 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)。
- map
- 只能遍历数组,不能中断,返回值是修改后的数组。
- Object.keys():返回给定对象所有可枚举属性的字符串数组。
27. forEach相关
(27.1) forEach是否会改变原数组
- 基本类型我们当次循环拿到的ele,只是forEach给我们在另一个地方
复制创建新元素,是和原数组这个元素没有联系的!所以,我们使命给循环拿到的ele赋值都是无用功! - 专业的概念说就是:JavaScript是有基本数据类型与引用数据类型之分的。对于基本数据类型:
number,string,Boolean,null,undefined它们在栈内存中直接存储变量与值。而Object对象的真正的数据是保存在堆内存,栈内只保存了对象的变量以及对应的堆的地址,所以操作Object其实就是直接操作了原数组对象本身。 - forEach 的基本原理也是for循环,使用
arr[index]的形式赋值改变,无论什么就都可以改变了。 - 所以如果是基本类型的数据,不能改变原数组,而引用类型的数组能改变。
(27.2) forEach怎么跳出循环
在遍历内部需要跳出循环的地方抛出错误,使用try...catch捕获
28. let、const 以及 var 的区别是什么?
- let 和 const 定义的变量不会出现变量提升,而 var 定义的变量会提升。
- let 和 const 是JS中的块级作用域
- let 和 const 不允许重复声明(会抛出错误),而var可以。
- let 和 const 定义的变量在定义语句之前,如果使用会
抛出错误(形成了暂时性死区),而 var 不会。 - const 声明一个只读的常量。一旦声明,常量的值就不能改变。如果声明是一个对象,那么不能改变的是对象的引用地址。
- const 声明变量时必须设置
初始值 - 顶层作用域中 var 声明的变量挂在window上(浏览器环境)
29. JS中什么变量提升与暂时性死区?
变量提升就是变量在声明之前就可以使用,值为undefined。
在代码块内,使用 let/const 命令声明变量之前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着 typeof 不再是一个百分百安全的操作。
typeof x; // ReferenceError(暂时性死区,抛错)
let x;
复制代码
typeof y; // 值是undefined,不会报错
复制代码
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
30. call/apply/bind区别?
- call/apply功能相同,一个函数被 call/apply 的时候,会直接调用
fn.call(obj, arg1, arg2, ...),调用一个函数, 具有一个指定的this值和分别地提供的参数(参数的列表)。fn.apply(obj, [argsArray]),调用一个函数,具有一个指定的this值,以及作为一个数组(或类数组对象)提供的参数。
- bind:一个函数被 bind 的时候,会创建一个新函数。
- bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
31. new的原理是什么?
new关键字主要做了以下的工作:
- 创建一个新的对象
obj - 将对象与构建函数通过原型链连接起来
- 将构建函数中的
this绑定到新建的对象obj上 - 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理
function mynew(Func, ...args) {
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = Func.prototype
// 3.将构建函数的this指向新对象
let result = Func.apply(obj, args)
// 4.根据返回值判断
return result instanceof Object ? result : obj
}
32. 通过new的方式创建对象和通过字面量创建有什么区别
- 字面量创建对象,不会调用 Object构造函数, 简洁且性能更好
- new Object() 方式创建对象本质上是方法调用,涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的 堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。
33. 防抖和节流的区别是什么?防抖和节流的实现。
防抖和节流的作用都是防止函数多次调用。
区别:
- 每次触发函数的间隔小于设置的时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间调用一次函数。
- 防抖(debounce): n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间
- 节流(throttle): 高频事件在规定时间内只会执行一次,执行一次后,只有大于设定的执行周期后才会执行第二次。
防抖的应用场景:
- 每次 resize/scroll 触发统计事件
- 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)
函数节流的应用场景有:
- DOM 元素的拖拽功能实现(mousemove)
- 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
- 计算鼠标移动的距离(mousemove)
- Canvas 模拟画板功能(mousemove)
- 搜索联想(keyup)
- 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户停止滚动后,才会判断是否到了页面底部;如果是 throttle 的话,只要页面滚动就会间隔一段时间判断一次
34. 取数组的最大值(ES5、ES6)
// ES5 的写法
Math.max.apply(null, [14, 3, 77, 30]);
// ES6 的写法
Math.max(...[14, 3, 77, 30]);
// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
return accumulator = accumulator > currentValue ? accumulator : currentValue
});
35. setTimeout倒计时为什么会出现误差?
setTimeout 只能保证延时或间隔不小于设定的时间。因为它实际上只是将回调添加到了宏任务队列中,但是如果主线程上有任务还没有执行完成,它必须要等待。
JS的运行机制
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在"任务队列"(task queue)。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
如 setTimeout(()=>{callback();}, 1000) ,即表示在1s之后将 callback 放到宏任务队列中,当1s的时间到达时,如果主线程上有其它任务在执行,那么 callback 就必须要等待,另外 callback 的执行也需要时间,因此 setTimeout 的时间间隔是有误差的,它只能保证延时不小于设置的时间。
如何减少 setTimeout 的误差
我们只能减少执行多次的 setTimeout 的误差。
例如倒计时功能。
倒计时的时间通常都是从服务端获取的。造成误差的原因:
1.没有考虑误差时间(函数执行的时间/其它代码的阻塞)
2.没有考虑浏览器的“休眠”
完全消除 setTimeout的误差是不可能的,但是我们减少 setTimeout 的误差。通过对下一次任务的调用时间进行修正,来减少误差。
36. promise相关
1、promise 有几种状态?优缺点?
promise有三种状态: fulfilled, rejected, pending。
优点:
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果
- 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
缺点:
- 无法取消 Promise
- 当处于pending状态时,无法得知目前进展到哪一个阶段
2、Promise构造函数是同步还是异步执行,then中的方法呢 ?promise如何实现then处理 ?
Promise的构造函数是同步执行的。then 中的方法是异步执行的。
promise的then实现,详见: Promise源码实现
3. Promise和setTimeout的区别 ?
Promise 是微任务,setTimeout 是宏任务,同一个事件循环中,promise.then总是先于 setTimeout 执行。
4. 如何实现 Promise.all ?
要实现 Promise.all,首先我们需要知道 Promise.all 的功能:
- 如果传入的参数是一个空的可迭代对象,那么此promise对象回调完成(resolve),只有此情况,是同步执行的,其它都是异步返回的。
- 如果传入的参数不包含任何 promise,则返回一个异步完成. promises 中所有的promise都“完成”时或参数中不包含 promise 时回调完成。
- 如果参数中有一个promise失败,那么Promise.all返回的promise对象失败
- 在任何情况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
let index = 0;
let result = [];
if (promises.length === 0) {
resolve(result);
} else {
function processValue(i, data) {
result[i] = data;
if (++index === promises.length) {
resolve(result);
}
}
for (let i = 0; i < promises.length; i++) {
//promises[i] 可能是普通值
Promise.resolve(promises[i]).then((data) => {
processValue(i, data);
}, (err) => {
reject(err);
return;
});
}
}
});
}
如果想了解更多Promise的源码实现,可以参考另一篇文章:Promise的源码实现(完美符合Promise/A+规范)
5. 如何实现 Promise.prototype.finally ?
不管成功还是失败,都会走到finally中,并且finally之后,还可以继续then。并且会将值原封不动的传递给后面的then.
Promise.prototype.finally = function (callback) {
return this.then((value) => {
return Promise.resolve(callback()).then(() => {
return value;
});
}, (err) => {
return Promise.resolve(callback()).then(() => {
throw err;
});
});
}
6、如何实现 Promise.race?
在代码实现前,我们需要先了解 Promise.race 的特点:
- Promise.race返回的仍然是一个Promise. 它的状态与第一个完成的Promise的状态相同。它可以是完成( resolves),也可以是失败(rejects),这要取决于第一个Promise是哪一种状态。
- 如果传入的参数是不可迭代的,那么将会抛出错误。
- 如果传的参数数组是空,那么返回的 promise 将永远等待。
- 如果迭代包含一个或多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。
Promise.race = function (promises) {
//promises 必须是一个可遍历的数据结构,否则抛错
return new Promise((resolve, reject) => {
if (typeof promises[Symbol.iterator] !== 'function') {
//真实不是这个错误
Promise.reject('args is not iteratable!');
}
if (promises.length === 0) {
return;
} else {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then((data) => {
resolve(data);
return;
}, (err) => {
reject(err);
return;
});
}
}
});
}
引申: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的实现原理,如果还不太会,戳:Promise源码实现
37. 什么是函数柯里化?实现 sum(1)(2)(3) 返回结果是1,2,3之和
函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
function sum(a) {
return function(b) {
return function(c) {
return a+b+c;
}
}
}
console.log(sum(1)(2)(3)); // 6
引申:实现一个curry函数,将普通函数进行柯里化:
function curry(fn, args = []) {
return function(){
let rest = [...args, ...arguments];
if (rest.length < fn.length) {
return curry.call(this,fn,rest);
}else{
return fn.apply(this,rest);
}
}
}
//test
function sum(a,b,c) {
return a+b+c;
}
let sumFn = curry(sum);
console.log(sumFn(1)(2)(3)); //6
console.log(sumFn(1)(2, 3)); //6
38. async/await 相关
1、谈谈对 async/await 的理解
- async/await 就是 Generator 的语法糖,使得异步操作变得更加方便
- async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。
- async函数内置执行器,函数调用之后,会自动执行,输出最后结果。而Generator需要调用next或者配合co模块使用。
- 更好的语义:async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值。
- 返回值是Promise,async函数的返回值是 Promise 对象,Generator的返回值是 Iterator,Promise 对象使用起来更加方便。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
2、使用 async/await 需要注意什么?
- await 命令后面的Promise对象,运行结果可能是 rejected,此时等同于 async 函数返回的 Promise 对象被reject。因此需要加上错误处理,可以给每个 await 后的 Promise 增加 catch 方法;也可以将 await 的代码放在
try...catch中。 - 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
- await命令只能用在async函数之中,如果用在普通函数,会报错。
- async 函数可以保留运行堆栈。
39. 可遍历数据(可迭代对象)
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,换个角度,也可以认为,一个数据结构只要具有 Symbol.iterator 属性(Symbol.iterator 方法对应的是遍历器生成函数,返回的是一个遍历器对象),那么就可以其认为是可迭代的。
(1)特点
- 具有
Symbol.iterator属性,Symbol.iterator()返回的是一个遍历器对象 - 可以使用
for ... of进行循环 - 遍历器对象根本特征就是具有next方法
- 每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。
一个对象如果要具备可被 for...of 循环调用的 Iterator 接口,就必须在其 Symbol.iterator 的属性上部署遍历器生成方法(或者原型链上的对象具有该方法)。
let arry = [1, 2, 3, 4];
let iter = arry[Symbol.iterator]();
console.log(iter.next()); //{ value: 1, done: false }
console.log(iter.next()); //{ value: 2, done: false }
console.log(iter.next()); //{ value: 3, done: false }
(2)原生具有 Iterator 接口的数据结构
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
- ES6 的数组、Set、Map 都部署了以下三个方法: entries() / keys() / values(),调用后都返回遍历器对象。
(3)自定义一个可迭代对象
一个对象只有具有正确的 Symbol.iterator 属性,那么其就是可迭代的,因此,我们可以通过给对象新增 Symbol.iterator 使其可迭代。
let obj = {
name: "Yvette",
age: 18,
job: 'engineer',
*[Symbol.iterator]() {
const self = this;
const keys = Object.keys(self);
for (let index = 0; index < keys.length; index++) {
yield self[keys[index]];//yield表达式仅能使用在 Generator 函数中
}
}
};
for (var key of obj) {
console.log(key); //Yvette 18 engineer
}
40. requestAnimationFrame 和 setTimeout/setInterval 有什么区别?使用 requestAnimationFrame 有哪些好处?
41. ['1', '2', '3'].map(parseInt)
- 将一个字符串
string转换为radix进制的整数,radix为介于2-36之间的数。最后都是以十进制形式返回。 - 函数将其第一个参数转换为一个字符串,对该字符串进行解析,然后返回一个整数或
NaN。 - 如果
radix没有指定或者是0,则会被指定为10进制来解析 - 如果
string以0x 0X开头,则指定为16进制来解析。 parseInt的第一个参数string要小于第二个参数radix(非必须)- 第一个字符不能转换为数字,
parseInt会返回NaN。
解析:
-
map(parseInt)可能会有很多疑问,这是什么写法?这其实是简写,让我们看看其他例子- 所以此处简写
['1', '2', '3'].map(parseInt) - 完整写法如下
['1', '2', '3'].map((v, i, arr) => parseInt(v, i))
- 所以此处简写
-
map():里面是个回调函数,三个参数:分别是当前值(v),下标(i),原始数组(arr) -
执行顺序:
['1', '2', '3'].map((v, i, arr) => parseInt(v, i)),依次输出:parseInt('1', 0),radix为0,按十进制解析,所以输出 1 。parseInt('2', 1),radix是2-36之间,没有1,所以输出NaNparseInt('3', 2) // NaN
42. ES6模块和CommonJS模块的差异?
CommonJS模块是运行时加载,ES6模块是编译时输出接口- ES6模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
CommonJS加载的是一个对象,该对象只有在脚本运行完才会生成。
- ES6 模块自动采用严格模式,无论模块头部是否写了
"use strict"; - require 可以做动态加载,import 语句做不到,import 语句必须位于顶层作用域中。
- 当使用require命令加载某个模块时,就会运行整个模块的代码。
- 当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
- ES6 模块的输入变量是只读的,不能对其进行重新赋值
js import name from './name'; name = 'Star'; //抛错 - ES6 模块中顶层的 this 指向 undefined,CommonJS 模块的顶层 this 指向当前模块。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
- ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import ,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
//name.js let name = 'William'; setTimeout(() => { name = 'Yvette'; hobbies.push('writing'); }, 300); export { name }; export var hobbies = ['coding']; //index.js import { name, hobbies } from './name'; console.log(name, hobbies); //William ["coding"] //name 和 hobbie 都会被模块内部的变化所影响 setTimeout(() => { console.log(name, hobbies); //Yvette ["coding", "writing"] }, 500); //Yvette - ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块
CommonJS输出的是一个值的拷贝(注意基本数据类型/复杂数据类型)
模块输出的值是基本数据类型,模块内部的变化就影响不到这个值
//name.js
let name = 'William';
setTimeout(() => { name = 'Yvette'; }, 300);
module.exports = name;
//index.js
const name = require('./name');
console.log(name); //William
//name.js 模块加载后,它的内部变化就影响不到 name
//name 是一个基本数据类型。将其复制出一份之后,二者之间互不影响。
setTimeout(() => console.log(name), 500); //William
- 模块输出的是对象,属性值是简单数据类型时:
模块输出的值是复杂数据类型
//name.js
let name = 'William';
setTimeout(() => { name = 'Yvette'; }, 300);
module.exports = { name };
//index.js
const { name } = require('./name');
console.log(name); //William
//name 是一个原始类型的值,会被缓存。
setTimeout(() => console.log(name), 500); //William
- 模块输出的是对象,属性值是复制数据类型时:
//name.js
let name = 'William';
let hobbies = ['coding'];
setTimeout(() => {
name = 'Yvette';
hobbies.push('reading');
}, 300);
module.exports = { name, hobbies };
//index.js
const { name, hobbies } = require('./name');
console.log(name); //William
console.log(hobbies); //['coding']
/*
* name 的值没有受到影响,因为 {name: name} 属性值 name 存的是个字符串
* 300ms后 name 变量重新赋值,但是不会影响 {name: name}
*
* hobbies 的值会被影响,因为 {hobbies: hobbies} 属性值 hobbies 中存的是
* 数组的堆内存地址,因此当 hobbies 对象的值被改变时,存在栈内存中的地址并
没有发生变化,因此 hoobies 对象值的改变会影响 {hobbies: hobbies}
* xx = { name, hobbies } 也因此改变 (复杂数据类型,拷贝的栈内存中存的地址)
*/
setTimeout(() => {
console.log(name);//William
console.log(hobbies);//['coding', 'reading']
}, 500);
43. js如何自定义事件?
自定义事件有三种方法:
-
- 使用
new Event()
获取不到event.detaillet btn = document.querySelector('#btn'); let ev = new Event('alert', { bubbles: true, //事件是否冒泡;默认值false cancelable: true, //事件能否被取消;默认值false composed: false }); btn.addEventListener('alert', function (event) { console.log(event.bubbles); //true console.log(event.cancelable); //true console.log(event.detail); //undefined }, false); btn.dispatchEvent(ev);
- 使用
-
- 使用
createEvent('CustomEvent')(DOM3)
要创建自定义事件,可以调用createEvent('CustomEvent'),返回的对象有 initCustomEvent 方法,接受以下四个参数:
- type: 字符串,表示触发的事件类型,如此处的'alert'
- bubbles: 布尔值: 表示事件是否冒泡
- cancelable: 布尔值,表示事件是否可以取消
- detail: 任意值,保存在 event 对象的 detail 属性中
let btn = document.querySelector('#btn'); let ev = btn.createEvent('CustomEvent'); ev.initCustomEvent('alert', true, true, 'button'); btn.addEventListener('alert', function (event) { console.log(event.bubbles); //true console.log(event.cancelable);//true console.log(event.detail); //button }, false); btn.dispatchEvent(ev);
- 使用
-
- 使用
new customEvent()(DOM4) 使用起来比createEvent('CustomEvent')更加方便var btn = document.querySelector('#btn'); /* * 第一个参数是事件类型 * 第二个参数是一个对象 */ var ev = new CustomEvent('alert', { bubbles: 'true', cancelable: 'true', detail: 'button' }); btn.addEventListener('alert', function (event) { console.log(event.bubbles); //true console.log(event.cancelable);//true console.log(event.detail); //button }, false); btn.dispatchEvent(ev);
- 使用
44. js异步加载的方式有哪些?
-
<script>的 defer 属性,HTML4 中新增
- defer要等到整个页面在内存中正常渲染结束,才会执行(“渲染完再执行”)
<script>标签打开defer属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
-
<script>的 async 属性,HTML5 中新增
- async是“下载完就执行”
- async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
-
- 动态插入 script 脚本
- 动态创建的
script,设置src并不会开始下载,而是要添加到文档中,JS文件才会开始下载。function downloadJS() { varelement = document.createElement("script"); element.src = "XXX.js"; // 添加到html文件中才会开始下载 document.body.appendChild(element); } //合适的时候,调用上述方法
-
- XHR 异步加载JS
let xhr = new XMLHttpRequest(); xhr.open("get", "js/xxx.js",true); xhr.send(); xhr.onreadystatechange = function() { if (xhr.readyState == 4 && xhr.status == 200) { eval(xhr.responseText); } }
- XHR 异步加载JS
defer 和 async 的区别:
defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在window.onload 之前执行;- async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
- defer是“渲染完再执行”,async是“下载完就执行”。
- 如果有多个 defer 脚本,会按照它们在页面出现的顺序加载。
- 多个async脚本是不能保证加载顺序的。
45. 下面这段代码的输出是什么?
function Foo() {
getName = function() {console.log(1)};
return this;
}
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
var getName = function() {console.log(4)};
function getName() {console.log(5)};
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
1.首先预编译阶段,变量声明与函数声明提升至其对应作用域的最顶端。
因此上面的代码编译后如下(函数声明的优先级先于变量声明):
function Foo() {
getName = function() {console.log(1)};
return this;
}
function getName() {console.log(5)}; //函数优先(函数首先被提升) 此行会被后面的声明式函数覆盖
var getName;//重复声明,被忽略
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
getName = function() {console.log(4)};
2.Foo.getName();直接调用Foo上getName方法,输出2
3.getName();输出4,getName被重新赋值了
4.Foo().getName();执行Foo(),window的getName被重新赋值,返回this;浏览器环境中,非严格模式,this 指向 window,this.getName();输出为1.
如果是严格模式,this 指向 undefined,此处会抛出错误。
如果是node环境中,this 指向 global,node的全局变量并不挂在global上,因为global.getName对应的是undefined,不是一个function,会抛出错误。
5.getName();已经抛错的自然走不动这一步了;继续浏览器非严格模式;window.getName被重新赋过值,此时再调用,输出的是1
6.new Foo.getName();考察运算符优先级的知识,new 无参数列表,对应的优先级是18;成员访问操作符 . , 对应的优先级是 19。因此相当于是 new (Foo.getName)();new操作符会执行构造函数中的方法,因此此处输出为 2.
7.new Foo().getName();new 带参数列表,对应的优先级是19,和成员访问操作符.优先级相同。同级运算符,按照从左到右的顺序依次计算。new Foo()先初始化 Foo 的实例化对象,实例上没有getName方法,因此需要原型上去找,即找到了 Foo.prototype.getName,输出3
8.new new Foo().getName(); new 带参数列表,优先级19,因此相当于是 new (new Foo()).getName();先初始化 Foo 的实例化对象,然后将其原型上的 getName 函数作为构造函数再次 new ,输出3
因此最终结果如下:
Foo.getName(); //2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3
46. Object.is() 与比较操作符 ===、== 有什么区别?
以下情况,Object.is认为是相等
两个值都是 undefined
两个值都是 null
两个值都是 true 或者都是 false
两个值是由相同个数的字符按照相同的顺序组成的字符串
两个值指向同一个对象
两个值都是数字并且
都是正零 +0
都是负零 -0
都是 NaN
都是除零和 NaN 外的其它同一个数字
Object.is() 类似于 ===,但是有一些细微差别,如下:
- NaN 和 NaN 相等
- -0 和 +0 不相等
console.log(Object.is(NaN, NaN));//true
console.log(NaN === NaN);//false
console.log(Object.is(-0, +0)); //false
console.log(-0 === +0); //true
47. 请实现一个 flattenDeep 函数,把嵌套的数组扁平化
1、利用 Array.prototype.flat
flat 默认只会 “拉平” 一层,如果想要 “拉平” 多层的嵌套数组,需要给 flat 传递一个整数,表示想要拉平的层数。
function flattenDeep(arr, deepLength) {
return arr.flat(deepLength);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]], 3));
当传递的整数大于数组嵌套的层数时,会将数组拉平为一维数组,JS能表示的最大数字为 Math.pow(2, 53) - 1,因此我们可以这样定义 flattenDeep 函数
function flattenDeep(arr) {
//当然,大多时候我们并不会有这么多层级的嵌套
return arr.flat(Math.pow(2,53) - 1);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
2、利用 reduce 和 concat
function flattenDeep(arr){
return arr.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
3、使用 stack 无限反嵌套多层嵌套数组
function flattenDeep(input) {
const stack = [...input];
const res = [];
while (stack.length) {
// 使用 pop 从 stack 中取出并移除值
const next = stack.pop();
if (Array.isArray(next)) {
// 使用 push 送回内层数组中的元素,不会改动原始输入 original input
stack.push(...next);
} else {
res.push(next);
}
}
// 使用 reverse 恢复原数组的顺序
return res.reverse();
}
console.log(flattenDeep([1, [2, [3, [4]], 5]]));
48. 实现一个 JSON.stringify
49. 实现一个 JSON.parse
50. 实现一个观察者模式
51、下列语句输出什么
console.log(([])?true:false);
console.log(([]==false?true:false));
console.log(({}==false)?true:false)
答案选C:“true、true、false”。
此题考察类型转换,三元运算符先 “分清是非” ,再决定今后该走哪条路,“==”运算符比较 “喜欢” Number类型。
请看《JavaScript权威指南》:
下面是题目的类型转换结果:
Boolean([]); //true
Number([]); //0
Number({}); // NaN
Number(false); //0
因此:
console.log(([])?true:fasle);// => console.log((true)?true:false);
console.log([]==false?true:false); // => console.log(0==0?true:false);
console.log(({}==false)?true:false); // => console.log((NaN==0)?true:false);
《JavaScript权威指南》的部分相关知识点
“==”运算符(两个操作数的类型不相同时)
- 如果一个值是null,另一个值是undefined,则它们相等
- 如果一个值是数字,另一个值是字符串,先将字符串转换为数学,然后使用转换后的值进行比较。
- 如果其中一个值是true,则将其转换为1再进行比较。如果其中的一个值是false,则将其转换为0再进行比较。
- 如果一个值是对象,另一个值是数字或字符串,则将对象转换为原始值,再进行比较。
对象到数字的转换
- 如果对象具有valueOf()方法,后者返回一个原始值,则JavaScript将这个原始值转换为数字(如果需要的话)并返回一个数字。
- 否则,如果对象具有toString()方法,后者返回一个原始值,则JavaScript将其转换并返回。(对象的toString()方法返回一个字符串直接量(作者所说的原始值),JavaScript将这个字符串转换为数字类型,并返回这个数字)。
- 否则,JavaScript抛出一个类型错误异常。
空数组转换为数字0
- 数组继承了默认的valueOf()方法,这个方法返回一个对象而不是一个原始值,因此,数组到数学的转换则调用toString()方法。空数组转换为空字符串,空字符串转换为数字0.
网络
摘录:
浏览器输入URL后发生了什么
大体上,可以分为六步:
- 1、URL解析
- 2、DNS域名解析
- 在客户端输入 URL 后,会有一个递归查找的过程,从
浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找,这个过程中任何一步找到了都会结束查找流程 - 如果本地DNS服务器无法查询到,则根据本地DNS服务器设置的转发器进行查询。包括:根 DNS 服务器、顶级域 DNS 服务器、权威 DNS 服务器
- 在客户端输入 URL 后,会有一个递归查找的过程,从
- 3、建立TCP连接(TCP三次握手)
- 4、发送HTTP请求
- 5、服务器处理请求
- 6、断开TCP请求
- 7、浏览器解析文件
- 8、浏览器渲染
三次握手?四次挥手?
三次握手的过程
- 第一次握手:
客户端给服务器端发送一个SYN报文,并指明自己的初始化序列号,此时客户端处于发送状态 - 第二次握手:
服务器收到客户端的SYN报文之后,会以自己的SYN报文作为应答,并且也指定了自己的初始化序列号,同时会把客户端的序列号+1作为ACK的值,表示自己已经收到了客户端的SYN,此时服务端处于同步收到状态 - 第三次握手:
客户端收到服务端的SYN报文后,会发送一个ACK报文,也是一样把服务端的序列号+1作为ACK的值,表示自己已经收到的服务端的SYN,此时客户端处于建立连接状态,服务端收到报文后,也处于建立连接状态
三次握手的作用是,确保客户端、服务端双方的发送和接受能力都是正常状态,并为之后的可靠性传输做准备。
- 第一次握手:客户端发送网络包,服务端收到了
这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。 - 第二次握手:服务端发包,客户端收到了
这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常 - 第三次握手:客户端发包,服务端收到了
这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常
四次挥手的过程
-
第一次挥手
- 发送端:向接收端发送带FIN标记的数据包,表示想要断开连接,进入终止等待1状态
- 接收端:接收数据包并暂存 FIN
-
第二次挥手
- 接收端:向发送端发送带ACK标记的数据包,表示已经收到要断开连接的请求,但不会立即断开,进入关闭等待状态
- 发送端:接受数据包并暂存ACK,进入终止等待2状态
-
第三次挥手
- 接收端:向发送端发送带FIN标记的数据包,表示我准备好要断开了,进入最后确认状态
- 发送端:接受数据包并验证 FIN
-
第四次挥手
- 发送端:向接收端发送带ACK标记的数据包,表示那你可以断开了,进入TIME_WAIT阶段,等待计时器设置的时间2MSL后,客户端才进入CLOSED状态
- 接收端:接收数据包并验证ACK,没有问题接收端就可以断开连接、回收端口了,进入关闭状态
介绍下 npm 模块安装机制,为什么输入 npm install 就可以自动安装对应的模块?
浏览器渲染相关
(1)浏览器渲染的主要流程?
创建 DOM 树 —> 创建 Style Rules -> 构建 Render 树 —> 布局 Layout -—> 绘制 Painting。
- 第一步,构建 DOM 树:用 HTML 分析器,分析 HTML 元素,构建一棵 DOM 树;
- 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
- 第三步,构建 Render 树:将 DOM 树和样式表关联起来,构建一棵 Render 树(Attachment)。每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象(又名 renderer),这些 render 对象最终会被构建成一棵 Render 树;
- 第四步,确定节点坐标(Layout):根据 Render 树结构,为每个 Render 树上的节点确定一个在显示屏上出现的精确坐标;
- 第五步,绘制页面(Paint):根据 Render 树和节点显示坐标,然后调用每个节点的 paint 方法,将它们绘制出来。
(2)优化渲染流程(减少白屏时间)
- 使用内联 JS、CSS ,减少 JS 、 CSS 文件的下载
- webpack 等工具对 JS、CSS 文件压缩,减少文件大小
- 使用 async 或者 defer
- 使用 CDN 等
(3)CSS 会阻塞 DOM 解析吗?
完整的渲染流程如下:
- 渲染进程解析 HTML 内容转换为 DOM 树结构,解析 CSS 为 CSSDOM
- 把 DOM 和 CSSOM 结合起来生成渲染树(Render Tree)
- 渲染树构建好了之后,将会执行布局过程,它将确定每个节点在屏幕上的确切坐标
- 把渲染树展示到屏幕上。再下一步就是绘制,即遍历渲染树,并使用UI后端层绘制每个节点。
值得注意的是:
关键的点在于上述的 4 步并不是以严格顺序执行的,也就是说,浏览器一边解析 HTML,一边构建渲染树,构建一部分,就会把当前已有的元素渲染出来。由于浏览器会尝试尽快展示内容,所以内容有时会在样式还没有加载的时候展示出来。这就是经常发生的FOCU(flash of unstyled content)或白屏问题。
由浏览器的渲染流程图可知,DOM 解析和 CSS 解析是两个并行的进程,所以CSS 加载不会阻塞 DOM 树的解析。
Render Tree是依赖于 DOM Tree 和 CSSOM Tree 的,所以无论 DOM Tree 是否已经完成,它都必须等待到 CSSOM Tree 构建完成,即 CSS 加载完成(或 CSS 加载失败)后,才能开始渲染。因此,CSS 加载会阻塞 DOM 树的渲染。
状态码有哪些?
- 2XX(成功处理了请求状态)
- 200 服务器已经成功处理请求,并提供了请求的网页
- 201 用户新建或修改数据成功
- 202 一个请求已经进入后台
- 204 用户删除成功
- 3XX(每次请求使用的重定向不要超过5次)
- 304 网页上次请求没有更新,节省带宽和开销
- 4XX(表示请求可能出错,妨碍了服务器的处理)
- 400 服务器不理解请求的语法
- 401 用户没有权限(用户名,密码输入错误)
- 403 用户得到授权(401相反),但是访问被禁止
- 404 服务器找不到请求的网页,
- 5XX(表示服务器在处理请求的时候发生内部错误)
- 500 服务器遇到错误,无法完成请求
- 503 服务器目前无法使用(超载或停机维护)
304的缓存原理(添加Etag标签.last-modified) 304 网页上次请求没有更新,节省带宽和开销
常见的web安全及防护原理
- 1.sql注入:
原理:是将sql代码伪装到输入参数中,传递到服务器解析并执行的一种攻击手法。也就是说,在一些对server端发起的请求参数中植入一些sql代码,server端在执行sql操作时,会拼接对应参数, 同时也将一些sql注入攻击的“sql”拼接起来,导致会执行一些预期之外的操作。
防范方法:- 1.对用户输入进行校验
- 2.不适用动态拼接sql
- 2.XSS(跨站脚本攻击):往web页面插入恶意的html标签或者js代码。
举例子:在论坛放置一个看是安全的链接,窃取cookie中的用户信息
防范方法:- 1.尽量采用post而不使用get提交表单
- 2.避免cookie中泄漏用户的隐式
- 3.CSRF(跨站请求伪装):通过伪装来自受信任用户的请求
举例子:黄轶老师的webapp音乐请求数据就是利用CSRF跨站请求伪装来获取QQ音乐的数据
防范方法:在客服端页面增加伪随机数,通过验证码 - XSS和CSRF的区别:
- 1.XSS是获取信息,不需要提前知道其他用户页面的代码和数据包
- 2.CSRF代替用户完成指定的动作,需要知道其他页面的代码和数据包
http协议的理解
- 1.超文本的传输协议,是用于从万维网服务器超文本传输到本地资源的传输协议
- 2.基于TCP/IP通信协议来传递数据(HTML,图片资源)
- 3.基于运用层的面向对象的协议,由于其简洁、快速的方法、适用于分布式超媒体信息系统
- 4.http请求信息request:
- 请求行(request line)、请求头部(header),空行和请求数据四部分构成
- 请求行,用来说明请求类型,要访问的资源以及所使用的HTTP版本.
- 请求头部,用来说明服务器要使用的附加信息
- 空行,请求头部后面的空行是必须的
- 请求数据也叫主体,可以添加任意的其他数据。
- 5.http相应信息Response
- 状态行、消息报头、空行和响应正文
- 状态行,由HTTP协议版本号, 状态码, 状态消息 三部分组成
- 消息报头,用来说明客户端要使用的一些附加信息
- 空行,消息报头后面的空行是必须的
- 响应正文,服务器返回给客户端的文本信息。
常用的Content-Type:
1、application/json:代表发送的实体数据的数据类型是JSON数据
2、application/x-www-from-urlencoded:代表发送的实体数据的数据类型是键值对(默认)
3、multipart/form-data
4、text/plain
说一下http1.0/http1.1/http2.0/http3.0
说一下http和https区别
- 概念
- http
http 是一种超文本传输协议,是一个客户端和服务器端请求和应答的标准, - https
简单讲就是http的安全版,作用是建立一个信息安全通道,来确保数据的传输,确保网站的安全性。
- 区别
- https需要CA证书,费用较高
- http是超文本传输协议,信息是明文传输;https是具有安全性的SSL\TLS加密传输协议
- http默认端口是80;https默认端口是443
- HTTPS 的连接很简单,是无状态的;HTTPS 协议是由 SSL+HTTP 协议构建的可进行加密传输、身份认证的网络协议,比 HTTP 协议安全。
- https的缺点
- https握手协议耗时较久,https缓存没有http高效
- CA证书费用较高
HTTPS原理?为什么https能保证安全?
1、对称加密
- 加密和解密算法是公开的,那个密钥是保密的
- 加密和解密用的是同一个密钥
- 对称密钥的弊端:能被中间人拦截,这样中间人就可以获取到了密钥,就可以对传输的信息就行窥视和篡改
2、非对称加密(RSA)
- 双方必须协商一对密钥,一个私钥一个公钥
- 用私钥加密的数据,只有对应的公钥才能解密
- 用公钥加密的数据, 只有对应的私钥才能解密
- 弊端:RSA 算法很慢
3、非对称密钥+对称密钥
- 使用对称密钥的好处是速度比较快
- 使用非对称密钥的好处是可以使得传输的内容不能被破解
- 结合两者的优点:
- 生成一个对称加密算法的密钥,使用 RSA 的方法发送
- 利用对称密钥来通信
4、中间人的攻击
在使用非对称密钥的时候,首先需要将 Bill 的公钥给张大胖,那么在这个过程中,安全是没有保障的,中间人可以拦截到 Bill 的公钥,就可以对拦截到的公钥进行篡改。
5、确认身份 —— 数字证书
简单来讲是这样的, Bill可以把他的公钥和个人信息用一个Hash算法生成一个消息摘要, 这个Hash算法有个极好的特性,只要输入数据有一点点变化,那生成的消息摘要就会有巨变,这样就可以防止别人修改原始内容。
CA:有公信力的认证中心。用它的私钥对消息摘要加密,形成签名。
验证过程:
用同样的Hash 算法, 再次生成消息摘要,然后用CA的公钥对数字签名解密, 得到CA创建的消息摘要, 两者对比,是否相同?
6、https的原理
把张大胖替换成浏览器, 把Bill 替换成某个网站就行了。
web缓存(前端缓存)
访问服务器获取数据都是很常见的事情,如果相同的数据被重复请求了不止一次,那么多余的请求必然会浪费网络带宽,以及延迟浏览器渲染所要处理的内容,从而影响用户的使用体验。(为什么需要缓存)
(1)定义
web缓存就是存在于客户端与服务器之间的一个副本、当你第一个发出请求后,缓存根据请求保存输出内容的副本。
(2)分类
前端缓存主要是分为HTTP缓存和浏览器缓存。其中HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置;而浏览器缓存则主要由前端开发在前端js上进行设置。
(2.1)浏览器缓存
浏览器缓存则主要由前端开发在前端js上进行设置。
浏览器缓存:比如:localStorage,sessionStorage,cookie等等。这些功能主要用于缓存一些必要的数据,比如用户信息。比如需要携带到后端的参数。亦或者是一些列表数据等等。
不过这里需要注意。像localStorage,sessionStorage这种用户缓存数据的功能,他只能保存5M左右的数据,多了不行。cookie则更少,大概只能有4kb的数据。
(2.2)http缓存
HTTP缓存是在HTTP请求传输时用到的缓存,主要在服务器代码上设置。
http缓存是web缓存的核心,是最难懂的那一部分,也是最重要的那一部分。
(2.3)CDN缓存
(2.4)代理服务器缓存
(3)缓存的优缺点
如下图所示,服务器需要处理http的请求,并且http去传输数据,需要带宽,带宽是要钱买的啊。而我们缓存,就是为了让服务器不去处理这个请求,客户端也可以拿到数据。
注意,我们的缓存主要是针对html,css,img等静态资源,常规情况下,我们不会去缓存一些动态资源,因为缓存动态资源的话,数据的实时性就不会不太好,所以我们一般都只会去缓存一些不太容易被改变的静态资源。
缓存的好处(解决的问题):
- 省钱:减少不必要的http的请求,节约宽带
- 减载:减少服务器负载,避免服务器过载的情况出现
- 加速:降低网络延迟,更快的加载页面(直接读取浏览器的数据)
缺点:
- 占内存(有些缓存会被存到内存中)
http缓存
(1)缓存原理
缓存的原理是在首次请求后保存一份请求资源的响应副本,当用户再次发起相同请求后,如果判断缓存命中则拦截请求,将之前存储的响应副本返回给用户,从而避免了重新向服务器发起资源请求。
(2)http缓存分类与流程
HTTP缓存应该算是前端开发中最常接触的缓存之一,它又可以细分为强制缓存和协商缓存,二者最大的区别在于判断缓存命中时,浏览器是否需要向服务器端进行询问以协商缓存的相关信息,进而判断是否需要就响应内容进行重新请求,下面让我们来看看HTTP缓存的具体机制及缓存的决策策略。
http缓存流程图:
(3)缓存决策及注意事项
注意事项:
假设在不考虑客户端缓存容量与服务器算力的理想情况下,我们当然希望客户端浏览器上的缓存触发率尽可能高,留存时间尽可能长,同时还要Etag实现当资源更新时进行高效的重新验证。但实际情况往往是容量与算力都有限,因此就需要制定合适的缓存策略,来利用有限的资源达到最优的性能效果,明确能力的边界,力求在边界内做到最好。
在面对一个具体的缓存需求时,我们可以参照如下的缓存决策树来逐步确定对一个资源具体的缓存策略。
- 是否使用缓存
- 否:no-store
- 是:
- 是否进行协商缓存
- 是:no-cache
- 否
- 是否会被代理服务器缓存
- 是:public(
- 否:private
- 配置强制缓存过期时间
- 配置协商缓存的Etag或last-modified。
- 配置强制缓存过期时间
- 是否会被代理服务器缓存
- 是否进行协商缓存
no-store:禁止使用缓存
no-cache:协商缓存
public:响应资源既可以被浏览器缓存,又可以被代理服务器缓存
private:限制了响应资源只能被浏览器缓存
public与private互斥,不设置默认是private
Etag:比较文件指纹(由文件内容计算出的唯一哈希值)
last-modified:比较修改资源文件的时间戳
Etag并不是last-modified的完全替代方案,而是补充方案,具体用哪一个,取决于业务场景。
(4)缓存位置
从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。
- Service Worker
- 行在浏览器背后的独立线程,一般可以用来实现缓存功能
- 因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全
- Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
- Memory Cache
- 内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。
- 读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。
- 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了
- 访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
- Disk Cache
- Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点
- Disk Cache 比Memory Cache胜在容量和存储时效性上。
- 在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的
- 它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。
- Push Cache
- Push Cache是推送缓存,是 HTTP/2 中的内容
- 只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂
- 在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。
那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。
(4)http缓存-强缓存
对于强制缓存而言,如果浏览器判断所请求的目标资源有效命中,则直接从强制缓存中返回请求响应,无须与服务器进行任何通信。返回200的状态码。
在浏览器控制台
NetWork中的体现为:
200 OK (from disk cache)或者200 OK (from memory cache)
释义
200 OK (from disk cache)HTTP状态码200,缓存的文件从硬盘中读取200 OK (from memory cache)HTTP状态码200,缓存的文件从内存中读取
强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。
(4.1)基于expires实现
缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和Last-modified结合使用。 Expires是Web服务器响应消息头字段,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。
- 在以前,我们通常会使用响应头的
Expires字段去实现强缓存,现在的项目中基本上不推荐使用expires - HTTP1.0协议中声明的用来
控制缓存失效日期时间戳的字段 - 由服务器端指定后通过响应头告知浏览器,浏览器在接收到带有该字段的响应体后进行缓存。
- 如果浏览器再次发起相同的资源请求,便会对比
expires与本地当前的时间戳。- 当前请求的本地时间戳小于expires的值,则说明浏览器缓存的响应还未过期,无须向服务器端再次发起请求
- 当本地时间戳大于expires值,缓存过期,重新向服务器发起请求。(仅过期才能允许再次发送请求)
- expires判断的局限性:
- 对本地时间戳过分依赖
- 如果客户端本地的时间与服务器端的时间不同步,或者对客户端的时间进行主动修改,那么对于缓存过期的判断可能就无法和预期相符。
- 解决expires判断的局限性:HTTP1.1协议开始新增了
cache-control字段- 比如:cache-control设置了maxage=31536000的属性值来控制响应资源的有效期,它是一个以秒为单位的时间长度,表示该资源在被请求到后的31536000秒内有效,如此便可避免服务器端和客户端时间戳不同步而造成的问题。
(4.2)基于cache-control实现
在HTTP/1.1中,Cache-Control是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。
Cache-Control:max-age=N,N就是需要缓存的秒数。从第一次请求资源的时候开始,往后N秒内,资源若再次请求,则直接从磁盘(或内存中读取),不与服务器做任何交互。
Cache-control中因为max-age后面的值是一个滑动时间,从服务器第一次返回该资源时开始倒计时。所以也就不需要比对客户端和服务端的时间,解决了Expires所存在的巨大漏洞。
基于cache-control实现的强缓存是当下项目中的常规方法,而基于expires实现的强缓存不被推荐使用。
Cache-Control 可以在请求头或者响应头中设置,并且可以组合使用多种指令
cache-control的参数:
- 1.
max-age:(单位为s)表示响应资源能被缓存多久max-age和expires同时存在,则以max-age为准
- 2.
s-maxage:(单位为s)缓存在代理服务器中的过期时长(仅当设置了public属性值时才是有效的)- max-age和s-maxage并不互斥。他们可以一起使用
- 3.
no-cache:强制进行协商缓存- Cache-control中设置了no-cache,那么该资源会直接跳过强缓存的校验,直接去服务器进行协商缓存
- 4.
no-store:禁止使用任何缓存- 客户端的每次请求都需要服务器端给予全新的响应
no-cache与no-store是两个互斥的属性值,不能同时设置
- 5.
public:表示响应资源既可以被浏览器缓存,又可以被代理服务器缓存 - 6.
private:限制了响应资源只能被浏览器缓存- public和private就是决定资源是否可以在代理服务器进行缓存的属性
- 如果这两个属性值都没有被设置,则默认为private
- public和private也是一组互斥属性
Cache-control如何设置多个值呢?用逗号分割
Cache-control:max-age=10000,s-maxage=200000,public
如果要考虑向下兼容的话,在Cache-control不支持的时候,还是要使用Expires,这也是我们当前使用的这个属性的唯一理由。
(4.3)强缓存两种方式的区别
- Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
- Expires 是http1.0的产物,Cache-Control是http1.1的产物
- expires/cache-control两者同时存在的话,Cache-Control优先级高于Expires
- 在某些不支持HTTP1.1的环境下,Expires就会发挥用处
- Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。
(4.4)强缓存的缺陷
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存策略。
(5)http缓存-协商缓存
顾名思义,协商缓存就是在使用本地缓存之前,需要向服务器发起一次GET请求,与之协商当前浏览器保存的本地缓存是否已经过期。通常是采用所请求资源的最近一次的修改时间戳来判断的。
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:
- 协商缓存生效(文件未更新),返回304和Not Modified。
- 协商缓存失效(文件更新),返回200和请求结果。
协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。
(5.1)基于last-modified实现
(5.1.1)实现方式/流程
基于last-modified的协商缓存实现方式是:
- 首先需要在服务器端读出文件修改时间,
- 将读出来的修改时间赋给响应头的
last-modified字段。 - 最后设置
Cache-control:no-cache
第一次请求,服务器端代码
const data = fs.readFileSync('./imgs/CSS.png');//读取资源
// 1.读取修改的时间
const { mtime } = fs.statSync('./imgs/CSS.png');
// 2.设置文件最后修改时间
res.setHeader('last-modified',mtime.toUTCString())
// 3.强制设置为协商缓存
res.setHeader('Cache-Control','no-cache');
res.end(data);
第二次及以后的每一次请求,服务器端代码
const data = fs.readFileSync('./imgs/CSS.png');//读取资源
const { mtime } = fs.statSync('./imgs/CSS.png');//读取修改的时间
const ifModifiedSince = req.headers['if-modified-since'];//读取请求头携带的时间(第一次返回给客户端的文件修改时间)
if (ifModifiedSince === mtime.toUTCString()) {
// 如果两个时间一致,则文件没被修改过,返回304
res.statusCode = 304;
res.end();//因为缓存生效,不需要返回数据
return;// 避免返回新的last-modified
}
res.setHeader('last-modified',mtime.toUTCString())// 设置文件最后修改时间
res.setHeader('Cache-Control','no-cache');// 强制设置为协商缓存
res.end(data);
流程:
- 客户端第一次请求目标资源的时,服务器返回的响应标头包含
last-modified字段,值是该资源的最后一次修改的时间戳,以及cache-control:no-cache - 当客户端再次请求该资源的时候,会携带一个
if-modified-since字段,其值正是上次响应头中last-modified的字段值 - 如果客户端请求头携带的
ifmodified-since字段对应的时间与目标资源的时间戳进行对比,如果没有变化则返回一个304状态码。
需要注意的是:协商缓存判断缓存有效的响应状态码是304,但是如果是强制缓存判断有效的话,响应状态码是200。
(5.1.2)last-modified的不足
- last-modified是根据请求资源的最后修改时间戳进行判断的,虽然请求的文件资源进行了编辑,但是内容并没有发生任何变化,时间戳也会更新,从而导致协商缓存时关于有效性的判断验证为失效,需要重新进行完整的资源请求。浪费了网络的带看资源,延长获取资源的时间
- 标识文件资源修改的时间戳单位是秒,如果文件修改的速度非常快,假设在几百毫秒内完成,那么通过时间戳的方式来验证缓存的有效性,是无法识别出该次文件资源的更新的。
综合,以上两种不足可知,基于last-modified实现的协商缓存,服务器无法根据资源修改的时间戳识别出真正的更新,进而导致重新发起了请求
(5.2)基于Etag实现
为了弥补通过时间戳判断的不足,从HTTP1.1规范开始新增了一个Etag的头信息,即实体标签。 其内容主要是服务器为不同的资源进行哈希计算所生成的一个字符串,该字符串类似于文件指纹,只要文件内容编码存在差异,对应的Etag对文件资源进行更精准的变化感知。
也就是说,ETag就是将原先基于last-modified协商缓存的比较时间戳的形式修改成了比较文件指纹。
文件指纹:根据文件内容计算出的唯一哈希值。文件内容一旦改变则指纹改变。
服务端代码
(5.2.1)实现方式/流程
- 第一次请求资源时,服务端将要返回给客户端的数据通过
ETag模块进行哈希计算生成一个字符串,这个字符串类似于文件指纹。 - 第二次请求资源时,客户端自动从缓存中读取出上一次服务端返回的
ETag也就是文件指纹。并赋给请求头的if-None-Match字段,让上一次的文件指纹跟随请求一起回到服务端。 - 检测客户端的请求标头中的
if-None-Match字段的值和第一步计算的值是否一致,一致则返回304。 - 如果不一致则返回etag标头和Cache-Control:no-cache。
(5.2.2)不足
在协商缓存中,Etag并非last-modified的替代方案而是一种补充方案,因为依旧存在一些弊端。
- 服务器对于生成文件资源的Etag需要付出额外的计算开销,如果资源的尺寸比较大,数量较多且修改频繁,那么生成的Etag的过程就会影响服务器的性能。
- Etag字段值的生成分为
强验证和弱验证,强验证根据资源内容进行生成,能够保证每个字节都相同,弱验证则根据资源的部分属性来生成,生成速度快但无法确保每个字节都相同,并且在服务器集群场景下,也会因为准确不够而降低协商缓存有效性的成功率,所以恰当的方式是根据具体的资源使用场景选择恰当的缓存校验方式。
(5.3)Etag/last-modified对比
-
首先在精确度上,Etag要优于Last-Modified。
Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。 -
第二在性能上,Etag要逊于Last-Modified
Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。 -
第三在优先级上,服务器校验优先考虑Etag
(6)怎么设置缓存
缓存是缓存在前端,但实际上代码是后端的同学来写的。如果你需要实现前端缓存的话啊,通知后端的同学加响应头就好了。
(7)http缓存总结
- http缓存可以减少宽带流量,加快响应速度。
- 关于强缓存:cache-control是Expires的完全替代方案,在可以使用cache-control的情况下不要使用expires
- 关于协商缓存:etag并不是last-modified的完全替代方案,而是补充方案,具体用哪一个,取决于业务场景。
- 有些缓存是从磁盘读取,有些缓存是从内存读取,有什么区别?答:从内存读取的缓存更快。
- 所有带304的资源都是协商缓存,所有标注(从内存中读取/从磁盘中读取)的资源都是强缓存。
(8)强缓存协商缓存资源应用分类
频繁变动的资源,首先需要使用Cache-Control: no-cache 使浏览器每次都请求服务器,然后配合 ETag 或者 Last-Modified 来验证资源是否有效。
不常变化的资源,给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),这样浏览器之后请求相同的 URL 会命中强制缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。 在线提供的类库 (如 jquery-3.3.1.min.js, lodash.min.js 等) 均采用这个模式。
- HTML: 协商缓存
- css、JS、图片:强缓存,文件名带上哈希
(9)http缓存机制(优先级)
强制缓存优先于协商缓存进行,若强制缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。
(10)用户行为对浏览器缓存的影响
所谓用户行为对浏览器缓存的影响,指的就是用户在浏览器如何操作时,会触发怎样的缓存策略。主要有 3 种:
- 打开网页,地址栏输入地址: 查找 disk cache(硬盘中的缓存) 中是否有匹配。如有则使用;如没有则发送网络请求。
- 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache(内存中的缓存) 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
- 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(协商缓存,为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。
CDN缓存
(1)什么是CDN?
CDN全称是内容分发网络,它是构建在现有网络基础上的虚拟智能网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、调度及内容分发等功能模块,使用户在请求所需访问的内容时能够就近获取,以此来降低网络拥塞,提高资源对用户的响应速度。
(2)不使用CDN的通信流程
- 向传统的DNS服务器请求域名解析。
- DNS服务器返回域名对应的服务器IP。
- 根据服务器IP请求服务器内容。
- 服务器返回响应资源。
(3)使用CDN的通信流程
- 客户端向传统的DNS服务器请求域名解析。
- 传统的DNS服务器将域名解析权交给了CNAME指向的专用DNS服务器,所以对用户输入域名的解析最终是在CDN专用的DNS服务器上完成的。
- CDN专用的DNS服务器将CDN负载均衡器的IP发给客户端。
- 浏览器会重新向CDN负载均衡器发起请求,经过对用户IP地址的距离、所请求资源内容的位置等的综合计算,返回给用户确定的缓存服务器IP地址。
- 浏览器最后对缓存服务器进行请求资源。
(4)静态资源适合使用CDN
静态资源指的是不需要网站业务服务器参与计算即可得到的资源,包括第三方库的JavaScript脚本文件、样式表文件以及图片等,这些文件的特点是访问频率高、承载流量大、但更新频次低,且不与业务有太多耦合。
如果是动态资源文件,比如依赖服务器端渲染得到的HTML页面,它需要借助服务器端的数据进行计算才能得到,所以这样的资源不适合存放在CDN缓存服务器上。
浏览器缓存相关面试题
前端性能
摘录:
一、前端有哪些页面优化方法?
- 减少 HTTP请求数
- 从设计实现层面简化页面
- 合理设置 HTTP缓存
- 资源合并与压缩
- 合并 CSS图片,减少请求数的又一个好办法。
- 将外部脚本置底(将脚本内容在页面信息内容加载后再加载)
- 多图片网页使用图片懒加载。
- 在js中尽量减少闭包的使用
- 尽量合并css和js文件
- 尽量使用字体图标或者SVG图标,来代替传统的PNG等格式的图片
- 减少对DOM的操作
- 在JS中避免“嵌套循环”和 “死循环”
- 尽可能使用事件委托(事件代理)来处理事件绑定的操作
二、SPA首屏加载优化有哪些?
-
- 动态加载路由
配置路由的时候,以函数的形式加载路由,只有在解析给定的路由时,才会加载路由
-
- 合理使用缓存
设置强缓存、协商缓存、localstorage等。
-
- UI框架按需加载
不要引入整个UI库,尽量进行按需加载。
-
- 避免重复加载组件
可以使用webpack将多次使用的包抽离出来,放入公共依赖文件,避免重复加载组件。
-
- 压缩图片资源:雪碧图、字体图标等
-
- 开启Gzip压缩
-
- 使用SSR服务端渲染
组件或页面通过服务器生成html字符串,再发送到浏览器。
三、怎么解决webpack打包文件体积过大?
1.异步加载模块 2.提取第三库 3.代码压缩 4.去除不必要的插件
四、如何优化webpack构建的性能
- 一、减少代码体积
- 1.使用CommonsChunksPlugin 提取多个chunk之间的通用模块,减少总体代码体积
- 2.把部分依赖转移到CDN上,避免每次编译过程都由Webpack处理
- 3.对一些组件库采用按需加载,避免无用的代码
- 二、减少目录检索范围
- 在使用
loader的时候,通过制定exclude和include选项,减少loader遍历的目录范围,从而加快webpack编译速度
- 在使用
- 三、减少检索路经:
resolve.alias可以配置webpack模块解析的别名,对于比较深的解析路经,可以对其配置alias
五、移动端300ms延迟
由来: 300毫米延迟解决的是双击缩放。双击缩放,手指在屏幕快速点击两次。safari浏览器就会将网页缩放值原始比例。由于用户可以双击缩放或者是滚动的操作, 当用户点击屏幕一次之后,浏览器并不会判断用户确实要打开至这个链接,还是想要进行双击操作 因此,safair浏览器就会等待300ms,用来判断用户是否在次点击了屏幕
解决方案:
- 1.禁用缩放,设置meta标签 user-scalable=no
- 2.fastclick.js
- 原理:FastClick的实现原理是在检查到touchend事件的时候,会通过dom自定义事件立即 发出click事件,并把浏览器在300ms之后真正的click事件阻止掉
- fastclick.js还可以解决穿透问题
六、你有对 Vue 项目进行哪些优化?
(1)代码层面的优化
- v-if 和 v-show 区分使用场景
- computed 和 watch 区分使用场景
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
- 如果需要使⽤v-for给每项元素绑定事件时使⽤事件代理
- SPA ⻚⾯采⽤keep-alive缓存组件
- 长列表性能优化
- 事件的销毁
- 图片资源懒加载
- 路由懒加载、异步组件
- 第三方插件的按需引入
- 优化无限列表性能
- 服务端渲染 SSR or 预渲染
(2)Webpack 层面的优化
- Webpack 对图片进行压缩
- 减少 ES6 转为 ES5 的冗余代码
- splitChunks抽离公共⽂件
- 模板预编译
- 提取组件的 CSS
- 优化 SourceMap
- 构建结果输出分析
- Vue 项目的编译优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使⽤cdn加载第三⽅模块
- 多线程打包happypack
(3)基础的 Web 技术的优化
- 开启 gzip 压缩
- 浏览器缓存
- CDN 的使用
- 使用 Chrome Performance 查找性能瓶颈
*⽤户体验
- 骨架屏
- PWA
七、页面卡顿
(1)原因-DOM操作
界面上UI的更改都是通过DOM操作实现的,并不是通过传统的刷新页面实现的。尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很高,页面前端代码的性能瓶颈也大多集中在DOM操作上,所以前端性能优化的一个主要的关注 点就是DOM操作的优化。
DOM操作优化的总原则是尽量减少DOM操作。
DOM操作对性能影响最大其实还是因为它导致了浏览器 的重绘(repaint)和回流(reflow) 。
(2)重绘与回流
浏览器的渲染原理:
创建 DOM 树 —> 创建 Style Rules -> 构建 Render 树 —> 布局 Layout -—> 绘制 Painting。
重绘指的是页面的某些部分要重新绘制,比如颜色或背景色的修改,元素的位置和尺寸并没用改变;(改变的是paint阶段)
回流则是元素的位置或尺寸发生了改变,浏览器需 要重新计算渲染树,导致渲染树的一部分或全部发生变化。渲染树重新建立后,浏览器会重新绘制页面上受影响的元素。(Layout阶段)回流的代价比重绘的代价高很多,重绘会影响部分的元素,而回流则有可能影响全部的元素。
如下的这些DOM操作会导致重绘或回流:
- 增加、删除和修改可见DOM元素
- 页面初始化的渲染
- 移动DOM元素
- 修改CSS样式,改变DOM元素的尺寸
- DOM元素内容改变,使得尺寸被撑大
- 浏览器窗口尺寸改变
- 浏览器窗口滚动
(3)怎么解决页面卡顿
- 1.合并多次的DOM操作为单次的DOM操作
- 多次样式修改,合并成一次
- 2.把DOM元素离线或隐藏后修改
把DOM元素从页面流中脱离或隐藏,这样处理后,只会在DOM元素脱离和添加时,或者是隐藏和显示时才会造成页面的重绘或回流,对脱离了页面布局流的DOM元素操作就不会导致页面的性能问题。这种方式适合那些需要大批量修改DOM元素的情况。具体的方式主要有三种:-
使用文档片段
文档片段是一个轻量级的document对象,并不会和特定的页面关联。// 创建一个文档片段 var fragment = document.createDocumentFragment(); // 一些基于fragment的大量DOM操作 ... // 将文档片段附加在页面上 // 仅此步 对页面性能有影响 document.getElementById('myElement').appendChild(fragment); -
通过设置DOM元素的display样式为none来隐藏元素
这类会引起页面重绘或回流的操作,就只有隐藏和显示DOM元素这两个步骤了var myElement = document.getElementById('myElement'); myElement.style.display = 'none'; // 一些基于myElement的大量DOM操作 ... myElement.style.display = 'block'; -
克隆DOM元素到内存中
把页面上的DOM元素克隆一份到内存中,然后再在内存中操作克隆的元素,操作完成后使用此克隆元素替换页面中原来的DOM元素。这样一来,影响性能的操作就只是最后替换元素的这一步操作了var old = document.getElementById('myElement'); var clone = old.cloneNode(true); // 一些基于clone的大量DOM操作 ... old.parentNode.replaceChild(clone, old);
-
- 3.设置具有动画效果的DOM元素的position属性为fixed或absolute
- 把页面中具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的回流,只涉及动画元素自身的回流了。
- 4.谨慎取得DOM元素的布局信息
-
重复获取DOM信息时,把这些值缓存到局部变量中。比如:重复获取元素的宽度等
var targetTop = targetElement.offsetTop; for (var i=0; i < len; i++) { myElements[i].style.top = targetTop+ i*5 + 'px'; } -
因为取得DOM元素的布局信息会强制浏览器刷新渲染树,并且可能会导致页面的重绘或回流,所以在有大批量DOM操作时,应避免获取DOM元素 的布局信息。
var newWidth = div1.offsetWidth + 10; div1.style.width = newWidth + 'px'; var newHeight = myElement.offsetHeight + 10; // 强制页面回流 myElement.style.height = newHeight + 'px'; // 又会回流一次 // 一共回流了两次 -
代码在遇到取得DOM元素的信息时会触发页面重新计算渲染树,会导致页面多次回流或重绘,因为浏览器会优化连续的DOM操作,提前取得DOM元素的布局信息,可以减少回流或重绘次数
// 将获取DOM信息提前 var newWidth = div1.offsetWidth + 10; var newHeight = myElement.offsetHeight + 10; // 只会回流一次 div1.style.width = newWidth + 'px'; myElement.style.height = newHeight + 'px';
-
- 5.使用事件代理方式绑定事件
- 在DOM元素上绑定事件会影响页面的性能,绑定事件本身会占用处理时间,浏览器保存事件绑定,也会占用内存
- 页面中 元素绑定的事件越多,占用的处理时间和内存就越大,性能也就相对越差
- 利用事件冒 泡机制,只在父元素上绑定事件处理,用于处理所有子元素的事件
八、大量数据列表,造成页面卡顿,怎么解决?
比如:显示一万条数据的列表,每次显示10条数据,如果通过上拉加载,给页面追加HTML元素,一直上拉加载,页面依然会有卡顿。
解决办法:使用虚拟节点
比如:让页面一直只显示10条,其他的数据是虚拟节点,不管怎么加载,页面都只有10条数据。
webpack篇
一、webpack核心概念
- Entry: 入口
- Module:模块,webpack中一切皆是模块
- Chunk:代码库,一个chunk由十多个模块组合而成,用于代码合并与分割
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容
- Plugin:扩展插件,在webpack构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要做的事情
- Output: 输出结果
二、webpack流程
webpack启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有Module.每找到一个Module,就会根据配置的Loader去找出对应的转换规则,对Module进行转换后,再解析出当前的Module依赖的Module.这些模块会以Entry为单位进行分组,一个Entry和其所有依赖的Module被分到一个组也就是一个Chunk。最后Webpack会把所有Chunk转换成文件输出。在整个流程中Webpack会在恰当的时机执行Plugin里定义的逻辑。
- (1)初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- (2)开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,通过执行对象的 run 方法开始执行编译;
- (3)确定入口:根据配置中的 entry 找出所有入口文件;
- (4)编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- (5)完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容及它们之间的依赖关系;
- (6)输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会;
- (7)输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中;
三、Vue项目Webpack优化实践
-
1、缩小文件的搜索范围
- 1.1 优化
Loader配置- (1)优化正则匹配
- (2)通过
cacheDirectory选项开启缓存 - (3)通过
include、exclude来减少被处理的文件
- 1.2 优化
resolve.modules配置:
安装的第三方模块都放在项目根目录的./node modules目录下时,可以指明存放第三方模块的绝对路径,以减少寻找 - 1.3 优化
resolve.alias配置:通过别名来将原导入路径映射成一个新的导入路径 - 1.4 优化
resolve.extensions配置
在导入语句没带文件后缀时,Webpack会在自动带上后缀后去尝试询问文件是否存在。按照.js => .json顺序查找,如果没找到,就报错。
优化措施:- 后缀尝试列表要尽可能小,不要将项目中不可能存在的情况写到后缀尝试列表中
- 频率出现最高的文件后缀要优先放在最前面,以做到尽快退出寻找过程
- 在源码中写导入语句时,要尽可能带上后缀,从而可以避免寻找过程
- 1.5优化
resolve.noParse配置:
noParse配置项可以让Webpack忽略对部分没采用模块化的文件的递归解析和处理
- 1.1 优化
-
2、减少ES6转ES5冗余代码
babel-plugin-transform-runtime是Babel官方提供的一个插件,作用是减少冗余的代码 。Babel在将ES6代码转换成ES5代码时,通常需要一些由ES5编写的辅助函数来完成新语法的实现,例如在转换classextent语法时会在转换后的ES5代码里注入extent辅助函数用于实现继承。babel-plugin-transform-runtime会将相关辅助函数进行替换成导入语句,从而减小babel编译出来的代码的文件大小。
- 3、使用
HappyPack多进程解析和处理文件
由于有大量文件需要解析和处理,所以构建是文件读写和计算密集型的操作,特别是当文件数量变多后,
Webpack构建慢的问题会显得更为严重。运行在Node之上的Webpack是单线程模型的,也就是说Webpack需要一个一个地处理任务,不能同时处理多个任务。Happy Pack就能让Webpack做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。
- 4、使用ParallelUglifyPlugin多进程压缩代码文件
当
Webpack有多个JavaScript文件需要输出和压缩时,原本会使用UglifyJS去一个一个压缩再输出,但是ParallelUglifyPlugin会开启多个子进程,将对多个文件的压缩工作分配给多个子进程去完成,每个子进程其实还是通过UglifyJS去压缩代码,但是变成了并行执行。所以ParallelUglify Plugin能更快地完成对多个文件的压缩工作。
-
5、使用自动刷新
借助自动化的手段,在监听到本地源码文件发生变化时,自动重新构建出可运行的代码后再控制浏览器刷新。Webpack将这些功能都内置了,并且提供了多种方案供我们选择。相关优化措施:
- (1)配置忽略一些不监听的一些文件,如:
node_modules。 - (2)
watchOptions.aggregateTirneout(监听到变化后多少毫秒后再执行)的值越大性能越好,因为这能降低重新构建的频率。 - (3)
watchOptions.poll(默认每秒询问多少次)的值越小越好,因为这能降低检查的频率。
- (1)配置忽略一些不监听的一些文件,如:
-
6、开启模块热替换
DevServer还支持一种叫做模块热替换(Hot Module Replacement)的技术可在不刷新整个网页的情况下做到超灵敏实时预览。原理是在一个源码发生变化时,只需重新编译发生变化的模块,再用新输出的模块替换掉浏览器中对应的老模块 。
- 7、提取公共代码
Webpack内置了专门用于提取多个Chunk中的公共部分的插件CommonsChunkPlugin,将多个页面的公共代码抽离成单独的文件
- 8、按需加载代码
把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件
- 9、优化SourceMap
开发环境推荐:
cheap-module-eval-source-map生产环境推荐:
cheap-module-source-map
- 10、构建结果输出分析
Webpack输出的代码可读性非常差而且文件非常大,vue项目中用到的分析工具:webpack-bundle-analyzer,以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。
四、webpack-路由懒加载原理(按需加载)
1、为什么vue需要懒加载(按需加载)
Vue的特点是SPA - Single Page Application(单页面应用程序),运用webpack打包后,一般情况下,会放在一个单独的js文件中。但是,如果很多的页面都放在同一个js文件中,必然会造成这个页面非常大。造成首页加载的内容过多,时间过长,会出现长时间的白屏。
优点:
- 只有第一次会加载页面,以后的每次页面切换,只需要进行组件替换
- 减少了请求体积,加快页面的响应速度,降低了对服务器的压力
2、什么是路由懒加载?
路由懒加载也叫延迟加载,即在需要的时候进行加载,随用随载。
通过Webpack编译打包后,会把每个路由组件的代码分割成一个个js文件,初始化时不会加载这些js文件,只当激活路由组件才会去加载对应的js文件。
3、路由懒加载前提
要实现懒加载,就得先将进行懒加载的子模块(子组件)分离出来。
4、懒加载实现的前提?
ES6的动态加载模块 - import()
简单来讲就是,通过import()引用的子模块会被单独分离出来,打包成一个单独的文件。也就是说,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。
5、懒加载实现原理
主要是借助函数实现懒加载(按需加载)。
JavaScript函数的特性:无论使用函数声明还是函数表达式创建函数,函数被创建后并不会立即执行函数内部的代码,只有等到函数被调用之后,才执行内部的代码。
只要将需要进行懒加载的子模块文件(children chunk)的引入语句(本文特指import())放到一个函数内部。然后再需要加载的时候执行该函数。这样就可以实现懒加载(按需加载)。这也就是是懒加载(按需加载)的原理了
懒加载(按需加载)原理分为两步:
- 将需要进行懒加载的子模块打包成独立的文件(
children chunk); - 借助函数来实现延迟执行子模块的加载代码;(
ES6提供了import())
五、vue-router路由懒加载方法?
- 1、将子组件加载语句封装到一个
function中,将function赋给component
component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
- 2、vue异步加载技术:
component: resolve => require(['放入需要加载的路由地址'], resolve)
- 3、webpack提供的require.ensure()
- require.ensure可实现按需加载资源,包括js,css等。他会给里面require的文件单独打包,不会和主文件打包在一起。
- 第一个参数是数组,表明第二个参数里需要依赖的模块,这些会提前加载。
- 第二个是回调函数,在这个回调函数里面require的文件会被单独打包成一个chunk,不会和主文件打包在一起,这样就生成了两个chunk,第一次加载时只加载主文件。
- 第三个参数是错误回调。
- 第四个参数是单独打包的chunk的文件名.
component: resolve=>{
require.ensure(['@/components/HelloWorld'],()=>{
resolve(require('@/components/HelloWorld'))
})
}
import和require的比较
1:import 是解构过程并且是编译时执行
2:require 是赋值过程并且是运行时才执行,也就是异步加载
3:require的性能相对于import稍低,因为require是在运行时才引入模块并且还赋值给某个变量
六、常见的loader及其作用
- babel-loader:将es6转译为es5
- file-loader:可以指定要复制和放置资源文件的位置,以及如何使用版本哈希命名以获得更好的缓存,并在代码中通过URL去引用输出的文件
- url-loader:和file-loader功能相似,但是可以通过指定阈值来根据文件大小使用不同的处理方式(小于阈值则返回base64格式编码并将文件的 data-url内联到bundle中)
- raw-loader:加载文件原始内容
- image-webpack-loader: 加载并压缩图片资源
- awesome-typescirpt-loader: 将typescript转换为javaScript 并且性能由于ts-loader
- sass-loader: 将SCSS/SASS代码转换为CSS
- css-loader: 加载CSS代码 支持模块化、压缩、文件导入等功能特性
- style-loader: 把CSS代码注入到js中,通过DOM 操作去加载CSS代码
- source-map-loader: 加载额外的Source Map文件
- eslint-loader: 通过ESlint 检查js代码
- cache-loader: 可以在一些开销较大的Loader之前添加可以将结果缓存到磁盘中,提高构建的效率
- thread-loader: 多线程打包,加快打包速度
Vue2.x篇
摘录自:
Vue 的响应式原理
- Vue 的响应式是通过
Object.defineProperty对数据进⾏劫持,并结合观察者模式实现。 - Vue 利⽤
Object.defineProperty创建⼀个observe来劫持监听所有的属性,把这些属性全部转为getter和setter。 - Vue 中每个组件实例都会对应⼀个
watcher实例,它会在组件渲染的过程中把使⽤过的 数据属性通过getter收集为依赖。之后当依赖项的setter触发时,会通知watcher,从⽽使它关联的组件重新渲染。
Vue 中 v-html 会导致什么问题
在⽹站上动态渲染任意 HTML,很容易导致 XSS 攻击。所以只能在可信内容上使⽤ v-html,且永远不能⽤于⽤户提交的内容上。
Vuex和单纯的全局对象有什么区别?
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发⽣变 化,那么相应的组件也会相应地得到⾼效更新。
- 不能直接改变 store 中的状态。改变 store 中的状态的唯⼀途径就是显式地提交 (commit)mutation。这样使得我们可以⽅便地跟踪每⼀个状态的变化,从⽽让我们能够实现⼀些⼯具帮助我们更好地了解我们的应⽤。
v-model是如何实现双向绑定的?
- vue 2.0
v-model是⽤来在表单控件或者组件上创建双向绑定的,他的本质是v-bind和v-on的语法糖,在 ⼀个组件上使⽤v-model,默认会为组件绑定名为value的prop和名为input的事件。 - Vue3.0 在 3.x 中,⾃定义组件上的
v-model相当于传递了modelValue prop并接收抛出的update:modelValue事件
Vue2.0双向数据绑定的原理?
(1)原理流程图:
(2)实现过程:
Vue双向数据绑定是通过数据劫持+发布订阅者模式来实现的。Vue采用的是MVVM架构,实现MVVM主要包含两个方面,一是数据变化更新视图,二是试图变化更新数据。 在实现过程上来说,主要有四个模块:
- 监听器Observer:执行劫持监听的所有属性,如果属性发生变化了,就通知订阅者Watcher看是否需要更新。
- 订阅者Watcher:可以受到属性的变化通知并执行相应的函数,从而更新视图。
- 消息订阅器Dep:因为订阅者有很多个,所以需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。
- 解析器Compile:可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
- Compile函数的主要工作是将模板中的变量替换成数据,然后渲染页面视图,并给每个节点绑定更新函数,创建订阅者,一旦数据有变化,收到通知就更新视图。
- 因为遍历的时候会多次操作DOM,为了提高效率会将根节点转换成文档碎片fragment进行离线DOM操作,解析完成之后再将fragment添加到真实的DOM中。
为什么 v-for 和 v-if 不建议⽤在⼀起
- 当
v-for和v-if处于同⼀个节点时,v-for的优先级⽐v-if更⾼,这意味着v-if将分别重复运⾏于每个v-for循环中。如果要遍历的数组很⼤,⽽真正要展示的数据很少时,这将造成很⼤的性能浪费。 - 这种场景建议使⽤
computed,先对数据进⾏过滤。
vue-router hash 模式和 history 模式有什么区别?
区别:
- url 展示上,hash 模式有 "#",history 模式没有
- 刷新⻚⾯时,hash 模式可以正常加载到 hash 值对应的⻚⾯,⽽ history 没有处理的话,会返回404,⼀般需要后端将所有⻚⾯都配置重定向到⾸⻚路由。
- 兼容性。hash 可以⽀持低版本浏览器和 IE
vue-router hash 模式和 history 模式是如何实现的
hash模式:
#后⾯hash值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新⻚⾯。同时通过监听hashchange事件可以知道hash发⽣了哪些变化,然后根据hash变化来实现更新⻚⾯部分内容的操作。history模式:
history模式的实现,主要是HTML5标准发布的两个API,pushState和replaceState,这两个API可以在改变url,但是不会发送请求。这样就可以监听url变化来实现更新⻚⾯部分内容的操作。
讲⼀讲MVVM吗?
- MVVM是
Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。 - Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并⾃动将数据渲染到⻚⾯中,视图变化的时候会通知viewModel层更新数据。
vue 中组件 data 为什么是 return ⼀个对象的函数,⽽不是直接是个对象?
- 如果将data定义为对象,这就表示所有的组件实例共⽤了⼀份data数据,因此,⽆论在哪个组件实例中修改了data,都会影响到所有的组件实例。
- 组件中的data写成⼀个函数,数据以函数返回值形式定义,这样每复⽤⼀次组件,就会返回⼀份新的data,类似于给每个组件实例创建⼀个私有的数据空间,让各个组件实例维护各⾃的数据。⽽单纯的写成对象形式,就使得所有组件实例共⽤了⼀份data,就会造成⼀个变了全都会变的结果。
Vue 中的 computed 是如何实现的
-
当组件初始化的时候,
computed和data会分别建⽴各⾃的响应系统,Observer遍历data中每个属性设置get/set数据拦截 -
初始化
computed会调⽤initComputed函数- 注册⼀个
watcher实例,并在内实例化⼀个Dep消息订阅器⽤作后续收集依赖(⽐如渲染函数的watcher或者其他观察该计算属性变化的watcher) - 调⽤计算属性时会触发其
Object.defineProperty的get访问器函数 - 调⽤
watcher.depend()⽅法向⾃身的消息订阅器dep的subs中添加其他属性的watcher - 调⽤
watcher的evaluate⽅法(进⽽调⽤watcher的get⽅法)让⾃身成为其他watcher的消息订阅器的订阅者,⾸先将watcher赋给Dep.target,然后执⾏getter求值函数,当访问求值函数⾥⾯的属性(⽐如来⾃data、props或其他computed)时, 会同样触发它们的get访问器函数从⽽将该计算属性的watcher添加到求值函数中属性的watcher的消息订阅器dep中,当这些操作完成,最后关闭Dep.target赋为null并 返回求值函数结果。
- 注册⼀个
-
当某个属性发⽣变化,触发
set拦截函数,然后调⽤⾃身消息订阅器dep的notify⽅法,遍 历当前 dep 中保存着所有订阅者wathcer的subs数组,并逐个调⽤watcher的update⽅ 法,完成响应更新。
Object.defineProperty有哪些缺点?
Object.defineProperty只能劫持对象的属性,⽽Proxy是直接代理对象。由于Object.defineProperty只能对属性进⾏劫持,需要遍历对象的每个属性。⽽ Proxy 可以直接代理对象。Object.defineProperty对新增属性需要⼿动进⾏Observe, 由于Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新 增属性再使⽤Object.defineProperty进⾏劫持。 也正是因为这个原因,使⽤ Vue 给data中的数组或对象新增属性时,需要使⽤vm.$set才能保证新增的属性也是响应式的。Proxy⽀持13种拦截操作,这是defineProperty所不具有的。- 新标准性能红利Proxy 作为新标准,⻓远来看,JS引擎会继续优化
Proxy,但getter和setter基本不会再有针对性优化。 Proxy兼容性差 ⽬前并没有⼀个完整⽀持Proxy所有拦截⽅法的Polyfill⽅案
Vue2.0中如何检测数组变化?
Vue 的 Observer 对数组做了单独的处理,对数组的⽅法进⾏编译,并赋值给数组属性的 __proto__属性上,因为原型链的机制,找到对应的⽅法就不会继续往上找了。编译⽅法中会对⼀些会增加索引的⽅法( push , unshift , splice )进⾏⼿动 observe。
nextTick是做什么⽤的,其原理是什么?
能回答清楚这道问题的前提,是清楚 EventLoop 过程。
- 在下次 DOM 更新循环结束后执⾏延迟回调,在修改数据之后⽴即使⽤ nextTick 来获取更新后的DOM。
nextTick对于 micro task 的实现,会先检测是否⽀持Promise,不⽀持的话,直接指向 macrotask,⽽ macro task 的实现,优先检测是否⽀持setImmediate(⾼版本IE和Etage⽀持),不⽀持的再去检测是否⽀持 MessageChannel,如果仍不⽀持,最终降级为setTimeout0;- 默认的情况,会先以 micro task ⽅式执⾏,因为 micro task 可以在⼀次 tick 中全部执⾏完毕,在⼀些有重绘和动画的场景有更好的性能。
- 但是由于 micro task 优先级较⾼,在某些情况下,可能会在事件冒泡过程中触发,导致⼀些问题,所以有些地⽅会强制使⽤ macro task (如
v-on)。
注意:之所以将 nextTick 的回调函数放⼊到数组中⼀次性执⾏,⽽不是直接在 nextTick 中执⾏回调函数,是为了保证在同⼀个tick内多次执⾏了 nextTcik ,不会开启多个异步任务,⽽是把这些异步任务都压成⼀个同步任务,在下⼀个tick内执⾏完毕。
Vue 的模板编译原理
vue模板的编译过程分为3个阶段:
-
第⼀步:解析
将模板字符串解析⽣成 AST,⽣成的AST 元素节点总共有 3 种类型,1 为普通元素, 2 为表达式,3为纯⽂本。
-
第⼆步:优化语法树
Vue 模板中并不是所有数据都是响应式的,有很多数据是⾸次渲染后就永远不会变化的,那么这部分数据⽣成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的⽐对。
此阶段会深度遍历⽣成的 AST 树,检测它的每⼀颗⼦树是不是静态节点,如果是静态节点则它们⽣成DOM 永远不需要改变,这对运⾏时对模板的更新起到极⼤的优化作⽤。
-
第三步:⽣成代码
通过 generate ⽅法,将ast⽣成 render 函数
const code = generate(ast, options)
复制代码