1. 能说说不同数据结果对应的一些场景吗?

1)数组

  • 优点:按照索引查询元素速度快,按照索引遍历数组方便
  • 缺点:数组的大小固定后就无法扩容了,数组只能存储一种类型的数据,添加、删除的操作慢,因为要移动其他的元素。
  • 应用场景:频繁查询,对存储空间要求不大,很少增加和删除的情况

2)栈

  • 应用场景:括号匹配问题;逆波兰表达式求值问题;实现递归功能方面的场景,例如斐波那契数列。

3)队列

  • 应用场景:因为队列先进先出的特点,在多线程阻塞队列管理中非常适用。

4)链表

  • 优点:链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快。
  • 缺点:因为含有大量的指针域,占用空间较大;查找元素需要遍历链表来查找,非常耗时。
  • 应用场景:数据量较小,需要频繁增加,删除操作的场景;快慢指针:求中间值问题、单向链表是否有环问题、有环链表入口问题;循环链表:约瑟夫问题。

5)树

  • 应用场景:JDK1.8中 HashMap的底层源码中用到了数组+链表+红黑树;磁盘文件中使用B树做为数据组织,B树大大提高了IO的操作效率;mysql数据库索引结构采用B+树

6)哈希表

  • 应用场景:哈希表的应用场景很多,当然也有很多问题要考虑,比如哈希冲突的问题,如果处理的不好会浪费大量的时间,导致应用崩溃。解决哈希冲突问题:可以对数组扩容;优化hash计算方式。

7)堆

  • 应用场景:因为堆有序的特点,一般用来做数组中的排序,称为堆排序。

8)图

  • 应用场景:道路畅通工程,最短路径

2. 作用域的理解

作用域:指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。

javascript 中大部分情况下,只有两种作用域类型:

  • 全局作用域:全局作用域为程序的最外层作用域,一直存在。
  • 函数作用域:函数作用域只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。

由于作用域的限制,每段独立的执行代码块只能访问自己作用域和外层作用域中的变量,无法访问到内层作用域的变量。

/* 全局作用域开始 */
var a = 1;

function func () { /* func 函数作用域开始 */
  var a = 2;
  console.log(a);
}                  /* func 函数作用域结束 */

func(); // => 2

console.log(a); // => 1

/* 全局作用域结束 */
复制代码

3. 闭包的理解

闭包:能够访问其他函数内部变量的函数,被称为 闭包。即闭包就是函数内部定义的函数,被返回了出去并在外部调用。

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // 这就形成了一个闭包
复制代码

闭包可以绕过作用域的监管机制,从外部也能获取到内部作用域的信息。

闭包的应用场景:闭包的应用,大多数是在需要维护内部变量的场景下。

滥用闭包容易导致内存泄漏。这种由于闭包使用过度而导致的内存占用无法释放的情况,我们称之为:内存泄露

内存泄露 是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。

内存泄露的排查手段:

  • Performance
  • Memory

如何避免内存泄漏:

  • 使用严格模式,避免不经意间的全局变量泄露
  • 关注 DOM 生命周期,在销毁阶段记得解绑相关事件
  • 避免过度使用闭包

4. 说说TCP和UDP的区别

TCP是面向连接的,需要经历三次握手建立连接,四次握手断开连接,因此具有可靠性,且不会对报文进行操作,只是报文的搬运工。TCP仅支持单播传输,面向字节流,可以提供拥塞控制。

UDP是面向无连接的,不需要进行三次握手,不具有可靠性,具有单播、多播以及广播的功能。且是面向报文的,头部开销小,传输数据报文时很高效。

简而言之:

  • TCP向上层提供面向连接的可靠服务 ,UDP向上层提供无连接不可靠服务。
  • 虽然 UDP 并没有 TCP 传输来的准确,但是也能在很多实时性要求高的地方有所作为
  • 对数据准确性要求高,速度可以相对较慢的,可以选用TCP

5. 为什么HTTP3.0要使用UDP协议

HTTP/3选择了UDP,主要是为了解决对头阻塞问题。它的底层协议,就是大名鼎鼎的QUIC,一个运行在传输层(也可以说是应用层)的协议。与TCP不同的是,QUIC代码并没有硬编码在操作系统内核中,而是完全运行在用户空间的,默认集成了TLS。

  • QUIC能够实现TCP协议的所有功能性需求,并集成了TLS,功能上赶超了TCP
  • 一条连接,多个stream并发传输,真正的多路复用,有效解决队头阻塞现象,性能上超越了TCP
  • 减少握手次数,尤其是带TSL传输的握手次数,有更低的握手延迟
  • 由于易于升级,为协议的更新和发展,提供了巨大的想象空间

6. 怎么实现一个v-model

v-model 实际上就是 $emit('input') 以及 props:value 的组合语法糖,只要组件中满足这两个条件,就可以在组件中使用 v-model。

父组件

new Vue({
 components: {
   CompOne,
 },
 data () {
   return {
     value: 'vue'
   }
 },
 template: `
   <div>
     <comp-one v-model="value"></comp-one> // 使用v-model 满足语法糖规则:属性必须为value,方法名必须为:input
   </div> 
 `,
}).$mount(root)

复制代码

子组件:

// 修改子组件 
const CompOne = {
  props: ['value1'],
  model: {
      prop: "value1", // 接收的数据 value => value1
      event: "change" // $emit 需要绑定的事件 input => change
  },
  template: `
    <div>
      <input type="text" @input="handleInput" :value="value1"></input>
    </div>
  `,
  methods: {
    handleInput (e) {
      this.$emit('change', e.target.value)
    }
  }
}

复制代码

7. 说说nextTick原理

nextTick中的回调,是在下次DOM更新循环之后执行的回调。在修改数据之后立即使用这个方法,获取更新后的DOM。

实现的主要思路就是:采用微任务优先的方式调用异步方法去执行nextTick中的方法。其实就是事件循环机制的。

8. 事件循环机制

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

macro-task大概包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

micro-task大概包括:

  • process.nextTick
  • Promise
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)

事件循环机制

主线程从”任务队列“读取异步任务执行,这个过程是循环不断的,所以我们又称这个过程是事件循环或者Event Loop。

JavaScript 的代码执行时,主线程会从上到下一步步的执行代码,同步任务会被依次加入执行栈中先执行,异步任务会在拿到结果的时候将注册的回调函数放入任务队列,当执行栈中的没有任务在执行的时候,引擎会从任务队列中读取任务压入执行栈(Call Stack)中处理执行。

完整的事件循环机制:

  • 首先,整体的script(作为第一个宏任务)开始执行的时候,会把所有代码分为同步任务、异步任务两部分。

  • 同步任务会直接进入主线程依次执行,异步任务会再分为宏任务和微任务。

  • 宏任务进入到Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中。微任务也会进入到另一个Event Table中,并在里面注册回调函数,每当指定的事件完成时,Event Table会将这个函数移到Event Queue中。

  • 当主线程内的任务执行完毕,主线程为空时,会检查微任务的Event Queue,如果有任务,就全部执行,如果没有就执行下一个宏任务

  • 上述过程会不断重复,这就是Event Loop,比较完整的事件循环

9. 先进行nextTick后修改值,nextTick取到的值是修改前的还是修改后的,为什么?

nextTick取的是修改前的值,因为每次更新数据都得在下次更新DOM才能重新渲染数据。

nextTick中的回调,是在下次DOM更新循环之后执行的回调。在修改数据之后立即使用这个方法,获取更新后的DOM。

10. 介绍一下为什么要使用mutationObserver?

Mutation observer 是用于代替 Mutation events 作为观察DOM树结构发生变化时,做出相应处理的API。

11. 实现Promise.all

Promise.all的特点在于:

  • 接收一个Promise实例的数组或具有Iterator接口的对象
  • 如果元素不是Promise对象,则使用Promise.resolve转成Promise对象
  • 如果全部成功,状态则变成resolved,返回值将组成一个数组传给回调
  • 只要有一个失败,状态就变成rejected,返回值将直接传递给回调
  • Promise.all()的返回值也是一个新的Promise对象