2022三四月前端面试总结

2,835 阅读14分钟

说啥也不能浪费金三银四啊

最近出去面试了几次,想着出去试试这一年的学习成果,结果。。。

html、css相关

  1. 盒模型
    • 标准盒模型:盒子总宽度/高度 = width/height + padding + border + margin。( 即 width/height 只是内容高度,不包含 padding 和 border 值 )
    • IE盒子模型:盒子总宽度/高度 = width/height + margin = (内容区宽度/高度 + padding + border) + margin。( 即 width/height 包含了 padding 和 border 值 )
  2. 元素居中的方式
/* 第一种居中方式 */
.box1 {
  position:relative;
}
.box1 .dv {
  position:absolute;
  left:50%;
  top:50%;
  margin-top: -75px;
  margin-left: -75px;
}

/* 第二种居中方式 */
.box1 {
  position:relative;
}
.box1 .dv {
  position:absolute;
  left:0
  right:0
  top:0
  bottom:0
  margin:auto;
}

/* 第三种居中方式 */
.box1 {
  display:flex;
  justify-content:center;
  align-items:center;
}

/* 第四种居中的方式 */
.box {
  width:600px;
  position:relative;
}
.box .dv {
  position:absolute;
  left:50%;
  top:50%;
  tarnsform:tarnslate(-50%,-50%)
}

/* 第五种居中方式 */
.box1 {
  display:grid;
  place-items:center垂直 center水平
}

  1. 两列布局,左边固定右边自适应有几种实现方式
    • flexbox
    • calc函数

js相关

  1. 什么是闭包,闭包的实际应用,闭包的缺点

    • 什么是闭包
      1. js高程一句话概括,一个使用了另一个函数内部变量的函数
      2. 由于 javascript 的特性,外层的函数无法访问内部函数的变量;而内部函数可以访问外部函数的变量(即作用域链)
      3. 函数嵌套,内部函数使用外部函数的变量就会形成闭包的条件
    • 闭包的实际应用
      1. 作用域隔离,避免污染全局作用域
      2. 模块化,AMD CMD Conmonjs
    • 闭包的缺点
      1. 大量的闭包会造成内存泄漏,如果不使用应及时释放,让内部函数变成垃圾对象,赋值为null,让浏览器回收闭包
  2. 变量提升,function声明和var提升的优先级

    • js在执行的时候,会进行预解析,当前作用域中使用var和function声明的变量和函数提升到作用域的顶端,var声明的变量只提升变量声明,函数整个提升并分配内存空间,所有js在函数声明之前可以使用函数
    • 函数的优先级要高于var声明的,因为js在预解析的时候如果解析到有值的变量并不会做处理
  3. 原型和原型链

    • 每个对象都有一个隐式原型(proto)它指向创建该对象构造函数的显示原型(prototype)
    • js对象在使用一个属性的时候,会在自己身上找,有就使用,没有会到__proto__上查找,有就使用,因为__proto__也是一个对象,它也有它的隐式原型(proto),这个查找属性的过程就是原型链
    • 原型链的顶端是Object的__proto__它指向null,如果查到这都没有找到的话,就返回undefind
  4. 说一说defer 和 async

    • 首先在正常情况下,script 标签是会阻塞 DOM 的解析的,所以我们要尽量不要把script 放到 head 里,要尽量放到 body 的最下方。这样不至于会有白屏的效果;然后是 defer和 async;他们两个是异步加载 js 代码的。
    • defer
      1. 给 script 标签添加 defer 属性,就不会阻塞 dom 的解析了,等 dom 渲染完之后才会运行该 js 代码,如果是多个 script 标签都添加了 defer 属性的话,那么它们是按照顺序执行的(第一个全部执行完毕之后才能执行第二个),defer 的 script 是在 DOMContentLoaded 之前运行的。
    • async
      1. 给 script 添加 async 属性之后,会异步下载 js 代码,等下载完成之后会立即运行js 代码。多个 script 标签同时设置了 async 是没有先后顺序的,谁先加载完谁就先运行。
    • 如果 script 标签没有操作任何 dom 信息,且不彼此依赖的话,可以使用 async
  5. 如何把异步变为同步

    • Promise+async/await
    • generator生成器 ,yield表达式
  6. Promise.all的实现

    • 返回一个promise,循环数组中的每个promise,调用then方法获取结果
    • 创建一个空数组,使用循环索引的方式为每个promise的结果赋值,可以定义一个变量记录then的次数,如果次数等于传入数组的长度,那么resolve出这个数组
  7. 为什么promise的then方法要返回一个新的promise而不是返回this

    • 因为promise的状态一旦确定就不可改变
  8. 浅拷贝和深拷贝的区别

    • 如果拷贝的对象里的元素只有值,没有引用,那浅拷贝和深拷贝没有差别,都会将原有对象复制一份,产生一个新对象,对新对象里的值进行修改不会影响原有对象,新对象和原对象完全分离开
    • 如果拷贝的对象里的元素包含引用(像一个对象里储存着另一个对象,存的就是另一个对象的引用),那浅拷贝和深拷贝是不同的,浅拷贝虽然将原有对象复制一份,但是依然保存的是引用,所以对新对象里的引用里的值进行修改,依然会改变原对象里的值,新对象和原对象完全分离开并没有完全分离开。而深拷贝则不同,它会将原对象里的引用也新创建一个,即新建一个对象,然后放的是新对象的属性,这样就可以将新对象和原对象完全分离开
  9. 手写深拷贝

function deepClone(target, map=new Map()) {
  // 被处理的目标是数组和对象
  if(target instanceof Array || (target!==null && typeof target === 'object')) {
    // map中存着对应的克隆对象,直接将其返回
    let cloneTarget = map.get(target)
    if(cloneTarget) {
      return cloneTarget // 不要对同一个对象进行多次克隆
    }
    // 创建克隆对象
    cloneTarget = target instanceof Array ? []:{}
    // 保存到map中
    map.set(target,cloneTarget)
    // 循环遍历被克隆对象
    for(const key in target) {
      if(target.hasOwnProperty(key)) {
        // 进行递归拷贝
        cloneTarget[key] = deepClone(target[key],map)
      }
    }
    // 返回拷贝对象
    return cloneTarget
  } else {
    // 如果不是对象或数组就直接返回被拷贝对象
    return target
  }
}

浏览器相关

  1. 输入url地址后都做了什么

    • 浏览器先查看浏览器缓存-系统缓存-路由器缓存,如果缓存中有,会直接在屏幕中显示页面内容。若没有,则跳到下步操作。
    • 在发送http请求前,需要域名解析(DNS解析),解析获取相应的IP地址。
    • 浏览器向服务器发起tcp连接,与浏览器建立tcp三次握手。
    • 握手成功后,浏览器向服务器发送http请求,请求数据包。
    • 服务器处理收到的请求,将数据返回至浏览器
    • 浏览器收到HTTP响应
    • 读取页面内容,浏览器渲染,解析html元素生成dom树,遇到link或style启动css解析器解析样式生成cssom树,遇到script启动js引擎执行js代码,最后根据dom树和cssom树生成渲染树,浏览器根据渲染树绘制页面
  2. 浏览器缓存

    • 缓存分为强缓存和协商缓存
    • 强缓存
      • 在客户端发起请求之前,先检查强缓存,查看强缓存的cache-control里的max-age,判断数据有没有过期,如果没有直接使用该缓存 ,有些用户可能会在没有过期的时候就点了刷新按钮,这个时候浏览器就回去请求服务端,要想避免这样做,可以在cache-control里面加一个immutable.这样用户再怎么刷新,只要 max-age 没有过期就不会去请求资源。
      • public 因为服务端返回数据的过程中可能会经过很多虚拟代理服务器,他们可以进行缓存,使用 public 就是允许它们缓存
      • private 不允许代理服务器缓存,允许客户端缓存
      • no-cache 不允许强缓存,可以协商缓存
      • no-store 不允许缓存
  • 协商缓存
    • 浏览器加载资源时,没有命中强缓存,这时候就去请求服务器,去请求服务器的时候,会带着两个参数,一个是If-None-Match,也就是响应头中的etag属性,每个文件对应一个etag;另一个参数是If-Modified-Since,也就是响应头中的Last-Modified属性,带着这两个参数去检验缓存是否真的过期,如果没有过期,则服务器会给浏览器返回一个304状态码,表示缓存没有过期,可以使用旧缓存。
    • etag的作用
      • 有时候编辑了文件,但是没有修改,但是last-modified属性的时间就会改变,导致服务器会重新发送资源,但是etag的出现就完美的避免了这个问题,他是文件的唯一标识
    • last-modified和etag各有各自的优点和缺点:
      • 每个文件都有一个 etag 和 last-modified 属性,etag 要优先于 last-modified,两个属性各有优缺点,比如 last-modified 很多时候不能感知文件是否改变,但 etag 能;last-modified 仅仅记录了时间点,性能肯定要高于etag,etag 记录的是哈希值
  • 缓存位置:
    • 内存缓存Memory-Cache
    • 离线缓存Service-Worker
    • 磁盘缓存Disk-Cache
    • 推送缓存Push-Cache

vue2/3相关

  1. vue双向绑定

    • vue在创建的时候会遍历data中所有的属性,通过Object.defineProperty()来劫持各个属性的setter,getter,在getter中收集依赖也就是一个个watcher(哪些地方使用该属性),在setter中(数据修改时)触发依赖并执行监听的回调。如果属性是对象类型,就递归去为该属性注册
  2. vue组件通信的方式

    • props+事件
    • provide/inject
    • eventbus
    • mitt
    • vuex
  3. Vue中的nextTick

    • 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
    • 想要在Vue生命周期函数中的created()操作DOM可以使用Vue.nextTick()回调函数
    • 在数据改变后要执行的操作,而这个操作需要等数据改变后而改变DOM结构的时候才进行操作,需要用到nextTick
  4. 可以说说vue中的diff算法吗

    • diff算法是指对新旧虚拟节点进行对比,并返回一个patch对象,用来存储两个节点不同的地方,最后利用patch记录的消息局部更新DOM
    • 步骤
      1. diff算法是虚拟节点的比较
      2. 先进行key值的比较
      3. 先进行同级比较
      4. 然后再比较是不是一方有儿子,一方没儿子,如果是这样,直接在旧节点中插入或删除儿子即可
      5. 在比较两方都有儿子的情况,有四种优化的比较(新前与旧前、新后与旧后、旧后与新前、新后与旧前)
      6. 递归比较子节点
  5. vue2于vue3有什么不同,vue3有什么优势

    • vue3相比vue2更灵活了,因为vue3使用ts编写的对ts的支持更友好了
    • 新增了composition API,可以抽离独立的业务逻辑,易于封装
    • vue3使用proxy重写了响应式系统
    • 模板不用必须用根节点了
    • vue3内部使用了tree-shaking,在打包项目的时候只会把我们使用的功能打包进去,而不是将整个vue源码打包

node、webpack相关

  • node事件循环
    1. 我大致说了nodejs实现循环的六个阶段,具体的请参考blog.csdn.net/local_peopl…
  • 讲讲koa洋葱圈模型,手写compose函数
    1. 中间件自顶向下执行,每次调用next方法,就会暂停当前中间件的执行,去执行下一个中间件,相当于next有调用下一个中间件的权利,执行到最后一个中间件的时候,又会回到上一个中间件并执行next后面的代码,最终回溯到最顶层中间件 参考:juejin.cn/post/684490…
  • webpack的Loader和Plugin的区别
    • Loader直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到loader。 所以Loader的作用是让webpack拥有了加载和解析非JavaScript文件的能力。
    • Plugin直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

算法相关

  • 讲讲冒泡排序的原理
    1. 依次比较相邻的两个数,如果前面的数大于后面的数就交换位置,这样第一次循环后最后一个数就是最大的,在下一轮循环中就无需比较,每次找出未排序列中最大的数,第二次找到第二大的数,以此类推...
  • 手写归并排序
    • 归并排序算法有两个基本的操作,一个是分,也就是把原数组划分成两个子数组的过程。另一个是治,它将两个有序数组合并成一个更大的有序数组。

    • 它将数组平均分成两部分: mid = (left + right)/2,当数组分得足够小时---数组中只有一个元素时,只有一个元素的数组自然而然地就可以视为是有序的,此时就可以进行合并操作了。因此,上面讲的合并两个有序的子数组,是从 只有一个元素 的两个子数组开始合并的。

const mergeObj = {
  mergeSort(arr) {
    // 数组第一个位置
    let lo = 0
    // 数值最后一个位置
    let hi = arr.length - 1
    // 开始排序,拆分数组
    this.sort(arr, lo, hi)
  },
  sort(arr, lo, hi) {
    // 如果数组不能再拆分了,就返回
    if (lo >= hi) return
    // 得到每次拆分的数组的中间位置
    let mid = Math.floor(lo + (hi - lo) / 2)

    // 使用得到的中间位置继续拆分,左边的序列
    this.sort(arr, lo, mid)
    // 使用得到的中间位置继续对右边序列进行拆分
    this.sort(arr, mid + 1, hi)
    // 每次拆分过后需要合并
    this.merge(arr, lo, mid, hi)
  },
  merge(arr, lo, mid, hi) {
    // 定义左边初始下标
    let left = lo
    // 定义右边初始下标
    let right = mid + 1
    let i = lo
    // 辅助数组
    let newArr = []

    // 当left或right其中有一个越界,就停止循环
    while(left <= mid && right <= hi) {
      // 每次移动都需要将i递增
      // 如果左边的元素比右边的元素小,添加到辅助数组中,左下标右移
      if (arr[left] < arr[right]) {
        newArr[i++] = arr[left++]
      } else {
        // 如果右边元素比左边元素小,将右边元素添加到辅助数组中,右下标右移
        newArr[i++] = arr[right++]
      }
    }
    // 到这里只会有一个序列还有剩余的元素
    // 如果左边序列还有元素,将左边剩余的元素添加到辅助数组中
    while(left <= mid) {
      newArr[i++] = arr[left++]
    }
    // 如果右边序列还有元素,将左边剩余的元素添加到辅助数组中
    while(right <= hi) {
      newArr[i++] = arr[right++]
    }

    // 此时辅助数组中的元素就是已经排序好的了,将辅助数组的元素逐个添加到原数组中
    for(let j = lo; j <= hi; j++) {
      arr[j] = newArr[j]
    }
  }
}
  • 手写快速排序
    • 选择数组中间数作为基数,并从数组中取出此基数;
    • 准备两个数组容器,遍历数组,逐个与基数比对,较小的放左边容器,较大的放右边容器;
    • 递归处理两个容器的元素,并将处理后的数据与基数按大小合并成一个数组,返回。
function quickSort(arr) {
  if (arr.length <= 1) { return arr }
  var pivotIndex = Math.floor(arr.length / 2)
  var pivot = arr.splice(pivotIndex, 1)[0]
  var left = []
  var right = []
  for (var i = 0; i < arr.length; i++){
    if (arr[i] < pivot) {
      left.push(arr[i])
    } else {
      right.push(arr[i])
    }
  }
  return quickSort(left).concat([pivot], quickSort(right))
}
  • 树的遍历
function preOrder(tree) {
  console.log(tree.data)
  if (tree.left) {
    preOrder(tree.left)
  }
  if (tree.right) {
    preOrder(tree.right)
  }
}

想起来在更新吧