前端常见面试题

171 阅读16分钟

前端本地存储的方式有哪些?

方式存储大小过期时间备注
localStorage5M永久存储,除非手动清除
sessionStorage5M会话级别(关闭浏览器就销毁; 可以设置过期时间)
Cookie4k默认是会话级别关闭浏览器就销毁; 可以设置过期时间请求自动携带; 原生操作极其麻烦(js-cookie)
Web SQL已废弃
IndexedDB几百M(应用场景极少)可以基于键值对可以存储大量的数据

JS 的参数是以什么方式进行传递的?

  • 原始数据类型: string number boolean null undefined symbol

    const a = 1 存的就是值本身, 简单类型的数据在传递参数时 传递就是值, 将来修改时不会互相影响的!!

  • 引用数据类型: array object function

    const a = { name: 'zs' } 存的是地址/引用, 复杂类型的数据在传递参数时 传递的是引用地址, 将来修改时会互相影响的!!

js中的垃圾回收?

前置理解: 将来浏览器对于不会再次使用的内存空间 是需要控制回收的, 核心问题: 这个内存到底是不是垃圾??? => 引用计数法 标记清除法

  1. 引用计数(ie): 是有问题的!!!

    ​ 浏览器在分配内存时, 当发现这块内存有一个引用指向他, 就会记录下来这块内存的引用数, 只有当引用数为0时, 才会被确认为垃圾!

    无法解决 循环引用 的问题!!! 见图, 引用计数的角度来说, 这块内存确实还有人用, 不认定为垃圾, 不能回收(本应该被回收)

  2. 标记清除(主流浏览器 google firefox):

    从根部(全局)出发, 如果找不到这个内存, 就会被标记为"无法到达的对象", 就被认定为垃圾!!!

    可以解决 循环引用 的问题!!!

作用域链?

共3种; 全局作用域 局部作用域 块级作用域

作用域存在嵌套的情况, 变量访问规则(就近访问): 从最近的作用域开始查找, 找到了直接用, 没找到往外层找,.. 一直找到全局,变量访问的链式结构 称为 作用域链

什么是闭包?

函数和声明该函数的词法环境的组合

通俗一点: 内层函数访问外层函数的变量

闭包的优势: 缓存数据, 私有化数据闭包的劣势: 如果不好好处理, 内存泄露(应该释放的内存没被释放)

总结: 因为内层函数访问了外层函数的变量, 如果内层函数被return出去, 将来这个内层函数会被缓存, 同时这个函数中用到的变量也会被缓存, 从而实现数据缓存 数据私有化; 伴随着内存泄露(重置null即可)

原型 与 原型链

​ 每一个构造函数 都有自己的原型对象, 这个原型对象上所有的属性和方法 都可以供实例访问原型的一家: 构造函数 原型 实例 的关系

​ 原型链: 每个实例都有自己的原型, 原型本身也是个对象, 他也有自己的原型, ... 一层层的链式结构 => 原型链属性的查找规则: 就近查找( obj.hh 从自己本身开始查找, 如果找不到, 去找他的原型, ... )

为什么需要原型??? 如果没有原型, 所有实例的方法都需要额外分配内存, 这样浪费内存,把公共的方法统一放在一个地方, 供所有实例使用 => 原型的意义

js的继承

原型继承 组合式继承 寄生组合式继承 3种

  1. 原型继承 => 换原型

    Student.prototype = new Person()

  2. 组合式继承 => 换原型 + 借调父类构造函数

    Student.prototype = new Person() 继承了父类的方法Student构造函数内部 Person.call(this, ...) 借父构造函数初始化自己实例的属性 继承了父类属性

  3. 寄生组合式继承 => 换原型 + 借调父类构造函数

    Student.prototype = new Person()Student构造函数内部 Person.call(this, ...)

    之前替换原型 需要通过new 父类构造函数得到实例(new做了很多事)Object.create(obj) 基于传入的对象, 得到一个新对象, 新对象的proto直接指向传入对象

    Student.prototype = Object.create(Person.prototype)这一步省略了new的过程, 提升了性能

  4. class Student extends Person {}

判断一个数据是否为数组? => 数组的方法

数组的一家

Array.isArray(数据) true 是数组; false 不是数组

Object.prototype.toString.call(数据) '[object Number]' '[object Object]' '[object Array]' ....

数组去重?

  1. new Set方法
[...new Set(arr)]
  1. 准备一个新的空数组, 遍历老数组, 每遍历到一项, 先判断这个数据是否在新数组中存在了, 不存在 push进新的空数组

const arr = [1, 2, 3, 3, 2, 1]
    const temp = []
    arr.forEach(item => {
      if (!temp.includes(item)) {
        temp.push(item)
      }
    })
    console.log(temp)

this指向问题?

  1. 默认绑定 fn() => window
  2. 隐式绑定 obj.fn() => 调用者
  3. 显式绑定 call apply bind => 第一个参数
  4. new绑定 => 创建的实例本身
  5. 箭头函数 不存在this => 外层作用域的this

Promise是什么? 构造函数 异步代码的容器

三个状态 等待中pending 失败rejected 成功fulfilled

new Promise((resolve, reject) => { ... 封装异步代码 结束之后需要手动修改状态 resolve() => 修改为成功状态 => .then reject() => 修改为失败状态 => .catch })

状态凝固 => 一旦状态变化了, 将不能再次改变状态

手写promo

// Promise.all([p1, p2, p3, p4]).then((values) => { ... })
Promise.myAll = function(arr) {
  let total = 0
  let temp = []
​
  return new Promise((resolve, reject) => {
    // resolve 必须传入的arr中每个都成功
    arr.forEach((item, index) => {
      item.then((val) => {
        total++
        // temp.push(val)
        temp[index] = val
    if(total === arr.length) {
      // 都成功了
      resolve(temp)
    }
  })
})
​
  })
}
​
// Promise.race([p1, p2, p3, p4]).then((value) => { ... })
Promise.myRace = function(arr) {
  return new Promise((resolve, reject) => {
    arr.forEach(item => {
      item.then((val) => {
        resolve(val)
      })
    })
  })
}
​
Promise.resolve(1) // 快速创建一个成功的promise
new Promise((resolve, reject) => {
  resolve(1)
})
​
Promise.reject(0) // 快速创建一个失败的promise
new Promise((resolve, reject) => {
  reject(0)
})

深拷贝 浅拷贝

  • 浅拷贝: 只拷贝一层

    • 浅拷贝指的是对象中对象A需要用到对象B的属性,那么可以将对象B的属性利用for in语句来进行遍历,将需要的属性赋值给对象A,这个过程叫做浅拷贝
    
    {...obj}
    
  • 深拷贝: 拷贝多层

    • 深拷贝指的是对象中对象A需要用到对象B的属性和方法,那么可以利用递归函数封装函数并且自调用的特点,将对象B的属性利用for in语句来进行多次遍历,将需要的属性、方法赋值给对象A,这个过程叫做深拷贝

      
      递归
      ​
      JSON.parse(JSON.stringify(obj))
      

    区别:

    • 浅拷贝只可以拷贝简单数据类型(堆内存),对于复杂数据类型只拷贝内存中的堆内存,而栈内存不会拷贝
    • 深拷贝不仅可以拷贝简单数据类型(堆内存),还可以拷贝复杂数据类型(栈内存)

http的请求方式

get 获取post 添加delete 删除put 更新(重置式)patch 更新(补丁式)

请求报文: 请求行 请求头 请求体; 响应报文: 响应行 响应头 响应体;

http常见状态码

  • 2xx

    200 成功201 新建

  • 3xx

    301/302 重定向304 协商缓存

  • 4xx

    400 接口传参错误401 权限认证失败404 找不到

  • 5xx 服务器错误

https是如何做到更加安全?

https比http更加安全 在进行数据传输时, 对数据进行加密处理, 所以更加安全

https加密方案:

  1. 非对称加密 + 对称加密 两者结合将来数据传输必须以 对称加密 为主!!! 但是容易一开始泄露对称加密的 密钥 用 非对称加密 传输 对称加密的密钥

    • 数据真正传输 还是 对称加密 数据传输效率得到保证
    • 非对称加密 传输 密钥 数据安全性得到保证
  2. 数字证书: 加密签发公钥将来你访问一个网站, 你希望得到服务端的响应数据, 数据是被对称加密 加密出来的!! 必须拥有密钥密钥必须让服务端给你, 通过非对称加密给你,

    初步互通消息时, 客户端发送请求, 得到数字证书, 基于数字证书中的公钥 解密出 对称加密的密钥,就可以解密传输的数据, 正常通信了...

    数字证书(权威的CA机构颁发): 包含网站基本信息, 到期时间, 非对称加密的公钥 ,

  3. 数字签名: 防证书被篡改数字证书: 网站: xxxx.xxx.xxxx 公钥: sdafasdfasdfasdfasdfsadfsadfgdsg 到期时间: 2040年10月20号 签发机构: xxx机构 签名: xxdaddsafsadhgdfhfldghkdfghdfg ==>> 将网站所有信息通过hash加密成签名

http常见的加密方案?

  1. 对称加密: 加密解密同一个密钥(对称) 可逆的过程

    • 优点: 加密效率很高, 加密速度快, 计算量小
    • 缺点: 如果一开始密钥就泄露了, 安全性完全无法得到保障!!!
  2. 非对称加密: 有两把钥匙 公钥 私钥 (公钥加密的数据私钥解密 私钥加密的数据公钥解密)

    • 优点: 安全性得到一定的保障
    • 缺点: 加密解密效率低 慢, 计算量大

    例如:我们想去gitee提交信息, 本地生产了两把公钥 私钥, 将公钥给gitee我(私钥加密) ==>> gitee(公钥解密)我(私钥解密) <<== gitee(公钥加密)

  3. hash加密: 不可逆128645 ===>>> asdfasdfasdafas

    一般后端数据库存储密码 一定是不可逆的hash加密 如md5 sha256

    撞库: 暴力 模拟各种各样的密码 加密 得到一本字典 记录下 123456 ===>>> asdfaasdfasdfasd 123456 ===>>> asdfaasdfasdfasd ......

http2.x优势?

  • http2.x 二进制传输数据(更高效) http1.x文本形式传输数据 计算机只认识 0 1
  • http2.x 头部压缩技术, 减少请求头中重复携带的数据(我用上一次的请求头中的数据), 降低网络负担
  • http2.x 服务器推送技术 可以主动给客户端响应数据, 提高页面加载效率
  • http2.x 多路复用机制, 一个tcp连接 可以承载任意双向数据流, 少创建很多tcp连接(三次握手 四次挥手)

要想发请求, 得先建立tcp连接, 浏览器对于单个域名有6-8连接限制, 一个tcp连接只能发一次请求

一次完整的http服务的过程?

在地址栏中输入 www.jd.com 具体发生了什么???

  1. dns解析: 先拿着你输入的域名, 去找真正的ip地址

    • 每个人的电脑上都有一个文件 hosts, 记录了一些域名和ip映射关系
    • 会优先去本机hosts文件中去获取ip, 如果没有
    • 去找公网dns域名服务器, 获取ip

    www.jd.com => 58.242.151.131

  2. 根据ip地址找到对应的服务器, 需要建立tcp连接, 三次握手!!!

  3. 成功建立tcp连接后, 进行http请求

  4. 服务器响应http请求, 浏览器得到网站的首页 index.html

  5. 浏览器解析html页面时, 解析到script link img, 会再发请求 获取 js文件/css文件/图片资源

  6. 浏览器渲染完整的网页给用户

  7. http服务完成后, 关闭tcp连接, 四次挥手!!!

三次握手?? 四次挥手??

三次握手 四次挥手 ==>> 体现出连接与断开的谨慎

三次握手 => 想要让双方确认收发消息的能力!!!

  • 第一次握手 客户端往服务端发消息, 你好, 在么? 能听到么??服务器能确认: 服务器可以收消息的能力 客户端有发消息的能力
  • 第二次握手 服务端回消息给客户端 在的! 你能听到我说话么?客户端确认: 客户端有发消息的能力, 客户端能收消息; 服务端能发消息 服务端能收消息
  • 第三次握手: 客户端再回消息给服务端 我能听到服务端确认: 客户端能发消息 能收消息 服务端能发消息 能收消息

四次挥手 => 为了保证数据传输的完整性

  • 第一次挥手 客户端发消息给服务端 服务端 你这个数据传完了么?
  • 第二次挥手 服务端先回一个消息 你等我一会 我检查一下
  • 第三次挥手 服务端回消息 确实没有了, 都说完了, 你可以走了
  • 第四次挥手 客户端回消息 那我走了 再见!!!

缓存

大类: 数据库缓存 服务器缓存 浏览器缓存浏览器缓存: http缓存 + Cookie/localStorage/sessionStorage/websql/IndexedDB

http缓存的必要性: 每次重复的资源 希望直接走缓存, 不想每次发请求 => 优化网页加载的效率

  • 强缓存
  • 协商缓存

强缓存: 类似于食品的过期时间 将来第一次请求服务器资源时, 服务器会正常响应给你一张图片, 同时会告诉你这张图片的有效期, expires: Sun, 25 Jan 2032 09:31:06 GMT (到期时间-绝对时间), 将来处于有效期内 直接走强缓存, 从缓存中获取该图片, 不会再发请求

expires将来会和本机时间对比, 本机时间是可以改的!! 有漏洞

cache-control: max-age=315360000 相对时间 315360000s = 10年 从你拿到该资源后, 10年后必过期

cache-control为主 expires为辅

一旦强缓存失效(未命中强缓存), 这个时候 该资源不能再次使用!!, 必须要发请求问服务端要了图片这种资源很少会更新, 不存在过期了不能用了!!!!

  1. 这次发请求 会带上之前过期的图片资源, 问服务端 哥们 还能用么?? 服务端一检查, 发现没更新,此时不需要响应新图片, 直接告诉客户端走缓存, 更新过期时间 304 => 协商缓存成功

  2. 将来如果服务器发现图片更新了, 会直接响应一张新图片(自带新的过期时间), => 协商缓存失败, 200

    服务端如何判断图片资源是否更新? 最后修改时间(最小单位秒) last-modified: Tue, 10 Sep 2019 05:51:30 GMT 不一致, 更新了 一致, 没更新

    1s内如果发生了多次更新, last-modified就有问题

    资源的唯一校验码: ETag: xdsddasfsd ETag可以保证每一个资源是唯一的,资源变化都会导致ETag变化。 如果1s内进行了多次更新, ETag是会实时变化的

强缓存与协商缓存 配合使用 缓存页面资源

浏览器如何解析css选择器?

div h3 span { ... }

以为的: 从左往右, 先找所有的div 再找所有div后代中的所有h3 , 再找左右h3后代中的左右span比如: 页面中有10000个元素, 有4000个div 找4000次; 你需要在这4000个div中找后代中有没有h3, h3 300次

遍历树形结构的所有子节点 查找 比如层级有40层, 子节点会特别多

实际上: 从右往左, 先找所有的span, 再找所有span中有祖辈是h3的, 再找祖辈有div的比如: 页面中有10000个元素, 有4000个span 找4000次,

你只需要找祖辈中有没有满足条件的, 比如层级有40层 也就40次

  1. 浏览器是如何进行界面渲染的?

    • 解析html, 得到dom树
    • 解析css, 得到样式规则
    • 基于dom树 和 样式规则 得到渲染树
    • 基于渲染树, 进行结构布局(layout) 重排/回流
    • 基于渲染树, 进行绘制(paint) 重绘

重排重绘?

  • 结构的变化 会引起重排

  • 非结构的变化 会引起重绘

    重排必将重绘, 重绘不一定重排!!!!

尽可能避免重排:

  • 将来如果真要改变盒子大小位置, transform只是视觉效果
  • 集中修改样式 (这样可以尽可能利用浏览器的优化机制, 一次重排重绘就完成渲染)
  • 尽量避免在遍历循环中, 进行元素 offsetTop 等样式值的获取操作, 会强制浏览器刷新队列, 进行渲染
  • 使用文档碎片(DocumentFragment)可以用于批量处理, 创建元素

git基本操作

git服务器 gitee github; 公司中用的多是 gitlab, 支持私有服务器

git常见操作

  • git init
  • git add . git commit m ''
  • git push origin xxx-zs
  • git merge
  • git clone xxx
  • git add remote origin ...
  • git checkout -b xxx

什么是MVVM? 是一种设计模式

  • M Model(数据层) ajax请求回来的数据

  • V View(视图层) 页面

  • VM ViewModel(视图数据) 既能操作数据 也能操作视图 - 数据变了, 操作视图自动更新 - 视图变了, 操作数据自动更新

  1. 双向数据绑定的原理?

    • 如何知道数据变了? Vue2 Object.definePropertyVue3 Proxy
    • 如何知道视图变了?@change @input

Vue2 和 Vue3 中监视数据的区别

  • Vue2 Object.defineProperty 劫持数据 针对于每个属性去劫持, 如果数据复杂, 需要递归劫持, 成本高, 效率低对于数组数据的劫持/监视, 有问题 ==>> $set
  • Vue3 Proxyproxy对于整个对象数据的劫持 对象内部的任意属性发生变化 都会经过外层的proxy, 无需递归, 效率高proxy对于数组数据的更新也没问题

Vue的响应式系统? => 观察者模式(一对多的设计模式)

响应式: 数据变化了, 会通知到所有用到该数据的视图自动更新

1666936926826.png

观察者模式 目的在于 需要通知对应的watcher进行响应; 依赖收集(找到数据的依赖者)

一上来vue会解析渲染, 会进行依赖收集(找到数据的watcher), 收集到一个大的数据中,当数据变化时, Object.defineProperty监视到数据变化了, 就会通知到你刚刚收集的watcher们进行响应(派发更新)

watcher也有分类: 侦听器watcher > 计算属性watcher > 渲染watcher

Vue的生命周期?


Vue2                       Vue3
beforeCreate               Setup
created                    Setup
beforeMount                onBeforeMount
mounted                    onMounted
beforeUpdate               onBeforeUpdate
updated                    onUpdated
beforeDestroy              onBeforeUnmount
destroyed                  onUnmounted
​
activated
deactivated

vue组件通信?

  1. 父传子 子传父父传子: 父组件给子组件标签上以添加属性的方式绑数据; 子组件内部通过props接收

    
       <son title="123" :num="456" />
    

    子传父: 子组件内部通过$emit触发自定义事件; 父组件中需要给子组件标签上注册对应的自定义事件, 提供处理函数

    
       ```js
    

    this.$emit('xx', 12) <son @xx="fn" /> fn(val) { ... } ```

  2. 事件总线(eventBus) vue3移除了理解: 组件A要和组件B通信(两个组件没有任何关系), 有个中介, 借助于eventBus通信

    
    const eventBus = new Vue() 
    export default eventBus
    

    组件A => 组件B

    组件A发消息:

    
    eventBus.$emit('ss', 123123)
    

    组件B收消息:

    
    eventBus.$on('ss', function(val) { ... })
    
  3. provide inject 用于某个组件共享数据/方法 给子孙后代组件使用vue2中有这个语法 但是不好用,vue3中增强了

    1. 提供数据

      
      provide('val', 123)
      provide('fn', () => { ... 某个数据更新的代码 })
      
    2. 注入数据使用

      
      const num = inject('val')
      const fn = inject('fn')    fn('123')
      
  4. ​children $parent => 可以拿到组件

    
    <Hello ref='hello' />   // 内部通过data提供了一个 money(组件自己的)   this.money
    this.$refs.hello.money   
    ​
    this.$children[0] // 获取组件中 使用到的第0个组件
    ​
    $parent  // 获取父组件
    
  5. ​listeners 使用场景: 有很多数据要隔代传

    
    $attrs 批量获取数据 向下传递
    $listeners  批量向上传递自定义事件
    
  6. Vuex

    • state 提供状态
    • mutations 提供修改状态的方法(同步的)
    • actions 提供消化异步操作的方法 - 不能直接在action中修改状态(异步操作结束后提交mutation)
    • getters 类似计算属性
    • modules 分模块namespaced: true 命名空间
    
    this.$store.commit('mutation名', 123) // 页面中触发mutation 同步派发任务
    this.$store.dispatch('action名', 123) // 页面中触发action  异步派发任务
    
  7. pinia 官方推荐的vue3使用的状态管理工具

    1. 提供状态

      
      state() {
        return {
          count: 0,
          money: 100
        }
      }
      
    2. actions用于修改状态(同步异步都可以) 修改状态 获取状态 直接通过this

      
      actions: {
      fn() {
        this.money = 1000
      },
      fnAsync() {
        const res = await Api()
        this.count = res.data.data
      }
      }
      
    3. getters

    4. store/user.js

    
    export default defineStore('user', {
      state() {
        return {
          username: 'zs',
          age: 19
        }
      },
      actions: {
        addAge() {
          this.age++
        }
      }
    })
    

    页面组件如何使用仓库数据

    
    import useUserStore from '@/store/user.js'
    const user = useUserStore()
    {{user.username}}  @click='user.addAge'
    

key的作用?

添加一个唯一标识, 优化对比复用策略(默认下标), 提高渲染性能

对比真实结构? 虚拟dom真实dom结构太复杂了, 属性特别多, 一个js对象模拟真实dom结构, 身上只有关键的几个属性

真实dom结构 树形的!! 虚拟dom结构模拟也还是树形! 遍历一层还是巨大的!!

diff算法:

  • 首先比根元素, 如果根元素不同, 直接销毁重建!!!
  • 如果根元素相同, 对比出其他差异(属性), 考虑往下继续复用
  • 如果是兄弟元素, 默认按照下标去比, 建议添加key属性, 优化对比策略, 提升渲染性能

路由跳转传参的方式

query传参(多) params传参(极少) params传参配合动态路由使用(多)

  • 在地址栏传递的 刷新不丢失

    
    this.$router.push('/login?name=zs&age=19')
    this.$router.push({
      path: '/login',
      query: {
        name: 'zs',
        age: 19
      }
    })
    this.$route.query.name
    
  • 刷新会丢失, 在内存中传递

    
    { path: '/test', component: Test, name: 'test' } // 路由规则中设置name
    this.$router.push({
      name: 'test'
      params: {
        money: 100
      }
    })
    
  • 地址栏中传递, 刷新不丢失

    
    { path: '/user/:id', component: User } // 必须配合动态路由使用
    this.$router.push('/user/123')
    this.$route.params.id
    

前端如何处理权限问题?

RBAC role based access control 基于角色的权限控制给员工分配角色 给角色分配权限 (给员工直接分配权限的危害)

1.页面访问权 在你点击登录之后, 在路由跳转之前, 获取你的个人信息(包含roles: ['home', 'salary', 'social']), 将来基于这个字段通过 addRoutes 动态添加对应的路由规则(动态路由), 因为你有这个权限, 所有你又这个路由规则, 所以你能访问这个页面

2.按钮操作权 在你点击登录之后, 在路由跳转之前, 获取你的个人信息(包含btns: ['del', 'edit']), 将来在页面中可以封装一个方法, 得到某个用户到底有没有这个按钮权, 有3种形式: 1. v-if 2. 禁用 3. 给你看给你点 给你提示 你权限不够

3.api访问权(后端)

首屏渲染加载过慢导致白屏?

  • 组件懒加载 路由懒加载 异步组件
  • 图片资源等压缩
  • cdn加速(花钱)

Tree-Shaking-树摇原理

juejin.cn/post/684490…