高频前端面试题1

260 阅读7分钟

Vue的响应式原理

数据响应式:数据变化,会自动运行依赖该数据的函数

Vue中数据变化未调用api,属于非侵入式

因为尤大找到了”上帝的钥匙“:Object.defineProperty() 数据劫持/数据代理,利用JavaScript引擎赋予的功能,检测对象属性的变化

image.png 图解:通过Object.defineProperty()遍历对象的每一个属性,把每一个属性变成getter和setter函数,读取属性时运行getter,给属性赋值时运行setter,从而变成一个响应式数据。响应式数据如何跟界面挂钩呢?通过render函数把模板里面的所有数据生成虚拟DOM树。组建的render函数会生成虚拟DOM树从而影响到界面。如何把render函数同响应式数据连接起来?当运行render函数时,render函数内部会用到一些数据,比方说用h函数来生成h1元素,h1元素的内容可能会用到一些数据,比方说是this.msg,this.msg就来自于响应式数据,使用this.msg会运行getter,getter会记录当前有一个函数叫做render用到了msg这个数据,这个过程叫做依赖收集(collect as dependency)。收集的内容包括Watcher。Watcher会监听setter,当setter被调用,就会通知Watcher数据变化。(Watcher之前是通过运行render函数来收集的依赖),Watcher收到通知后就会重新触发render函数的执行,render函数重新执行即生成新的虚拟DOM树。

实现响应式要做的事:

  1. 数据劫持:劫持变化的数据
  2. 依赖收集:视图层的内容(DOM)依赖了哪些数据(state)
  3. 派发更新:数据变化后,如何通知依赖这些数据的DOM

闭包

认识闭包的前提:认识作用域和作用域链

作用域(JS静态作用域):程序源代码定义的范围

作用域链:

what:能够访问另一个函数作用域中变量的一个函数【定义在一个函数内部的函数】

Why:使用闭包的目的:为了实现JS中的封装(设置一些私有的方法和变量)

闭包的优点:避免全局变量的一个污染

闭包的缺点:闭包会常驻在内存中,增大内存的使用量,使用不当会造成内存泄漏

什么是内存泄漏:该回收的垃圾对象没有被回收,发生了内存泄漏,垃圾对象越堆越多,可用内存越来越少,若可用内存无法存放新的垃圾对象,就导致内存溢出。(拓展:内存溢出:当前创建的对象的大小大于可用的内存容量大小,发生内存溢出)

如何避免内存泄漏:使用完函数或变量后就把变量销毁

闭包形成的条件或特征:1.函数嵌套函数;2.函数内部可以引用外部函数的这个参数跟变量;3.参数和变量不会被JS的垃圾回收机制回收。

Where:闭包的使用场景:防抖和节流;延长变量的生命周期

防抖和节流

防抖:操作时不执行,确定不操作了才执行

--手机一直玩不息屏,隔一分钟不玩了就息屏

每次操作都会先清除定时器并开启定时器,如果连续操作就会不停地清除掉定时器使定时器没办法运行完成,只有不操作了定时器才会运行完成并执行动作

function debounce(fun,delay){    // 接收一个回调函数
    let timer
    return function(){           // 并返回一个回调函数
        if(timer) clearTimeout(timer)   // 每次执行前前清除定时器
        let args = arguments   
        timer = setTimeout(() => {  // 定义定时器并开启定时器
            fun.apply(this,args)        // 传入对应的参数并改变this指向
        },delay)
    }
}

节流:到时间了必须执行一次

--公交车不是来一个客户发一趟车,而是10分钟发一趟

当我们持续触发事件,需要记录初始执行的时间,每次执行时要记录当前时间,用当前时间减去初始时间,如果小于设定值就不执行,只有当前时间减去初始时间大于设定值的时候才会执行,并将现在的时间赋值给初始时间。

function throttle(fun){    // 接收回调函数
    let start = 0          // 初始时间
    return function(){     // 返回回调函数
        let now = new Date()    // 每次执行时获取当前时间
        if(now - start > time){    // 当前时间减去初始时间,大于设定值就执行对应的方法
            fun.apply(this,arguments)    // 改变this指向并传入对应的参数
            start = now      // 并用现在的时间赋值给初始时间,下次执行时,初始时间就变成了更新后的值
        }
    }
}

JS垃圾回收机制

目标:清除不再使用的对象,腾退出所占内存

当定义一个变量并赋值,然后再重新赋值一个新的对象后,原来的对象就不再使用,就可以把它清除掉

策略:1.引用计数法;2.标记清除法

引用计数法

当一个对象被引用一次,引用数就+1,被取消引用一次,引用数就-1;当引用数为0时,就可以触发垃圾回收机制回收。

image.png

问题(缺陷):循环引用时,引用次数永远不可能为0,无法回收,内存无法释放

标记清除法

首先将所有的对象标记为0,从根对象开始遍历,将存活的对象标记为1,然后将标记为0地对象清除掉;最后将所有标记为1的对象重新标记为0,方便下一次垃圾回收机制回收

image.png

问题:清除后,位置是不连续的,如果进来一个新的对象,找位置是个新的问题,如何为新的对象找合适的位置?

image.png

找位置策略:

1.First-fit (找到能放置下新对象的第一块位置)【性能最好,主流】

2.Best-fit(找到能放置下新对象的对小块位置)

3.Worst-fit(找到最大的块,切一块空间给新对象)【产生更多不连续的空间】

V8对垃圾回收机制的优化

优化标记算法

有的对象需要频繁地回收,而有的对象不需要;一般小的,新的存活时间短的对象需要频繁回收,而大的老的,存活时间长的不需要频繁回收,所以在堆内存中分出两个区域来:1.新生代区域;2.老生代区域

image.png

新生代区域:FROM空间(使用空间)和TO空间(闲置空间)。当FROM空间中的对象要满时,我们就开始标记,将存活的对象标记好,标记好后将它们复制到闲置空间中,最后把FROM空间清空,然后交换FROM和TO的名称

老生代区域:采用标记清除算法和标记压缩算法,首先将所有对象标记为0,然后将存活的对象标记为1,将标记为0对象全部清空,然后将标记为1的对象全部标记为0,最后再用标记压缩算法,使他们的位置整理好

image.png

因为JS是单线程语言,所以在执行垃圾回收机制时,JS执行会被暂停,称之为全停顿;V8机制对此情况做出优化,垃圾回收机制支持多线程并行回收,如此一来,垃圾回收机制的时间加快了,时间变短,最大化保证JS执行,但是这样依然会阻塞JS执行,所以诞生了增量标记,增量标记最大的特点是垃圾回收可以分段执行,也就是说执行一段JS就执行一段垃圾回收机制,交叉执行,最大化地保证了JS执行。新问题:如何记录上一次标记的位置?三色标记法:就是将上次标记到的位置标记为灰色,当下次执行时,从灰色开始重新标记。同时V8机制还支持了并发回收,也就是说,JS在主线程执行,垃圾回收在辅助线程执行。