前端常见面试题大全
- 前端本地存储的方式有哪些?
- JS 的参数是以什么方式进行传递的?
- js中的垃圾回收?
- 作用域链?
- 什么是闭包?
- 原型 与 原型链
- js的继承
- 判断一个数据是否为数组? => 数组的方法
- 数组去重?
- this指向问题?
- Promise是什么? 构造函数 异步代码的容器
- 手写promise
- 深拷贝 浅拷贝
- http的请求方式
- http常见状态码
- https是如何做到更加安全?
- http常见的加密方案?
- http2.x优势?
- 一次完整的http服务的过程?
- 三次握手?? 四次挥手??
- 缓存
- 浏览器如何解析css选择器?
- 重排重绘?
- git基本操作
- 什么是MVVM? 是一种设计模式
- Vue2 和 Vue3 中监视数据的区别
- Vue的响应式系统? => 观察者模式(一对多的设计模式)
- Vue的生命周期?
- vue组件通信?
- key的作用?
- 路由跳转传参的方式
- 前端如何处理权限问题?
- 首屏渲染加载过慢导致白屏?
- Tree-Shaking-树摇原理
前端本地存储的方式有哪些?
方式 存储大小 过期时间 备注 localStorage 5M 永久存储,除非手动清除 sessionStorage 5M 会话级别(关闭浏览器就销毁; 可以设置过期时间) Cookie 4k 默认是会话级别关闭浏览器就销毁; 可以设置过期时间 请求自动携带; 原生操作极其麻烦(js-cookie) Web SQL 已废弃 IndexedDB 几百M(应用场景极少) 可以基于键值对可以存储大量的数据
JS 的参数是以什么方式进行传递的?
- 原始数据类型: string number boolean null undefined symbol
const a = 1 存的就是值本身, 简单类型的数据在传递参数时 传递就是值, 将来修改时不会互相影响的!!
- 引用数据类型: array object function
const a = { name: ‘zs’ } 存的是地址/引用, 复杂类型的数据在传递参数时 传递的是引用地址, 将来修改时会互相影响的!!
js中的垃圾回收?
前置理解: 将来浏览器对于不会再次使用的内存空间 是需要控制回收的,
核心问题: 这个内存到底是不是垃圾??? => 引用计数法 标记清除法
- 引用计数(ie): 是有问题的!!!
浏览器在分配内存时, 当发现这块内存有一个引用指向他, 就会记录下来这块内存的引用数, 只有当引用数为0时, 才会被确认为垃圾!
无法解决 循环引用 的问题!!! 见图, 引用计数的角度来说, 这块内存确实还有人用, 不认定为垃圾, 不能回收(本应该被回收)
- 标记清除(主流浏览器 google firefox):
从根部(全局)出发, 如果找不到这个内存, 就会被标记为"无法到达的对象", 就被认定为垃圾!!!
可以解决 循环引用 的问题!!!
作用域链?
共3种; 全局作用域 局部作用域 块级作用域
作用域存在嵌套的情况, 变量访问规则(就近访问): 从最近的作用域开始查找, 找到了直接用, 没找到往外层找,… 一直找到全局,
变量访问的链式结构 称为 作用域链
什么是闭包?
函数和声明该函数的词法环境的组合
通俗一点: 内层函数访问外层函数的变量
闭包的优势: 缓存数据, 私有化数据
闭包的劣势: 如果不好好处理, 内存泄露(应该释放的内存没被释放)总结: 因为内层函数访问了外层函数的变量, 如果内层函数被return出去, 将来这个内层函数会被缓存, 同时这个函数中用到的变量也会被缓存, 从而实现数据缓存 数据私有化; 伴随着内存泄露(重置null即可)
原型 与 原型链
每一个构造函数 都有自己的原型对象, 这个原型对象上所有的属性和方法 都可以供实例访问
原型的一家: 构造函数 原型 实例 的关系 原型链: 每个实例都有自己的原型, 原型本身也是个对象, 他也有自己的原型, … 一层层的链式结构 => 原型链
属性的查找规则: 就近查找( obj.hh 从自己本身开始查找, 如果找不到, 去找他的原型, … )为什么需要原型???
如果没有原型, 所有实例的方法都需要额外分配内存, 这样浪费内存,把公共的方法统一放在一个地方, 供所有实例使用 => 原型的意义
js的继承
原型继承 组合式继承 寄生组合式继承 3种
原型继承 => 换原型
Student.prototype = new Person()
组合式继承 => 换原型 + 借调父类构造函数
Student.prototype = new Person() 继承了父类的方法
Student构造函数内部 Person.call(this, …) 借父构造函数初始化自己实例的属性 继承了父类属性寄生组合式继承 => 换原型 + 借调父类构造函数
Student.prototype = new Person()
Student构造函数内部 Person.call(this, …)之前替换原型 需要通过new 父类构造函数得到实例(new做了很多事)
Object.create(obj) 基于传入的对象, 得到一个新对象, 新对象的__proto__直接指向传入对象Student.prototype = Object.create(Person.prototype)
这一步省略了new的过程, 提升了性能class Student extends Person {}
判断一个数据是否为数组? => 数组的方法
Array.isArray(数据) true 是数组; false 不是数组
Object.prototype.toString.call(数据) ‘[object Number]’ ‘[object Object]’ ‘[object Array]’ …
数组去重?
- new Set方法
[...new Set(arr)]
- 准备一个新的空数组, 遍历老数组, 每遍历到一项, 先判断这个数据是否在新数组中存在了, 不存在 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指向问题?
- 默认绑定 fn() => window
- 隐式绑定 obj.fn() => 调用者
- 显式绑定 call apply bind => 第一个参数
- new绑定 => 创建的实例本身
- 箭头函数 不存在this => 外层作用域的this
Promise是什么? 构造函数 异步代码的容器
三个状态 等待中pending 失败rejected 成功fulfilled
new Promise((resolve, reject) => {
… 封装异步代码
结束之后需要手动修改状态
resolve() => 修改为成功状态 => .then
reject() => 修改为失败状态 => .catch
})状态凝固 => 一旦状态变化了, 将不能再次改变状态
手写promise
// 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加密方案:
非对称加密 + 对称加密 两者结合
将来数据传输必须以 对称加密 为主!!! 但是容易一开始泄露对称加密的 密钥
用 非对称加密 传输 对称加密的密钥
- 数据真正传输 还是 对称加密 数据传输效率得到保证
- 非对称加密 传输 密钥 数据安全性得到保证
数字证书: 加密签发公钥
将来你访问一个网站, 你希望得到服务端的响应数据, 数据是被对称加密 加密出来的!! 必须拥有密钥
密钥必须让服务端给你, 通过非对称加密给你,初步互通消息时, 客户端发送请求, 得到数字证书, 基于数字证书中的公钥 解密出 对称加密的密钥,
就可以解密传输的数据, 正常通信了…数字证书(权威的CA机构颁发): 包含网站基本信息, 到期时间, 非对称加密的公钥 ,
数字签名: 防证书被篡改
数字证书:
网站: xxxx.xxx.xxxx
公钥: sdafasdfasdfasdfasdfsadfsadfgdsg
到期时间: 2040年10月20号
签发机构: xxx机构
签名: xxdaddsafsadhgdfhfldghkdfghdfg ==>> 将网站所有信息通过hash加密成签名
http常见的加密方案?
- 对称加密: 加密解密同一个密钥(对称) 可逆的过程
- 优点: 加密效率很高, 加密速度快, 计算量小
- 缺点: 如果一开始密钥就泄露了, 安全性完全无法得到保障!!!
- 非对称加密: 有两把钥匙 公钥 私钥 (公钥加密的数据私钥解密 私钥加密的数据公钥解密)
- 优点: 安全性得到一定的保障
- 缺点: 加密解密效率低 慢, 计算量大
例如:我们想去gitee提交信息, 本地生产了两把公钥 私钥, 将公钥给gitee
我(私钥加密) >> gitee(公钥解密)
我(私钥解密) << gitee(公钥加密)
- 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 具体发生了什么???
- dns解析: 先拿着你输入的域名, 去找真正的ip地址
- 每个人的电脑上都有一个文件 hosts, 记录了一些域名和ip映射关系
- 会优先去本机hosts文件中去获取ip, 如果没有
- 去找公网dns域名服务器, 获取ip
www.jd.com => 58.242.151.131
根据ip地址找到对应的服务器, 需要建立tcp连接, 三次握手!!!
成功建立tcp连接后, 进行http请求
服务器响应http请求, 浏览器得到网站的首页 index.html
浏览器解析html页面时, 解析到script link img, 会再发请求 获取 js文件/css文件/图片资源
浏览器渲染完整的网页给用户
http服务完成后, 关闭tcp连接, 四次挥手!!!
三次握手?? 四次挥手??
三次握手 四次挥手 ==>> 体现出连接与断开的谨慎
三次握手 => 想要让双方确认收发消息的能力!!!
- 第一次握手 客户端往服务端发消息, 你好, 在么? 能听到么??
服务器能确认: 服务器可以收消息的能力 客户端有发消息的能力- 第二次握手 服务端回消息给客户端 在的! 你能听到我说话么?
客户端确认: 客户端有发消息的能力, 客户端能收消息; 服务端能发消息 服务端能收消息- 第三次握手: 客户端再回消息给服务端 我能听到
服务端确认: 客户端能发消息 能收消息 服务端能发消息 能收消息四次挥手 => 为了保证数据传输的完整性
- 第一次挥手 客户端发消息给服务端 服务端 你这个数据传完了么?
- 第二次挥手 服务端先回一个消息 你等我一会 我检查一下
- 第三次挥手 服务端回消息 确实没有了, 都说完了, 你可以走了
- 第四次挥手 客户端回消息 那我走了 再见!!!
缓存
大类: 数据库缓存 服务器缓存 浏览器缓存
浏览器缓存: http缓存 + Cookie/localStorage/sessionStorage/websql/IndexedDBhttp缓存的必要性: 每次重复的资源 希望直接走缓存, 不想每次发请求 => 优化网页加载的效率
- 强缓存
- 协商缓存
强缓存: 类似于食品的过期时间
将来第一次请求服务器资源时, 服务器会正常响应给你一张图片, 同时会告诉你这张图片的有效期,
expires: Sun, 25 Jan 2032 09:31:06 GMT (到期时间-绝对时间), 将来处于有效期内 直接走强缓存,
从缓存中获取该图片, 不会再发请求expires将来会和本机时间对比, 本机时间是可以改的!! 有漏洞
cache-control: max-age=315360000 相对时间 315360000s = 10年 从你拿到该资源后, 10年后必过期
cache-control为主 expires为辅
一旦强缓存失效(未命中强缓存), 这个时候 该资源不能再次使用!!, 必须要发请求问服务端要了
图片这种资源很少会更新, 不存在过期了不能用了!!!
- 这次发请求 会带上之前过期的图片资源, 问服务端 哥们 还能用么?? 服务端一检查, 发现没更新,
此时不需要响应新图片, 直接告诉客户端走缓存, 更新过期时间 304 => 协商缓存成功- 将来如果服务器发现图片更新了, 会直接响应一张新图片(自带新的过期时间), => 协商缓存失败, 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次
- 浏览器是如何进行界面渲染的?
- 解析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(视图数据) 既能操作数据 也能操作视图
- 数据变了, 操作视图自动更新
- 视图变了, 操作数据自动更新
- 双向数据绑定的原理?
- 如何知道数据变了?
Vue2 Object.defineProperty
Vue3 Proxy- 如何知道视图变了?
@change @input
Vue2 和 Vue3 中监视数据的区别
- Vue2 Object.defineProperty 劫持数据
针对于每个属性去劫持, 如果数据复杂, 需要递归劫持, 成本高, 效率低
对于数组数据的劫持/监视, 有问题 ==>> $set- Vue3 Proxy
proxy对于整个对象数据的劫持 对象内部的任意属性发生变化 都会经过外层的proxy, 无需递归, 效率高
proxy对于数组数据的更新也没问题
Vue的响应式系统? => 观察者模式(一对多的设计模式)
响应式: 数据变化了, 会通知到所有用到该数据的视图自动更新
观察者模式 目的在于 需要通知对应的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组件通信?
- 父传子 子传父
父传子: 父组件给子组件标签上以添加属性的方式绑数据; 子组件内部通过props接收<son title="123" :num="456" />
子传父: 子组件内部通过$emit触发自定义事件; 父组件中需要给子组件标签上注册对应的自定义事件, 提供处理函数
```js
this.$emit(‘xx’, 12) <son @xx=“fn” /> fn(val) { … }
```
- 事件总线(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) { ... })
provide inject 用于某个组件共享数据/方法 给子孙后代组件使用
vue2中有这个语法 但是不好用,
vue3中增强了提供数据
provide('val', 123) provide('fn', () => { ... 某个数据更新的代码 })
注入数据使用
const num = inject('val') const fn = inject('fn') fn('123')
$refs $children $parent => 可以拿到组件
<Hello ref='hello' /> // 内部通过data提供了一个 money(组件自己的) this.money this.$refs.hello.money this.$children[0] // 获取组件中 使用到的第0个组件 $parent // 获取父组件
- $attrs $listeners 使用场景: 有很多数据要隔代传
$attrs 批量获取数据 向下传递 $listeners 批量向上传递自定义事件
- Vuex
- state 提供状态
- mutations 提供修改状态的方法(同步的)
- actions 提供消化异步操作的方法 - 不能直接在action中修改状态(异步操作结束后提交mutation)
- getters 类似计算属性
- modules 分模块
namespaced: true 命名空间this.$store.commit('mutation名', 123) // 页面中触发mutation 同步派发任务 this.$store.dispatch('action名', 123) // 页面中触发action 异步派发任务
pinia 官方推荐的vue3使用的状态管理工具
提供状态
state() { return { count: 0, money: 100 } }
actions用于修改状态(同步异步都可以) 修改状态 获取状态 直接通过this
actions: { fn() { this.money = 1000 }, fnAsync() { const res = await Api() this.count = res.data.data } }
getters
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加速(花钱)