前端面试题 持续更新

目录

目录

1.深拷贝和浅拷贝 如何实现

2.图片懒加载

3.自定义指令

4.全局注册组件方法

5.页面访问控制如何实现

6.如何实现权限控制

路由级别权限

按钮级别的权限

7.对promise的理解

8.new的执行过程

9..原型链

10.hash和history路由的异同

11 网页从输入url到呈现的过程

12 路由缓存问题

13 对REM的理解

14 对vuex的理解

15 对事件委托的理解

16 对防抖和节流的理解

17 call,apply bind的异同

18 对箭头函数的理解

19 evenloop 事件循环

20 数据类型检测方法

21 伪数组转真数组的方法

22 函数传参简单数据类型和复杂数据类型

23 watch和computed的区别 

24 EventLoop事件循环机制

 25 对闭包的理解

26 localStorage ,sessionStorage和cookie的差异

27 说一下对vuex的理解

28 vue哪些操作数据不是响应式的

 29 hash 和 history的异同

30 父传子的方法

 31 你会在 Vue 的哪个生命周期钩子里面发请求,为什么?

32 v-of 和v-in 的区别

33 v-for和v-if可以同时使用吗

34 打包减少体积的的方法

35 vue3的优点 

1.深拷贝和浅拷贝 如何实现

赋值   浅拷贝 深拷贝

  • 浅拷贝 只拷贝对象的第一层,如果对象还有内容的话,其实拷贝是一个引用地址,对此对象(对象的对象)的修改会相互影响
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象。也就是递归浅拷贝的过程

区别是 浅拷贝会相互影响[修改对象中的复杂数据类型中的内容会相互影响]   

深拷贝  改变新对象不会影响原对象,他们之间不会互相影响.

const newObj = {}
for (let attr in obj) {
   
   

newObj[attr] = obj[attr]

}

// 这叫改内容

// newObj.info.name = 'yyy'

// 这叫改引用地址,不会影响原来的

newObj.info = { name: 'yyy' }

console.log(obj.info.name) // 'xxx'

方法  浅拷贝的方法

(1) object.assign() 

//浅拷贝
let obj1 = { name: '张三', action: { say: 'hi'};
let obj2 = Object.assign({}, obj1);
obj2.name = '李四';
obj2.action.say = 'hello'
console.log('obj1',obj1) 
// obj1 { name: '张三', action: { say: 'hello'}
console.log('obj2',obj2) 
// obj2 { name: '李四', action: { say: 'hello'}

 (2) 展开运算符...

//浅拷贝
let obj1 = { name: '张三', action: { say: 'hi'};
let obj2 = {... obj1};
obj2.name = '李四';
obj2.action.say = 'hello'
console.log('obj1',obj1) 
// obj1 { name: '张三', action: { say: 'hello'}
console.log('obj2',obj2) 
// obj2 { name: '李四', action: { say: 'hello'}

concat() 和slice()也属于浅拷贝

深拷贝的方法

JSON.parse(JSON.stringify())

//深拷贝
let obj1 = { name: '张三', action: { say: 'hi'};
let obj2 = JSON.parse(JSON.stringify(obj1));
obj2.name = '李四';
obj2.action.say = 'hello'
console.log('obj1',obj1) 
// obj1 { name: '张三', action: { say: 'hi'}
console.log('obj2',obj2) 
// obj2 { name: '李四', action: { say: 'hello'}

2.图片懒加载

如果图片进入可视区域,就把图片上装地址的某个属性值给图片真实的src

图片顶部距离窗口数值 小于或等于 可视区文档的高度 说明进入了可视区域

getBoundingClientRect()用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。 

<div class="imgBox">
  <img src="" data-img="https://www.itcast.cn/images/newslide/homepageandphone/20224109184146299.jpg" alt="" />
</div>
<script>
  const oImg = document.querySelector('img')

  window.addEventListener('scroll', function () {
    // 图片顶部距离窗口数值
    // 可视区文档的高度
    if (oImg.getBoundingClientRect().top <= document.documentElement.clientHeight)
 oImg.src = oImg.dataset.img
  })
</script>

图片进入页面的可视区域时,才加载图片   通过导入useIntersectionObserver方法

 通过 isIntersection 判断是否进入可视区  当图片进入可视区后把url给 图片真正的src属性

通过浏览器提供的 IntersectionObserver 对象创建一个实例,调用实例的 observe 方法可以观测某个 img 元素;

在 IntersectionObserver 的参数回调里面可以通过 isIntersecting 属性来判断这个 img 元素是否进入可视区;

如果进入就把 img 元素上装地址的某个属性给图片真正的 src 属性。

<div class="imgBox">
  <img src="" data-img="https://www.itcast.cn/images/newslide/homepageandphone/20224109184146299.jpg" alt="" />
</div>
<script>
  const oImg = document.querySelector('img')
开始观察
  const observer = new IntersectionObserver(([{ isIntersecting }]) => {
    if (isIntersecting) {
      oImg.src = oImg.dataset.img
//停止观察
      observer.unobserve(oImg)
    }
  })
  observer.observe(oImg)
</script>

3.自定义指令

全局注册指令

全局注册指令需要使用Vue.directive接口 

局部注册指令

对于局部注册 我们需要在钩子函数directives中声明

4.全局注册组件方法

一  直接注册

1.新建文件夹   2.在文件夹下建立 index.vue 3.在main.js中 引入并注册为全局组件

 二 注册为插件

Vue官方提供的插件有 Vue Router、Vuex和Vue 服务端渲染三个 Vue.use可以接受一个对象,Vue.use(obj) 对象obj中需要一个 install 函数 在 Vue.use(obj)时,会自动调用该 install 函数,并传入到 Vue构造器

1. 新建文件夹  2.在文件夹中建立index.vue和index.js  3.在index.js中 引入文件 通过install 函数来注册和导出插件 4.在main.js中 use使用插件

src/components文件夹下新建yyy文件夹,在yyy文件夹下新建index.vue文件

<template>
	<div>
		通过注册插件的方式,注册全局组件
	</div>
</template>

5.页面访问控制如何实现

在全局前置路由守卫(beforeEach)里面设置

 如果有token就放行,如果没有token 先判断是否在白名单中,如果在白名单中直接放行.如果不在白名单中就返回登录页  处理token失效问题 在响应拦截器中判断状态码401 是就跳转到登录页

6.如何实现权限控制

路由级别权限

1.用户登录后 ,后端返回当前用户的标识.

2.前端拿到这个标识后,筛选出 有权限的路由(动态路由)

3.接下来做了两件事

3.1.页面级别权限     通过addrouters或者 addrouter将路由添加在路由表中,一旦添加到这个路由表中 这个用户也就有了访问某个路由的权限.

3.2.把筛选后的 路由也添加一份到vuex中 为了后面对左侧菜单栏的渲染  

按钮级别的权限

封装一个全局的指令 这个方法只做一件事 ,接受一个功能标识,看一下这个标识在不在后端返回的数据列表中,如果有的话就返回一个true ,如果没有的话就返回一个false .

在做权限控制的地方,调用这个方法并传递过去当前的功能标识,根据这个方法泛会所true还是false,对当前的按钮进行隐藏或显示操作 禁用或启用操作

7.对promise的理解

是什么

promise是ES6新增语法 用来解决回调地狱

怎么用 

一般作为一个构造函数来使用,需要new一下来创建一个Promise实例 ,它里面有三种状态 分别是pending(进行中)  fulfilled(已成功) rejected(已失败),成功会触发then 失败会触发catch,finally是永远都会触发

Promise的静态方法

1.Promise.race():接收多个Promise实例,可以得到最先处理完毕的结果(结果可能是成功的也可能是失败的)

2.Promise.all():接收多个Promise实例,都成功了会触发then,有一个失败的就会出发catch

3.Promise.any():接收多个Promise实例,可以得到最先处理成功的结果,都失败才会触发catch

解决问题

解决了回调地狱的问题

替代方法 

Promise虽然解决了回调地狱问题,但是不能简化代码,所以一般工作中,我会配合async/await来使用

8.new的执行过程

1.创建一个空对象

2.this指向这个对象

3.执行构造函数的代码,给空对象增加属性方法

4.返回这个对象

9..原型链

原型链:多个对象之间通过 `__proto__` 链接起来的这种关系就是原型链。

10.hash和history路由的异同

兼容性

还是兼容到IE8 history兼容到IE10

实现原理

hash模式是通过监听onhasgchange事件做的处理

history模式是利用H5新增的History相关的API实现例如onpopstate事件,pushState,replaceState等

刷新页面时,对于后端的表现

刷新页面是,还是地址也就是(#后面的内容)不会作为资源发送到服务端,后端拿到的都是/这个地址

https://www.baidu.com/#/news
https://www.baidu.com/#/user

刷新页面时,history地址对于服务端来说是一个新的请求,后端拿到的是不同的请求地址,也就意味着需要服务端对这些请求做处理,否则会 404。

https://www.baidu.com/news
https://www.baidu.com/user

 做什么处理呢?匹配到相关 GET 请求,统一返回 index.htmlindex.html 加载的有路由相关的代码,所以也就转换为由前端路由来处理啦。

11 网页从输入url到呈现的过程

1.将图片的域名解析为ip地址

2.http三次握手建立起安全的网络协议,保证http传输的可靠性

3. 建立起http连接

         (1) 客户端发送请求

          (2) 服务器处理请求

          (3)服务器响应请求

4.渲染引擎 

         (1)解析html :将得到dom树

          (2)解析css :将得到样式树

          (3)将dom树 + 样式树 合并成 渲染树

          (4)绘制渲染树

          (5)呈现页面

项目开发中遇到的问题

当项目打包上线后,需要修改一些数据时,用户需要刷新才能获取到最新的数据

如何使用户不刷新也能拿到最新的数据

这和浏览器的缓存机制和webpack的打包机制有关

hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。

1.在浏览器缓存机制中,获取数据分为强缓存和协商缓存,协商缓存的数据保存在浏览器缓存中,用户获取数据时 只能从以及保存的缓存中获取文件名未修改,即使里面内容发生了修改,获取数据时还是从缓存中修改,无法获取到操作后的数据

2. 在webpack打包机制中 配置output出口时配置 filename 来打包文件名,此时我们只需要将文件吗修改,便可以解决这一问题. 将配置改为 filename:'[name].[contexthash].js' 操作可以修改文件名

12 路由缓存问题

Vue出于对性能的考虑,对应的路由组件会被复用,也就意味着,几遍路由参数发生了变化,路由组件生命周期钩子也就只会触发一次(路由地址会发生变化),但是加载的组件是同一个,所以路由只会获取到旧数据 

 在路由出口处 加:key 检测key的变化,通过$route.fullpath

 watch监听路由地址变化,根据地址变化后的新值来发送请求

watch(()=>route.params.id,
(newId)=>{
getDetail(newId as string)
})

 vue内置方法,当路由参数发生变化时,执行钩子函数,走对应的回调,to 表示要到达的路由对象

onBeforeRouteUpdate((to)=>{
getDetail(to.params.id as string)
})

13 对REM的理解

em相对于元素自身字体大小的一个单位,rem是相对于根元素字体大小的一个单位

元素字体大小的单位

解决移动端适配问题

原理 利用媒体查询或js动态检测设备宽度,不同宽度下设置对应的根元素的字体大小,根元素字体大小发生变化,所有使用rem做单位的元素也发生变化 

开发中有使用VW/ rem做适配是对vw的模拟

14 对vuex的理解

全局的状态管理的js库

解决了非关系性组件之间的传递和数据共享的问题,其中有state mutations,actions getters

一般用来存储token和用户信息

当组件中关系比较清晰时,会利用其他方式  父子组件 在父组件中的子组件标签上利用自定义属性来传递数据 在组件中通过props来接收传递过来的数据  祖孙组价中  proviede 提供数据 inject获取数据. 

15 对事件委托的理解

把要绑定的事件绑定给他的祖先元素

原理是事件冒泡

好处;性能搞高 2.对后续新增的元素同样具有绑定的效果

16 对防抖和节流的理解

防抖 持续触发不执行,停止触发后一段时间才会执行 节流 持续触发频率变低 

防抖应用场景 input输入框事件  

节流应用场景onload下拉加载事件

17 call,apply bind的异同

1.第一个参数都是用来改变this指向

2.都可以利用后续参数来传参

不同点

call和apply会直接调用函数,而bind返回的是一个新函数

call和bind可以传递任意多个参数,而apply只能传递两个参数(第二个参数是数组或者伪数组)

call和apply是立即执行,而bind是获取到修改后的this后才执行

18 对箭头函数的理解

箭头函数是ES6新增语法

1.箭头函数没有原型对象 prototype,不能作为构造函数使用(不能被new)

2.箭头函数没有arguements ,可以使用...拿到所有实参的集合数组

3.箭头函数中的this在定义时就已经确定,取决于父级的环境

4.箭头函数不能通过call,apply,bind来修改他的this指向 

19 evenloop 事件循环

js代码分同步代码和异步代码 ,先执行同步代码后执行异步代码 

同步代码会直接在执行栈中执行,异步代码添加到异步任务队列中,同步代码执行完毕后 执行异步代码中的微观,后执行宏观任务

常见的宏观任务 settimeout定时器  fs.readFile读取文件 ajax请求 事件

常见的微观任务 async /await promise.then

20 数据类型检测方法

数据类型分为八种, 简单   number ,string ,boolean null ,undefined bigint symbol

复杂 ({}, [] ,函数 日期,正则)   

1. typeof 不能检测数组和对象   typeof的返回值是一个字符串

2. intanceof 是用来判断左边能不能通过 _proto_找到右边的原型

只有 Object.prototype 上的 toString 才能输出有规律的结果,就可以根据这个规律来判断类型

3. object.protype.toString.call()

调用这个方法的同时 改变了这个方法内部的this指向

// 6. 封装一个判断类型方法
  const isType = (data) => {
    const r = Object.prototype.toString.call(data)
    switch (r) {
      case '[object Object]':
        return '真对象'
      case '[object Array]':
        return '真数组'
      case '[object Null]':
        return '真 null'
      case '[object Undefined]':
        return '真 undefined'
      case '[object Function]':
        return '真函数'
      case '[object RegExp]':
        return '真正则'
    }
  }

  console.log(isType([]))
  console.log(isType({}))
  console.log(isType(null))
  console.log(isType(function () {}))
  console.log(isType(undefined))

对arguements 的理解

arguements :普通函数内部使用,所以实参的集合是一个伪数组

21 伪数组转真数组的方法

1.array.form

2....展开运算符

3.Array.prototype.slice

22 函数传参简单数据类型和复杂数据类型

简单数据类型传递的是值的拷贝,函数内部对参数的修改不会影响外部;

复杂数据类型传递的是引用地址,函数内部对参数内容的修改会影响外部,对参数引用的修改不会影响外部。

简单数据类型传参

let username = 'abc'

function fn(username) {
  // 简单数据类型传参传递的是值的拷贝,把这个值的拷贝给了 username 局部变量
  // 所以对这个 username 局部变量的修改当然不会影响外部的
  username = 'def'
}
fn(username)

console.log(username) // 'abc'

复杂数据类型传参的 2 种处理情况

修改内容

const obj = {
  name: 'ifer',
}

function foo(obj) {
  // 赋值数据类型传参传递的是引用地址,把这个引用地址拷贝给了 obj 局部变量,而这个 obj 引用地址指向的还是曾经的那个空间
  // 所以对这个 obj 局部变量【内容的修改】当然会影响外部的
  obj.name = 'elser'
}
foo(obj)

console.log(obj) // 'elser'

 修改引用地址

let obj = {
  name: 'ifer',
}

function foo(obj) {
  // 赋值数据类型传参传递的是引用地址,把这个引用地址拷贝给了 obj 局部变量,而这个 obj 引用地址指向的还是曾经的那个空间
  // 所以对这个 obj 局部变量【引用的修改】当然不会影响外部的
  obj = {
    name: 'elser',
  }
}
foo(obj)

console.log(obj) // 'ifer'

23 watch和computed的区别 

  1、功能上:computed是计算属性,watch是监听一个值的变化,然后执行对应的回调。

  2、是否调用缓存:computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch在每次监听的值发生变化的时候都会执行回调。

  3、是否调用return:computed中的函数必须要用return返回,watch中的函数不是必须要用return。

  4、使用场景:computed----当一个属性受多个属性影响的时候,使用computed-------购物车商品结算。watch----当一条数据影响多条数据的时候,使用watch-------搜索框。


24 EventLoop事件循环机制

EventLoop又叫做事件循环机制,是单线程语言js在运行代码时不被阻塞的一种机制

js代码分为同步代码和异步代码,当碰到同步代码时就会直接在执行栈中执行,当遇到异步代码并且实际符合时时,将异步代码加入到任务队列中执行,当执行栈中的同步代码执行完毕后,就去任务队列中把异步代码拿到执行栈中执行,这种反复轮训任务队列并吧异步代码拿到执行栈中执行的操作,就是EventLoop.(promise本身是同步 promise.then和promise.catch是异步)

异步任务

常见的宏任务:定时器,事件 ,fs.readFile读取文件 ,ajax

常见的微任务有 async/await  promise.then

执行顺序 宏=>同步=>微=>宏

先执行宏任务,如果执行宏任务期间产生了同步代码和微任务,会先执行此期间的同步代码和微任务,在执行宏任务

 25 对闭包的理解

一个函数使用了外部函数中的局部变量,使用变量的地方称之为发生了闭包现象,变量定义所在的函数我们称之为闭包函数

特性 :普通函数调用完毕,内部局部变量马上销毁

闭包函数调用完毕,会使内部形成这个闭包函数的变量age常驻内存,所以滥用闭包会造成浪费

在开发中用到闭包了吗

防抖时就用到了闭包 

26 localStorage ,sessionStorage和cookie的差异

生命周期不一样

localStorage:永久存储,除非手动删除.

sessionStorage :关闭当前页面后自动清除

cookie : 默认关闭浏览器后失效,如果设置了过期时间,则到达过期时间后才会生效

生效范围不一样

localStorage : 同域都可以共享

sessionStorage:只有当前标签页才能访问

cookie :同域下且path匹配的情况下才能访问

存储大小不一样 

localStorage :5MB

sessionStorage:5MB

cookie:4kb

操作主体不一样 localStorage和sessionStorage 只有客户端才能设置

cookie:客户端(document.cookie) 和服务端(set-Cookie)都可以设置

请求是否会携带

localStorage和sessionStorage 请求时不会自动携带

cookie:每次请求都会随着请求头带到后端

26 v-show和v-if的异同

v-show和v-if都是用来对元素进行显示和隐藏

V-Show是对css进行操作display:none和display:block  v-if是通过对Dom树的操作来控制元素

v-show有较高的初始渲染开销     v-if有较高的切换开销

27 说一下对vuex的理解

是什么 vuex是一个全局的状态管理js 库  解决了非关系型组件数据的传递和共享问题

怎么用 一般使用的时候里面有state ,mutation ,action getter 和modules

触发流程  在视图当中 通过dispatch调用action里面的请求 拿到结果之后 通过commit方法 调用mutation方法修改state里面存储的数据(响应式数据) 修改后会更新视图. 在视图中不需要异步操作时,直接通过commit 调用mutation进行修改state里面的数据 

替代方法 如果各个组件中的关系很明确的话可以通过父传子和子传父以及兄弟组件之间进行互相传值,

衍生问题 为什么mutation里面只建议写同步代码 ,写同步代码可以保证在代码执行完之后可以获取到一个确定的状态,  如果在mutations中写异步代码,在严格模式下会报错,因为监听不到状态何时变更,给调试造成困扰

28 vue哪些操作数据不是响应式的

三种情况

1.给对象后续新增的key进行赋值

data(){
  return{
     obj:{}
 }
}
this.obj.age=18

2.删除对象中的某一项

3.通过索引去修改数组的元素

为什么不是响应式的

1.给对象后续新增的key进行赋值

原因 :因为object.defineProxy是递归劫持的对象中的一个属性,如果这个属性是后续添加的就没有被劫持到

2.删除对象中的某一项

原因: Object.defineProxy不支持第删除操作的劫持

3.通过索引去修改数组的元素

原因: 出于性能的原因

怎么解决 

1.给对象后续新增的key进行赋值

可以通过Vue.set或者this.$set()

vue.set(this.obj,'age',18)

2.删除对象中的某一项

解决 :Vue.delete()或this.$delete()

vue.delete(this.obj,'age')

3.通过索引去修改数组的元素

解决:Vue.set()或this.$set()或arr.splice

Vue.set(this.arr,0,'d')

arr.splice(0,1,'d')

 29 hash 和 history的异同

兼容性

hash兼容到IE8 ,history兼容到IE10

实现原理

hash模式第通过监听onhashchange事件做的处理

history模式是利用H5新增的history相关的API实现的,例如onpopstate事件pushState

刷新页面时,对于后端的表现

刷新页面时,hash地址#后面的的内容不会作为资源发送到后端

刷新页面时,history地址对于服务端来说是一个新的请求,后端拿到的是不同的请求地址,也就意味着后端要做一些处理,否则就会404

(后端处理) 统一返回index.html加载的相关路由代码

30 父传子的方法

1.父亲通过自定义属性来传递数据,子组件通过defineprops来接收

2.父传子  v-model

 3.$attrs

 31 你会在 Vue 的哪个生命周期钩子里面发请求,为什么?

1 首先,发请求的时机肯定越早越好,那么 beforeCreate 最早;

2 但是还有一点,有时候请求前需要依赖 data 里面的数据或调用 methods 里面的方法(请求后可能也需要);

3 而 data 和 methods 都是需要实例创建完毕后(created)才具有的,所以一般我会在 created 里面发请求;

4 既保证了请求实际相对较早,又保证了可以使用 data 里面的数据或 methods 里面的方法。

不过有时候这个请求需要依赖 DOM 相关操作的话,我会选择在 mounted 里面进行,因为 mounted 阶段才能保证页面已经渲染完毕了,也就可以操作 DOM 啦。

32 v-of 和v-in 的区别

1.功能不同: for-in是编历数组的下标 for-of是遍历数组的元素

2.原型的属性: for-in会遍历原型的属性 for-of不会遍历原型的属性

3.数据类型 : for-in可以遍历Obiect类型 for-of不可以编历Obiect类型

推荐在循环对象属性的时候,使用for...in,在遍历数组的时候的时候使用for...of。

33 v-for和v-if可以同时使用吗

不可以在vue2中v-for的优先级高于v-if 还没被判断就进行了循环,一些不满足条件的数据也会被渲染,造成了资源的浪费.

在vue3中v-if的优先级高于v-for,还没有获取到数据就进行判断,会报错

34 打包减少体积的的方法

先在项目中 npm run build 进行打包

 1. 按需加载第三方库

例如ElementUI loadsh等 

a装包 

npm install babel-plugin-compotent -D

b.配置babel.config.js 查看官方文档 

2.通过externals配合使用CDN的方式进行处理

3.移除console.log

4.路由懒加载 

配置路由跳转地址 component()=>import('地址')

详见  vue项目打包优化的方法_一只小菜鸡111的博客-CSDN博客

35 vue3的优点 

(1) 性能更高     ① 相对于vue2而言 底层响应原理转换为proxy  ②  虚拟 DOM 的算法进行了优化

(2) 体积更小    删除了一些不常用的api   ①   过滤器 ② EventBus ③ 代码支持按需导入 ④配合Webpack打包工具支持 TreeShaking

(3) 对TS的支持更好    源码就是用TS重写的

(4) Composition API     ①   能把相同功能的数据和业务逻辑组合到一起,代码更容易复用和维护。

② 更适合大型项目

(5)  新特性  Fragment (不必只有一个根节点,减少了标签的嵌套) Teleport (DOM结构转移到特定的节点,方便管理) Suspense
 

猜你喜欢

转载自blog.csdn.net/weixin_68531033/article/details/125944774