> 文章列表 > 高级前端面试题(react + 原生js + es6)

高级前端面试题(react + 原生js + es6)

高级前端面试题(react + 原生js + es6)

React hooks相关:
       hook本质是将直接修改组件的state,改成了一个消息队列,通过useState中的set方法将修改事件推到队列中,react会进行合并和修改,然后自动触发setState。       
        useEffect在使用时候需要注意不能在外部使用判断,因为如果有判断的话,会导致react无法找到它而引发报错。第一个参数是执行函数,当不传递第二个参数的时候,每次render后都会执行,传递空数组的话,生命周期和componentDidMount一致,只会在组件挂载后执行一次,传递相关变量,那么会在第一次挂载和每次变量变化时执行。
        useMemo(),缓存计算函数的结果,只有当第二个参数关心的变量发生变化时候才会再次执行计算,因为每次render都会让整个函数重新运行,如果不进行useMemo()缓存结果的话,那么每次render都会进行运算,会造成性能上的浪费。
        useCallback(),缓存回调函数,和useMemo的区别就是一个是缓存计算结果,一个是缓存函数,同样如果不进行缓存的话,那么每次都会重新生成函数。
        useContext主要是解决组件间多层传递props的问题,与class方式中的Context是一样的功能,只是在获取context值上的方式有一些不同,class方式需要声明静态属性contextType = MyContext,这样在组件中就可以通过this.context访问。在函数中使用useContext(MyContext)进行获取。
        useReduce(),类似redux中的reduce,有两个参数,第一个是一个描述reduce怎么更新state的函数,第二个是初始的state,返回值数组中第一项是默认state的值,第二项是dispatch函数。与redux一样,useReduce只接受dispatch触发,并传递一个actions来描述应当如何更新。调用dispatch方法后会进入到reduce函数中,对actions进行解析处理后返回新的state来进行更新。

React class和生命周期相关:
 

React新特性:
1. createRoot和ReactDOM.render的区别
        ReactDOM.render是会遍历整个虚拟dom树后进行更新,过程不可中断,这样是能保证所有节点都会执行到,但是也由于可能部分节点更新缓慢,导致用户看起来感受到卡顿。
        createRoot是基于fiber模式,通过createRoot会创建出来fiber的根节点,fiber会将每个组件作为一个节点记录到队列中,并且会记录每个组件的父节点、子节点、兄弟节点,以此可以实现不需要遍历整颗dom树,就可以进行局部更新,从局部渲染上会比ReactDOM的方式更快,但是整体效率上不好说,因为可能会碰到有高频更新的局部节点占用,导致其他的节点始终在排队。
        在react18版本中,推出了Concurrent特性,Concurrent是基于fiber实现的一种新的渲染模式,它将局部更新分为多个等级的任务,并且可以对正在执行的任务进行中断并去执行更高等级的任务,以此保证用户可以获得更好的体验。

2. lazy()和Suspense
        lazy是基于import动态加载实现,对于页面上有多个组件,但是有一些是需要用户操作后才会载入的模块,可以使用lazy进行懒加载。
        在react中,对于组件的加载是有状态的,所以可以使用上Suspense来包裹通过lazy加载的模块,fallback属性作为未加载模块时的渲染。需要注意的一个问题是,如果使用了Suspense包裹lazy的话,那么对于子组件嵌套lazy模块的时候,任何子组件lazy的时候都会导致外层的Suspense重新触发fallback,所以为了避免这种情况,最好是在每个使用lazy加载的模块外,都套用上一层Suspense。

原生相关:
1. 事件循环机制
        因为js是运行在浏览器环境中的,浏览器只给js一个线程,也是我们常说的主线程,所以当我们执行setTimeout和ajax的时候,就需要借助浏览器的定时进程和网络进程。
        当浏览器的其他进程处理好后,在将结果告诉js线程,因为js只有单线程,所以不能立马执行,由此产生了一个事件队列的机制,其他进程的结果都会放到事件队列中,等待js主线程执行完毕后,在从事件队列中取出相应的事件开始执行,不断循环这个过程就是事件循环。
        需要注意的是由于主线程的任务什么时候能执行完是不确定的,所以对于setTimeout事件的定时一定是会有偏差的。
        另外在事件队列中对事件还分为两种等级,setTImeout和ajax是宏任务,promise.then是微任务,微任务总是要快于宏任务执行。不过微任务严格意义上并不算是异步,因为只是修改了调用顺序,而没有借助其他的线程执行。

2. 闭包
        简单一句话就是在函数体内部能够访问函数体外部的变量。常见的写法大部分都是一个函数内,返回另外一个函数,在返回的这个函数中又引用了外部函数所定义的变量。比较常用的一些场景是模块封装,避免变量污染。在多个请求中保存一些中间结果,方便后续合并数据等等。
        可能会导致的问题就是内存泄漏,因为js的回收机制对于还存在引用的变量是无法回收的,所以为了避免内存泄漏,我们在用完引用变量的时候,需要对这个变量置为null,这样下次垃圾回收的时候,就会被销毁掉了。

3. 垃圾回收
        一种是计数方式,变量每多一次引用计数就+1,减少一次引用就-1,当为0的时候就进行销毁,可能会存在的问题是两个对象互相引用,那计数就永远不会为0,所以要避免这种互相循环引用的实现。
        另外一种是标记状态,对象被调用的时候标记为活跃状态,调用结束后标记为结束状态,对所有结束状态的变量进行删除。同时没有被引用的变量因为没有状态,所以也会被回收。

4. 防抖和节流
        防抖简单说是在一定时间内,触发了执行事件,如果在这个时间内,又再次或者多次触发事件,那么时间会继续顺延,直到最后一次触发满足了设定的时间才会执行。
        节流是在一定时间内,无论触发多少次事件,最终都只会在到期时间后,执行一次。防抖和节流都是比较常见的闭包实现例子。

5. 作用域和作用域链
        分为全局作用域和局部作用域,也就是说在window上定义的变量和函数,在全局任何地方是都能够访问的,但是如果在函数内部定义的变量和方法,在函数外部是无法访问的。
        当我们在函数内部访问一个函数体内不存在的变量时,会去当前函数所在的环境中查找这个变量,一直查找到window,这个过程就是作用域链。

6. 原型和原型链
        涉及到几个方面,首先是构造函数,我们可以通过function和class关键字来创建构造函数,每个构造函数都有自己的原型,可以通过指定prototype和extends的方式来实现继承。
        其次是实例化对象,通过new关键字创建,每个实例化对象都会有原型上的属性和方法,并且可以定义自己的属性和方法。当我们通过调用或访问实例化对象的方法和属性时,如果实例化对象自身没有这个方法或者属性,那么就会沿着这个实例化对象的原型上去查找,一直找到Object。这个向上查找得过程就是原型链。(ObjectFunction为构造函数)
        常用的几个方法是Object.getPrototypeof(obj),返回这个实例化对象的原型。
        ObjectFunction.isPrototypeof(obj),判断这个实例化对象是否含有这个构造函数的原型。
        obj instanceof ObjectFunction,判断实例化对象的原型是否等于构造函数的原型。等价于Object.getPrototypeof(obj) === ObjectFunction.prototype;

7. Set和Map
        Set是一个不会重复的集合,可以通过Array.from和结构[...new Set()]的方式转换成数组,在设置值和取值上需要通过add和values方法,判断是否存在需要用has,如果只是要一个不会变的集合,那么数组要简单。
        Map和Object最主要的区别是遍历顺序是固定的,并且可以使用变量作为key,增加方法是set,读取是get。

ES6新增特性:
1.  箭头函数
        箭头函数没有args,但是有绑定this,编写代码的时候,如果需要用到this,那么就可以考虑使用箭头函数了。

2. 字符串模板
        使用``来拼接字符串,变量可以直接使用${},比传统的字符串 + 字符串看起来要更直观书写上更加方便。

3. 解构和扩展运算符
        解构会让取值的过程更加的方便,扩展运算符算是浅拷贝的一种方式,对于一些需要重新生成变量并且含有自身属性的赋值过程会更简单。

4. class
        可以告别function的方式来写构造函数,也可以写自己的静态方法,在方法前增加static标记,静态方法实例化是无法继承的,只能通过构造类的名字调用。私有属性目前还未正式发布,在预案上增加#关键字,目前大家约定俗称的规矩是在变量前增加_来区分,虽然其实还是能在实例化的对象中访问到的。

5. Promies
        可以告别以前的回调地狱,书写起来更加直观,接受一个函数包含resolve和reject参数,主要用来做异步的操作,对于一个请求依赖更外一个请求的结果可以通过.then的方式编写,多个请求并发执行可以通过promise.all实现。

6. async和await
        想使用await必须要在外部函数中声明async关键字才可以,在需要同步等待结果的方法前添加await关键字,这样会让执行顺序看起来像是同步执行,本质上是改变了代码的调用顺序。

7. 数组相关的一些方法
        find,findIndex,参数都是接收一个函数,给定条件,满足条件既返回对应的一项或者角标。
        some和every,参数是一个函数,给定条件,返回值是布尔类型,some只需要有一项满足就为true,every需要全部满足。
        map会重新生成新的数组,参数函数中必须要有return。
        includes判断是否包含某一项,比indexOf要更方便一些。
        filter对原数组进行过滤,并生成新的数组。
        reduce对所有子项进行累加器的函数,第二个参数是初始值,并返回最后累加后的结果。

8.深拷贝的方式
        JSON.parse(JSON.stringify(obj))可能存在的问题是如果有值是undefined会报错,对于函数引用也有问题。
        递归来实现。
        

function deepClone(obj) {if(obj === null || typeof obj !== 'object') {return obj;}let clone = Array.isArray(obj) ? [] : {};for(let key of obj) {clone[key] = deepClone(obj[key])}return clone
}

css3相关:
        flex布局、@keyframes动画、transform、transition、border、多个背景、渐变色。

性能优化相关:
        webpack压缩代码,删除未使用的模块和代码,loader相关插件。
        对于一些第三方库可以使用cdn缓存引用。
        对图片素材进行压缩。
        css提取公共样式表。
        组件载入对于非必须的组件使用import动态载入。