Vue 面试题

340 阅读10分钟

一、vue的基本原理

- 当一个Vue实例创建时,vue会遍历data中的属性,用Object.defineProperty(vue 3.0 使用proxy)将它们转化为getter/setter,并在内部追踪其相关依赖,在属性被访问和修改时通知变化
- 
- 

二、双向数据绑定的原理

  vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProerty来劫持各个属性的settergetter,在数据变动的时候,发布消息給订阅者,触发相应的监听回调
  主要分为三个模块
  1、 对需要observe的对象,递归遍历,给每个属性添加getter,setter,当数据发生变化的时候,就会调用setter,这样就能监听到数据的变化
  2、 compile对每个指令进行编译,将其中的变量替换成数据,然后每个指令的节点绑定更新函数,添加监听数据的订阅者,一旦数据发生变化就能收到通知
  3、watcher订阅者是observe和compile的通信桥梁,主要做的事情
      - 在自身实例化的的时候,在属性订阅器中添加自己
      - 自身必须有一个update方法
      - 当属性发生变化的时候,就会发出dep.notice通知,自己再调用update方法,并触发compile的回调函数
  4、MVVM作为数据绑定的入口,整个observe、compile、watcher,objerve监听数据的变化,complie编译模版指令,达到数据发生变化视图更新,页面交互变化达到数据发生变化的双向绑定效果

三、Object.definePerporty与Proxy的区别

Object.definePerporty

   Object.definePerporty共有三个参数,第一个参数是目标对象,第二个参数是目标对象的key,第三个参数是一个对象,其内部包含对对象属性key的增删改查配置
   Object.defineProperty(obj,key,{
      value:xxx,
      writable:false,//是否可修改,默认是false
      enumerable:false,//是否可枚举,默认是false
      configurable:false//是否可删除,默认是false
    })
   通过Object.defineProperty操作对象的属性可以配置其值,是否可以枚举,是否可以修改,是否可删除

Object.defineProperty缺点

   1、对对象直接增加的属性,无法监听
   2、对数组未监听的索引以及部分数据的方法,无法监听,比如push
   3、对数据的深度监听,需要深度递归遍历整个对象,耗费性能

vue2的Object.defineProperty

  vue2使用Object.defineProperty完成数据响应式时,有几个特殊点
   - 对操作数据的方法重新并监听数据变化,指听过数据的方法push,pop,shift等操作响应式依然存在,通过索引下标直接修改书的响应式无是无法生效的
   - 对象属性时是深度监听,但是对对象新增加的属性,不具备响应式
   - 在对象和数据的响应式丢失的时候,可以通过this.$set(data,key,newValue)重新重新具备响应式
   

proxy优势

    1、完美的支持对数据各种方法和索引的拦截监听
    2、对对象的新增属性支持拦截监听
    3、vue3基于proxy也做了深度监听,但是一开始不会全部拦截对象的每一个属性,当我们访问或者操作属性时,才会动态的拦截具体属性

总体对比而言

   - proxy直接对对象新增属性监听
   - proxy直接对数组任何操作的监听
   - proxy重新来多达13种数组操作方法
   - proxy返回的是一个新的对象,我们可以直接操作新对象达到目的,而Object.defineProxy 是在原对象中直接修改
   - 因为proxy是Es6新语法,所以兼容性没Object.defineProperty好
   

四、插槽

匿名插槽

   作用: 对父组件的子元素,通过slot放在合适的位置使用
   使用:
         - 直接在组件的标签中加入要传入组件内部中要使用的内容
         - 组件内容通过slot标签进行接收

具名插槽

   作用: 对父组件的子元素进行分类,然后通过slot分发作用,将其放在合适的位置上
   使用:
         - 父组件使用template标签 使用v-slot:或者使用#进行命名
         - 子组件 slot标签 使用name属性进行接收

作用域插槽

   作用: 当数据在组件自身,但是数据生成的结果是父组件定的
   使用:
       - 在slot标签中自定义属性名,值为要传递的内容
       - 在template标签中 通过scope属性进行值的接收

组件的封装

  #### 全局组件
      在vue创建createApp实例后,通过.component属性创建,第一个值是属性名,第二参数是组件内容
      直接使用,不需要导入
  #### 局部组件
      创建一个组件后,使用的时候需要导入,并且在父组件中components 进行注册
      

vue组件传值

父传子组件通信: props

子传父: ref

兄弟组件通信: eventBus;veux

   import { bus } from '@/bus.js';
   
   在A组件中使用 bus.$emit('addition',{
       num: this.num ++
   })
   在B组件使用 bus.$on('addition',arg => {
       this.count = this.count + arg.num;
   })
   如果想要移除事件的监听,可以 bus.$off('addition',{})

vue的on,on,once,emit,emit,off

 - $on(eventName:string|Array,callbacl) 监听事件
     监听当前实例上的自定义事件,事件由vm.$emit触发,回调函数会接受所有传入的参数
  - $once(eventName,callback)
     - 监听一个自定义事件,但是只触发一次,一旦触发后,监听器自动就会被移除
     - 同一个once事件,可以绑定多个回调函数,触发后,顺序执行 
     
  - $off(eventName,callback) 移除自定义事件监听器
      - 如果没有提供参数,则移除所有的事件监听器
      - 如果只提供了事件,则移动该事件所有的监听器;
      - 如果同时提供了事件与回调,则只移除这个回调的监听器
  - $emit(eventName,[...args]) 触发事件
      触发当前实例上的事件,附加参数都会传给监听器的回调

   

Vue 常用的修饰符都有哪些

表单修饰符 v-model

   - .lazy 在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步
   - .trim 自动过滤用户输入的首空格字符,而中间的空格不会过滤
   - .number 动将用户的输入值转为数值类型,但如果这个值无法被`parseFloat`解析,则会返回原来的值

事件修饰符

   - .stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法
   - .prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法
   - .self 只当在 event.target 是当前元素自身时触发处理函数
   - .once 绑定了事件以后只能触发一次,第二次就不会触发
   - .capture 使事件触发从包含这个元素的顶层开始往下触发
   - .passive 在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
   - .native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件   

v-bind 修饰符

   .sync: 能对props进行双向绑定
       - 使用sync需要注意:使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致.
       - 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用,将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的
    父组件
        <comp :myMessage.sync= 'bar'></Comp>
    子组件
        this.$emit('update:myMessage',params)

Vue-router 有哪几种路由守卫?

全局守卫

beforeEach

   - 路由进入前全局调用。可以用来做权限验证,如果验证失败可以通过返回 `false` 或调用 `next(false)` 来取消导航。
   - 示例
       router.beforeEach((to, from, next) => {
          if (to.meta.requiresAuth) {
            // 验证用户是否登录
            if (isAuthenticated()) {
              next();
            } else {
              next({
                name: 'Login' // 跳转到登录页面
              });
            }
          } else {
            next(); // 无需验证,直接放行
          }
        });

beforeResolve:

  -  路由解析前全局调用,它在 `beforeEach` 之后和 `afterEach` 之前调用。由于它在路由解析之后被调用,因此你可以访问到 `to``from` 和即将要渲染的组件实例 `to.matched.components`

afterEach

  - 路由确认后全局调用。不会接收 `next` 函数也不会改变导航本身。

路由独享守卫

 #### beforeEach:
     -   针对某个路由配置的守卫,只有在访问这个路由时才会被调用。可以在路由配置中使用 `beforeEnter` 函数。
     - 示例
         const router = new VueRouter({
              routes: [
                {
                  path: '/some-path',
                  component: SomeComponent,
                  beforeEnter: (to, from, next) => {
                    // 路由独享的守卫逻辑
                    next();
                  }
                }
              ]
            });

组件内守卫

beforeRouteEnter /beforeRouterUpdate/ beforeRouteLeave

 -   这两个守卫是组件内的守卫,分别在路由进入和离开时调用。它们不能访问 `next` 函数,因为它们是组件内部的方法,而不是全局路由守卫。
 -   示例:
     export default {
          beforeRouteEnter(to, from, next) {
            // 在渲染该组件的对应路由被确认前调用
            // 不能访问 `this`,因为守卫执行前实例还没被创建
            next();
          },
          beforeRouteLeave(to, from, next) {
            // 在离开该组件的对应路由时调用
            // 可以访问 `this`,因为守卫执行时组件已经被创建
            next();
          }
        };
        
        

Vue 怎么实现跨域

1. 后端代理

在后端服务器上设置代理,将前端发出的跨域请求先发送到后端服务器,然后由后端服务器转发到目标服务器。这样,前端和后端服务器之间是同源的,从而避免了浏览器的跨域限制。

2. CORS(跨源资源共享)

CORS 是一种浏览器安全特性,它允许服务器在响应头中添加特定的头信息,从而告诉浏览器允许跨域请求。后端服务器需要设置如下响应头
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Requested-With

3. JSONP

JSONP是一种利用`<script>`标签不受同源策略限制的特性来实现跨域请求的技术。虽然JSONP只支持GET请求,但它可以作为一种简单的跨域通信手段。需要注意的是,JSONP存在安全风险,因为它容易受到XSS攻击

4. 在Vue中实现跨域请求

在开发环境中,如果你的Vue项目运行在本地服务器上,你可能还需要配置本地服务器以允许跨域请求。例如,如果你使用的是`vue-cli`创建的项目,可以在`vue.config.js`中配置代理:

module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: 'http://example.com',
        changeOrigin: true,
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
};

说说你对 SPA 单页面的理解,它的优缺点分别是什么?(必会)

优点:

- 实现局部刷新,而不是刷新整个页面
- 前后端分离:开发和维护更加灵活
- 前端路由: spa使用前端路由来管理应用的导航,这使得url更加友好和标签化,便于seo优化
- 状态管理: spa框架状态管理的解决方案,如vuex,redux
- 性能优化: spa可以通过缓存、懒加载等技术来优化性能,减少不必要的网络请求和加载时间

缺点

- 初始化加载时间: SPA在首次加载时可能需要加载较大的JavaScript和CSS文件,这可能导致初始加载时间较长。
- seo: 虽然现代SPA框架提供了SEO解决方案,如服务器端渲染(SSR)和预渲染(Prerendering),但SPA仍然可能面临搜索引擎优化的挑战。

如何对 Vue 首屏加载实现优化?

一、依赖模块采用第三方cdn资源(对于第三方js库的优化,分离打包)

 - 目前采用引入依赖包生产环境的js文件方式加载,直接通过window可以访问暴露出的全局变量,不必通过import引入,Vue.use去注册
 - 使用 CDN 的好处有以下几个方面:
     - 加快打包速度。分离公共库以后,每次重新打包就不会再把这些打包进 vendors 文件中。
     - CDN减轻自己服务器的访问压力,并且能实现资源的并行下载。浏览器对 src 资源的加载是并行的(执行是按照顺序的)。
  - 使用:修改vue.config.js
  module.exports = {
    ...
    externals: {
        'vue': 'Vue',
        'vuex': 'Vuex',
        'vue-router': 'VueRouter',
        'axios': 'axios',
        'element-ui': 'ELEMENT',
        'underscore' : {
          commonjs: 'underscore',
          amd: 'underscore',
          root: '_'
        },
        'jquery': {
          commonjs: 'jQuery',
          amd: 'jQuery',
          root: '$'
        }
    }    
    ...
}

二、 异步组件和懒加载方式

 - 组件在使用的时候,采用异步引入 
 - 在配置router的时候,异步引入,即路由懒加载
 - 图片懒加载:使用vue-lazyload插件
 //引入,配置vue懒加载
     
        import VueLazyload from 'vue-lazyload'

        //方法一:  没有页面加载中的图片和页面图片加载错误的图片显示
        // Vue.use(VueLazyload)

        //方法二:  显示页面图片加载中的图片和页面图片加载错误的图片
        //引入图片
        import loading from '@/assets/images/load.jpg'
        //注册图片懒加载  
        Vue.use(VueLazyload, {
          // preLoad: 1.3,
          error: '@/assets/images/error.jpg',//图片错误的替换图片路径(可以使用变量存储)
          loading: loading,//正在加载的图片路径(可以使用变量存储)
          // attempt: 1
        })
    // 使用:
      <div class="lazyLoad">
        <ul>
          <li v-for="img in arr">
            <img v-lazy="img.thumbnail_pic_s">
          </li>
        </ul>
      </div>

三、webpack开启gzip压缩文件传输模式

 - gzip压缩是一种http请求优化方式,通过减少文件体积来提高加载速度,html,js,css,都可以用它压缩,可以减少60%以上的加载速度,通过compression-webpack-plugin
 - 使用: 如果浏览器不支持这种方式压缩,也不用担心,自动会访问源代码
     const CompressionPlugin = require('compression-webpack-plugin');//引入gzip压缩插件
    module.exports = {
        plugins:[
            new CompressionPlugin({//gzip压缩配置
                filename: '[path][base].gz',
                algorithm: 'gzip',  // 压缩算法,官方默认压缩算法是gzip
                test:/\.js$|\.css$|\.html$|\.eot$|\.woff$/,// 使用gzip压缩的文件类型
                threshold:10240,//对超过10kb的数据进行压缩,默认是10240
                deleteOriginalAssets:false,//是否删除原文件
                minRatio: 0.8,  // 最小压缩比率,默认是0.8
            })
        ]
    }

四、webpack 代码分割与优化

模块拆分:  配置Webpack将代码拆分成多个小块,利用Tree Shaking、代码压缩等技术减少代码体积。这将减少初始加载所需的下载时间,提高页面加载速度。

    // vue.config.js
    module.exports = {
      configureWebpack: {
        optimization: {
          splitChunks: {
            chunks: 'all'
          }
        }
      }
    };

五、禁止生成map文件

- 在设置了productionSourceMap: false之后,就不会生成map文件,map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。也就是说map文件相当于是查看源码的一个东西。如果不需要定位问题,并且不想被看到源码,就把productionSourceMap 置为false,既可以减少包大小,也可以加密源码。

module.exports = {
    productionSourceMap: false, // 生产环境是否生成 sourceMap 文件,一般情况不建议打开
}

六、 图片资源的压缩、icon资源使用、雪碧、代码压缩

七、代码层面

- 合理使用v-if和v-show

- 合理使用watch和computed

- 使用v-for必须添加key, 最好为唯一id, 避免使用index, 且在同一个标签上,v-for不要和v-if同时使用

- 定时器的销毁。可以在beforeDestroy()生命周期内执行销毁事件;也可以使用$once这个事件侦听器,在定义定时器事件的位置来清除定时器。详细见vue官网

- 长列表性能优化

- 图片资源懒加载
- 前端接口防止重复请求实现方案-CSDN博客
- 用innerHTML代替dom操作,减少dom操作的次数,优化js性能
- 合理使用requestAnimationFrame动画代替setTimeOut
- 通过创建文档碎片 document.createDocumentFragment()-创建虚拟dom来更新dom

vue.nextTick的原理

- Vue 的 nextTick 方法允许你在 Vue 实例的数据变化之后,等待 DOM 更新完成,然后再执行某些操作。
- nextTick 的原理基于 JavaScript 的异步和事件循环机制。当你调用 `nextTick` 方法时,你提供的回调函数会被放入另一个队列中。在 DOM 更新完成后,Vue 会检查这个队列,并执行所有等待的回调函数。这意味着 `nextTick` 的回调总是在 DOM 更新之后执行

JS 内存泄漏的解决方式

内存泄漏是JavaScript开发中常见的问题,它可能导致应用程序性能下降或崩溃。为了解决内存泄漏问题,开发者可以采取以下措施:

1. 避免意外创建全局变量。

- 在非严格模式下,未声明的变量会默认成为全局对象的属性,这可能导致内存泄漏。
- 解决方法是使用varletconst声明变量,或者在JS文件开头添加'use strict'来开启严格模式2

3. 正确处理闭包

- 闭包可以导致内存泄漏,因为它们会保持对外部函数变量的引用。
- 解决方法是在不再需要闭包时,手动解除对其引用,例如closureFunction = null,并确保使用let关键字声明变量,避免在全局范围内创建变量

3. 移除不再需要的事件监听器

- 未移除的事件监听器会导致DOM元素无法被垃圾回收。
- 解决方法是使用removeEventListener来移除不再需要的事件监听器,并确保在元素被销毁时解除引用。

4. 解除DOM引用

- 保留对已删除DOM元素的引用会阻止垃圾回收。
- 解决方法是将引用设置为null,例如a = null,以确保垃圾回收器可以回收对应的DOM元素7

5.清理定时器

- 未清理的setIntervalsetTimeout定时器会导致回调函数及其内部依赖的变量不能被回收。
- 解决方法是在不再需要定时器时,使用clearIntervalclearTimeout来清除它们7

6. 处理循环引用

- 在JavaScript中,循环引用可能导致内存泄漏,特别是在使用对象和数组时。
- 解决方法是确保在不再需要对象时手动断开循环引用,或者使用`WeakMap``WeakSet`来存储对象的弱引用,使其更容易被垃圾回收、

new 操作符具体做了什么

- 创建一个新的空对象,作为要返回的对象实例
- 将构造函数的原型赋值给新对象的原型:新创建的对象会继承构造函数的原型对象的属性和方法,这样新对象就可以访问构造函数原型上定义的属性和方法
- 将构造函数的作用域赋值给新对象: 通过call,或者apply方法,将构造函数的作用域this绑定到新对象上,这样就可以执行构造函数的代码
- 返回新对象: 如果构造函数没有显示的返回其他对象,那么new操作符会隐式返回新创建的对象实例。如果构造函数显示地返回一个非对象值,则忽略该返回值,仍返回新创建的实例
-
    function mynew(fun,...args){
        ```
         // 1.创建一个新对象
         const obj = {}
         
         // 2.新对象原型指向构造函数原型对象
         obj.__proto__ = fun.prototype
         
         // 3.将构建函数的this指向新对象

         const result = Fun.apply.call(obj,args)

          // 4.根据返回值判断

         return result instanceOf Object ? result:obj
    }

  - 测试
        function Person(name, age) {
            this.name = name;
            this.age = age;
        }
        Person.prototype.say = function () {
            console.log(this.name)
        }

        let p = mynew(Person, "huihui", 123)
        console.log(p) // Person {name: "huihui", age: 123}
        p.say() // huihui

JavaScript 中如何对一个对象进行深度 clone?

方法一、使用 JSON.parse和 JSON.stringify

- 但它有一些限制,例如无法复制函数、undefined、循环引用等。

方法二、使用递归函数

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return null; // 如果是null或者undefined,直接返回
  if (typeof obj !== 'object') return obj; // 非对象直接返回
  if(typeof obj=='object'){
        if(obj instanceof Array){
            var result = [];
                for(var i=0;i<obj.length;i++){
                    result[i] = cloneObj(obj[i]);
                }
                return result;
        }else{
            var result = {};
            for(var i in obj){
                result[i] = cloneObj(obj[i]);
            }
            return result;
        }
   }else{
       return obj;
   }


}

let original = { a: 1, b: { c: 2 }, d: [3, 4] };
let clone = deepClone(original);
console.log(clone); // 输出: { a: 1, b: { c: 2 }, d: [3, 4] }

使用lodash.cloneDeep

数组去重

1. 使用Set对象

Set是一个集合数据结构,它只允许存储唯一的值。你可以利用这个特性来去重数组。

let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = [...new Set(array)];
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]

2. 使用filter 搭配 indexOf方法

你可以使用filter方法结合一个对象来检查数组中的元素是否已经存在。

let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = array.filter((item, index) => array.indexOf(item) === index);
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]

3. 使用reduce方法

reduce方法可以遍历数组并累积结果,你可以用它来创建一个新数组,其中只包含唯一的值。

let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = array.reduce((accumulator, currentValue) => {
  if (!accumulator.includes(currentValue)) {
    accumulator.push(currentValue);
  }
  return accumulator;
}, []);
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]

4. 使用for循环和splice方法

通过for循环遍历数组,并使用splice方法删除重复的元素。

复制
let array = [1, 2, 2, 3, 4, 4, 5];
for (let i = 0; i < array.length; i++) {
  for (let j = i + 1; j < array.length; j++) {
    if (array[i] === array[j]) {
      array.splice(j, 1);
      j--; // 调整索引,因为数组长度已经改变
    }
  }
}
console.log(array); // 输出: [1, 2, 3, 4, 5]

5. 一层for循环,创建一个数组 配个使用

谈谈你对 Javascript 垃圾回收机制的理解?

JavaScript的垃圾回收机制是一种自动内存管理机制,它负责释放不再使用的内存空间,以便这些空间可以被重新分配给新的变量或对象。这种机制对于防止内存泄漏和提高程序性能至关重要。

垃圾回收的基本原理

JavaScript主要依赖于以下几种垃圾回收算法:

  1. 标记-清除(Mark and Sweep)
    • 标记阶段:垃圾回收器遍历所有根对象(如全局对象、活动函数的调用栈等),并标记所有从根对象开始可达的对象。
    • 清除阶段:垃圾回收器遍历整个堆内存,清除所有未标记的对象,释放它们占用的内存。
  2. 标记-整理(Mark and Compact)
    • 这个算法在标记-清除的基础上增加了一个整理阶段,它会将所有存活的对象移动到内存的一端,以消除内存碎片并创建连续的内存空间。
  3. 复制(Copying)
    • 这种算法将内存分为两个相等的区域,每次只使用一个区域。当需要分配新对象时,它会将所有存活的对象复制到另一个区域,并释放当前区域的内存。这种方法可以避免内存碎片,但效率较低,因为它需要复制所有存活的对象。
  4. 分代回收(Generational Garbage Collection)
    • 这种算法将对象分为几个代(Generation),通常分为新生代(Young Generation)和老生代(Old Generation)。新创建的对象在新生代中,如果它们经过几次垃圾回收仍然存活,就会被移动到老生代。这种算法基于这样一个观察:大多数对象的生命周期都很短,因此频繁地回收新生代可以提高垃圾回收的效率。

垃圾回收的触发

垃圾回收不是连续进行的,而是周期性地触发。在JavaScript中,垃圾回收通常是自动的,但某些操作(如执行大量分配操作的循环)可能会触发垃圾回收。此外,一些JavaScript引擎提供了手动触发垃圾回收的API,但现代引擎已经足够智能,能够根据内存使用情况自动优化垃圾回收的时机。

内存泄漏的预防

尽管垃圾回收机制可以自动管理内存,但开发者仍然需要注意避免内存泄漏。内存泄漏通常发生在以下情况:

  • 全局变量被无意中创建或未被正确清理。
  • 未移除的事件监听器或定时器。
  • 闭包错误使用,导致变量无法被回收。
  • 循环引用,特别是当对象和DOM元素相互引用时。

为了避免这些问题,开发者应该:

  • 使用严格模式('use strict')来避免全局变量的意外创建。
  • 移除不再需要的事件监听器和定时器。
  • 正确使用闭包,确保不再需要的闭包可以被回收。
  • 避免不必要的循环引用,特别是在处理DOM元素时。

结论

JavaScript的垃圾回收机制是确保程序性能和稳定性的关键。虽然它可以帮助开发者管理内存,但开发者仍然需要了解其工作原理,并采取适当的措施来避免内存泄漏。随着JavaScript引擎的不断优化,垃圾回收的效率和智能性也在不断提高,使得开发者可以更专注于业务逻辑的实现。

Class 和普通构造函数有何区别?

    Class 在语法上更加贴合面向对象的写法
    Class 实现继承更加易读、易理解
    本质还是语法糖,使用 prototype
    class继承比构造函数继承方便

防抖 节流 代码演示

以下是防抖(debounce)和节流(throttle)的简单代码示例,展示了它们的基本用法和实现方式。

防抖(Debounce)示例

防抖函数通过延迟执行来限制事件处理函数的触发频率。只有当一定时间内没有新的事件触发时,事件处理函数才会执行。

function debounce(func, wait) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(function() {
      func.apply(context, args);
    }, wait);
  };
}

// 示例:连续输入时触发的搜索功能
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function() {
  console.log('搜索: ' + this.value);
}, 300)); // 每300毫秒触发一次搜索

节流(Throttle)示例

节流函数确保事件处理函数在特定的时间间隔内只被执行一次,无论事件触发了多少次。


function throttle(fn, interval) {
  // 1.记录上一次的开始时间
  let lastTime = 0
 
  // 2.事件触发时, 真正执行的函数
  const _throttle = function () {
 
    // 2.1.获取当前事件触发时的时间
    const nowTime = new Date().getTime()
 
    // 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长时间需要去触发函数
    const remainTime = interval - (nowTime - lastTime)
    //第一次会执行,原因是nowTime刚开始是一个很大的数字,结果为负数
    //若最后一次没能满足条件,不会执行
    if (remainTime <= 0) {
      // 2.3.真正触发函数
      fn()
      // 2.4.保留上次触发的时间
      lastTime = nowTime
    }
  }
 
  return _throttle
}

// 示例:滚动时触发的事件处理
window.addEventListener('scroll', throttle(function() {
  console.log('滚动事件触发');
}, 200)); // 每200毫秒触发一次滚动处理

在这两个示例中,我们创建了自定义的防抖和节流函数,并将其应用于实际的事件处理中。对于防抖,我们在输入框的input事件上应用了防抖函数,以避免用户每次按键时都触发搜索。对于节流,我们在窗口的scroll事件上应用了节流函数,以限制滚动事件的处理频率。

这些简单的实现可以帮助你理解防抖和节流的基本原理,并根据需要将它们应用到你的项目中。在生产环境中,你可能会使用更复杂的库(如Lodash)提供的防抖和节流函数,因为它们提供了更多的功能和更好的性能。

栈和堆的区别?

栈(Stack)和堆(Heap)是计算机内存中用于存储数据的两个重要区域,它们在内存管理、数据结构和算法实现中扮演着关键角色。尽管它们都用于存储数据,但它们在结构、用途和管理方式上有着本质的区别。

栈(Stack)

  1. 用途:栈主要用于存储局部变量和函数调用的上下文。当一个函数被调用时,一个新的栈帧(stack frame)被推入(push)到栈中,包含函数的局部变量、参数和返回地址。函数执行完毕后,这个栈帧被弹出(pop)栈。
  2. 结构:栈是后进先出(LIFO)的数据结构,即最后推入栈的元素会最先被弹出。
  3. 管理:栈的内存管理是自动的,由操作系统或运行时环境负责。当函数调用结束时,栈帧会自动释放。
  4. 大小:栈通常有大小限制,因此不能用于存储大量的数据。
  5. 访问速度:栈的访问速度较快,因为它使用连续的内存空间,且由CPU直接管理。

堆(Heap)

  1. 用途:堆用于存储动态分配的内存,如对象、数组和其他复杂的数据结构。在JavaScript中,几乎所有的对象和数组都是通过堆来分配的。
  2. 结构:堆不遵循特定的数据结构,它是一个自由的内存空间,可以动态地分配和释放内存。
  3. 管理:堆的内存管理是手动的,需要程序员通过new关键字分配内存,并使用delete操作符或垃圾回收机制来释放内存。
  4. 大小:堆的大小通常远大于栈,但受到可用系统内存的限制。
  5. 访问速度:堆的访问速度较慢,因为它可能包含内存碎片,且分配和释放操作相对复杂。

总结

栈和堆是两种不同的内存分配策略,它们各自适用于不同的场景。栈适用于存储生命周期确定的局部变量和函数调用上下文,而堆适用于存储生命周期不确定的动态数据。在编程时,了解栈和堆的区别对于优化内存使用、避免内存泄漏和提高程序性能至关重要。在JavaScript中,开发者通常不需要直接管理内存,因为垃圾回收机制会自动处理堆内存的分配和释放。然而,理解栈和堆的工作原理仍然有助于编写更高效和更可靠的代码。

var、let、 const

var 声明的变量具有函数作用域或全局作用域,有变量提升 let const 只有块级作用域,没有变量提升 letconst 都是 JavaScript 中的声明变量的关键字,它们都具有块级作用域的特性。这意味着它们声明的变量只在包含它们的代码块(通常是一个括号块或者函数体)内部可见和可用。

块级作用域(Block Scope)

块级作用域是指变量的作用域被限定在一个特定的代码块内,例如 if 语句、for 循环、while 循环、函数体等。这种作用域的概念与其他语言中的作用域规则相似,它有助于避免变量名冲突和意外的全局变量污染。

手写promise

new Promise((resolve,reject)=> {
    resolve('petra')

}).then((value1)=> {},(value2)=>{})
function MyPromise(executor){
    this.status = 'pending'
    this.value = ''
    this.reason=''

    const resolve= (value) => {
        if(this.status === 'pending') {
            this.status = 'fulfilled'
            this.value = value
        }

    }
    const reject = (reason)=> {
        if(this.status === 'pending') {
            this.status = 'rejected'
            this.reason = reason
        }

    }
    try {
        executor(resolve,reject)
    } catch (error) {
        reject(error)
    }


}
MyPromise.prototype.then = (onFulFilled,onRejected)=> {
    if(typeof onFulFilled !== 'function') {
        onFulFilled = (value) => value
    }
    if(typeof onRejected !== 'function') {
        onRejected = (value) => onRejected
    }

    if(this.value  === 'fulfilled') {
        onFulFilled(this.value)

    }
    if(this.value === 'rejected') {
        onRejected(this.reason)
    }

    // 如果promise还在pending中 
    let result ;
    if(this.status === 'pending') {
        result = this
    }

    setTimeout(()=> {
        if (this.status === 'fulfilled' && result === this) {
            onFulfilled(this.value);
        } else if (this.status === 'rejected' && result === this) {
            onRejected(this.reason);
        }
    },0)


}

git 撤销 的几个命令

在Git中,撤销更改是一个常见的操作,可以通过几个不同的命令来实现。以下是一些常用的Git撤销命令及其用途:

  1. git checkout: 这个命令可以用来撤销工作目录中的修改。当你想要撤销对某个文件的修改,可以使用:

    git checkout -- <file>
    

    这将会用最后一次提交的版本覆盖工作目录中的文件,撤销所有未暂存的更改。

  2. git reset: 这个命令用于撤销暂存区(索引区)中的更改。有两种模式:软回退(soft)和混合回退(mixed)。

    • 软回退(--soft): 将暂存区的更改回退到工作目录,但不会撤销工作目录中的修改。

      git reset --soft HEAD^
      
    • 混合回退(--mixed,这是默认模式): 将暂存区和工作目录的更改都回退到上一次提交。

      git reset --mixed HEAD^
      
    • 硬回退(--hard): 将暂存区、工作目录和本地仓库的更改都回退到指定的提交。这将会丢失所有后续更改,因此使用时需要谨慎。

      git reset --hard <commit-hash>
      
  3. git revert: 这个命令用于创建一个新的提交,它撤销指定提交的更改。这是一个安全的操作,因为它不会重写历史记录。

    git revert <commit-hash>
    

    如果需要撤销一系列的提交,可以使用:

    git revert <commit-hash>^..<commit-hash>
    
  4. git clean: 这个命令用于删除未跟踪的文件和目录,即那些从未被Git添加过的文件。

    git clean
    

    如果你想删除特定目录下的未跟踪文件,可以使用:

    git clean -d <directory>
    

    加上-f--force参数可以强制删除未跟踪的文件,即使它们被.gitignore文件排除。

    git clean -f
    

使用这些命令时,需要根据你的具体需求和当前的Git状态来选择合适的命令。例如,如果你只是想要撤销工作目录中的修改,git checkout可能是一个好的选择。如果你想要撤销暂存区的更改,git reset可能更适合。而如果你需要撤销已经提交的更改,并且想要保持历史记录的完整性,git revert是最佳选择。最后,git clean用于清理工作目录中的未跟踪文件。

cookie、session、localStorage、sessionStorage区别

区分cookie与session

cookie的作用是在客户端保持状态,比如登录状态。它被存储在本地硬盘或内存里,并且在发送http请求的时候会被放进请求头中参与通信。每个cookie最大为4k,每个域名可以拥有的cookie数量在不同浏览器中是不同的,但都多于20个,早期的20个限制已经不存在了。
session的作用是在服务器端保持状态,它被存储在服务器上。session被创建的时候会生成一个sessionid,它被存储在cookie中用来访问session。由于关闭浏览器不会导致session被删,迫使服务器为session设置了失效时间。

2.共同点:

    都是保存在浏览器端、且同源的 

3.区别

    1、cookie数据浏览器自动携带,而sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。
    2、存储大小限制也不同,cookie数据不能超过4K,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大 。
    3、数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效;                         localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie:只在设置的cookie过期时间之前有效,没有设置的话浏览器关闭就会失效。
    4、作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localstorage在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的 。
    5、web Storage支持事件通知机制,可以将数据更新的通知发送给监听者 。
    6、web Storage的api接口使用更方便。

热更新原理

热更新是指在开发过程中,当你修改了源代码后,Webpack会自动将修改后的代码注入到运行中的应用程序中,而无需刷新整个页面或重新加载整个应用程序。这使得开发者可以更快地看到修改的结果,提高开发效率。

工作原理

  1. 在Webpack配置中,你需要设置devServer.hottrue,以启用热模块替换功能。

  2. 当你启动Webpack Dev Server时,它会创建一个Socket服务器,用于与浏览器建立WebSocket连接。

  3. 在浏览器中访问应用程序时,Webpack Dev Server会将一个运行时脚本(runtime script)注入到页面中。这个脚本会建立与Webpack Dev Server的WebSocket连接,以便实时接收来自服务器的更新通知。

  4. 当你修改了源代码并保存时,Webpack会监听文件系统的变化,并编译修改后的模块。

  5. 当编译完成后,Webpack会将更新的模块信息发送给Webpack Dev Server。

  6. Webpack Dev Server会通过WebSocket连接将更新的模块信息推送给浏览器。

  7. 浏览器接收到更新的模块信息后,会使用Webpack的HMR Runtime(热模块替换运行时)来处理这些更新。HMR Runtime会根据模块的更新信息,将新的模块代码插入到应用程序中,而无需重新加载整个页面。

webpack 来优化前端性能

1. 压缩和混淆代码

使用TerserPlugin可以压缩JavaScript代码,减少文件大小。

复制
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [new TerserPlugin()],
  },
};

2. 代码分割

使用Webpack的splitChunks功能,将第三方库和应用程序代码分离,实现按需加载。

复制
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    },
  },
};

3. Tree Shaking:启用Tree Shaking移除未使用的代码。

复制
module.exports = {
  optimization: {
    usedExports: true,
  },
};

4. 使用CDN: 将常用的库和框架通过CDN引入,减少从服务器加载的时间。

// webpack.config.js
module.exports = {
  // ...
  output: {
    // ...
    publicPath: 'https://cdn.example.com/assets/',
  },
};

5. 图片优化:使用url-loaderimage-webpack-loader来压缩图片。

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpe?g|gif|svg)$/i,        
        use: [          
            {    
                loader: 'url-loader',            
                options: {     
                    limit: 8192, // 小于8KB的图片转Base64            
                 },          
             },          
             {            
                 loader: 'image-webpack-loader',            
                 options: {             
                     mozjpeg: {   
                         progressive: true,                
                         quality: 65,              
                     },             
                         // 其他图片处理选项...           
                  },          
               },       
               
               
               ],      
           },    
      ],  
    },
 };

6. 缓存优化使用cache-loader来缓存loader的结果,加快重新构建的速度。

    {  
        module:
             {    
                 rules: [      
                     {        
                         test: /\.js$/,
                         use: ['cache-loader', 'babel-loader'],
                     },
                 ],
             },
   };

7. 避免不必要的打包

使用externals配置排除不需要打包的依赖。

复制
module.exports = {
  externals: {
    react: 'React',
    'react-dom': 'ReactDOM',
  },
};

请详细说明一下 Babel 编译的原理是什么?

- babel 首先将js 转换成语法树,
- 然后遍历将语法树,并根据预设的规则对语法树进行变换,它以嵌套的节点形式展示优化的代码的结果。
- 在将转换后的语法树进行生成阶段之前,在这个阶段,babel会遍历转换后的语法树,然后将其生成新的源代码

怎么实现 webpack 的按需加载?

Webpack 的按需加载(也称为懒加载或代码分割)允许你将应用程序分割成多个独立的代码块,并在运行时按需加载它们。这样可以提高应用程序的初始加载速度,因为它只加载初始渲染所需的最小代码集。以下是实现按需加载的几种方法:

1. 使用 import() 语法

在 JavaScript 中使用 import() 函数来动态地导入模块。Webpack 会将这些动态导入转换为单独的代码块,并在运行时异步加载它们。

// 假设我们有一个 `loadableComponent.js` 文件
// 使用动态导入来按需加载组件
import('./loadableComponent.js').then(LoadableComponent => {
  // 当模块加载完成后,你可以使用 LoadableComponent
  console.log(LoadableComponent);
});

2. 配置 splitChunks 插件

在 Webpack 配置中使用 optimization.splitChunks 选项来分割代码。这可以帮助你将第三方库和公共模块提取到单独的文件中,从而实现更好的按需加载。

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

3. 使用 React.lazy 和 React.Suspense

对于 React 应用程序,可以使用 React.lazy 函数来按需加载组件,并结合 React.Suspense 来处理加载状态。

import React, { lazy, Suspense } from 'react';

// 动态导入组件
const LazyComponent = lazy(() => import('./MyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

export default App;

4. 预加载和预获取

你可以使用 React.lazy 的预加载和预获取方法来提前加载或获取模块。

import { lazy, preload, prefetch } from 'react';

// 预加载组件
preload(() => import('./MyComponent'));

// 预获取组件
prefetch('./MyComponent');

5. 路由懒加载

在 Webpack 4 中,你可以结合 HtmlWebpackPluginreact-router 来实现路由级别的懒加载。

import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Switch>
        <Route path="/" exact component={Home} />
        <Route path="/about" component={lazy(() => import('./About'))} />
      </Switch>
    </Router>
  );
}

注意事项

  • 确保服务器配置了正确的 MIME 类型,以便能够正确地提供 JavaScript 文件。
  • 考虑使用 publicPath 选项来指定资源的加载路径。
  • 如果使用 CDN,确保 CDN 能够处理按需加载的文件。
  • 对于服务端渲染(SSR),需要确保服务器能够处理按需加载的模块。

通过以上方法,你可以有效地实现 Webpack 的按需加载,从而优化应用程序的性能和用户体验。

vue生命周期

vue生命周期及执行顺序

1、系统自带生命周期一共有8个
	beforeCreate(创建前)
	created(创建后)
	beforeMount(载入前)
	mounted(载入后)
	beforeUpdate(更新前)
	updated(更新后)
	beforeDestroy(销毁前)
	destroyed(销毁后)
	
2、页面组件一旦加载执行生命周期
	beforeCreate(创建前)
	created(创建后)
	beforeMount(载入前)
	mounted(载入后)
	
3、页面组件一旦加载执行生命周期的不同点
	beforeCreate    ===》没有data没有el
	created  	    ===》有data没有el
	beforeMount     ===》有data没有el
	mounted  		===》有data有el
	
4、主要生命周期应用场景
	created  	    ===》一般发送请求
	mounted  		===》操作获取dom的插件
	
5、如果用到了vue内置的组件keep-alive,会多两个生命周期.
	activated
	deactivated

	keep-alive这个组件的作用就是能够缓存不活动的组件。
	组件进行切换的时候,默认会进行销毁,如果有需求,某个组件切换后不进行销毁,而是保存之前的状态,
	那么就可以利用keep-alive来实现,或者使用路由中的meta属性控制,meta中的keepAlive为true
	进行缓存,否侧不进行缓存。
	
	keep-alive上有两个属性:
	a:include 值为字符串或者正则表达式匹配的组件name会被缓存
	b:exclude 值为字符串或正则表达式匹配的组件name不会被缓存。

	例如:
	<keep-alive include="home">
		<router-view />
	</keep-alive>
	
	使用路由中的meta属性控制
	<keep-alive>
		<router-view v-if="$route.meta.keepAlive" />
	</keep-alive>
	<router-view v-if="!$route.meta.keepAlive" />
	
	存在的问题,因为组件被缓存,并没有被销毁,所以组件在切换的时候也就不会被重新创建,也就不会
	调用created等生命周期函数,所以此时要使用activated与deactivated来获取当前组件是否处于活动状态。
	
6、如果用到了keep-alive组件,生命周期的执行有以下变化

	第一次进入组件会执行
	beforeCreate(创建前)
	created(创建后)
	beforeMount(载入前)
	mounted(载入后)
	activated

	第二次、第三次.....进入组件会执行
	activated

父子组件生命周期及执行顺序

1、只有父组件时,页面组件一旦加载,生命周期及执行顺序
        beforeCreate(创建前)
        created(创建后)
        beforeMount(载入前)
        mounted(载入后)

2、存在父子组件时,页面组件一旦加载,生命周期及执行顺序
        父组件:beforeCreate、created、beforeMount
        子组件:beforeCreate、created、beforeMount、mounted
        父组件:mounted

vue 如何做到样式只在本组件生效,而不影响到其他组件

在style标签中 加scoped属性

vue 如何影响组件中其他组件的样式

使用样式穿透 /deep/ 或者 >>> 这两种写法都行

vue props和data的优先级

props比data高,依据源码,看先处理的谁

vuex

  1. module:
  2. state
  3. getter: 类死computer 有缓存,只有依赖发生变化才会重新执行
  4. mutation:
  5. commit
  6. vuex 是单向数据流,不允许随意改动,只能通过mutation中的方法进行改动

vue proxy 代理


module.exports = {
    derServer:{
        proxy: 'http://localhost:3000'
    }
}
axios({
    url:'/home',
    method: 'get'
})
虽然启动的项目地址是http://localhost:8000,但是经过proxy配置就会转成下面的地址
请求的地址会变成  http://localhost:3000/home

module.exports exports 和export的区别

参考文章: https://blog.csdn.net/qq_37012533/article/details/115794316?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522169235198316800215016605%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=169235198316800215016605&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduend~default-1-115794316-null-null.142%5Ev93%5EchatsearchT3_2&utm_term=CommonJS%E5%92%8CAMD&spm=1018.2226.3001.4187

  1. 总结: exports是module.exports的一个引用,指向同一个地址
  2. module.exports与export的区别
    • 使用方法不一样,commonJs是通过require导入,module.exports导出,ES6模块是通过import export实现导入导出

    • commonJs是对值的拷贝,ES6是对值的引用,指向同一个地址

    • commonJs是运行时加载,ES6模块导入导出时编译时加载