博主目前大三,在上周也是尝试去面了一些企业,今天就给大家上一篇面经@!
自我介绍
首先也是简单介绍一下自己的情况,目前就读,然后在大学期间干了些什么,有过什么奖项,学习了哪些技术栈,对AIGC比较感兴趣等等!
面试官了解了一下我目前的课程情况,是否支持外出实习
因为我正处于大三下学期,课程量不多,拿到实习offer是可以向老师申请外出实习的,也是简单介绍了这样一个情况!
学前端学了多久
面试官对我学习前端的时间了解了一下,我也是简单介绍了一下我是什么时候开始去学前端的,简单介绍了一下我自己的学习规划。
项目
问了一下我简历上的两个练手项目,问了我为什么要去写这个两个项目,也是简单介绍了我为什么要写这两个项目,为什么要写全栈(后端是我自己写的一些接口),项目灵感来自哪里。
封装过哪些项目组件
这一块内容应该根据你们自己的项目内容去说,比如一些分类的导航栏,顶部导航栏,底部导航栏,我也是简单介绍了一些自己在练手的时候,写过的一些组件。
说一下对防抖节流概念的理解
防抖和节流是我们前端开发过程中常用的两种函数优化手段,一般是用来限制函数的执行次数,提升性能和用户体验。
防抖:当某个函数持续,频繁的触发,那么只在它最后一个函数,且一段时间内没有再次触发,这个函数才会执行,拿一个按钮的点击事件来说,当你多次点击这个按钮并且触发事件对应的回调函数,这个函数将不会执行,只有当你最后一次点击之后,且规定的时间之内没有再次去点击这个按钮,这个回调才会触发!
一般防抖的应用场景有:
- 输入框搜索:当用户在搜索框中输入关键字时,使用防抖可以避免频繁发送搜索请求,而是在用户停止输入一段时间后才发送请求,减轻服务器压力。
- 按钮点击:当用户点击按钮时,使用防抖可以避免用户多次提交或重复操作。
手写:一般防抖的实现思路是通过利用定时器,每次函数触发都会将上一次的定时器掐灭掉,然后重新设置一个定时器
html
<button id="btn">提交</button>
<script>
let btn = document.getElementById('btn');
function send(e) {
console.log('已提交');
}
//addEventListener会把this指向你绑定的对象
btn.addEventListener('click', debounce(send,1000))
function debounce(fn,delay) {
let timer;
let _this = this
return function() {
//arguments
let args = arguments
if(timer)clearTimeout(timer);//clearTimeout(timer);掐灭定时器
timer = setTimeout(()=>{
fn.call(this,...args)
},delay)
}
}
</script>
更具体大家可以参考:手把手教你实现JavaScript手搓"防抖"优化代码----专业的事用专业的方法来做! - 掘金 (juejin.cn)
节流:在一段时间内,某个函数持续,频繁的触发,只会执行第一次函数的触发,忽略掉后面函数的执行。
一般的应用场景:
- 页面滚动:通过节流,限制滚动事件的触发次数,提供页面浏览的性能。
- 按钮点击:当用户点击按钮时,使用防抖可以避免用户多次提交或重复操作。
手写:通过定时器,保证在一个固定的时间间隔内至少执行一次事件处理函数。
html
<button id="btn">提交</button>
<script>
let btn = document.getElementById('btn')
function send(){
console.log('提交了');
}
btn.addEventListener('click',throttle(send,2000))
function throttle(fn,delay){
let prevTime = Date.now()
return function(){
if(Date.now()-prevTime>delay){
fn.apply(this,arguments)
prevTime =Date.now()
}
}
}
</script>
具体大家可以参考:带你轻松手搓"限流"优化代码! - 掘金 (juejin.cn)
说一说你对垃圾回收机制的了解
js代码在运行时,需要分配内存空间储存变量和值,当这个变量不再引用时,需要对其占用的内存进行回收。如果不及时清理,会造成卡顿,内存溢出,这就是需要垃圾回收机制的存在。
在 V8 中,会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象
垃圾回收机制分为
主垃圾回收机制:负责清理生存时间比较长的数据
副垃圾回收机制:负责清理存在时间比较短的数据
副垃圾回收机制一般负责对新生代数据的垃圾回收,大多数的对象都会被分配在新生代,该存储空间相对较小,分为两个空间:from空间
对象区,to空间
空闲区
- 新增变量会放到
to
空间,空间满后会执行一次垃圾清理操作 - 对垃圾数据进行标记,标记完成后将还存活的数据复制到
from
空间,并且进行有序排列 - 交换两个空间,原来的
to空间
变成from空间
,原来的from空间
变成to空间
主垃圾回收器主要负责⽼⽣代中的垃圾回收。存储一些占用空间大、存活时间长的数据,采用标记清除算法进行垃圾回收。
标记清楚法
有两个步骤 标记、清除
- 标记:为所有变量标记为0,从根节点开始遍历,将仍然存活的变量标记为1
- 清除:将变量标记为0的数据进行清除,然后将标记为1的数据修改为0,进行下一轮回收
注意:对⼀块内存多次执⾏标记清除算法后,会产⽣⼤量不连续的内存碎⽚ 。⽽碎⽚过多会导致⼤对象⽆法分配到⾜够的连续内存,于是⼜引⼊了另外⼀种算法------标记整理。
标记整理 的标记过程仍然与标记清除算法⾥的是⼀样的,先标记可回收对象,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉这⼀端之外的内存。
垃圾回收机制还有一种清除策略:引数计数法
一个数据被引用一次,引用的次数就加1,如果没有引用,引用的次数就减1,当引用的次数为0时,就会对其进行回收
这种方式会产生一个问题,在循环引用时,引用数永远不会为0,无法回收。
聊一聊深拷贝和浅拷贝
这里我们可以先聊一下js中数据的存储方式,js中数据大体分为两部分原始数据类型 和引用数据类型 ,一般我们的原始数据会存储在栈内存 当中,而引用数据类型存储在一个堆内存 ,会在栈当中维护一个指向其在堆中位置的指针。
浅拷贝:就是指当我们在复制对象时,仅仅是复制了对象的表面结构,而没有复制对象内部的嵌套对象!换句话说,浅拷贝创建的新对象,无时不刻不在受到原对象的影响,也就是说这个新对象的某些属性仍然是原对象的引用!
浅拷贝是一种对象复制的操作,它创建了一个新对象,并将原始对象的属性值复制到新对象中。然而,与深拷贝不同,浅拷贝只复制基本数据类型的属性值,而对于对象类型的属性,它们仍然是原始对象中相同对象的引用。换句话说,浅拷贝创建了一个新对象,但对于原始对象中的对象属性,新对象中的属性仍然指向原始对象中相应的属性。
深拷贝:是指创建一个新对象,同时递归地复制原始对象及其所有嵌套的对象,确保新对象和原对象的每个属性都是相互独立的,没有共享引用。它不仅复制对象本身,还会递归地复制对象内部的所有嵌套对象。
在JavaScript中,对象赋值默认是浅拷贝,这意味着当我们将一个对象赋值给另一个变量时,实际上是传递了对象的引用,而不是对象的副本。这样的赋值会导致两个变量指向相同的内存地址,修改其中一个变量会影响到另一个变量。深拷贝的作用在于创建对象的完全独立副本,避免对象之间的关联。
这个时候面试官就会问你怎么实现一个深拷贝
能不能手写深拷贝
我们可以说一下这个方法!
JSON.stringify
和JSON.parse
利用JSON.stringify
和JSON.parse
是一种简单的深拷贝方法。这种方法的优势是简单易懂,但它有一些限制,比如不能处理包含函数、循环引用等情况的对象。
自己手写思路:就是在我们的方法当中,每次我们都开辟一个新的对象,使用一个新的地址,然后去遍历我们传过来的对象,如果是引用类型,我们就再调用我们自己的方法,用同一个逻辑,每次有引用类型,都去开辟新的引用地址,用新的内存去存储,直到对象当中没有引用类型!
注意!!我们手写的方法应对不了循环引用(这个我们再这里就不多阐述)
如果是原始类型,我们可以直接进行拷贝,因为原始类型的拷贝都是值拷贝,其实差不多就是深拷贝!、
这样我们深拷贝的逻辑就实现了!
例如:
js
let obj = {
name:'小黑',
age:18,
like:{
type:'coding'
}
}
var copyobj = deepCopy(obj)
obj.type = 'running'
console.log(copyobj);//输出:{ name: '小黑', age: 18, like: { type: 'coding' } }
//换一个地址
//这个如果对象存在循环引用就会爆栈
function deepCopy(obj) {
let objCopy = {}
for(let key in obj){
if(obj.hasOwnProperty(key)){
if(obj[key] instanceof Object)//obj[key] i是引用类型
{
objCopy[key] = deepCopy(obj[key])
}else
{
objCopy[key] = obj[key]
}
}
}
return objCopy
}
这里大家可以参考一下:[干货]面试手写浅拷贝和深拷贝?今天我们就来学一下!待 - 掘金 (juejin.cn)
js的类型判断可以通过哪些方式
typeof
:一般只能判断原始数据类型,null
除外,也能判断function
,返回值是一个运算数的基本类型
instanceof
:可以准确判断引用数据类型,但是不能判断原始数据类型,返回的是一个布尔值
Object.prototype.toString.call()
:通过原型链的方法进行判断,能判断所有的数据类型,判断某个对象是否存在于另外一个对象的原型链值,返回类似于'[Object Function]'
Array.isArray
:只能判断数据是否是数组。
为什么Object.prototype.toString.call()
这里要用.call
在js当中,我们通常通过Object.prototype.toString.call()
来获取一个对象内部的[[class]]
,这可以帮助我们确定对象的真实类型。
这里使用 .call()
是为了改变 toString
方法内部的 this
上下文,使其指向我们想要检查的对象,而不是默认的 Object.prototype
。
说一下什么是闭包
闭包一般是只我们可以去访问另外一个函数作用域中的变量的函数。一般我们会通过在一个函数内创建另外一个函数。
这里我们说一下js中的调用栈,每次函数在执行完都会被清除,而出于闭包中的变量一般不会被清除,一般需要我们自己手动地去释放掉!
优点:
- 创建局部变量,避免变量全局污染
- 可以实现封装,缓存等
缺点:
- 变量不会被回收,容易消耗内存,使用不当导致内存溢出
闭包的使用场景:
- 创建全局私有变量
- 封装类和模块
- 实现函数柯里化
说一下ES6中箭头函数和普通函数的区别
- 箭头函数是匿名函数,不能作为构造函数去使用
new
关键字/ - 箭头函数没有
伪类argument
- 箭头函数没有自己的
this
,会将所在的上下文作为自己的this
call()、apply()、bind()
方法不能改变箭头函数中的this
指向- 箭头函数没有
prototype
- 箭头函数不能做
generator
函数,不能使用yeild
关键字
说一下css怎么实现一个水平居中!
- 绝对定位:absolute + translate
css
.father{
display:relative
}
.son{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
- 绝对定位:宽高已知 + margin负值
css
.father{
width: 500px;
height: 500px;
display:relative
}
.son{
width: 200px;
height: 200px;
position: absolute;
left: 50%;
top: 50%;
margin-top:-100px:
margin-left:-100px:
}
- 弹性容器flex
css
.box{
dispaly:flex;
justify-content: center;
align-items: center;
}
- 网格布局grid
css
.box{
display: grid;
justify-content: center;
align-items: center;
}
- 表格布局table (子容器不能是块级)
css
.father{
dispaly:table;
text-align: center;
vertical-align: center;
}
.son{
display:inline-block;
}
- 绝对定位 + margin:auto 盒子有宽高的情况
css
.father {
position: relative;
}
.son {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}
说一下vue双向数据绑定的原理
这里我居然听成了响应式数据绑定的原理,回顾一遍才发现是说双向数据绑定的原理。真的碎了!!!
vue是一个mvvm框架(双向绑定),当数据发送变化的时候,视图也发送相应的变化,当视图发发生变化的时候,数据也会发生同步变化,是通过采用数据劫持和发布-订阅模式,通过Object.defineProperty
方法属性拦截的方式,把data
对象里的每个数据转写成getter
和setter
,然后通过监听数据的变化触发相应的回调通知视图进行更新!
我们所说的数据双向绑定,一定是对于UI控件来说的,非UI控件不会涉及到数据双向绑定。 单向数据绑定是使用状态管理工具(如redux)的前提。如果我们使用vuex,那么数据流也是单项的,这时就会和双向数据绑定有冲突。
说一说插槽的分类和作用
slot
插槽,一般是我们在封装组件的时候使用!当我们不知道组件内的内容不知道以哪种形式来展示内容的时候,可以用slot
来占据一个位置,最后可以通过父组件以内容的形式传递过来,展示相应的内容,主要分为三种:
- 默认插槽 :又叫匿名插槽,当
slot
没有指定一个相应的name
属性值的时候,显示一个默认的插槽,一个组件内只能有一个默认插槽。 - 具名插槽 :顾名思义,就是带有一个具体名字的插槽,也就是带有
name
属性的slot
插槽,在一个组件内是允许出现多个具名插槽的。 - 作用域插槽:默认插槽、具名插槽一个结合体,可以是匿名插槽,也可以是具名插槽。这个不同之处在于子组件渲染作用域插槽的时候可以将子组件内部的数据传递给父组件,让父组件根据子组件传过来的数据决定要展示的具体内容,该如何渲染该插槽。
实现原理:当子组件的vm
实例化的时,获取到父组件传入的slot
标签的内容,存放在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.具体名字
,当组件执行渲染函数时,遇到slot
标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可以称该插槽为作用域插槽。
手写题
js
const tree = [{
id: 1,
name: '一级 1',
children: [{
id: 4,
name: '二级 1-1',
children: [{
id: 9,
name: '三级 1-1-1'
}, {
id: 10,
name: '三级 1-1-2'
}]
}, {
id: 5,
name: '二级 1-2',
children: [{
id: 11,
name: '三级 1-2-1'
}]
}]
}, {
id: 2,
name: '一级 2',
children: [{
id: 6,
name: '二级 2-1',
children: [{
id: 13,
name: '三级 2-1-1'
}]
}]
}]
// 实现一个方法,getAllIdsByLevel(tree, level)获取指定小于等于level层级的所有id
这个手写题,也是一个比较简单的递归算法!我给出我的解法,看看大家有没有更好的解法。
js
function getAllIdsByLevel(tree, level){
let allid= []
if(level===1)
{
for(let i = 0 ;i<tree.length;i++)
{
allid.push(tree[i].id)
}
}else if(level > 1)
{
for(let i = 0 ;i<tree.length;i++)
{
allid.push(tree[i].id)
allid.push(...getAllIdsByLevel(tree[i].children,level-1))
}
}
return allid
}
console.log(getAllIdsByLevel(tree,3));
最后
如果,你觉得这篇文章有帮助的话,可以帮博主点赞+评论+收藏,三连一波!感谢!
后面我还会出一些面经!大家可以持续关注一波!