前言
目前大三,在最近的诸多面试中,这是我的第一个面试的面经,也是唯一一个只问八股的面试,从百草园(CSS/JS面经
)问到三味书屋(Vue面经
),就着打算讨教面试官的目的而来,从刚开始我还是紧张的情绪到我的回答如何让面试官感觉身体被掏空,最终因为汗流浃背到实在想不到还有啥可以问的而结束了这场酣畅淋漓的面试。现在,让我们近距离感受一下。
面试了这么多天,觉得一个人的力量还是太过薄弱,如果你和我一样有向前冲的勇气,欢迎掘友们私聊我交流面经(wechat: sAnL1ng)
先自我介绍一下
雷打不动的定律,先上来面试官让我进行自我介绍,引用了之前看一些大佬总结的文章里面自我介绍的公式:我是谁+从哪里来+我做过什么+有什么成绩+为什么能胜任。
面试官您好,我叫sAnL1ng,来自东华理工大学25届软件工程专业本科大三在读,想来贵公司前端岗位实习。低年级的时候,我跟着学长一起在B站学习、打些比赛,课余时间还开始了健身、参加各种社团俱乐部丰富自己的社交范围和兴趣爱好,奖学金也拿了一些。个人比较喜欢前端,也能用node写一些简单的后端代码。chatgpt火了后,又对AIGC非常感兴趣,了解目前各种流行的langchain、openai、transform等大模型,并且有一些使用经验,平时写代码会用一些AI Copilot工具提升编程效率。仔细读过《你不知道的JavaScript》,因为个人喜欢学习知识的过程中进行输出和交流,在掘金上写了关于JS基础和vue底层源码相关系列文章,同时我是掘金lv4等级和2023年人气创作者。平时主要使用vue全家桶来进行前端开发。阅读过vue、axios、ElementPlus的源码,未来会持续学习。
CSS
能说一下CSS中的盒模型吗
当时我脑海里出现的首先是这张图片,然后接着套公式:是什么 + 对应内容
(1) 是什么?
浏览器在页面布局时,将所有的元素表示为一个个矩形盒子,每一个盒子包含四个部分: content,padding,border,margin
(2) 标准盒模型
盒子宽度 =
content
(3) 怪异(IE)盒模型
盒子宽度 =
content
+border
+padding
接着,再补充一些修改盒子模型的方法,通过修改box-sizing可以修改元素的盒模型
- 标准盒模型(默认):
box-sizing:content-box
- IE盒模型(怪异盒模型):
box-sizing:border-box
讲一下CSS中水平垂直居中的方法
当面试官问出这个问题,瞬间有点兴奋,依旧是简单的CSS问题,于是一套行云流水的拳法给到面试官。
- position:
absolute + translate || margin负值(已知宽高)
- flex:
justify-content: center + align-items: center
- grid:
justify-content: center + align-items: center
- table-cell:
text-align: center + vertical-align: middle;
(子容器不能是块级)
- margin(已知宽高)
你知道哪些CSS中隐藏元素的方法
面试官的出招依旧是CSS,给他一个无懈可击的答案:
display:none
脱离文档流 无法响应事件 回流重绘visibility:hidden
占据文档流 无法响应事件 重绘opacity:0
占据文档流 响应事件 回流 || 不重绘position:absolute
脱离文档流 无法响应事件 回流重绘clip-path: circle(0)
占据文档流 无法响应事件 重绘
JS
说说你知道的JS的数据类型
果不其然,CSS系列过渡到来了JS系列,没关系,依旧是我们擅长的区域:
原始数据类型:
Number
,String
,Boolean
,undefined
,null
,Symbol
,Bigint
。引用数据类型:
Object
,Function
,Array
,Map
,Set
,Weakmap
,Weakset
,date
,正则
。
然后,光聊这些可不够,这可展现不出我们的实力,于是我们提到在JavaScript中这两种数据类型的存储也不同。
原始数据类型的值和引用数据类型的值分别是存储在调用栈和堆内的,并且引用数据类型会将其在堆内的地址存放在调用栈内。
如果面试官此时不说话,我还能接着扯到深拷贝和浅拷贝的概念以及实现方法,最后还能来个手写将丝滑小连招给到面试官。面试官此时感觉到似乎不对劲,于是赶紧让我介绍一下JS中的类型转换,于是犹如胸有成竹般滔滔不绝....
你能给我介绍一下JS中的类型转换吗
首先,JavaScript中的类型转换其实通常是在比较两个数据的值时发生的,这里涉及到原始数据类型之间相互转换,引用数据类型相互转换,最后就是两种不同数据类型相互转换三种情况,这里面其实是通过Number()
,String()
,Boolean()
,Object()
,ToPrimitive(ValueOf + ToString)
这些方法实现的。
当时,我记得我在这个问题上滔滔不绝了一大堆,完整内容在我的这两篇文章:
聊聊深浅拷贝
自从面试官问道数据类型然后我附带聊了它们的存储方式后,其实他已经被我带入到数据类型的包围圈了。
深浅拷贝的概念
正如前面所说,由于两种数据类型在JS中值存放的方式其实是不同的,其实我们经常使用到了浅拷贝,浅拷贝
的意思是当原数据的值发生改变时,拷贝的数据的值也会随之改变,比如我们声明一个变量为原始数据类型的时候,再声明一个变量值为上一个变量名,当第一个变量发生改变时,拷贝的变量也会发生变化。当我们声明一个引用数据类型的时候,然后再声明一个变量,值为上一个变量名,其实这里并不会将应用数据类型的值赋给拷贝对象而是将地址赋给这个变量,所以当引用数据类型变量的值发生变化的时候,拷贝的对象的值也会发生变化。深拷贝
则反之。
如何实现深拷贝
JSON.stringify()
将js对象序列化,再通过JSON.parse
反序列- 通过递归
手写实现深拷贝
- 使用第三方库(比如Lodash)
【手写实现深拷贝】
如何实现浅拷贝
Object.assign({}, x)
拷贝对象Object.create(x)
创建新对象concat
连接多个数组返回一个新数组slice
不传递参数返回一个新数组数组解构
将原数组的元素解构赋值给新数组
【手写实现浅拷贝】
闭包
首先根据JavaScript中的词法作用域规则,内部函数总是可以访问其外部函数中声明的变量。当内部函数被返回到外部函数之外时,即使外部函数执行结束了,但是内部函数引用了外部函数的变量,这些变量仍然会被保存在内存中。这个现象称为闭包。
闭包可以实现一个变量私有化、延长变量的生命周期等等优点,然后大家经常会说闭包一定会造成资源泄露,但是当我们合理使用闭包,手动释放内存,通过WeakSet|WeakMap来合理声明变量等操作也可以避免内存泄露的缺点。
防抖和节流
- 防抖是指当用户不断触发一个事件的时候,我们一直不予执行,直到最后一次触发该事件并且一段时间内没有再次触发该事件的时候就进行回调函数的执行,通常我们在输入框搜索建议,按钮提交等场景会用到防抖。
【手写实现防抖】
- 节流是用来限制用户触发事件的频率,当用户在一个时间段内一次或多次触发一个事件的时候,回调函数只会执行一次,以此可以优化性能,比如在滚动加载窗口大小调整等场景会用到节流
【手写实现节流】
你知道回调地狱吗
当时面试官问我回调地狱的时候,我随即想将JS中异步的发展史讲出来跟面试官聊聊。首先,JavaScript作为一门单线程语言
,与java相比它没有锁
的概念,同一时间只能执行一个任务 ,它当初被打造出来的初衷就是为了作为浏览器的脚本语言
。
但是在业务中,我们时常会遇到一个耗时函数
的运行需要借助另一个耗时更久函数
的运行结果,但是我们不通过特殊操作方法会导致运行顺序
的出错
,所以我们一般会将耗时短的函数放进耗时更久的函数内部,这样就可以达到我们的目的,这就是最早的处理异步的方式。
显而易见,这样做会出现一个弊端,当函数嵌套过多时,不仅代码可读性差,程序出错误时,我们也无法准确捕获到确切的错误位置,这样就形成了一个回调地狱
般的代码。
那接着聊聊你对Promise的理解
为了解决回调地狱,官方后来推出了一个Promise对象用来处理异步,Promise有三个状态:
- Pending(进行中) : 初始状态,表示操作正在进行中。
- Fulfilled(已成功) : 表示操作已经成功完成。
- Rejected(已失败) : 表示操作失败。
并且,Promise的状态一经改变就无法逆转,然后Promise中有个then
和catch
方法,这样也就保证了 then 和 catch 不可能同时触发。
- .then :默认返回一个状态为
Fulfilled
的Promise对象。并且接受两个回调函数作为参数
- 当then前面的promise状态为fulfilled,then里面的回调直接执行
- 当then前面的promise状态为rejected,then里面第二个回调直接执行
- 当then前面的promise状态为pending,then里面的回调需要被缓存起来交给resolve或者reject执行
并且Promise内部还有一个resolve
和reject
函数,Promise接受resolve
和reject
作为参数。
- 当调用resolve函数时,会将当前Promise的状态更改为
Fulfilled
,并且可以携带参数,将参数保存供.then中第一个回调函数使用。- 当调用reject函数时,会将当前Promise的状态更改为
Rejected
,并且可以携带参数,将参数保存供.then中第二个回调函数使用,或者触发.catch中的回调函数。
async/await
当面试官继续让我谈谈对 async/await
的认识,我迫不及待回答道async/await是es6提供的一种新的处理异步的方案,不仅解决了回调地狱中代码的可读性差的问题,也解决了Promise中.then嵌套不美观的问题。
async
里面有一个await
方法,比如当我们用async
声明一个函数时,我们可以在里面用await
关键字来声明一个函数的调用,并且await
声明的代码会变成同步任务,并且会阻塞后续代码的执行,将后续代码推入微任务队列,于是,通过这个关键字我们就也可以实现更优雅的处理异步的方法了。但是我们还需要补充到async/await里面没有错误捕获机制,一般我们也可以在外层嵌套一个
try/catch
来保证代码能够正常的运行。
到这里你以为结束了吗?面试官!
async/await
也可以由promise
结合generator
实现,generator
是继Promise
后推出的一种处理异步的方式,比如大家可能很熟悉的co函数
的语法糖其实就是generator
,当我们声明一个generator
函数我们可以用它里面提供的yield关键字
来声明函数或者变量,然后通过调用.next()
方法来控制内部函数的执行与暂停,并且可以控制每个阶段的返回值,最终来达到一个处理异步的效果。很多人说官方提供的
async/await
本质是就是promise
结合generator
实现的,其实并不是,只能说我们可以在generator
基础上集合Promise
通过递归的方式来自动执行一个又一个的next
函数,当done
为true时,结束递归。
Vue
过五关斩六将,接下来才是面试官迫不及待准备的重头戏,Vue------面经!
Vue组件通讯
父子组件通讯
:父组件v-bind
绑定属性用于传值,子组件props
接收(props)是单向数据流,子组件只能用,不建议修改,改了父组件也不受影响。
子父组件通信
:父组件订阅一个事件,子组件通过$emit
发布该事件且携带事件参数,让父组件的订阅生效
兄弟组件通信
: vuex、pinia等等状态管理工具
继续聊聊双向绑定
Vue中双向绑定主要是通过指定v-model
实现的,当数据发生变化时试图也会随之改变,当视图发生改变时数据也会被修改,通常双向绑定作用在表单或者组件中,这二者的实现原理也会有所不同:
作用在表单
:通过v-bind:value
来绑定数据,再通过v-on:input
来监听输入框中的值来实现数据的变化并修改value
作用在组件
:通过props
结合$emit
语法糖实现,也就是父子组件通讯
的方式。
也就是当面试官以为我只能回答到这的时候,我继续介绍了vue2和vue3中双向绑定的底层原理。
-
在vue2中,双向绑定主要是通过
数据劫持
和发布-订阅模式
实现的 ,当我们声明一个数据源data
的时候,会实例化一个Observer类,首先这个类通过递归遍历的方式对data
数据源中的每一个数据进行递归遍历。然后通过Object.defineProperty
方法给遍历到的每一个属性添加上getter
和setter
方法,当对数据进行读取操作时,会触发getter
方法进行依赖收集,当数据进行修改操作的时候,会触发setter
方法对之前收集到的依赖进行依赖触发,并且更新依赖通知视图进行渲染。 (但是这种办法效率低、功能弱。当我们使用Object.defineProperty()
来进行数据劫持,只有当数据被修改或者读取操作的时候才会触发组件的渲染,当涉及到组件中数据的增加和删除操作时就不能触发组件更新渲染,还需要我们手动在数组的增删方法内通过重写的方式,在拦截里面进行手动收集依赖和触发依赖进行试图更新) -
在vue3中,与vue2不同的是vue3采用了ES6新增的一个特性------Proxy ,Proxy可以创建一个
代理对象
的函数,相较于使用Object.defineProperty()
对一个个数据进行迭代遍历循环,它可以对整个对象进行监听拦截,包括对数据增删改读
操作。
发布订阅模式
由于是小厂的面试官,在我解释上面的底层原理时并未打断我,但好像我当时语速有点过快只听见了发布订阅
关键字,那么我就给你讲讲发布订阅模式。
发布订阅模式,顾名思义有一个发布者与订阅者,简单来讲当订阅者订阅一个事件并可以设置相应的回调函数,当发布者在全局发布该事件的时候,订阅了该事件的对象就会触发相应的回调函数。 而当我们想去实现一个发布订阅模式,官方提供了两个对象
Event
和CustomEvent
分别是默认事件和自定义事件:
Event
对象用于处理和响应在 DOM 中发生的事件。例如click、input、mouseenter、mouseleave等等。- 而
CustomEvent
是一种自定义事件,它是继承自Event
的对象,允许我们创建和触发自定义事件。 与普通的事件不同,自定义事件可以携带额外的数据。
这两个事件中存在两种属性,分别是1.事件传播机制 2.取消事件默认行为
Event
对象涉及到事件的传播机制,这是指事件如何从文档树的根节点传递到目标元素,然后再从目标元素传播回根节点。这一过程分为三个阶段:捕获阶段、目标阶段和冒泡阶段。
- 捕获阶段(Capture Phase): 事件从根节点向目标元素传递,途中的元素可以捕获事件。
- 目标阶段(Target Phase): 事件到达目标元素,触发与目标元素关联的事件监听器。
- 冒泡阶段(Bubble Phase): 事件从目标元素向根节点再次传递,途中的元素可以响应事件。
Event
对象的 bubbles
属性表示事件是否在冒泡阶段传播,默认为 false
。
- 取消事件默认行为
Event
对象的另一个重要特性是能够取消事件的默认行为。在某些情况下,当特定事件发生时,浏览器会执行与之相关的默认行为,例如点击链接时跳转页面。通过preventDefault
方法,可 以阻止事件的默认行为
通过这些提供的API我们可以去丰富一个事件,然后发布该事件后,订阅者就会触发相应的回调函数。
谈谈你对diff算法理解
diff算法发生在Vue生命周期中的beforeUpdate
和Updated
之间,也就是数据发生改变的时候,在响应式数据发生改变前,会生成一个虚拟Dom树,其实它是一个对象,diff会将新旧Dom进行差异比较,顺序如下:
比较新旧虚拟Dom的
标签
是否是同类,若不是则直接废弃,否则继续比较;比较
节点
的上的属性,不同的地方会生成一个补丁
用来记录不同的地方;继续比较下一层的
子节点
的属性,采用双端diff
算法,它会创建四个指针
分别指向新旧两个节点的首尾,首和尾指针向中间移动,也就是头头比较
、尾尾比较
、头尾比较
、尾头比较
,看看他们的key值是否是一样的,也就是是否可以复用。重复上述过程,对比结束后,如果新节点还有剩余就保留,如果旧节点有多余就删除。
并且,我们在用v-for循环后不建议用属性的下标值作为key,原因就是双端diff算法会比较key值是否一样来判断是否可以复用,如果列表中发生了增加或者删除操作,它以及后面的index值都会发生改变,这样一来就会导致原本可以复用的节点被舍弃,双端diff算法也就失去了它的意义。
为什么不建议v-if和v-for一起使用
在vue3中v-for
的优先级要高于v-if
,如果一起使用就会导致每次遍历都要进行一v-if判断,产生了许多冗余的计算,大大降低了性能。
mixin和mixins的区别
这两个API都是用于逻辑混入的操作,比如当我们有两个组件存在一部分相同的逻辑时,我们可以选择分成两个组件互不干扰,当逻辑发生改变时,我们需要对每一个组件进行单独处理,这样就很麻烦,或者我们采用props来传值的方式创建差异化分区,但这样做的话又会存在props过多导致代码可读性差的缺点。
这时候,我们就可以借助minxin,它允许我们封装一个多个组件中都可以使用函数,通过这个函数我们可以改变函数作用域外部的任何东西,哪怕多次执行。
- mixin用于全局混入,这样就会有一个缺点就是会影响到每个组件的实例,通常情况下我们是这样对插件进行初始化的。
- minins允许我们在多个组件存在相同逻辑的情况下,将那部分相同的逻辑剥离出来,比如上拉下拉加载数据。这也是我们最常使用的扩展组件的方式了
最后
聊完这些后,面试官也暂时想不到啥问题了,感觉此时被我输出的干货正在处于头脑风暴ing,最后就是问我有没有想对面试官问的,我就简单问了下公司的技术栈,以及有没有考虑过投入或者梭哈AIGC等等我好奇的问题,就此第一家面试到此结束。
【Base:杭州 薪资:150/天】
总结
在这次面试中,虽然是小厂,当时当他全部问面经也正是考验我们基础的时候,可光有基础还不够,在后面的一些中厂中目前我也拿到了两三个offer,难度跟这个不是一个等级,接下来我会持续总结我的这些面经,有过喜也有过被问碎的悲,Never Give Up!