2022前端面试题汇总

7,716 阅读17分钟

最近面试一个月,记录一下遇到的面试题 后续还会做一些补充更改

语义化

页面整体结构
代码结构清晰,易于阅读
利于开发和维护
有利于搜索引擎优化(SEO)

无障碍开发(盲人)

<html lang="en">
语义化的 HTML
<address>
<article>
<aside>
<footer>
<header>
label for属性
通过键盘完成所有的功能

SEO

Title:掘金 - 代码不止,掘金不停
Keywords:掘金,稀土,Vue.js,前端面试题
description: 描述内容

url输入之后

1、查找缓存
2、DNS解析
3、建立TCP连接
4、HTTP请求
5、服务器响应请求并返回结果
6、关闭TCP连接
7、浏览器渲染
8、构建DOM树
9、构建CSS规则树
10、合并生成render树
11、布局-绘制
强制缓存失效-携带缓存标识向服务器发起请求-返回304,协商缓存生效-协商缓存失效,返回200和请求结果

回流和重绘

重绘:元素外观改变,改变布局
重排/回流:重新计算元素,重新生成布局

避免回流或重构

定位\集中改变样式,不要一条一条地修改 DOM 的样式

css

Flex 布局

容器的属性:
​
flex-direction:决定主轴的方向 flex-direction: row|row-reverse|column|column-reverse;
flex-wrap:决定换行规则 flex-wrap: nowrap | wrap | wrap-reverse;
flex-flow.box { flex-flow: || ; }
justify-content:对其方式,水平主轴对齐方式
align-items:对齐方式,竖直轴线方向
align-content
​
项目的属性(元素的属性):
​
order 属性:定义项目的排列顺序,顺序越小,排列越靠前,默认为 0
flex-grow 属性:定义项目的放大比例,即使存在空间,也不会放大
flex-shrink 属性:定义了项目的缩小比例,当空间不足的情况下会等比例的缩小
flex-basis 属性:定义了在分配多余的空间,项目占据的空间。
flex:是 flex-growflex-shrinkflex-basis 的简写,默认值为 0 1 auto。
align-self:允许单个项目与其他项目不一样的对齐方式,可以覆盖
align-items,默认属 性为 auto,表示继承父元素的 align-items 

元素水平垂直居中

.parent {
    position: relative;
}
.child {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    margin: auto;
}
.parent {
    display: flex;
    /* 定义项目在主轴上如何对齐 */
    justify-content: center;
    /* 定义项目在交叉轴上如何对齐 */
    align-items: center;
}
.parent {
    position: relative;
}
​
.child {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

文本溢出

.ellipsis{    
  overflow: hidden;    text-overflow:ellipsis;    
  white-space: nowrap; display: inline-block;
}
.ellipsis{
  overflow: hidden;    display: -webkit-box;
  text-overflow:ellipsis;    -webkit-line-clamp:2;
  -webkit-box-orient: vertical
}
​
设置相对定位的容器高度,用包含省略号(…)的 元素/伪元素 模拟实现 
一些开源的js库 

js

es6新增语法

letconst、解构赋值、展开运算符、模板字⾯量、箭头函数、PromisesGeneratorsIteratorfor ... infor ... ofMapSetProxyClass、es module

JS中的数据类型

基本类型(值类型):NumberStringBooleanSymbol,null,undefined       栈内存储
引用类型(复杂数据类型):ObjectFunctionArray                         堆内存储

JS中的数据类型检测方案

typeof
instanceof
Object.prototype.toString.call()

作用域和作用域链

简单来说作用域就是变量与函数的可访问范围
​
1.全局作用域:代码在程序的任何地方都能被访问,window 
2.函数作用域:在固定的代码片段才能被访问,function3.新增块级作用域,大括号内{}
​
一般情况下查找变量会在当前作用域先找。但是如果在当前作用域中没有查到,就会向上级作用域去查
直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

闭包

闭包是指能访问另一个函数作用域中的变量的函数, 正常就是函数嵌套函数延长外部函数的作用域
优点:模仿块级作用域、封装私有化变量、创建模块(IIFE)
缺点:一直保存在内存中,过多的闭包可能会导致内存泄漏

new运算符的实现机制

1.创建了一个对象
2.this指向这个对象
3.执行构造函数的代码
4.返回这个对象
注意的是要 判断函数定义的返回值类型(函数默认返回undefined),如果是基本数据类型,返回创建的对象。
如果是引用类型,就返回这个引用类型的对象。

原型 && 原型链

构造函数   通过new得到实例对象,内部prototype指向原型对象
实例对象   __proto__指向原型对象
原型对象   constructor指向构造函数
​
在对象上查找一个属性或者方法时,如果找不到就会去原型对象找,再找不到再去原型对象的原型对象,
最后找到object的原型对象为null时就表示没有
​
JavaScript对象是通过引用来传递的。当我们修改原型时,与之相关的对象也会改变

EventLoop

js: 先会执行栈中的内容 - 栈中的内容执行后执行微任务 - 微任务清空后再取出一个宏任务压入执行栈执行 - 
再去执行微任务 - 然后在取宏任务清微任务这样不停的循环。
宏任务:ajax、定时器、一些浏览器api、script...
微任务:promise.then、mutationObersve

防抖节流

防抖:高频触发事件n 秒内函数只会执行一次, 思路:每次触发前都取消之前的延时调用方法
节流:高频事件触发, n 秒内只会执行一次, 思路:每次触发事件时都判断当前是否有等待执行的延时函数,
     有等待执行的延时函数就直接return

vue

组件中的data为什么是一个函数

在Vue中组件是可以复用的,如果data是对象,当组件被多次引入后,由于对象属于引用类型,
一个组件内部的data中的属性发生改变就会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,
data必须是一个函数

key 的作用

key的作用是为了在diff运算时更快的找到相同的节点让其复用,避免大量不必要的dom操作,影响性能。

// 这里是Vue中对比两个节点是否是相同节点的代码
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

举个🌰,在一个列表根据不同部分条件排序时,页面结构如下,当我下次的排序条件使当前列表发生翻转,而反转后内容不会发生变化,在上述情况下我们分析当写key和不写key两种情况的dom对比。首先如果不写key,Vue内部会默认根据索引来生成一个key。

<template>
  <div v-for="item in arr"  class="box">
      <p>标题:{{item.title}}</p>
      <span>创建时间:{{item.createTime}}</span>
      <span>创建人:{{item.createUser}}</span>
      <div>内容:{{item.content}}</div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      arr: [
        {
          id: 111,
          title:'111111-title',
          createTime:'111111-createTime',
          createUser:'111111-createUser',
          content:'111111-content',
        },
        {
          id: 222,
          title:'222222-title',
          createTime:'222222-createTime',
          createUser:'222222-createUser',
          content:'222222-content',
        },
        {
          id: 333,
          title:'333333-title',
          createTime:'333333-createTime',
          createUser:'333333-createUser',
          content:'333333-content',
        },
      ]
    };
  },
}
</script>
不写key:当不写key时,Vue内部会默认根据索引来生成一个key。内部的结果就是
//旧dom数据				    //新dom数据					
{					    {
    key:1,					key:1,	
    id: 111,					id: 333,
    ...						...
},					    },
{					    {
    key:2,					key:2,	
    id: 222,					id: 222,
    ...						...
},					    },
{					    {
    key:3,					key:3,
    id: 333,					id: 111,
    ...						...	
},					    },
  
在上述情况下,当数据发生翻转后,Vnode的key值还是不发生变化的,因为都是根据索引生成的,
所以在做虚拟dom对比的时候会判断id为111的旧dom与id为333的dom为同一个节点,
而进入内部字节点的时候发现是不同的dom,就会做dom的删除、创建、新增
写key时
//旧dom数据				    //新dom数据					
{					    {
    key:111,					key:333,
    id: 111,					id: 333,
    ...						...
},					    },
{					    {
    key:222,					key:222,
    id: 222,					id: 222,
    ...						...
},					    },
{					    {
    key:333,					key:111,
    id: 333,					id: 111,
    ...						...	
},					    },
  
在写key情况下,当数据发生翻转后,Vnode的key值是不变的,所以在做虚拟dom对比的时候会判断
id为111的旧dom与id为333的dom 不是 同一个节点,而同一个节点只是位置发生了改变,只需要做dom的移动,
而不是删除、创建、添加,这样就提高了渲染的性能

vue组件的通信方式

1、props
2$on$emit
3$parent$children$refs
4、Event Bus ==> Vue.prototype.$bus = new Vue()
5$attrs$listeners
6、Provide、inject
7、vuex

双向数据绑定

Vue 使用 Object.defineProperty 遍历和递归对data中的所有属性做数据劫持,把这些 属性 全部转为 
getter/setter,当获取的时候会触发getter,设置的时候会触发setter

发布者-订阅者模式

监听(Observer): 遍历和递归对data中的所有属性做数据劫持,当获取的时候会触发get,设置的时候会触发set
模版编译(Compile): 解析模板指令,将模板中变量替换成数据,然后渲染初始化的页面视图。并且给所有的 变量
                  添加监听数据的 订阅者,一旦数据有变动,收到通知,更新试图
依赖收集(Dep): 收集者,每个变量会有一个收集者,收集变量所对应的所有的订阅者,
             当数据发生变化的时候通知订阅者更新
订阅者(Watcher):数据监听 和 模版编译之间通信的桥梁,当收到通知更新时更新页面

vue1.0的订阅发布模式

1.遍历和递归对data中的所有属性做数据劫持,添加getset,并且同时会给每个属性创建一个对应的dep
2.模版编译,将模版中所有的变量替换成数据,同时会给每个变量创建一个订阅者(Watcher),⚠️并且会将当前的
  Watcher添加到当前变量所对应的dep中。添加到dep的操作就是 订阅。
  具体实现方式是触发一次当前属性的get,把当前的Watcher传递过去。(这里dep有可能会对应多个Watcher)
3.往后当数据发生改变的时候就会触发每个dep的notice(),然后就是遍历dep中保存的Watcher,去执行更新,
  这里就是发布

Vue2.0的订阅发布模式

在vue1.0中会给每个动态的属性添加一个Watcher,当项目体量较大时这无疑是一笔很大的开销,因为会占据很多
内存,同时,在频繁的操作dom也是对性能有了很大的消耗。所以在vue2.0引入了虚拟dom
​
1.遍历和递归对data中的所有属性做数据劫持,添加getset,并且同时会给每个属性创建一个对应的dep
2.vue2.0不会给每个属性添加一个Watcher,而是给一个组件添加一个Watcher
3.当数据改变,触发dep的notice(),遍历dep中保存的Watcher,去执行更新时,不会做大量的dom操作,
  而是执行虚拟dom对比,完成更新。

Vue.set

由于 Vue 会在初始化实例时对 属性 执行 getter/setter 转化,所以 属性 必须在 data 对象上存在才能让 Vue 将它转换为响应式的

弊端就是监听不到新增的属性,以及无法检测数组通过索引下标length的修改,Vue.set就是让这些新的属性也变成响应式的数据

查看源码会发现Vue.set函数内部执行了defineReactive方法,而defineReactive方法就是Vue使用 Object.defineProperty对属性执行 getter/setter 转化的方法。

/*Vue 源码中set方法的实现(精简版) */
export function set (target, key, val) {
  const ob = (target: any).__ob__
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

Vue.nextTick

在下次 DOM 更新循环结束之后执行延迟回调。

因为Vue 在更新 DOM 时是异步执行的,当我们设置一个属性变更时,组件不会立即渲染,所以我们在同步代码中获取不到变更后的渲染结果。

以下是属性变更,通知Watcher的DOM更新函数,可以看出,内部也是用了nextTick方法

/*Watcher内部的更新函数 update() */
update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    // 这里是监听数据变化通知视图更新的方法,会用异步的方式去做页面更新
    queueWatcher(this)
  }
}
​
export function queueWatcher (watcher: Watcher) {
   nextTick(flushSchedulerQueue)
}

而nextTick函数内部是在用微任务去执行所有的更新函数,而我们的Vue.nextTick传入的回掉函数也会被加入到以下的callbacks数组中,也就变成了异步执行函数

// 保存当前的所有回掉函数
const callbacks = []
​
export function nextTick (cb, ctx) {
  callbacks.push(() => {
    // 将当前的回掉函数保存
    cb.call(ctx)
  })
  // 执行函数
  timerFunc()
}
​
let timerFunc = () => {
  // 用微任务去执行保存的所有回掉函数
  Promise.resolve().then(flushCallbacks)
}
​
function flushCallbacks () {
  // 遍历执行所有的回掉函数
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

注意⚠️:Vue内部也做了兼容降级处理,依次是

Promise - MutationObserver - setImmediate - setTimeout0秒

computed与watch

计算属性依赖于多个属性,有缓存。只有当依赖的属性发生变化时才会更新。监听器用于观察单个属性的变化,会立即执行。

计算属性:当模版内的表达式过于复杂时,会让模版过重且难以维护 例如: {{ message.split('').reverse().join('') }}所以,对于复杂逻辑,应该使用计算属性

computed: {
  // 计算属性的 getter
  reversedMessage: function () {
    // `this` 指向 vm 实例
    return this.message.split('').reverse().join('')
  }
}

我们提供的函数将作为声明的计算属性的getter 函数,由于Vue知道计算属性是依赖于那些data里的属性的,因此Vue内部做了缓存,只有计算属性依赖的属性发生改变时,计算属性的getter 函数才会更新

注意⚠️:计算属性使用箭头函数时,箭头函数绑定了父级作用域的上下文, this 不会指向Vue 实例,但是会默认传入到第一个参数vm => vm.a * 2,或者用function声明函数

监听器:观察一个属性/表达式/回掉结果,

注意⚠️:不应该使用箭头函数来定义 watcher 函数 (例如 search: newVal => this.update(newVal))。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会指向 Vue 实例

/*Watcher内部的更新函数 update() */
update () {
  if (this.lazy) {
    //这里是页面中的computed选项,只有所依赖的属性发生变化时才会惰性的做更新
    this.dirty = true
  } else if (this.sync) {
    //这里是页面中的watch选项,表示当监听到数据变化的时候会同步执行传入的回掉函数
    this.run()
  } else {
    // 这里是监听数据变化通知视图更新的方法,会用异步的方式去做页面更新
    queueWatcher(this)
  }
}

computed与watch以及dom更新在Vue中都是new 了一个Watcher,只是参数有所不同而已,在Watcher中的update更新函数中做了区分

虚拟dom

虚拟dom本质上就是一个普通的JS对象,来表示dom节点的内容

在vue中,渲染视图的时候会调用render函数,这个渲染 在组件创建时,和视图依赖的数据更新时。如果在渲染的时候,直接使用真实DOM操作的创建、更新、插入等操作会带来大量的性能损耗,这样就会极大的降低渲染效率。

因此,vue在渲染时,使用虚拟dom来替代真实dom,主要为解决渲染效率的问题。

vue中的虚拟dom是基于Snabbdom的虚拟DOM修补算法

vue3和vue2的区别

生命周期、Composition API、Teleport、响应式原理、虚拟DOM静态节点、更好的Tree-shaking

webpack

vue有哪些性能优化

对于第三方js库CDN
图片资源的压缩,icon资源使用雪碧图
开启gizp压缩
生产环境关闭SourceMap
mini-css-extract-plugin-提取CSS到单独的文件,压缩CSS
vue-router使用懒加载
合理使用watch和computed
v-for必须添加key
销毁定时器
keep-alive
Object.freeze
​
压缩代码
CDN加速
Tree Shaking
提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,可以⻓期缓存这些⽆需频繁变动的公共代码

webpack优化

include、exclude配置项来缩⼩loader的处理范围
多配置别名
happypack=>thread-loader(多进程loader)
cache-loader\babel-loader开启缓存
hard-source-webpack-plugin缓存中间步骤
​
webpack5

treeShaking机制的原理

利用es6模块的规范
ES6 Module引入进行静态分析,编译的时候判断到底加载了那些模块
判断那些模块和变量未被使用或者引用,然后删除对应代码

Loader和Plugin

webpack自身只支持js和json这两种格式的文件,对于其他文件需要通过loader将其转换。
loader,它是一个转换器,将A文件进行编译成B文件,单纯的文件转换
plugin是工具不操作文件,在webpack打包过程中,执行一些任务(打包优化、文件管理、环境注入)

常见的plugin

ProvidePlugin:自动加载模块,代替require和import
html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件
clean-wenpack-plugin 清理每次打包下没有使用的文件
DefinePlugin 编译时配置全局变量
HotModuleReplacementPlugin 热更新
optimize-css-assets-webpack-plugin 不同组件中重复的css可以快速去重
compression-webpack-plugin 生产环境可采用gzip压缩JS和CSS
webpack-bundle-analyzer:可视化Webpack输出文件的体积

常⻅的Loader

file-loader:把⽂件输出到⼀个⽂件夹中
url-loader:和 file-loader 类似,base64 的⽅式
source-map-loader:加载额外的 Source Map ⽂件,以⽅便断点调试
babel-loader:把 ES6 转换成 ES5
css-loader:加载 CSS,⽀持模块化、压缩、⽂件导⼊等特性
style-loader:把 CSS 代码注⼊到 JavaScript 中,通过 DOM 操作去加载 CSS。
eslint-loader:通过 ESLint 检查 JavaScript 代码
thread-loader(多进程loader)

webpack 构建流程

1、初始化参数,合并传入的和webpack.config.js文件中的配置参数
2、注册/监听 插件,监听构建的生命周期
3、拿到配置从入口处开始构建这个AST语法树,它会根据依赖一直递归下去
4、webpack默认只能编译js和json,遇到其他文件类型会根据配置的loader进行转换
5、得到文件的结果和依赖关系后生成代码块chunk
6、输出到dist目录

webpack 热跟新

1、首先启动dev-server,webpack开始构建时会向 入口 文件注入热更新代码
2、浏览器打开的时候,浏览器与本地服务会基于Socket建立通讯
3、本地服务会监听文件的改动,然后会再次编译
4、编译完成后通过socket 发送消息告诉浏览器,(通过维护hash值和state状态);
5、浏览器再去请求新的模块替换掉之前旧的模块

webpack 的hash

1、hash: 每次webpack编译中生成的hash值
2、chunkhash: chunkhash基于入口文件及其关联的chunk形成,文件的改动只会影响与它有关联的chunk的
   hash值,不会影响其他文件
3、contenthash: contenthash根据文件内容创建。当文件内容发生变化时,contenthash发生变化

网络

请求跨域问题

跨域是由浏览器的同源策略造成,(协议、域名、端口)有一个不同就视为跨域
​
JSONP
CORS 服务器设置Access-Control-Allow-Origin 响应头,允许跨域请求
proxy 代理 目前常用方式,通过服务器设置代理

http2

http1: 每次请求都会建立一次HTTP连接-3次握手4次挥手(每个TCP连接只能发送一个请求)
http1.1: TCP 连接默认不关闭,可以被多个请求复用,但是同一个TCP连接里面,所有的数据通信是按次序进行。
         服务器只有处理完一个回应,才会进行下一个回应,前面慢的时候,后面的就需要长时间等待
http2:http2的传输是基于二进制帧的。每一个TCP连接中承载了多个双向流通的流。同一域名只需占用一个 
       TCP 连接,就是可以并行的发出多个请求,不会堵塞

http状态码3开头

301 永久性重定向, 可促进搜索引擎优化效果。 
302 临时性重定向, 影响搜索引擎优化效果。 
304 协商缓存生效。

浏览器的缓存机制

浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识,查找到就是强缓存生效
​
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,当协商缓存生效,返回304,
当协商缓存失效,返回200和请求结果结果