CSS相关面试题:
1.说一下CSS权重优先级
!important(无穷大) > 内联样式(1000) > id选择器(100) > 类选择器(属性选择器,伪类选择器)(10) > 标签选择器(伪元素选择器)(1) > 通配符选择器(0) > 继承(0)
2.说一下CSS伪类,伪元素都有哪些
参考:www.w3school.com.cn/css/css_pse…
伪类选择器:
1.动态伪类
:link => 链接没有被访问前的样式效果
:visited => 链接被访问后的样式效果
:hover => 鼠标悬停在元素上面时的样式效果
:active => 点击元素时的样式效果
:focus => 用于元素成为焦点时的样式效果,一般用于表单元素
2.结构伪类
:first-child => 选择某个元素下的第一个子元素
:last-child => 选择某个元素下的最后一个子元素
:nth-child => 选择某个元素下的一个或多个特定的子元素
:nth-last-child => 选择某个元素的一个或多个特定的子元素,从后往前数
:nth-of-type => 选择指定的元素
:nth-last-of-type => 选择指定的元素,从后往前数
:first-of-type => 选择一个父级元素下的第一个同类型子元素
:last-of-type => 选择一个父级元素下的第一个同类型子元素,从后往前数
:only-child => 选择的元素是其父元素的唯一 一个子元素
:only-of-type => 选择一个元素是上级元素下唯一相同类型的子元素
:empty => 选择的元素里没有任何内容(空标签)
3.否定伪类
:not => 排查或者过滤特定元素
4.状态伪类
:enabled => 选择匹配指定范围内所有可用的UI元素
:disabled => 选择匹配指定范围内所有不可用的UI元素
:checked => 选择匹配指定范围内所有可用的UI元素
5.目标伪类
:target => 选择匹配父元素的所有元素,并且匹配元素被相关URL指向
伪元素选择器:
:after => 在标签里的元素之后插入内容
:before => 在标签里的元素之前插入内容
:first-letter => 选择每个标签里的元素的首字母
:first-line => 选择每个标签里的元素首行
:selection => 选择用户选择的元素部分
3.如何用css实现div水平垂直居中,常见一共都有几种方法
<div class="box">
<div class="item"></div>
</div>
1.绝对定位,上右下左四个值都为0,margin:auto
.box {
width:300px;
height:300px;
border:1px solid #000;
position: relative;
}
.item {
width:50px;
height:50px;
background-color: #f00;
position: absolute;
top:0;right:0;bottom:0;left:0;
margin:auto;
}
2.子元素相对父元素绝对定位,top和left都设为50%,然后平移50%
.box {
width:300px;
height:300px;
border:1px solid #000;
position: relative;
}
.item {
width:50px;
height:50px;
background-color: #f00;
position: absolute;
left:50%;top:50%;
transform: translate(-50%,-50%);
}
3.利用绝对定位,calc函数动态计算实现水平垂直居中,但是缺点就是需要知道父元素的宽高,不灵活,不推荐使用
.box {
width:300px;
height:300px;
border:1px solid #000;
position: relative;
}
.item {
width:50px;
height:50px;
background-color: #f00;
position: absolute;
left:calc((300px - 50px) / 2);
top:calc((300px - 50px) / 2);
}
4.flex布局:
.box {
width:300px;
height:300px;
border:1px solid #000;
display:flex;
align-items: center;
justify-content: center;
}
.item {
width:50px;
height:50px;
background-color: #f00;
}
5.grid布局
.box {
width:300px;
height:300px;
border:1px solid #000;
display: grid;
grid-template-columns: 50px;
grid-template-rows: 50px;
place-content: center center;
}
.item {
width:50px;
height:50px;
background-color: #f00;
}
4.实现一个三栏布局,中间div自适应,都有几种方法
<div class="box">
<div class="left"></div>
<div class="right"></div>
<div class="center">1234</div>
</div>
1.浮动 + margin
.box {
width:500px;
height:300px;
background-color: #ff0;
}
.left {
float:left;
width:150px;
height:inherit;
background-color: #f00;
}
.right {
float:right;
width:150px;
height:inherit;
background-color: #000;
}
.center {
height:inherit;
background-color: #ccc;
margin-left:150px;
margin-right:150px;
}
2.浮动 + BFC:
BFC的特点:
1.元素不会被浮动的元素所覆盖
2.子元素和父元素的外边距不会重叠
3.父元素可以包含浮动的子元素
.box {
width:500px;
height:300px;
background-color: #ff0;
}
.left {
float:left;
width:150px;
height:inherit;
background-color: #f00;
}
.right {
float:right;
width:150px;
height:inherit;
background-color: #000;
}
.center {
height:inherit;
background-color: #ccc;
overflow: hidden;
}
3.flex布局:
.box {
width:500px;
height:300px;
background-color: #ff0;
display: flex;
}
.left {
width:150px;
height:inherit;
background-color: #f00;
}
.right {
width:150px;
height:inherit;
background-color: #000;
}
.center {
height:inherit;
background-color: #ccc;
flex:1;
}
4.table布局:
.box {
width:500px;
height:300px;
background-color: #ff0;
display: table;
}
.left {
display: table-cell;
width:150px;
height:inherit;
background-color: #f00;
}
.right {
display: table-cell;
width:150px;
height:inherit;
background-color: #000;
}
.center {
display: table-cell;
height:inherit;
background-color: #ccc;
}
5.圣杯布局:
<div class="box">
<div class="center">1234</div>
<div class="left"></div>
<div class="right"></div>
</div>
<style>
* {margin:0;padding:0;}
.box {
height:300px;
background-color: #ff0;
margin-left:150px;
margin-right:150px;
}
.left {
float:left;
margin-left:-100%;
position: relative;
left:-150px;
width:150px;
height:inherit;
background-color: #f00;
}
.right {
float:left;
margin-left:-150px;
position: relative;
left:150px;
width:150px;
height:inherit;
background-color: #000;
}
.center {
float:left;
width:100%;
height:inherit;
background-color: #ccc;
}
</style>
6.双飞翼布局:
<div class="box">
<div class="center">1234</div>
<div class="left">zz</div>
<div class="right"></div>
</div>
<style>
* {margin:0;padding:0;}
.box {
height:300px;
background-color: #ff0;
margin-left:150px;
margin-right:150px;
}
.left {
float:left;
margin-left:-100%;
width:150px;
height:inherit;
background-color: #f00;
}
.right {
float:left;
margin-left:-150px;
width:150px;
height:inherit;
background-color: #000;
}
.center {
float:left;
width:100%;
height:inherit;
background-color: #ccc;
}
</style>
5.用grid布局实现一个九宫格
<div class="box">
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
<div class="item"></div>
</div>
<style>
* {margin:0;padding:0;}
.box {
height:300px;
width:300px;
background-color: #ccc;
display:grid;
grid-template-columns: repeat(3,1fr);
grid-template-rows: repeat(3,1fr);
gap: 10px;
padding:10px;
box-sizing: border-box;
}
.item {
background-color: #f00;
}
</style>
5.说说移动端适配em和rem:
在CSS中,用的最多的就是px,em,rem这三个长度单位,他们的区别是:
1.px是固定的单位长度,一旦设置了就无法随页面的大小而适应改变
2.em是相对长度定位,比px更具有灵活性,em的长度是相对于父元素
3.ren的长度是相对于根元素,也就是html的字体大小
1.em的使用:
1.子元素设置em,大小是取决于父元素字体的大小
2.元素的width,height,margin,padding用em,是取决于该元素的font-size大小
em的继承效果:
1.如果使用em存在继承效果,每个元素都自动继承其父元素的字体大小
2.只要父级元素及上面一直有fontsize为em单位,则会一直继承
谷歌浏览器默认字体大小是16px,谷歌浏览器字体大小最小为12px,如果想要设置小于12px的字体,可以通过CSS3的transform的scale(),缩放方法来解决
2.rem的使用:
rem是css3新引进的一个度量单位,rem的长度是相对于根元素,即html的长度,同样是给html设置一个标准值,然后其他元素设置rem都是以html的标准值为1单位来设置
如果用rem做响应式,直接改变html的font-size,那么凡事用rem作为单位的元素的大小都会响应发生改变
3.换算关系:
1.px和em:
通常,1em等于16px,为了更方便的进行换算,我们可以在body中设置font-size为62.5%,这样使em值变成16px * 62.5% = 10px,这样12px就等于1.2em,10px就等于1em,也就是说只需要将你原来的px数值除以10,然后换乘em作为单位就可以了
2.px和rem:
谷歌浏览器默认字体大小为16px,如果不指定具体大小,那么就是1rem等于16px,12像素的大小就是12除以16等于0.75rem,14像素的大小就是14除以16等于0.875rem,如果body字体大小设置为62.5%,则会变成12px等于1.2rem(12 / 10)
原生JavaScript:
1.原生JS的数据类型都有哪些?
基本数据类型:Number/String/Boolean/Null/Undefined
引用数据类型:分两种:
1)Object(Object/Array/Date/RegExp/Match)
2)Function
2.基本数据类型和引用数据类型的区别是什么?
1.基本数据类型存储在栈内存中
2.数据存储时,基本数据类型在变量中存的是值,引用数据类型在变量中存储的是空间地址
3.什么是堆栈内存?
1.堆内存和栈内存是浏览器形成的两个虚拟内存
2.栈内存主要是用来存储基本数据的值,栈内存是一种简单的存储,但是存储的数据都是有范围上线的,一旦超过,就会造成栈溢出
3.堆内存主要是用来存储引用数据类型的
堆内存:存储引用类型,对象类型就是键值对,函数就是代码字符串
堆内存释放:将引用类型的空间地址变量赋值成null,或者没有变量占用堆内存了,浏览器就会释放掉这个地址
栈内存:提供代码执行的环境和存储基本类型值
栈内存释放:一般当函数执行完后,函数的私有作用域就会被释放掉
4.Null和Undefined的区别是什么?
1.Null表示为空,用来占位,但是以后可以重新赋值,Undefined表示为定义
2.Null表示空对象指针,可以给变量赋值为Null,来清空变量,可以用来释放堆内存
3.如果变量未定义,那么默认存储值为undefined
如果对象某个属性不存在,获取到的值也是undefined
如果函数的形参没有对应的实参,那么形参默认的存储值也是undefined
如果函数没有返回return的值,那么默认返回还是undefined
5.为什么JS是单线程?
1.JS的主要用途是和用户互动以及操作DOM,如果不是单线程,那么就会造成很复杂的同步问题,所以JS只能被设计成单线程
2.为了利用多核CPU的计算能力,H5提出了web worker标准,允许JS脚本创建多线程,但是子线程完全受主线程控制,并且无法操作Dom,所以这个新标准并没有改变JS单线程的本质
6.讲讲同步任务和异步任务(讲讲async和await)
单线程是指一次只能完成一个任务,如果在同时间执行多个任务,那么这些任务就得排队,只有前一个任务执行完成,才会执行下一个任务,但是如果有一个任务执行时间很长,就会导致后面的任务一直处于等待状态,这样就会造成用户体验问题,所以为了解决这个问题,JS将任务执行模式分为同步(Async)和异步(Await)
7.讲讲JS同步模式和异步模式
1.同步模式:同步模式就是前一个任务执行完成之后,再执行下一个任务,程序的执行顺序与任务的排列顺序是一致的,也是同步的
2.异步模式:异步模式就是每一个任务有一个或多个回调函数,前一个任务结束后,不是执行队列上的最后一个任务,而是执行回调函数,后一个任务不需要等前一个任务的回调函数执行完成之后再执行,所以程序执行顺序与任务的排列顺序是不一致的,也是异步的
8.讲讲JS事件循环
JS事件循环,Event Loop
1.同步任务和异步任务分别进入不同的执行场所,同步任务进入主线程,异步任务进入Event Table并且注册回调函数
2.当执行的事情完成之后,EventTable会将这个函数植入任务队列,等待主线程的任务执行完毕
3.当栈中的代码执行完毕,执行栈中的任务为空时,就会读取任务的回调
4.如此循环,就形成了事件循环的机制
9.讲讲什么是事件表格?
JS事件表格,Event Table
1.Event Table 可以理解为一张事件和回调函数的对应表
2.Event Table 用来存储JS中的异步事件以及对应的回调函数的列表
3.当执行的事件完成时,Event Table会将这个回调函数移入宏任务队列或微任务队列
10.讲讲什么是宏任务,什么是微任务?
1.JS是单线程语言,简单的说就是只有一条通道,那么在任务多的情况下,会出现拥挤情况,这种情况下就产生了多线程,实际上这种多线程是通过单线程模仿的,也就是假的,那么就会用到同步任务和异步任务
2.宏任务是由node和浏览器发起的,微任务先执行,宏任务后执行
宏任务具体事件:setTimeout,setInterval,XMLHttpRequest,setImmedia,I/O,UI rendering等等
微任务是由JS引擎发起的,微任务具体事件:Promise,Process.nextTick,Object.observe,MutationObserver等等
11.讲讲JS深拷贝和浅拷贝
1.基本数据类型的值放在栈区,可以直接访问和修改,并且相互之间不会影响
2.引用数据类型的地址放在栈区,值放在堆区,所以当你进行赋值操作的时候,实际上赋值的是地址
浅拷的方法:
1.我们可以自己写一个浅拷贝,实现原理是通过Object.protype.toString.call获取数据类型,通过for循环判断,用数据私有化属性hasOwnProperty进行赋值
2.Object.assign()
3.Array.prototype.slice()
4.数据的浅拷贝用 ... 运算符和concat()
深拷贝的方法:
把一个对象中所有的属性或者方法一个一个的找到,并且在另一个对象中开辟对应的空间,然后一个一个的存储到另一个对象中
1.JSON.parse() / JSON.stringify()
具体的代码:
浅拷贝:
let data = [1,2,3,4,5,6]
let copy01 = data.slice()
console.log(data,copy01)
// [1,2,3,4,5,6]
// [1,2,3,4,5,6]
let copy02 = [].concat(data)
console.log(data,copy02)
// [1,2,3,4,5,6]
// [1,2,3,4,5,6]
let obj = {
name:"xiaoming",
age:22,
sex:"男"
}
let copy = {}
Object.assign(copy,obj)
console.log(obj, copy)
// {name: 'xiaoming', age: 22, sex: '男'}
// {name: 'xiaoming', age: 22, sex: '男'}
深拷贝:
let obj = {
name:"xiaoming",
age:22,
sex:"男"
}
let str = JSON.stringify(obj)
let copy = JSON.parse(str)
console.log(obj, copy)
// {name: 'xiaoming', age: 22, sex: '男'}
// {name: 'xiaoming', age: 22, sex: '男'}
// 实现一个深拷贝的方法
let obj = {
aaa: "zzz",
bbb: {
ccc: "xxx",
ddd: {
eee: "yyy",
fff: "ttt"
}
}
}
const deepClone = (obj) => {
let newObj = {}
for (let key in obj) {
if (typeof obj[key] === 'object') {
newObj[key] = deepClone(obj[key])
} else {
newObj[key] = obj[key]
}
}
return newObj
}
let cloneData = deepClone(obj)
console.log("原数据", obj)
console.log("克隆数据", cloneData)
12.讲讲深拷贝和浅拷贝的区别是什么?
浅拷贝只赋值对象的第一层属性,深拷贝可以对对象的属性进行递归赋值
浅拷贝就是赋值,相当于把一个对象中所有的内容赋值一份给另一个对象,直接赋值,或者说就是把一个对象的地址给了另一个对象,他们指向相同,两个对象之间的共同属性或者方法都可以使用
13.讲讲JS的Promise
Promise的使用场景:处理异步回调,多个异步函数同步处理,异步依赖异步回调,封装统一的入口办法或者错误处理
Promise是JS中进行异步操作的新解决方案,Promise是一个构造函数,Promise对象来封装一个异步操作并可以获取成功和失败的返回值,Promise支持链式调用
Promise异步操作有三个状态,分别是pending(进行中)reslove(成功)reject(失败)任何其他操作都不能改变这个状态
状态缺点:
无法取消Promise,一旦新建就会立即执行,无法中途取消。如果不设置回调函数,Promise内部抛出的作物,不会反应到外部,当处理pending状态时,无法得知目前进展到哪一个阶段
then方法接收俩哥哥函数当作参数,分别是成功和失败,两个函数指挥有一个被调用
then支持多次调用
Promise常见的API方法:
.then:得到Promise内部任务的执行结果
.catch:得到Prmise内部任务失败的结果
.finally:无论是成功还是失败,都会返回
.all:按顺序指定多个Promise并且都执行结束之后分别返回结果
.race:
Promise执行顺序:
console.log(1)
setTimeout(function(){
console.log(2)
}, 0)
new Promise(function(resolve){ // 这里的回调是同步的
console.log(3)
resolve()
}).then(function(){ // 异步微任务
console.log(4)
})
console.log(5) // 1,3,5,4,2
setTimeout(() => {
console.log(1)
})
new Promise((resolve) => {
console.log(2)
for(let i = 0;i < 10000;i++){
if(i == 10){
console.log(3)
}
i == 9999 && resolve(4)
}
}).then((val) => {
console.log(val)
})
console.log(5)
// 2,3,5,4,undefined,1
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('start')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
console.log('processing')
new Promise((resolve, reject) => {
for (let i = 0; i <= 2; i++) {
console.log(1)
resolve()
}
}).then(function () {
console.log('promise1')
}).then(function () {
console.log('promise2')
})
console.log('end')
/*
start
async1 start
async2
processing
1(三次)
end
async1 end
promise1
promise2
setTileout
*/
14.讲讲箭头函数和function的区别,以及this指向的区别
1.function定义函数,this指向对着调用环境的变化而变化,箭头函数中的this指向是固定不变的
2.function可以定义构造函数,箭头函数不可以,箭头函数不能使用new,也不能使用argument对象,因为箭头函数不存在,如果使用,可以用rest代替
3.由于js内存机制,function级别最高,因为变量提升。因为var定义的变量不能得到变量提升,所以箭头函数一定要定义在调用之前
4.箭头函数不能使用yield命令,所以不能作为Generator函数
5.call()、apply()、bind()等方法不能改变箭头函数中this的指向
15.讲讲JS闭包
闭包是什么:闭包是指有权访问另一个函数作用域中变量的函数。
形成闭包的原因:内部的函数存在外部作用域的引用就会导致闭包。
闭包的作用:
1.保护函数的私有变量不受外部的干扰,形成不销毁的栈内存。
2.把一些函数内的值保存下来,闭包可以实现方法和属性的私有化。
闭包的使用场景:
闭包的缺点:
1.容易导致内存泄漏
2.闭包会携带包含其他的函数作用域,因此会比其他函数占用更多的内存
3.过度使用闭包会导致内存占用过多,所以要谨慎使用闭包
16.讲讲原生JS,new一个对象的过程
1.创建空对象
2.新建对象执行prototype连接原型
3.绑定this到新对象上
4.执行构造函数
5.返回新对象
17.讲讲JS中的原型,原型链的理解:
在JS中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个prototype属性,它的属性值就是一个对象,这个对象包括了可以由该构造函数的所有实例共享的属性和方法,当使用构造函数新建一个对象的时候,在这个对象的内部将包含一个指针,这个指针指向的构造函数的prototype属性对应的值,在ES5中这个指针,就被成为对象的原型。
ES5中新增了一个方法,叫 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象中去寻找这个属性,这个属性对象又会偶自己的原型,于是就这样一直找下去,这个就是原型链。
18.讲讲JS中的原型链终点是什么?
原型链的终点是 null,因为Object是构造函数,原型链终点是Object.prototype.proto,因为Object.prototype.proto === null // true,所以原型链的终点是null
19.讲讲如何获取对象非原型链上的属性?
使用 hasOwnProperty() 方法来判断属性是否属于原型链的属性
20.讲讲原生JS对作用域,作用域链的理解
1)全局作用域:
最外层函数和最外层函数外面定义的变量拥有全局作用域
所有未定义直接赋值的变量自动声明为全局作用域
所有window对象的属性拥有全局作用域
全局作用域有很大的弊端,比如过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
2)函数作用域
函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
作用域是分层的,内层作用域可以访问外层作用域,但是外层作用域却无法访问内层作用域
3)块级作用域
使用ES6中新增的let和const指令可以声明会计作用域,块级作用域可以在函数中创建,也可以在一个代码块中创建
let和const声明的变量不会有变量提升,也不可以重复声明
再循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制再循环内部
4)作用域链
在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象
21.讲讲原生JS的let,const,var的区别:
1.块级作用域:let和const具有块级作用域,var不存在块级作用域,块级作用域解决了ES5的两个问题:分别是内层变量可以覆盖外层变量和用来计数的循环变量泄露为全局变量
2.变量提升:var存在变量提升,let和const不存在变量提升
3.给全局添加新属性:浏览器的全局对象是window,node的全局对象是global,var声明的变量为全局变量,并且会并且会将该变量添加为全局对象的属性,但是let和const不会
4.重复声明:var声明变量时,可以重复声明,后声明的同名变量名会覆盖之前生命的变量,const和let不允许重复声明变量
5.暂时性死区:再使用let和const命令声明变量之前,该变量都是不可用的,var声明变量就不会存在暂时性死区
6.初始值设置:再变量生命的时候,var和let可以不用设置初始值,但是const必须设置初始值
7.指针指向:let和const时ES6新增的,let可以更改指针指向,const不允许改变指针指向
22.讲讲JS类数组对象的方法:
通过call调用数组的 slice 方法来实现转换:
Array.prototype.slice.call(arrayLike);
通过call调用数组的 splice 方法来实现转换:
Array.prototype.slice.call(arrayLike);
通过call调用数组的 concat 方法来实现转换:
Array.prototype.concat.apply([], arrayLike);
通过call调用数组的 Array.from 方法来实现转换:
Array.from(arrayLike);
23.讲讲JS/ES6中的数组方法都有哪些?
数组和字符串方法:toString(),toLocalString(),join()
数组增删(前增后增,前删后删)的方法:pop() 和 push(),shift() 和 unshift()
数组排序方法:reverse() 和 sort()
数组连接方法:concat() 不影响原数组
数组截取方法:slice() 不影响原数组
数组插入方法:splice(),影响原数组
数组通过索引查找方法:indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach()
数组归并方法:reduce() 和 reduceRight() 方法
数组判断是否存在值:includes()
JS中哪些可以改变原数组,哪些不可以改变?
JS数组中,能改变原数组的方法是:shift,unshift,pop,push,reverse,sort,splice,copyWithin,fill
JS数组中,不能改变原数组的方法是:concat,join,slice,map,filter,forEach,some,every,reduce
24.讲讲原生JS判断数据类型的方法
typeof:其中数组,对下个,null都会返回object类型
instanceof:只能正确判断引用数据类型,不能精准判断基本数据类型,其中运行机制是判断在原型链中是否能找到改类型的原型,所以instanceof可以用来测试一个对象在原型链中是否存在一个构造函数的prototype属性
constructor:有两个作用,一个是判断数据类型,第二个是对象实例通过constructor对象访问的构造函数
Object.prototype.toString.call():通过js原型链去判断数据类型
讲一讲原生JS如何判断当前值是否为NaN?
NaN:Not a Number,代表不是数字的值,NaN的类型是number
NaN不等于他自己:NaN == NaN // false
let a = 123 typeof a // number
Object.prototype.toString.call(NaN) // '[object Number]'
判断当前数字是否为NaN的方法:
1.isNaN:
let a = 123
let b = "hello"
let c = NaN
console.log(isNaN(a)) // false
console.log(isNaN(b)) // false
console.log(isNaN(c)) // true
我们可以封装一个判断是否为NaN的函数:
const is_NaN = (data) => {
if(isNaN(data) == true && typeof data == 'number'){
return true
} else {
return false
}
}
console.log(is_NaN(a)) // false
console.log(is_NaN(b)) // false
console.log(is_NaN(c)) // true
2.Number.isNaN()
Number.isNaN(a) // false
Number.isNaN(b) // false
Number.isNaN(c) // true
3.Object.is:
在这里,NaN等于他本身
console.log(Object.is(a,b)) // false
console.log(Object.is(b,c)) // false
console.log(Object.is(c,c)) // true
25.讲讲原生JS判断数组类型的方法有哪些?
通过 Object.prototype.toString.call() 判断
通过ES6中的 Array.isArray() 判断
通过 instanceof 判断
call,apply,bind的作用是相同的,都是用来动态改变当前函数内部环境对象的this指向的
call,apply,bind的相同点是都不会改变原函数的this指向
不同点是执行方式不同,call,apply是改变后页面加载之后立即执行,是同步代码,bind是异步代码,改变后不会立即执行,而是返回一个新的函数
其次是传参不同,call和bind传参是一个一个的传入,不能使用剩余参数的方式传参,apply可以使用数组的方式传入,只要是数组方式,就可以使用剩余参数的方式传入
最后是修改this的性质不同,call,apply只是临时修改一次,当再次调用原函数的时候,他的指向还是原函数的指向,bind是永久修改函数this指向,但是他修改的不是原来的函数,而是返回一个修改之后的新函数通过 Array.prototype.isPrototypeOf(obj) 判断
26.讲一下call,apply,bind的区别
call,apply,bind的作用是相同的,都是用来动态改变当前函数内部环境对象的this指向的
call,apply,bind的相同点是都不会改变原函数的this指向
不同点是执行方式不同,call,apply是改变后页面加载之后立即执行,是同步代码,bind是异步代码,改变后不会立即执行,而是返回一个新的函数
其次是传参不同,call和bind传参是一个一个的传入,不能使用剩余参数的方式传参,apply可以使用数组的方式传入,只要是数组方式,就可以使用剩余参数的方式传入
最后是修改this的性质不同,call,apply只是临时修改一次,当再次调用原函数的时候,他的指向还是原函数的指向,bind是永久修改函数this指向,但是他修改的不是原来的函数,而是返回一个修改之后的新函数
27.讲讲js遍历对象的方法都有哪些?
1.for in,可以遍历对象所有的可枚举属性,包括对象本身的属性和对象继承来的属性
2.Object.keys(),可以遍历到所有对象本身可美剧的属性,但是返回值是数组类型,也就是对象的key
3.Object.values(),遍历对象的特性是相同的,但是返回的结果是以遍历的属性值构成的数组,也就是对象的value
4.Object.entries(),返回值为Object.keys()和Object.values()的结合,也就是会返回一个嵌套数组,数组中包含属性名和属性值
5.Object.getOwnPropertyNames(),返回结果与Object.keys()对应,但是他得特性与其相反,会返回对象得所有属性,包括了不可枚举属性
6.Object.getOwnPropertySymbols(),返回对象内的所有Symbol属性,返回形式依然是数组,需要注意的是,在对象初始化的时候,内部是不会包含任何Symbol属性
7.Reflect.ownKeys(),返回的是一个大杂烩数组,即包含了对象的所有属性,无论是否可枚举还是属性是symbol,还是继承,将所有的属性返回
28.for in可以遍历对象,for of可以遍历对象吗?
for of 不可以直接遍历对象,因为for of遍历数组的时候,会自动循环请求数组的迭代器,并且自动使用这个迭代器遍历数组的值,如果想要使用for of来循环数组,那么需要用Object.keys()或者Object.values()返回一个数组,然后根据获得的对象属性数组来遍历对象,这样做和for in的方法类似,但是不同的是Object.keys()不包含对象原型链上的属性,而for in循环包含原型链上的属性
var array = [1,4,3,4,5,2]
for(var value of a){
console.log(value);
}
// 1 4 3 4 5 2
var obj = {name:'xiaoming',age:'22'}
for(var value of obj){
console.log(value);
}
// Uncaught TypeError: obj is not iterable
var obj = {name:'xiaoming',age:'22'}
for(var value of Object.keys(obj)){
console.log(value);
}
// name age
var obj = {name:'xiaoming',age:'22'}
for(var value of Object.values(obj)){
console.log(value);
}
// xiaoming 22
29.说说数组去重方法都有哪些
1.filters和indexOf()
let arrList = [1,2,2,3,3,4,4,5,6,7,8,9,9,0]
let newArr = arrList.filter((item,index,array) => {
return array.indexOf(item) === index
})
console.log(newArr)
原理:遍历数组并且检查当前检查项的索引是否与原数组中项的索引通过indexOf返回是否相同,如果不同,则索引值一定是相同的
2.reduce()和includes()
let arrList = [1,2,2,3,3,4,4,5,6,7,8,9,9,0]
let newArr = arrList.reduce((unique, item) => {
unique.includes(item) ? unique : [...unique, item]
}, [])
3.通过运算符new Set 或者 Array.from 实现去重
let originalArray = [1, 2, 3, 4, 1, 2, 3, 4]
let uniqueArray = array => [...new Set(array)]
let originalArray = [1, 2, 3, 4, 1, 2, 3, 4]
let uniqueArray = Array.from(new Set(originalArray))
4.双重for循环实现去重
let arr = [1, 2, 3, 4, 1, 2, 3, 4]
for(let i = 0;i <= arr.length-1-1;i++){
for(let j = i+1;j <= arr.length;j++){
if(arr[i] === arr[j]){
arr.splice(j,1)
j--
}
}
}
5.利用indexOf(),判断该数值第一次出现的索引下标是不是和本身的索引下标一样,如果不一样就说明有重复
let arr = [1, 2, 3, 4, 1, 2, 3, 4]
for(let i = 0;i <= arr.length-1;i++){
if(arr.indexOf(arr[i]) !== i){
arr.splice(i,1)
i--
}
}
6.声明一个空数组,利用indexOf()如果当前数据数值的索引下标与第一次出现的索引下标一样的话,就给空数组增加
let arr = [1, 2, 3, 4, 1, 2, 3, 4]
let newArr = []
for(let i = 0;i <= arr.length-1;i++){
if(!newArr.includes(arr[i])){
newArr.push(arr[i])
}
}
TypeScript
1.你觉得typescript和javascript对比,TypeScript有哪些优势?
TypeScript的好处:
1.Next-ES新特性:TypeScript是JavaScript的超集,具有可选的静态类型,并可以编译为纯JavaScript
2.静态类型系统:无需运行项目,即可对代码进行实时的静态类型检查,避免很多低级错误的发生
3.降低代码重构和维护的成本
TypeScript的优势:
1.可选性增强:基于语法解析TsDoc,ide增强
2.可维护性增强:在编译阶段暴露大部分错误
3.多人合作项目中,获得更好的稳定性和开发效率
4.包含于兼容所有的js特征,支持共存
5.支持渐进式引入与升级
2.TypeScript中的原始类型和内置对象有哪些?
原始类型:boolean, number, string, void, undefined, null, sumbol, bigint
其中,number类型除了支持十进制和十六进制,还支持二进制和八进制
内置对象:Boolean, Number, String, Date, RegExp, Error
BOM和DOM的内置对象:Window, Document, HTMLElement, DocumentFragment, Event, NodeList
3.TypeScript中如何定义布尔类型的变量
let on: boolean = true
let off: boolean = Boolean(0)
通过 new Boolean 的方式去声明布尔值,返回的是一个 Boolean 对象
let myBoolean: boolean = new Boolean(1)
4.TypeScript中在什么时候定义void以及定义void的意义是什么?
在JavaScript中,没有空值void的概念,在TypeScript中,可以使用void定义没有任何返回的函数
使用void定义一个函数,表示该函数没有返回值,如果函数定义了void,就无法通过return返回了
void类型一般用于我们不希望调用者关心函数返回值的情况下,比如通常的异步回调函数
5.void和undefined和null的区别是什么?
void:表示该函数没有返回值
undefined和null是所有类型的子类型,也就是说undefined类型的变量可以赋值给string类型的变量
null:不能赋予void类型,undefined可以赋予void类型
6.TypeScript中声明变量有哪些不同的关键字?
7.type类型别名和interface的区别是什么?
interface用于描述数据结构,type用于描述数据类型
interface和type都可以描述一个对象或者一个函数
只有type可以声明基本类型别名,联合类型,元组等类型
8.any和unknown和never和void类型的区别是什么?
any和unknown:
any可以完全不受类型系统的约束,我们可以很自由的使用any,any可以完成跳过TS的类型系统的检测
unknown比any安全一些
相同点是:在TypeScript中,any和unknown类型都属于顶级类型
不同点是:unknown需要先进行类型判断,才可以执行相应的类型操作,所以unknown可以被看成是更安全的any
never和void:
在TypeScript中,使用never类型来表示不应该存在的状态
如果声明一个函数为void的类型,则函数只能赋予undefine和null
9.谈谈你对ts中的泛型的理解,以及常用的泛型有哪些?
10.TypeScript中class类有哪些修饰符?
public: 在类的内部和外部都能访问,默认类型都是public
private:私有变量只能在类的内部访问,外部访问不到,子类也访问不到
protected:外部访问不到,只能在内部和子类中访问
11.TypeScript类的定义以及类包含哪几个模块?
TypeScript使用 class 关键字定义类
类可以包含以下几个模块(类的数据成员):
属性:字段的类里面声明变量,字段表示对象的有关数据
构造函数:类实例化时调用,可以为类的对象分配内存
方法:方法为对象要执行的操作
12.如果我定义了两个interface,我有什么办法进行合并?
13.讲讲你对ts中的函数重载的理解
14.ts中的联合类型,交叉类型,类型断言的区别是什么?
15.ts中的class的修饰符都有哪些?他们之间的区别是什么?
public: 在类的内部和外部都能访问,默认类型都是public
private:私有变量只能在类的内部访问,外部访问不到,子类也访问不到
protected:外部访问不到,只能在内部和子类中访问
16.ts中的静态类和静态函数怎么定义?
17.在静态类中,能访问public方法吗?
不能,在静态类中,只能访问static静态方法,不能访问public,private,protected
18.TypeScript支持静态类吗?为什么?
不支持,这个和C#和Java等面向对象的编程语言不同,在TypeScript中,我们可以将任何数据和函数创建按为简单对象,而无需创建包类型,因此,TypeScript不需要静态类,单例类只是TypeScript中的一个简单的对象
19.TypeScript面向对象编程的四个主要原则是什么?
封装,继承,抽象,多态
20.我定义了一个interface,我如何实现关键字关联?
21.你是如何理解ts中抽象类的?
22.抽象类和派生类的区别是什么?
可以通过抽象类或者接口来实现,定义抽象类的关键字是 abstract 来定义类和方法
23.如何继承一个抽象类?
TypeScript支持继承类,即我们可以在创建类的时候继承一个已存在的类,这个已存在的类,就是父类,继承的类就是子类
继承类的关键字是 extends
子类无法继承父类的私有成员,方法以及属性和构造函数,其他的都可以继承
TypeScript一次只能继承一个类,TypeScript不支持继承多个类,但是TypeScript支持多重继承,比如A类继承B类,B类继承C类
24.抽象类可以被实例化吗?派生类可以被实例化吗?
首先,抽象类和接口都包含了可以由派生类继承的成员,接口和抽象类都不能直接被实例化,但是可以声明他们的变量,然后利用堕胎性,把继承这两种类型的对象指定给他们的变量,最后通过这些变量来访问他们的成员,但是不能直接访问派生对象的其他成员
派生类只能继承一个基类,即只能直接继承一个抽象类,但可以使用一个继承链来来包含多个抽象类,而类可以使用任意多个接口
抽象类可以拥有抽象成员和非抽象成员,而接口成员则必须在使用接口的类上面实现
另外,接口的成员都是公共的,但抽象类的成员则可以是私有的,内部的,或者受保护的内部成员等
此外,接口不能包含字段,构造函数,析构函数,静态成员和常量
25.ts中,如何定义一个枚举,枚举的关键字是什么?常见的枚举类型都有哪些?
首先,枚举是定义常量的集合(以对象形式),定义枚举的关键字是 enum,并且只能用const定义,不能用var或者let定义,常见的枚举类型有,数字枚举,字符串枚举,异构枚举,接口枚举,const枚举,和反向映射枚举(通过value取key)
26.ts中,类型推论和类型别名的意义是什么?
27.你是如何理解ts中Symbol的,尝试介绍一下?
28.什么是泛型,泛型的具体作用是什么?
29.泛型的核心原理是什么?
30.常见的泛型方法有哪些,介绍一下它们的作用
31.ts中的命名空间是什么,如何在TypeScript中声明命名空间?
定义命名空间的关键字是 namespace
命名空间是用于对功能进行逻辑分组的一种方式
命名空间用于在内部维护TypeScript的遗留代码
它封装了共享某些关系的特性和对象
命名空间页称为内部模块
命名空间还可以包括接口,类,函数和变量,用来支持一组相关的功能
命名空间可以在多个文件中定义,并且允许每个文件都定义在一个地方,维护更方便
32.解释一下ts中的装饰器
33.什么是混合Mixins?
34.TypeScript中如何在函数中支持可选参数?
35.什么是TypeScript Declare关键字?
36.TypeScript中的三斜线指令的作用是什么?
37.什么是JSX,我们在TypeScript中如何使用JSX?
38.什么是Rest参数?
39.什么是TypeScript映射文件?
40.TypeScript中的getter和setter是什么?
Getter和setter是特殊类型的方法,可以帮助我们根据程序的需要委派对私有变量的不同级别的访问
vue2/vue3
1.说一下vue2的生命周期函数和vue3的生命周期函数
vue3的setup语法糖没有beforeCreate和created,直接用setup代替
onBeforeMont:创建前 => 在onBeforeMont读不到dom,因为这个时候,dom还没有被渲染
onMounted:创建后 => 在onMounted可以读到dom,因为这个时候,dom已经被渲染
onBeforeUpdate:更新前 => 在更新组件中获取dom,onBeforeUpdate获取到的同样也是更新之前的dom
onUpdated:更新后 => 在更新组件中获取dom,onUpdated获取到的才是更新之后的dom
onBeforeUnmount:销毁前
onUnmounted:销毁后
还有用于调试的两个生命周期函数:
onRenderTracked:默认先执行
onRenderTriggered:当触发更新生命周期函数的时候执行并返回更新之后的数据
2.说说vue中虚拟dom原理,diff算法源码
虚拟dom:
虚拟dom是通过JS生成的一个AST节点树,AST是抽象语法树,之所以使用AST抽象语法树,是因为很多地方都会用到,比如TS转JS,babel插件(ES6转ES5),或者JS通过V8引擎帮我们转字节码的时候,都会用到,当然在Vue中,也是帮我们将dom转换为虚拟dom,为什么不直接操作dom?因为一个dom的属性是非常多的,如果直接操作dom,会十分浪费性能。操作虚拟dom的好处还可以做很多算法的优化,比如比较常见的diff算法
diff算法:
v-for中的key只能是唯一值,不能重复
一.无key的diff算法:
dom会生成两个vnode,一个是新的vnode,一个是旧的vnode,第一步是通过for循环去patch,重新渲染元素,第二步是删除旧的vnode,第三步是新增新的vnode。diff算法会做替换,虚拟dom会做新旧对比,新的会把旧的替换掉,如果发现多了的,就新增并插入,如果发现少了就删除。
二.有key的diff算法:
同样也会进行新旧对比,有key的diff算法一共分为五步:
前置算法,只对比前面的,while循环,通过isSameVNodeType方法进行判断type和key比较是否一样,如果一样,返回true,则复用,其中type就是div,key就是我们绑定的key值,当发现不一样的时候,就会break跳出循环,进行尾序算法对比,前两种算法的作用是头和头,尾和尾进行对比
在这里,vue3和vue2的双端diff算法,不同的是,vue2也是头和头,尾和尾,头和尾,尾和头,这是和vue3的区别,vue3进行了diff算法优化,vue3还做了最长递增子序列算法,当头尾比对完之后,发现多了,就会走第三步,第三步是新增,首先还是while循环,通过patch,如果参数为null,就会走新增,如果发现少了,就会通过unmount执行卸载
三.乱序无序等等不可控因素:
1.构建新节点的映射关系:key值对应 0 1 2 3 4 索引,构建map关系
2.记录新节点在旧节点中的位置数组:如果有多余的旧节点,则删除掉,如果新节点不包含旧节点,也给删掉,如果节点出现交叉,说明是移动,并且要去求最长递增子序列,并且moved赋值为true
3.求最长递增子序列升序算法:写了一个getSequence函数方法,实现了贪心算法+二分查找实现最长递增子序列算法,求出来之后判断,如果当前遍历的这个节点不在子序列中,说明要求移动,否则如果在子序列中,那么就直接跳过
以上就是所有的diff算法原理
3.说说vue3中的响应式api都有哪些,他们的作用以及使用场景以及底层原理
ref:
ref接受一个内部值并返回一个响应式,且可变的reg对象,ref一般用来声明基本数据类型,还可以声明一个对象,需要通过.value或者.value.xxx来访问。ref也可以用于获取dom
// 父页面:
<xxx ref="aaa" />
<button @click="vvv">click</button>
const aaa = ref<InstanceType<typeof xxx>>()
const vvv = () => {
console.log(aaa.value?.fn())
}
// 子组件:
const fn = () => {
return "hello"
}
defineExpose({
fn
})
reactive:
reactive是返回一个对象的响应式副本,即proxy对象,如果我们修改了变量,目标对象也会随之改变
reactive一般用于声明一个复杂的对象类型,不能用来声明基本数据类型,如果声明了基本数据类型,就会返回一个基本数据类型的副本,不是proxy对象
readonly:
readonly接受一个响应式对象或纯对象或ref并返回原始对象的只读代理,只读代理是深层的,任何被访问的嵌套,属性也是只读的
shallowReactive:
shallowReactive创建一个响应式代理,它分总其自身property的响应性,但不执行嵌套对象的深层响应式转换
shallowReadonly:
shallowReadonly创建一个proxy,使其自身的property为只读,但不执行嵌套对象的深度只读转换
shallowRef:
shallowRef创建一个跟踪自身.value变化的ref,但不会使其值也变成响应式的
toRef:
toRef可以用来为源响应式对象上的某个property新创建一个ref,然后ref可以被传递,它回保持对其源property的响应式链接
toRefs:
toRefs将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的 ref
4.说说vue3中的reactive的作用,使用场景,以及尝试源码讲解
5.说说vue3中的toRef,toRefs,toRaw的作用,使用场景,以及底层实现原理
1.toRef:只能修改响应式的值,非响应式的视图无变化,但是值有变化。toRef只针对响应式的值做修改
const xxx = reactive({name:"aaa",age:18,sex:true})
const info = toRef(xxx,'name') // aaa
toRef在实际开发中的使用场景是:方法接收ref属性,但是reactive是对象,这个时候我们就可以通过toRef把属性解出来赋给函数
2.toRefs:把每一个属性都调用一遍toRef,把每一个属性都变成ref对象
简单实现toRefs原理:
const toRefs = (object: T) => { const map: any = {} for(let key in object) { map[key] = toRef(object, key) } return map }
toRefs在实际开发中的使用场景是响应式对象结构取值:
const xxx = reactive({name:"aaa",age:18,sex:true})
const { name, age, sex } = toRefs(xxx) // aaa,18,true
如果不加toRefs,会直接打印出普通的值,普通的值是不能做响应式操作的,只有加toRefs,才能实现响应式结构
3.toRaw:当你不想让当前对象变成响应式对象的时候,这个时候就可以用toRaw把响应式对象变成普通原始对象
toRaw将响应式对象变成普通对象的实现方法是:console.log(xxx('__v_raw')),就一行代码搞定
6.介绍一下vue2和vue3的响应式原理,以及底层源码讲解
响应式原理:vue2响应式原理是是 Object.defineProperty,vue3使用的是proxy
因为 Object.defineProperty是有很多问题的,比如说对象只能劫持预先定义好的属性,如果想要新增一个属性,是没有办法去做劫持的,但是vue2提供了$set去帮我们解决这个问题,数组只是重写了数组的原型,对于数组的操作,只能操作七种方法(pop/push/unshift/shift/splice/sort/reverse)
数组不能直接修改某项值,需要通过$set去操作才能实现,但是Object.defineProperty是可以做到数组修改的,只是存在非常大的性能问题
Object.defineProperty的原理是: vue2是使用Object.defineProperty,它只能劫持对象的属性,所以它需要深度遍历data中的每一个属性,这种方式对于数组很不友好,而且对象观测后,新增的属性就不是响应式的,只能用$set来添加新属性
Proxy的原理是: vue3使用的是Proxy,Proxy可以劫持整个data对象,然后递归返回属性的值的代理,即可实现响应式,但是Proxy的兼容性不是很好
补充:Object.defineProperty是一个静态方法,通过定义i属性的元数据信息精确的控制属性的行为,对象是由多个键值对组成的无序集合,对象中每个属性可以是任意类型的值
Object.defineProperty接收三个参数:分别是:
obj:表示在要在其定义属性的对象
prop:接收String,Symbol类型,表示要定义或者属性的名称
descriptor:通过它来限制属性的读写行为
返回值:返回一个Object,返回被传递给函数的对象obj
const Obj = {} // 创建obj对象
Object.defineProperty(Obj,"a",{
// 表示该属性的初始值,默认为undefined
value: 100,
// 布尔值,是否为可写,默认为false
writable:true,
// 是否可枚举
enumerable:true,
// 可以再次修改属性描述符
configuable:true
})
const Obj = {}
let x = null
Object.defineProperty(Obj,'a',{
// 给属性提供fetter的方法,如果没有,getter为undefined
get(){
return x
},
// set接受一个参数,将这个值赋给变量
set(newValue){
x = newValue
}
})
Obj.a = 10000
console.log(Obj.a) // 1000
7.computed和watch,watchEffect的区别以及在实际场景中的作用
8.讲讲watch侦听器以及watchEffect高级侦听器的源码实现原理
9.讲讲vue3中的异步组件,代码分包的实现方法
10.讲讲vue3中的v-model双向绑定底层实现原理
webpack/vite:
1.说一下webpack常见的Loader有哪些?
raw-loader:加载文件原始内容(UTF-8)
file-loader:把文件输出到一个文件夹中,在代码中通过相对URL去引用输出的文件(处理图片和文字)
source-map-loader:加载额外的Source Map文件,用来方便断点调试
svg-inline-loader:将压缩后的SVG内容注入到代码中
image-loader:加载并且压缩图片文件
json-loader:加载JSNON文件
babel-loader:将ES6转换为ES5
ts-loader:将TypeScript转换为JavaScript
awesome-typescript-loader:将TypeScript转换为JavaScript,性能高于ts-loader
sass-loader:将SCSS/SASS代码转换为CSS
css-loader:加载CSS,支持模块化,压缩,文件导入等特性
style-loader:把CSS代码注入到JavaScript众,通过DOM操作区加载CSS
postcss-loader:扩展CSS语法,使用下一代CSS,可以配合autoprefixer插件自动补齐CSS3的前缀
vue-loader:加载vue.js单文件组件
2.说一下webpack常见的Plugin有哪些?
define-plugin:定义环境变量
ignore-plugin:忽略部分文件
html-webpack-plugin:简化HTML文件创建(依赖于html-loader)
web-webpack-plugin:可方便地为单页面引用输出HTML,比html-webpack-plugin好用
uglifyjs-webpack-plugin:在webpack4之前,不支持ES6压缩
terser-webpack-plugin:支持压缩ES6(webpack4)
webpack-parallel-uglify-plugin:多进程执行代码压缩,提升构建速度
mini-css-extract-plugin:分离样式文件,CSS 提取为独立文件,支持按需加载
serviceworker-webpack-plugin:为网页应用增加离线缓存功能
clean-webpack-plugin:目录清理,每次 npm run build 打包后自动删除上次打包后的dist文件夹
3.说一下webpack中的Loader和Plugin的区别?
loader本质就是一个函数,在该函数众对接收到的内容进行转换,然后返回转换后的结果
因为webpack只认识JavaScript,所以loader的作用就是对其他类型的资源进行转译的预处理工作
plugin是插件,plugin基于事件流框架Tapable,插件可以扩展webpack的功能,在webpack运行的生命周期中会生成很多事件,plugin可以接收这些事件,在合适的时机,通过webpack提供的API改变输出结果
loader需要在module.rules中配置,由于作为模块的解析规则,所以类型是输入。输入的每一项都是一个Object。loader中内部包含了test类型文件,loader和options参数等属性
plugin 在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入
4.webpack的作用是什么?
目前前端页面功能丰富,特别是SPA单页面应用(single page web application)技术流行之后,JavaScript的复杂度增加和需要一大堆依赖包,还需要解决scss,less等,webpack的作用就是扩展写法和编译工作。目前前端主流框架都已经完全依赖于webpack的辅助了
5.webpack的工作原理?
webpack的主要作用是模块打包,webpack的工作原理是分析你的项目结构,找到JavaScript模块以及其他的一些浏览器不能直接运行的扩展语言,比如Scss,TypeScript等,然后将其转换或者打包成浏览器认识的语言
6.webpack的打包原理?
把一切都当作模块处理,不管是css还是js还是image还是html,都可以相互引用,通过定义entry.js,对所有依赖的文件进行跟踪,将各个模块通过loader和plugin处理,最后打包在一起。其次是按需加载,在打包过程中webpack通过code splitting功能将文件分为多个chunks,还可以将重读的部分单独提取出来作为commonChunk,从而实现按需加载,最后把所有依赖的文件打包成一个bundle.js,通过代码分割成单元片段并按需加载
7.webpack的核心里面?
Entry:入口,weback执行构建的第一步是从Entry开始,可以理解为输入,告诉webpack要使用哪个模块作为构建项目的起点,默认为根目录src下面的index.js
output:出口,告诉webpack在哪里输出它,打包好的代码以及如何明明,默认为dist文件夹
Module:模块,在webpack中一切都是模块,一个模块对应着一个文件,webpack会从配置的入口(Entry)开始递归查找出所有依赖的模块
Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并和分割
Loader:模块转换器,用于把模块原内容按照需求转换成新内容
Plugin:扩展插件,在webpack构建流程中的特定时机,会广播出对应的事件,插件可以监听这些事件的发生,在特定的时际做对应的事情
8.webpack的基本功能有哪些?
代码转换:TypeScript编译成JavaScript,SCSS编译成CSS等
文件优化:压缩JavaScript,CSS,HTML代码,压缩合成图片等
代码分割:提取多个页面的公共代码,提取首屏不需要执行部分的代码,让其异步加载
模块合并:在采用模块化的项目由很多模块和文件,需要构建功能把模块分类合并成一个文件
自动刷新:监听本地源代码的变化,自动构建,刷新浏览器
代码校验:在代码呗提交到仓库钱需要检测代码是否符合规范,以及单元测试是否通过
自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统
9.什么是bundle,什么是chunk,什么是module
bundle:是由webpack打包出来的文件
chunk:是指webpack在进行模块依赖分析的时候,代码分割出来的代码块
module:是开发中的单个模块
10.ExtractTextPlugin插件的作用是什么?
ExtractTextPlugin插件的作用是提取出 JavaScript 代码里的 CSS 到一个单独的文件
11.sourceMap的作用是什么?
sourceMap是一个映射关系,将打包后的文件映射到源代码中,作用是用于定位报错位置
配置方法:
devtool:'source-map'
加不同前缀的含义:
inline:不胜澈给映射关系文件,打包进main.js
cleap:1.只精确到位,不精确到列,打包速度快,2.只管业务代码,不管第三方模块
module:不仅管业务代码,而且还管第三方代码
eval:执行效率最快,性能最好
12.讲一下HMR模块热更新
HMR借助webpack.HotModuleReplacementPlugin(),devServer开启hot,我们可以实现之刷新css,不影响js,或者js中实现热更新,只更新指定的js模块
if (module.hot) {
module.hot.accept(’./library.js’, function() {
// Do something with the updated library module…
});
}
13.webpack如何配置多个入口文件?
entry: {
home: resolve(__dirname, "src/home/index.js"),
about: resolve(__dirname, "src/about/index.js")
}
用于描述入口的对象。你可以使用如下属性:
dependOn: 当前入口所依赖的入口。它们必须在该入口被加载前被加载。
filename: 指定要输出的文件名称。
import: 启动时需加载的模块。
library: 指定 library 选项,为当前 entry 构建一个 library。
runtime: 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk。
publicPath: 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址
14.什么是模热更新?模块热更新有什么优点?模块热更新到底是怎么实现的?讲一下实现原理
15.什么是长缓存?在webpack中如何做到长缓存优化?
浏览器在用户访问页面的时候,为了加快加载速度,会对用户访问的静态资源进行存储,但是每一次代码升级或者更新,都需要浏览器去下载新的代码,最方便和最简单的更新方式就是引入新的文件名称
在webpack中,可以在output给出输出的文件制定chunkhash,并且分离经常更新的代码和框架代码,通过NameModulesPlugin或者HashedModulesPlugin使再次打包文件名不变
16.什么是Tree-sharking?
Tree-sharking指打包中去除那些引入了但在代码中没用到的死代码。在wepack中js treeshaking通过UglifyJsPlugin来进行,css中通过purify-CSS来进行
17.webpack-dev-server 和 http服务器的区别?
webpack-dev-server使用内存来存储webpack开发环境下的打包文件,并且可以使用模块热更新,比传统的http服务对开发更加有效
18.介绍一下webpack和vite的区别:
底层:
webpack是基于nodejs构建,js是以毫秒计数,
vite是基于esbulid预构建依赖,esbulid是采用go语言编写的,go语言是纳秒级别的
因为js是毫秒级别,go语言是纳秒级别的,所以从本质上讲,vite比webpack打包要快很多
1.webpack是先打包再启动开发服务器
webpack是先打包,再启动开发服务器,请求服务器时,直接给予打包后的结果
2.vite是直接启动开发服务器,然后按需编译依赖文件
vite时直接启动开发服务器,请求哪个模块再对哪个模块进行实时编译
原理:
vite是将开发环境下的模块文件作为浏览器要执行文件,vite启动的时候不需要打包,也不需要分析模块依赖,编译,所以启动速度非常快,当浏览器请求需要的模块时,再对指定的模块进行编译,这种按需编译的模块,所以极大的缩短了时间
HMR热更新原理:
webpack:重新将该模块的所有依赖重新编译
vite:当某个模块内容改变时,让浏览器重新请求该模块
项目优化 / 开发经验
1.关于项目性能优化,你都有哪些方案?
路由懒加载:
SPA项目,一个路由对应一个页面,如果不做处理,项目打包后,会把所有页面都打包成一个文件,当用户打开首页时,会一次性加载所有的资源,这样就会造成首页加载很慢,降低用户体验,所以需要将路由全部改成懒加载
补充:路由 懒加载原理:
调用 import 时,会被作为分离的模块起点,意思是被请求的模块和它引用的所有子模块,会分离到一个单独的chunk中。webpackChunkName的作用是webpack在打包的时候,对异步引入的库代码进行代码分割时,设置代码模块名字,webpack会将任何一个异步模块与相同的块名称组合到相同的异步块中
组件懒加载
假设index.vue和about.vue都引入了dialog弹窗组件,如果我们直接import引入,当我们打包的时候,就会造成index.js和about.js均包括了该弹窗组件的代码。也就是说当用户打开index首页时,会一次性加载该页面所有的资源,这样就会造成性能问题,所以我们需要把组件改成异步组件的方式引入
<script>
const dialogView = () => import('./componments/doalog.vue')
export default {
name:"index",
components:{
dialogView
}
}
</script>
这样,打包之后就没有弹窗组件的代码了,组件代码会被单独打包成dialog.js,当用户点击按钮时,才会去加载组件模块的js和css
合理的使用 Tree-shaking
首先 tree shaking的作用是:消除无用的js代码,减少代码体积。
tree shaking的原理:
依赖于ES6的模块特性,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,静态分析就是不需要执行代码,就可以从字面量上对代码进行分析
骨架屏优化白屏时长
SPA单页应用,无论是vue还是react,最初的html都是空白的,就需要通过加载js将内部挂载到根节点上,这套机制的副作用就会造成长时间的白屏,常见的股价平插件就是基于这种原理,在项目中打包时,将股价平的内容直接放在html文件的根节点中
推荐使用 vue-skeleton-webpack-plugin 插件,安装方法:
npm install vue-skeleton-webpack-plugin
长列表虚拟滚动
当列表中数据过多时,所需要的渲染时间就会很长,滚动的时候,还会造成页面卡顿,整体体验会很不好,虚拟滚动是指只渲染可视区域的列表项,非可见区域的不渲染,在滚动时,动态更新可视区域
web Worker优化长任务
由于浏览器GUI渲染线程与JS引擎线程是互斥的关系,当页面中有很多长任务时,就会造成页面UI阻塞,出现界面卡顿,掉帧等状况
requestAnimationFrame制作动画
requestAnimationFrame是浏览器专门为动画提供的API,他的刷新频率与显示器的频率保持一致,使用该API可以解决用 setTimeout和setInterval制作动画卡顿的情况
setTimeout和setInterval属于JS引擎,requestAnimationFrame属于GUI引擎
JS引擎与GUI引擎是互斥的,也就是说GUI引擎在渲染时,会阻塞JS引擎的计算
requestAnimationFrame刷新频率是固定且准确的,但是setTimeout和setInterval是宏任务,根据事件轮询机制,其他任务会阻塞或延迟JS任务的执行,会出现定时器不准的情况
比如当页面被隐藏或者最小化时,setTimeout和setInterval定时器仍会在后台执行动画任务,但是如果使用requestAnimationFrame,当页面处于未激活的状态下,屏幕刷新任务会被系统暂停
图片的优化
图片的动态裁剪:
很多云服务,比如阿里云或七牛云,都提供了图片的动态裁剪功能,具体参考相关网站了解详情
图片懒加载:
假如说在首页中有一个图片列表,当用户打开页面时,只需要呈现出当前可视化屏幕区域的图片,当与欧股滚动页面时,再请求加载屏幕内的其他图片,以优化图片的加载效果
使用iconfont字体图标:
图片转base64模式:
图片转base64的好处是减少http请求
该题参考网站是:
作者:海阔_天空