最近找工作,一边复习面试题之余,也会整理一些面试官提出的问题。想到自己做一个面试题库,一方面温故而知新,巩固自己的前端知识,另一方面也是方便日后找工作复习的时候可以回来看看。
1. 前端性能优化相关
这是一个很大的话题,知乎上有一篇回答写的非常完整,贴出这份回答的链接,这里只列出一些常见的点:
- 路由懒加载
- 去除掉已经废弃的或者冗余的css样式代码
- 图片懒加载(在屏幕以外的图片先不加载)
- 图片使用webP格式可以减少图片大小,加快渲染速度
- 优化http请求的数量和时机
2. 原型和原型链
每个函数都有prototype属性,这个就是原型。而因为prototype属性的值是一个对象,所以也被称作是原型对象。原型的作用是可以存放一些属性和方法,共享给实例对象使用。
非常常见的例子是:我们通过new Array创建一个数组,这个数组可以调用sort reverse等方法,就是因为Array这个函数的原型对象上挂载了这些方法。而每一个对象身上都有一个__proto__的属性,这个属性指向了构造函数的原型对象,所以通过new Array创建出来的实例中,他的__proto__属性里就有诸如sort reverse这些方法,就可以调用了。
而原型链,比如一个person实例,他的__proto__属性指向的是Person函数的原型对象,而因为任意一个对象都有__proto__属性,原型对象的__proto__属性是Object的原型对象,这样一层一层形成的链式结构称为原型链,可以参考下图:
3. eventloop事件循环
- 可以把js代码分为同步代码和异步代码,绝大多数代码(比如运算,打印等)都是同步代码,而像setTimeout/setInterval/Promise.then()则是异步代码;
- 同步代码给js引擎执行,异步代码交给宿主环境(浏览器或者node);
- 同步代码放入执行栈中,异步代码等待时机成熟,送入任务队列中排队;
- 执行栈执行完毕,会去任务队列看是否有异步任务,有就送到执行栈中。这种反复循环查看执行的过程就是事件循环。
4. 宏任务和微任务
- js中的异步任务可以分为宏任务和微任务,微任务包括Promise.then和catch,async/await,宏任务包括setTimeout,setInterval等;
- 在上面的事件循环中,异步代码的任务队列可以进一步细分为宏任务队列和微任务队列;
- 执行栈执行完同步代码后,先去微任务队列查看异步代码,有的话推入执行栈中执行,微任务全部执行完毕后,再去宏任务队列里查看。
5. 深拷贝
如果只是通过等号赋值的方式去拷贝一个对象,由于两个对象的内存地址是一样的,修改其中一个对象的属性值,会导致另一个变量的属性值也相应改变,这就是浅拷贝。 如果想实现深拷贝,可以通过json.stringfy()和json.parse()去实现。
6. 在浏览器输入url发生了什么
- DNS解析,把字母url(如baidu/bilibili)解析为ip地址,根据ip地址找到对应的服务器;
- 客户端发送syn数据包,表示请求连接;
- 服务端响应syn和ACK数据包表示同意建立连接;
- 客户端再发送ACK数据包表示成功连接;(这就是三次握手,建立连接通道)
- 浏览器向服务器发送http请求;
- 服务器向浏览器返回响应数据。
7. css中使用translate属性代替left/top来做位移有什么好处
top/left是布局类的样式,这个样式的变化会导致重排(reflow/relayout),所谓重排即指对这些节点以及受这些节点影响的其它节点,进行CSS计算->布局->重绘过程,这个过程的前2步是消耗大量资源的。
translate是一个绘制样式,这个样式的变化会导致重绘(repaint),即在屏幕上重新画一下,不会进行CSS计算和布局这2个性能大户,所以translate性能上要明显好于top/left。
8. 面向对象三大特征
- 封装
- 隐藏了细节,调用对象的方法时无需知道里面实现的细节;
- 继承
- 子类会继承父类的所有功能,在js中可以联系到原型和原型链;
- 多态
- 重写重载父类的方法和属性,比如猫类,狗类都继承了动物类,并对"叫"这个方法进行了重写;
9. 聊一聊Vue的生命周期
从实例创建到销毁的整个过程就是生命周期,而在这个期间有很多个关键节点,我们需要在这些节点做一些事情(比如发送请求获取数据,或是销毁定时器),就可以在Vue给我们提供的特定的生命周期钩子中去完成。 常用的生命周期函数有以下8个:
- beforeCreate 组件实例被创建之初
- created 组件实例被完全创建
- beforeMount 组件挂载之前
- mounted 组件挂载到实例上之后(通常在这里发送请求,把数据渲染到DOM上)
- beforeUpdate 组件数据发生变化, 更新之前(此时view层还未更新,在beforeUpdate修改数据不会再次触发更新方法)
- updated 数据更新之后(此时view层已经更新,若在updated修改数据,会再次触发更新方法)
- beforeDestroy 组件实例销毁之前(通常在这里销毁定时器或是取消事件订阅)
- destroyed 组件实例销毁之后
10. 工作中用到的设计模式
- 工厂模式,更加方便的创建实例,对于前端来说最常见的就是我们经常用的axios,有的时候我们发送的接口可能需要对应不同的后端服务url并且拦截器上的处理也不同,就需要create多个axios实例。
const http1 = axios.create()
const http2 = axios.create()
11. 计算属性computed和方法method的区别
计算属性有一层缓存,在页面上多次调用计算属性时,如果计算属性值不变,那么get方法只会调用一次。而methods不同,页面上调用几次methods,methods就会执行几次,不会使用缓存。因此计算属性的性能要更高一些。
12. 为什么vue2中的data要用函数去返回
在组件中,data必须是一个函数。这是因为JavaScript的对象是引用类型。如果使用对象而不是函数,所有实例将共享同一个数据对象,这可能导致数据混乱。通过使用函数,每个实例可以维护一份被返回对象的独立拷贝。 当一个函数返回的是一个对象时,多次调用函数得到的对象不是同一个。这样就可以实现多个组件各用各自的数据对象。
13. 聊聊uniapp中的一些路由跳转api(如navigateTo reLaunch redirectTo switchTab)的区别
- switchTab 用于跳转到pages.json中tabList里面的那些tabbar页面,这些页面会出现在移动端下方的tabbar导航中,如果需要跳转到这些页面,需要使用switchTab方法
- navigateTo 可以用于跳转到应用内的页面,也就是除了tabList下面的页面之外,其他的页面都可以使用,在下一个页面可以返回到当前页
- redirectTo 关闭当前页面并跳转到应用内的页面,由于当前页面已经关闭,因此在下个页面如果调用navigateBack,则会返回到当前页面的上一个页面(可以理解为当前页面在历史记录里被清空了)
- reLaunch 关闭所有页面并跳转到应用内的页面,由于所有页面都关闭,在下个页面调用navigateBack不会返回到其他页面,只会停留在原地
14. 聊一聊在uniapp中用到的插件(如一键登录,自动更新)
这个问题我个人的经历可以讲讲用过uview-ui这个插件,或者说UI库,用他去做navbar,tabbar,popup弹窗等。
15. 你在工作中封装过组件吗?
这个问题最好能涵盖vue相关的多个知识点,包括slot插槽,父子组件通信等。 我个人的工作经历来说,回答这个问题最好的是uniapp的顶部的navbar的封装经历,左侧默认是返回箭头,做成插槽,右侧默认空白,做成插槽,中间则是接收父组件传过来的标题,左侧和右侧的点击事件都要向外发送事件。另外背景色,高度,是否fixed这些都做成props由父组件传过来,围绕这些展开讲就可以了。
16. v-for中of和in的区别
for...in循环的是键,对于数组而言,循环的是索引,对于对象而言循环的是对象的每一个key-value的key for...of循环的是值,对于数组而言,循环的是数组中的元素,对于对象而言循环的是对象key-value的value
17. uniapp的条件编译
uniapp的优势就是一套代码多端编译,在代码中经常会出现需要在不同环境执行不同代码的情况,例如h5端不需要自动更新,但是在app端就需要判断版本号差异并自动更新,微信小程序只能微信支付,而APP端就可以同时支持微信支付和支付宝支付。这时候就需要用到uniapp提供的条件编译。
18. uniapp微信小程序一键登录
19. uniapp下单支付
调用后端接口获得订单号和小程序支付参数后,调用uni.requestPayment即可唤起支付
20. 箭头函数和普通函数的区别
首先可以从简单的这两个函数的特征说起,普通函数是function声明的,而箭头函数是字面量声明,参数如果只有一个参数可以不写括号,函数体内如果只有返回值,可以不写return。
他们最大的区别是this指向不同,普通函数的this指向的是调用这个函数的对象,例如setTimeout方法,这个方法是window对象调用的,我们在setTimeout里面的函数中如果用普通函数去打印this,会打印出window。而箭头函数的this指向的是,在哪里定义函数,this就指向谁,比如箭头函数是定义在一个对象的成员里,那么this就会指向这个对象。
21. js中apply和call这两个调用函数的区别
call是函数的方法,通过fun.call()可以调用函数,并且call可以改变函数this的指向,call方法的传参就是调用函数的this指向(call方法的第一个参数是this指向,后面的参数则会作为fun函数的参数)
apply和call基本相同,唯一的区别是,call给函数传参时,参数是一个个往后写的,比如fun.call(cat, 'a', 'b'),而apply给函数传参时,是用数组传参的,比如fun.apply(cat, ['a', 'b'])
最后说一下bind,bind和call也很像,唯一的区别是bind并不会直接调用函数,而是把新函数作为一个返回值返回。
fun.call(cat, 'a', 'b')
fun.apply(cat, ['a', 'b'])
const fun2 = fun.bind(cat, 'a', 'b')
22. rpx rem em这些单位是什么含义,有什么区别
rem和em的区别在于多一个r,这个r代表的是root根元素。 一般情况下根元素都代表的是html元素,另外也可以通过:root这个选择器去修改根元素的样式,默认则是浏览器的字体大小,谷歌浏览器默认1rem=16px,当然用户可以设置浏览器的字体大小,如果设置成14,那么1rem=14px。 rem是基于根元素的font-size乘以倍数。em基于元素本身(如果元素本身没定义font-size就找父元素)的font-size乘以倍数。
23. 你在工作中遇到的难题,是怎么解决的?
可以聊聊我遇到的输入框防抖问题,以及uniapp保存图片时用海报插件解决的问题。
24. react是不是mvvm模式,vue呢?
mvvm可以理解为是model-view-ViewModel,也就是数据,视图的双向绑定。 vue是mvvm模式,react不是,react是一种view = render(state)的单向数据流,当然我们在开发过程中可以通过状态管理等方法来做MVVM风格的开发。
25. react中函数式组件和类组件的区别
- 语法和写法上来说,类组件是使用类(class)的语法去定义的,它需要继承React.Component类,并且需要实现render方法来返回组件的jsx。函数式组件是使用函数的语法去定义的,他接收props对象作为参数,并且返回组件的jsx。
- 状态管理来说,类组件使用state属性来储存和管理组件数据状态,使用setState去修改状态,并重新渲染组件。函数式组件则是使用useState这样一个react hook去管理组件状态。
- 生命周期,类组件具有生命周期函数(如componentDidMount),可以在组件的不同阶段去执行操作。函数式组件则是使用useEffect这个hook函数去实现生命周期的效果。 总的来说,类组件和函数式组件可以实现相同的功能,但是从代码的简洁性,可读性,和未来趋势来说,函数式组件的应用更广一些。
26. react父子组件传值有哪些方式
父组件给子组件传值,在调用子组件的地方用attritube的写法传值即可,比如name={xxx},子组件就可以通过props对象接收到父组件传过来的值。函数式组件是通过props参数,类组件就是this.props对象。 子组件给父组件传值,这时就需要父组件把修改数据状态的函数传给子组件,子组件调用这个函数,就修改了父组件的值。 此外还可以通过context,redux状态管理库去传值,这种一般是用于兄弟组件。
27. 聊一聊常用的react hooks
- useState,函数式组件进行组件状态管理hook函数,const [x, setX] = useState(0),这是一个自变量hook函数,也就是说这个hook函数不依赖别的变量
const [X, setX] = useState(0)
- useMemo,用来缓存一个因变量,如代码所示,这个y是2倍的x+1,这个因变量依赖x变化。y会被缓存下来,如果x不变的话,组件重新渲染时,不会重新计算y的值,而是会拿上一次的缓存
const y = useMemo(() => 2 * X + 1, [X])
- useCallBack,用来缓存一个函数类型的因变量。useMemo和useCallBack这种显示的指明因变量的好处是,y和changeX会被缓存下来,只要依赖X不变,那么读取的就是缓存值,可以提升性能。但是写法比较麻烦,在遇到性能瓶颈之前可以不使用这两个hooks。
const changeX = useCallback(() => setX(X + 1), [X]);
- useEffect,执行有副作用的函数,实际开发时经常拿来作为生命周期去用。所谓副作用函数,就是说他不是个纯函数,会涉及到外部的UI变化,日志记录,状态修改等等。
- useContext,用来处理组件层级很多时的组件传值问题,在第一级组件使用createContext创建一个上下文对象,在后面的子组件或者孙组件里使用useContext可以直接消费第一级组件创建的context。类似vue里面的provide和inject
- useRef,这个经常用于,我们有一个值要去管理,可以储存和修改,但是又不希望修改他的时候要重新渲染页面。这时可以使用useRef,另外useRef还可以获取dom元素。
const count = useRef(0)
const changeCount = ( () => { count.current += 1 } )
// 这里修改count.current并不会让页面重新渲染,页面上的count.current并不会变化,直到页面重新渲染了,才会变为最新的值
<h1 onClick={changeCount} >{count.current}</h1>
28. vue2和vue3的区别
能说的点很多,这里说一些重点:
- 实现双向绑定响应式的方法不同。vue2是通过Object.defineProperty()实现,vue3是通过es6的proxy特性实现的,vue2的这种实现方法,给对象新增加属性或者给数组中的某个成员修改值是不会劫持到的,页面就不会更新,必须使用vue提供的Vue.$set方法才能处理这种情况。而vue3的proxy实现就不会有这样的问题.
- vue2的一个vue文件里面的template只能有一个根节点,vue3可以有多个根节点
- vue3没有beforeCreated和created这两个生命周期,被setup函数取代了
- vue2在props里定义接收的属性,通过this.$emit发送事件,而在vue3中用defineProps和defineEmits来定义组件中的属性和事件
- vue2开发使用options API,我们定义的数据,方法,计算属性等,要分门别类的在vue实例中的data methods这些选项下去定义,而在vue3的开发中则是使用组合式API,允许我们把相关联的代码放在一起,增强了代码的可维护性和可读性
- vue2和v-for的优先级比v-if高,不推荐v-for和v-if一起使用。而在vue3中,v-if优先级高于v-for,可以作为判断循环的一个条件,可以一起使用
- vue2中调用子组件的方法直接用this.$refs.name.test()即可调用name组件的方法,而在vue3中,必须在子组件里defineExpose暴露方法,外部才可以调用
- 说一下vue3相比vue2性能提升的原因
- watch这个api在vue2和vue3的区别