likes
comments
collection
share

「万字预警」详解前端19个经典面试题

作者站长头像
站长
· 阅读数 3

这套面试题主要涵盖了前端开发中的各个方面,包括基础知识、框架使用、性能优化、异步处理、模块化、跨域解决、手写简单算法等,希望能对正在找工作的你有帮助。

一、深浅拷贝的区别,实现方式:

浅拷贝复制对象的引用,而深拷贝创建一个新对象并递归复制所有嵌套的对象。下面详细解释它们的区别和实现方式:

浅拷贝:

  • 浅拷贝仅复制对象或数组的第一层结构,对于嵌套的对象或数组,仅复制引用而不是实际的对象或数组。
  • 常见的浅拷贝方法有 Object.assign()、扩展运算符(...)、Array.slice() 等。

示例代码:

   const originalArray = [1, 2, { a: 3 }];
   const shallowCopy = [...originalArray];

   originalArray[2].a = 99;

   console.log(shallowCopy); // [1, 2, { a: 99 }]

深拷贝:

  • 深拷贝会递归地复制对象或数组及其所有嵌套的对象或数组,生成一份完全独立的副本。
  • 深拷贝能够解决浅拷贝中引用传递的问题,确保复制的对象和原对象互不影响。
  • 常见的深拷贝方法有使用递归、JSON.parse(JSON.stringify())、第三方库如 lodash 的 _.cloneDeep() 等。

示例代码:

   const originalArray = [1, 2, { a: 3 }];
   const deepCopy = JSON.parse(JSON.stringify(originalArray));

   originalArray[2].a = 99;

   console.log(deepCopy); // [1, 2, { a: 3 }]

实现方式:

浅拷贝:

  • Object.assign()

    const shallowCopy = Object.assign({}, originalObject);
    
  • 扩展运算符(...)

    const shallowCopy = [...originalArray];
    
  • Array.slice()

    const shallowCopy = originalArray.slice();
    

深拷贝:

  • 递归实现

    function deepClone(obj) {
      if (obj === null || typeof obj !== 'object') {
        return obj;
      }
    
      const clone = Array.isArray(obj) ? [] : {};
    
      for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
          clone[key] = deepClone(obj[key]);
        }
      }
    
      return clone;
    }
    
  • JSON.parse(JSON.stringify())

    const deepCopy = JSON.parse(JSON.stringify(originalObject));
    
  • 第三方库,如 lodash

    const deepCopy = _.cloneDeep(originalObject);
    

需要注意的是,递归实现深拷贝可能会因为循环引用而陷入死循环,因此在实际使用中需要添加循环引用的判断和处理逻辑。 JSON 序列化方法也有一些局限性,比如不能复制函数、正则表达式等特殊对象。选择深拷贝方法时,需要根据具体情况进行权衡和选择。

二、letconst的区别

letconst 是 ES6 中引入的两个关键字,用于声明变量,它们之间的区别涉及到变量的可变性、作用域和声明时机。

1. 可变性:

  • let 声明的变量是可变的(mutable),可以被重新赋值。
  • const 声明的变量是不可变的(immutable),一旦赋值就不能再改变。
   let x = 10;
   x = 20; // 合法

   const y = 30;
   y = 40; // 报错,const 声明的变量不能被重新赋值

2. 作用域:

  • letconst 都遵循块级作用域,即在 {} 内声明的变量只在该块内部有效。
  • 使用 letconst 声明的变量在块级作用域外无法访问。
   if (true) {
     let blockScoped = 10;
     const alsoBlockScoped = 20;
   }

   console.log(blockScoped); // 报错,blockScoped 在此处不可见
   console.log(alsoBlockScoped); // 报错,alsoBlockScoped 在此处不可见

3. 声明时机:

  • 使用 let 声明的变量在声明之前无法访问,存在暂时性死区(Temporal Dead Zone,TDZ)。
  • 使用 const 声明的变量也存在 TDZ,但相对于 letconst 要求在声明时就进行初始化。
   console.log(a); // 报错,a 在此处未定义
   let a = 10;

   console.log(b); // 报错,b 在此处未定义
   const b = 20;

4. 全局对象属性:

  • 使用 let 声明的变量不会成为全局对象的属性。
  • 使用 const 声明的变量不会成为全局对象的属性,但如果声明同时进行初始化,会在全局对象上创建一个只读属性。
   let globalVar = 30;
   console.log(window.globalVar); // undefined

   const globalConst = 40;
   console.log(window.globalConst); // undefined

示例:

// 示例 1
let mutableVar = 10;
mutableVar = 20; // 合法

const immutableVar = 30;
immutableVar = 40; // 报错

// 示例 2
{
  let blockScopedLet = 50;
  const blockScopedConst = 60;
}
console.log(blockScopedLet); // 报错
console.log(blockScopedConst); // 报错

// 示例 3
console.log(letBeforeDeclaration); // 报错,存在暂时性死区
let letBeforeDeclaration = 70;

console.log(constBeforeDeclaration); // 报错,存在暂时性死区
const constBeforeDeclaration = 80;

// 示例 4
console.log(window.mutableVar); // undefined
console.log(window.immutableVar); // undefined

// 示例 5
const globalConstWithInit = 90;
console.log(window.globalConstWithInit); // 90
window.globalConstWithInit = 100; // 报错,const 声明的全局变量是只读的

这些区别需要在实际编码中充分理解,以确保在不同场景下正确使用 letconst

三、Vue2和Vue3的主要区别

Vue 2 和 Vue 3 是 Vue.js 框架的两个主要版本,Vue 3 在保留 Vue 2 的核心特性的基础上,进行了一系列的改进和优化。以下是它们的一些主要区别:

1. 响应式系统的改进:

  • Vue 2: 使用 Object.defineProperty 实现响应式系统,对对象的嵌套结构支持有限。
  • Vue 3: 引入了 Proxy 对象,使得对对象和数组的监听更加灵活,支持更多的嵌套结构,性能也得到了提升。

2. Composition API:

  • Vue 2: 主要使用选项式 API,将数据、计算属性、生命周期等分散在不同选项中。
  • Vue 3: 引入了 Composition API,允许开发者根据逻辑功能组织代码,提高代码的可读性和可维护性。

3. Teleport 组件:

  • Vue 2: 需要使用第三方库或手动管理 DOM 结构来实现在组件外部渲染。
  • Vue 3: 引入 Teleport 组件,可轻松实现在组件外部的渲染,例如在应用的根节点之外渲染弹出框等。

4. Fragments:

  • Vue 2: 组件必须有一个根元素。
  • Vue 3: 支持组件返回多个根元素,无需包裹额外的父元素。

5. Custom Directives:

  • Vue 2: 自定义指令的 API 有限。
  • Vue 3: 引入了 customDirectives API,使自定义指令更加灵活,可以使用函数式 API,接受更多参数。

6. 静态树提升:

  • Vue 2: 静态树的优化有限。
  • Vue 3: 引入了静态树提升,通过将静态节点提升为常量,减少了虚拟 DOM 的创建和比对,提升了渲染性能。

7. 更好的 Tree-Shaking 支持:

  • Vue 2: 由于使用的是 Object.defineProperty,Tree-Shaking 效果有限。
  • Vue 3: 使用 Proxy 改进响应式系统,更好地支持 Tree-Shaking,减少了不必要的代码体积。

8. 更小的体积:

  • Vue 3: 通过优化和拆分模块,更小的体积,提高了加载速度。

9. TypeScript 支持:

  • Vue 2: 需要使用额外的声明文件。
  • Vue 3: 对 TypeScript 有更好的原生支持,提供了更强大的类型推导。

10. 性能优化:

  • Vue 3: 通过优化虚拟 DOM 算法、提升渲染性能、减少内存占用等方面进行了多项性能优化。

这些是 Vue 2 和 Vue 3 主要的一些区别,每个版本都有其适用的场景,选择版本时需要考虑项目需求和开发团队的熟悉程度。

四、mixin的概念和优缺点:

Mixin 的概念:

Mixin 是一种在面向对象编程中用于将一个对象的属性和方法复制到另一个对象的技术。在 Vue 中,Mixin 是一种为组件提供可复用功能的方式,它允许你在多个组件之间共享相同的逻辑和代码。

优点:

  1. 代码复用: Mixin 允许将一组功能提取出来,以便在多个组件中重复使用。这有助于减少代码冗余,使代码更加可维护。

  2. 逻辑分离: 通过将不同关注点的代码组织成 Mixin,可以更好地分离逻辑。例如,将数据处理逻辑、事件处理逻辑等分离成不同的 Mixin,提高代码的清晰度。

  3. 灵活性: Mixin 提供了一种在组件中注入功能的方式,使得你可以选择性地使用某个 Mixin,从而在不同的组件中实现不同的功能组合。

  4. 解耦和: Mixin 可以帮助解耦组件,使其不过于依赖具体实现。这使得你可以更容易地更改或替换 Mixin,而不影响组件的其余部分。

缺点:

  1. 命名冲突: Mixin 中的数据、方法等都会被注入到组件中,可能导致命名冲突。如果不注意命名,可能会覆盖掉组件原本的数据或方法,导致意外的问题。

  2. 依赖顺序: Mixin 的功能注入顺序是有影响的,不同的注入顺序可能导致不同的结果。这使得 Mixin 的使用需要谨慎,并且开发人员需要了解各个 Mixin 之间的相互影响。

  3. 不透明性: 使用 Mixin 后,组件的某些功能可能来自于 Mixin,开发人员阅读代码时难以追溯到具体功能的来源,降低了代码的可读性。

  4. 全局污染: Mixin 中的数据和方法会被注入到组件中,可能造成全局污染,特别是在大型项目中,可能会出现难以追踪和调试的问题。

示例:

// 定义一个名为 `logMixin` 的 Mixin
const logMixin = {
  created() {
    console.log('Component created');
  },
  methods: {
    logMessage(message) {
      console.log(message);
    }
  }
};

// 在组件中使用 Mixin
export default {
  mixins: [logMixin],
  data() {
    return {
      message: 'Hello, Mixin!'
    };
  },
  created() {
    this.logMessage(this.message);
  }
};

上述示例中,logMixin 提供了在组件创建时打印日志的功能,并注入了 logMessage 方法。组件通过 mixins 选项使用了这个 Mixin,并在创建时调用了 logMessage 方法.

五、Promise、async/await的工作原理

Promise 的工作原理:

Promise 是 JavaScript 中用于处理异步操作的对象,它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

  • Pending(进行中): Promise 的初始状态,表示异步操作正在进行中,但并未完成。

  • Fulfilled(已成功): 异步操作成功完成,Promise 进入该状态,并提供异步操作的结果。

  • Rejected(已失败): 异步操作失败,Promise 进入该状态,并提供一个失败的原因。

Promise 的工作原理主要包括以下几个步骤:

  1. 创建 Promise 实例: 使用 new Promise() 创建一个 Promise 实例,该实例包含一个执行器函数,该函数接受两个参数 resolvereject

    const promise = new Promise((resolve, reject) => {
      // 异步操作
    });
    
  2. 异步操作执行: 在执行器函数中进行异步操作,根据操作的成功或失败情况调用 resolvereject

    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('Operation succeeded'); // 或者 reject('Operation failed');
      }, 1000);
    });
    
  3. 处理状态变化: Promise 的状态变化会触发注册的回调函数。可以通过 .then() 处理异步操作成功的情况,通过 .catch() 处理异步操作失败的情况。

    promise
      .then((result) => {
        console.log(result); // 打印 'Operation succeeded'
      })
      .catch((error) => {
        console.error(error); // 打印 'Operation failed'
      });
    

async/await 的工作原理:

async/await 是建立在 Promise 基础上的语法糖,用于更方便地处理异步操作。async 关键字用于定义一个返回 Promise 的异步函数,而 await 用于等待 Promise 解决,然后获取其结果。

  1. 定义异步函数: 使用 async 关键字定义一个异步函数。

    async function myAsyncFunction() {
      // 异步操作
    }
    
  2. await 表达式: 在异步函数中使用 await 表达式等待一个 Promise 解决,获取解决后的结果。

    async function myAsyncFunction() {
      const result = await somePromise(); // 等待 somePromise 解决
      console.log(result);
    }
    
  3. Promise 的状态: async/await 依然依赖于 Promise 的状态,如果等待的 Promise 处于 pending 状态,async 函数会暂停执行,直到 Promise 解决。

    async function example() {
      const result = await new Promise((resolve) => {
        setTimeout(() => {
          resolve('Resolved after 1000ms');
        }, 1000);
      });
    
      console.log(result); // 会在 Promise 解决后打印
    }
    
    example();
    
  4. 错误处理: 使用 try...catch 结构进行错误处理,可以捕获 await 表达式中的 Promise 被拒绝时的错误。

    async function example() {
      try {
        const result = await somePromise();
        console.log(result);
      } catch (error) {
        console.error(error); // 处理 Promise 被拒绝的情况
      }
    }
    
    example();
    

通过使用 async/await,可以使异步代码看起来更像同步代码,提高了可读性和维护性。

六、Promise.all和Promise.race的应用场景

Promise.all 的应用场景:

Promise.all 用于将多个 Promise 实例包装成一个新的 Promise 实例,等待所有的 Promise 实例都成功解决(fulfilled)或其中一个失败(rejected)。

典型应用场景:

  1. 并行请求: 在前端开发中,当需要同时请求多个接口数据并在全部请求完成后进行处理时,可以使用 Promise.all

    const fetchData = async () => {
      try {
        const [data1, data2, data3] = await Promise.all([
          fetch('api/data1'),
          fetch('api/data2'),
          fetch('api/data3'),
        ]);
    
        // 处理数据
      } catch (error) {
        // 处理错误
      }
    };
    
    fetchData();
    
  2. 多个异步任务的并行执行: 在需要执行多个异步任务,并在它们都完成后进行下一步操作时,可以使用 Promise.all

    const tasks = [task1(), task2(), task3()];
    
    try {
      await Promise.all(tasks);
      // 所有任务完成后执行下一步操作
    } catch (error) {
      // 处理错误
    }
    

Promise.race 的应用场景:

Promise.race 用于将多个 Promise 实例包装成一个新的 Promise 实例,该实例的状态取决于第一个解决(成功或失败)的 Promise。

典型应用场景:

  1. 竞速请求: 当需要从多个接口中获取数据,只需获取最快返回的数据时,可以使用 Promise.race

    const fetchRace = async () => {
      try {
        const result = await Promise.race([
          fetch('api/data1'),
          fetch('api/data2'),
          fetch('api/data3'),
        ]);
    
        // 处理第一个返回的数据
      } catch (error) {
        // 处理错误
      }
    };
    
    fetchRace();
    
  2. 超时处理: 当需要设置一个超时时间,如果在规定时间内未完成某个异步任务,则执行超时处理。

    const timeoutPromise = (promise, timeout) => {
      return Promise.race([
        promise,
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('Timeout')), timeout)
        ),
      ]);
    };
    
    const fetchData = async () => {
      try {
        const result = await timeoutPromise(fetch('api/data'), 5000);
        // 处理数据
      } catch (error) {
        // 处理超时或其他错误
      }
    };
    
    fetchData();
    

在以上场景中,Promise.all 用于等待全部 Promise 解决,而 Promise.race 用于等待第一个解决的 Promise。

七、解释一下前端性能中的"懒加载"是什么,以及如何实现懒加载

懒加载(Lazy Loading) 是一种优化前端性能的策略,它的主要思想是将页面上的某些资源(如图片、脚本、样式表等)推迟加载,只有当这些资源即将进入用户的视野或确实需要使用时才进行加载。这可以显著减少初始页面加载时所需的资源量,提高页面的加载速度,尤其在对于大型、图片较多的网页,懒加载是一种常见的性能优化手段。

如何实现懒加载:

  1. 图片懒加载:

    • 将图片的src属性设置为占位符或者一张小尺寸的默认图片。
    • 使用data-*属性或其他自定义属性存储实际图片的地址。
    • 监听页面滚动事件或其他触发条件。
    • 当图片即将进入用户视野时,通过JavaScript将实际图片的地址赋给src属性,触发图片加载。

    示例代码:

    <img src="placeholder.jpg" data-src="actual-image.jpg" alt="Lazy-loaded Image" class="lazy-load">
    
    document.addEventListener("DOMContentLoaded", function() {
      const lazyImages = document.querySelectorAll('.lazy-load');
      const lazyLoad = function() {
        lazyImages.forEach(img => {
          if (img.getBoundingClientRect().top < window.innerHeight && img.getAttribute('data-src')) {
            img.src = img.getAttribute('data-src');
            img.removeAttribute('data-src');
          }
        });
      };
    
      document.addEventListener('scroll', lazyLoad);
      window.addEventListener('resize', lazyLoad);
      window.addEventListener('DOMContentLoaded', lazyLoad);
    });
    
  2. JavaScript模块的懒加载:

    • 使用动态import()语法,该语法返回一个Promise对象,可以在需要的时候异步加载模块。
    • 在需要使用的地方调用import()加载模块,只有在调用时才会真正加载。

    示例代码:

    // 在需要的地方调用 import()
    const fetchData = async () => {
      const module = await import('./my-module.js');
      module.doSomething();
    };
    
  3. 其他资源的懒加载:

    • 使用<script>标签的asyncdefer属性,将脚本的加载推迟到文档解析完成后执行。
    • 使用rel="preload"的方式提前加载重要资源,但不执行,等到需要使用时再执行。

    示例代码:

    <!-- 推迟加载脚本 -->
    <script async src="my-script.js"></script>
    
    <!-- 提前加载但不执行,等待后续使用时执行 -->
    <link rel="preload" href="my-styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
    

懒加载在提高网页性能方面是一个有效的策略,但需要谨慎使用,根据具体场景进行合理选择,以避免影响用户体验。

八、浏览器缓存的机制,包括强缓存和协商缓存

浏览器缓存机制是一种通过将资源存储在本地,以便在用户再次访问相同资源时加快加载速度的方式。浏览器缓存主要分为两种类型:强缓存和协商缓存。

强缓存:

强缓存是指在缓存期间,浏览器不向服务器发送请求,而是直接从本地缓存中获取资源。强缓存可以通过设置 HTTP 响应头来实现。

常见的强缓存控制头:

  1. Expires:

    • 一个表示资源过期时间的 HTTP 头。由服务器指定一个未来的日期,浏览器将在该日期前直接使用缓存,无需向服务器发起请求。
    • 缺点是依赖于客户端和服务器时间的一致性,容易出现时间不同步的问题。
    Expires: Wed, 21 Oct 2023 07:28:00 GMT
    
  2. Cache-Control:

    • 用于设置资源的缓存策略,可以包含多个指令,常见的有:
      • max-age:指定资源被认为是新鲜的最大时间(秒)。
      • s-maxage:同 max-age,但仅适用于共享缓存(如代理服务器)。
      • public:表明响应可以被任何对象(包括代理)缓存。
      • private:表明响应只能被单个用户缓存,不允许代理缓存。
      • no-store:禁止缓存,每次都要向服务器重新请求。
    Cache-Control: max-age=3600, public
    

协商缓存:

协商缓存是指在强缓存失效后,浏览器与服务器进行通信,通过协商确定是否使用缓存。协商缓存也可以通过设置 HTTP 响应头来实现。

常见的协商缓存控制头:

  1. Last-Modified 和 If-Modified-Since:

    • 当浏览器发起请求时,服务器返回资源的最后修改时间(Last-Modified)。
    • 浏览器再次请求资源时,使用 If-Modified-Since 头将上次返回的最后修改时间发送给服务器。
    • 如果资源在这段时间内没有被修改,则服务器返回 304 Not Modified,浏览器直接使用缓存。
    Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
    
    If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
    
  2. ETag 和 If-None-Match:

    • 当浏览器发起请求时,服务器返回资源的唯一标识符(ETag)。
    • 浏览器再次请求资源时,使用 If-None-Match 头将上次返回的 ETag 发送给服务器。
    • 如果资源的 ETag 与浏览器发送的一致,则服务器返回 304 Not Modified。
    ETag: "abcdef12345"
    
    If-None-Match: "abcdef12345"
    

协商缓存相对于强缓存,更加灵活,可以有效减少不必要的数据传输,但需要服务器的支持。在实际应用中,通常会同时使用强缓存和协商缓存,以提供更好的性能和用户体验。

九、闭包的概念和实际应用

闭包的概念:

在 JavaScript 中,闭包是指能够访问独立变量的函数,即在函数内部定义的函数。闭包使函数拥有了保留局部变量值的能力,即使在外部函数已经执行结束的情况下,内部函数仍然可以访问外部函数的变量。

实际应用:

  1. 封装私有变量: 通过闭包可以模拟类似面向对象编程语言中的私有变量。外部无法直接访问闭包内的变量,从而达到数据的封装。

    function createCounter() {
      let count = 0;
      return function() {
        count++;
        return count;
      };
    }
    
    const counter = createCounter();
    console.log(counter()); // 1
    console.log(counter()); // 2
    
  2. 定时器和事件处理: 闭包可以用于处理定时器和事件,保留定时器或事件处理函数执行时的上下文。

    function delayedGreeting(message, delay) {
      setTimeout(function() {
        console.log(message);
      }, delay);
    }
    
    delayedGreeting("Hello!", 1000);
    
  3. 模块模式: 利用闭包可以创建模块,将一组相关的函数和变量封装在一个单一的作用域中,避免了全局命名空间的污染。

    const module = (function() {
      let privateVar = 42;
    
      function privateFunction() {
        return "Private Function";
      }
    
      return {
        publicVar: "Public Variable",
        publicFunction: function() {
          return "Public Function";
        },
      };
    })();
    
    console.log(module.publicVar);
    console.log(module.publicFunction());
    
  4. 循环中的闭包问题: 在循环中创建闭包时,常常会遇到变量共享的问题。可以使用立即执行函数来解决这个问题。

    for (var i = 1; i <= 5; i++) {
      setTimeout((function(j) {
        return function() {
          console.log(j);
        };
      })(i), i * 1000);
    }
    

总体而言,闭包是 JavaScript 中强大而灵活的特性,能够解决一些常见的问题,同时也需要注意在使用时防止出现内存泄漏等问题。

十、ES6模块化和CommonJS的区别

ES6 模块化和 CommonJS 的区别:

  1. 语法差异:

    • ES6 模块化: 使用 importexport 语法。

      // 导入
      import { myFunction } from "./myModule";
      
      // 导出
      export const pi = 3.14;
      export function square(x) {
        return x * x;
      }
      
    • CommonJS: 使用 requiremodule.exports 语法。

      // 导入
      const myModule = require("./myModule");
      
      // 导出
      exports.pi = 3.14;
      exports.square = function(x) {
        return x * x;
      };
      
  2. 加载时机:

    • ES6 模块化: 在编译阶段静态分析,加载时可以进行优化。
    • CommonJS: 在运行时加载,是同步加载模块的方式。
  3. 模块值的拷贝:

    • ES6 模块化: 模块值是动态映射关系,只读视图,通过实时的映射关系来获取值。
    • CommonJS: 是值的拷贝,一旦加载,被加载模块的值变化不会影响导入模块。
  4. 导出方式:

    • ES6 模块化: 导出的是值的引用,修改导入值会影响到导出值。
    • CommonJS: 导出的是值的拷贝,修改导入值不会影响到导出值。
  5. 适用场景:

    • ES6 模块化: 主要用于浏览器端和现代前端开发,支持异步加载。
    • CommonJS: 主要用于服务器端,是同步加载模块的一种较为简单的方式。
  6. ES6 模块化的静态特性: ES6 模块化在编译时可以静态分析模块之间的依赖关系,这使得一些工具(如 tree shaking)能够更好地优化代码,去除未使用的部分。

总的来说,ES6 模块化在语法上更加优雅,支持静态分析,异步加载等特性,适用于现代前端开发;而 CommonJS 更适用于服务器端的模块化开发。

十一、使用Vue Router的导航守卫实现权限控制

Vue Router 的导航守卫是一种有效的权限控制手段。通过在路由导航的过程中插入一些特定的逻辑,可以控制用户是否有权限访问某个页面。以下是使用 Vue Router 导航守卫实现权限控制的基本步骤:

  1. 定义路由: 在 Vue Router 中定义需要进行权限控制的路由,给需要权限控制的路由设置 meta 字段。

    // router.js
    import Vue from 'vue';
    import Router from 'vue-router';
    import Home from './views/Home.vue';
    import Admin from './views/Admin.vue';
    
    Vue.use(Router);
    
    const router = new Router({
      routes: [
        {
          path: '/',
          name: 'home',
          component: Home,
        },
        {
          path: '/admin',
          name: 'admin',
          component: Admin,
          meta: { requiresAuth: true }, // 设置需要权限控制
        },
      ],
    });
    
    export default router;
    
  2. 添加导航守卫: 利用 Vue Router 提供的导航守卫,在路由跳转前进行权限验证。

    // router.js
    router.beforeEach((to, from, next) => {
      // 检查路由是否需要权限控制
      if (to.meta.requiresAuth) {
        // 判断用户是否登录,这里假设有一个 isAuthenticated 的方法用于判断用户登录状态
        if (isAuthenticated()) {
          // 用户已登录,允许访问
          next();
        } else {
          // 用户未登录,重定向到登录页面
          next('/login');
        }
      } else {
        // 不需要权限控制的路由,直接放行
        next();
      }
    });
    
    export default router;
    

    在上述代码中,beforeEach 是全局前置守卫,会在路由切换前执行。它检查目标路由是否需要权限控制,如果需要,则判断用户是否登录,根据情况决定是否放行。

  3. 应用导航守卫: 在 Vue 实例中应用上述定义的路由。

    // main.js
    import Vue from 'vue';
    import App from './App.vue';
    import router from './router';
    
    new Vue({
      el: '#app',
      router,
      render: (h) => h(App),
    });
    

这样,通过在路由定义和导航守卫中设置权限标记,可以在需要的时候有效地进行权限控制,确保用户在未登录的情况下无法访问受保护的页面。

十二、解释requestAnimationFramerequestIdleCallback的区别

requestAnimationFramerequestIdleCallback 是两种用于调度任务的 JavaScript API,它们有一些关键的区别:

  1. 调用时机:

    • requestAnimationFrame 用于在浏览器每一帧渲染前执行任务,通常用于动画或其他需要在每一帧之间执行的操作。它的调用频率通常是每秒60次,但可能会根据浏览器的刷新率而变化。
    • requestIdleCallback 则会在浏览器的空闲时段执行任务。空闲时段是指浏览器没有其他任务需要执行,可以用来执行一些不那么紧急的任务,以避免阻塞主线程。
  2. 执行时机的权衡:

    • requestAnimationFrame 主要用于实现动画等需要连续、平滑执行的任务。因为它在每一帧之前执行,适合处理与界面渲染紧密相关的逻辑。
    • requestIdleCallback 主要用于执行那些不紧急,但对性能要求较高的任务。它会在浏览器空闲时执行,避免了在关键渲染时段执行可能导致掉帧的任务。
  3. 回调函数参数:

    • requestAnimationFrame 的回调函数中会传递一个时间戳参数,表示当前帧开始渲染的时间。这对于计算动画的帧间隔等操作很有用。
    • requestIdleCallback 的回调函数中会传递一个 IdleDeadline 对象,该对象包含有关浏览器空闲时间的信息。通过这个对象,可以在任务执行时进行性能优化,确保不会超过分配的时间。
  4. 兼容性:

    • requestAnimationFrame 具有较好的兼容性,广泛支持于现代浏览器。
    • requestIdleCallback 在一些早期版本的浏览器中可能不被支持。为了兼容性,可以使用 polyfill。

综上所述,选择使用 requestAnimationFrame 还是 requestIdleCallback 取决于任务的性质。如果任务需要与每一帧的渲染相关,选择 requestAnimationFrame;如果任务不紧急,且对性能有较高要求,选择 requestIdleCallback。在实际应用中,它们也可以结合使用,根据任务的紧急性和性能需求来灵活调度。

十三、如何解决跨域问题,使用Nginx的配置

解决跨域问题通常涉及到在服务器端进行配置,Nginx 是一种常用的服务器,通过其配置可以实现跨域资源共享(CORS)。

以下是使用 Nginx 进行跨域配置的一般步骤:

  1. 在 Nginx 配置文件中添加跨域配置:

    打开 Nginx 的配置文件,一般是位于 /etc/nginx/nginx.conf/etc/nginx/conf.d/default.conf

    server {
        listen 80;
        server_name your_domain.com;
    
        location / {
            add_header 'Access-Control-Allow-Origin' '*';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
            add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    
            if ($request_method = 'OPTIONS') {
                add_header 'Access-Control-Allow-Origin' '*';
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
                add_header 'Content-Type' 'text/plain charset=UTF-8';
                add_header 'Content-Length' 0;
                return 204;
            }
    
            # 其他配置...
        }
    
        # 其他服务器配置...
    }
    

    在上述配置中,Access-Control-Allow-Origin 允许任何域进行访问,Access-Control-Allow-Methods 指定了允许的 HTTP 方法,Access-Control-Allow-Headers 设置了允许的请求头,Access-Control-Expose-Headers 则设置了允许被前端 JavaScript 访问的响应头。

  2. 重启 Nginx 服务:

    在修改完 Nginx 配置文件后,需要重新加载配置或重启 Nginx 服务,使配置生效。

    sudo service nginx reload
    

    或者

    sudo service nginx restart
    

请注意,'*' 通配符允许所有域进行访问。在生产环境中,应根据实际需求设置允许访问的域名,而不是使用通配符。

此外,如果使用 HTTPS,请确保在 location 配置中也添加相应的 HTTPS 配置。以上配置仅作为示例,实际配置需根据项目需求进行适度调整。

十四、Object.prototype.toString的作用和实现原理

Object.prototype.toString 方法是用来检测 JavaScript 对象的类型的。该方法返回一个表示对象的字符串,具体格式为 "[object 类型]",其中 "类型" 表示对象的内部 [[Class]] 属性。通过这种方式,可以更准确地获取对象的实际类型,而不仅仅是使用 typeof 运算符的类型。

作用:

  • 获取对象类型: 主要用于获取对象的实际类型,对于复杂的数据类型,如数组、日期、正则表达式等,可以通过 Object.prototype.toString 得到更精确的类型信息。

实现原理:

Object.prototype.toString 是一个内置的 JavaScript 方法,其实现原理基于对象的内部 [[Class]] 属性。下面是一个简化的实现:

Object.prototype.toString = function () {
    // this 指向调用 toString 方法的对象
    if (this === null) return '[object Null]';
    if (this === undefined) return '[object Undefined]';
    
    // 获取对象的内部 [[Class]] 属性
    var classString = Object.prototype.toString.call(this);
    
    // 从 "[object 类型]" 中提取类型部分
    return classString.substring(8, classString.length - 1);
};

// 示例
var arr = [1, 2, 3];
console.log(arr.toString()); // 默认 toString 方法
console.log(Object.prototype.toString.call(arr)); // "[object Array]"

这里的关键在于使用 Object.prototype.toString.call(this) 来获取对象的内部 [[Class]] 属性,然后通过截取字符串的方式提取其中的类型信息。

需要注意的是,对于一般对象,Object.prototype.toString 得到的是 "[object Object]"。对于其他内置对象,如数组、日期、正则表达式等,得到的结果是 "[object 类型]",如 "[object Array]""[object Date]""[object RegExp]"。这种方式可以有效地区分不同类型的对象。

十五、Vue中keep-alive的作用和使用场景

keep-alive 是 Vue 中的一个内置组件,主要用于缓存组件的状态,避免在组件切换时反复创建和销毁组件实例。这样可以提高性能,减少不必要的资源开销。下面详细说明 keep-alive 的作用和使用场景:

作用:

  1. 缓存组件状态: keep-alive 可以缓存被包裹的组件的实例及其状态,当组件被切换出去再切换回来时,不会重新创建组件实例,而是直接使用之前缓存的实例。

  2. 优化性能: 适用于一些页面切换频繁、但内容较为静态的场景,避免频繁创建和销毁组件,提高页面渲染性能。

使用场景:

  1. Tab 切换: 在页面中有多个 Tab,每个 Tab 对应一个组件,可以使用 keep-alive 缓存当前激活的 Tab 对应的组件,避免切换 Tab 时反复创建和销毁组件。

    <keep-alive>
      <component :is="currentTabComponent"></component>
    </keep-alive>
    
  2. 动态组件: 在动态组件的切换中,使用 keep-alive 缓存当前动态组件的状态,提高切换效率。

    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
    
  3. 路由导航: 在使用 Vue Router 进行路由导航时,可以利用 keep-alive 缓存一些公共页面组件,如导航栏、侧边栏等,以提高导航的响应速度。

    <router-view></router-view>
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    
  4. 列表滚动: 在包含大量数据的列表滚动场景中,可以使用 keep-alive 缓存列表项的组件,以提高滚动性能。

    <keep-alive>
      <list-item v-for="item in items" :key="item.id"></list-item>
    </keep-alive>
    

需要注意的是,keep-alive 会保留被包裹组件的状态,包括数据、生命周期等,因此对于一些需要每次进入都重新加载数据的场景,不宜过度使用 keep-alive。在一些动态数据更新频繁的页面,可能需要通过手动触发组件的更新钩子来处理。

十六、手写一个简单的Promise

下面是一个简化版的 Promise 实现,包含 pendingfulfilledrejected 三个状态,以及 then 方法用于处理异步操作完成后的回调。请注意,这只是为了演示基本原理,实际中的 Promise 还需要处理更多的细节和边界情况。

class MyPromise {
  constructor(executor) {
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

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

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

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

  then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) => value;
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : (reason) => {
            throw reason;
          };

    const handleFulfilled = (resolve, reject) => {
      if (this.status === 'fulfilled') {
        try {
          const result = onFulfilled(this.value);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      }
    };

    const handleRejected = (resolve, reject) => {
      if (this.status === 'rejected') {
        try {
          const result = onRejected(this.reason);
          resolve(result);
        } catch (error) {
          reject(error);
        }
      }
    };

    return new MyPromise((resolve, reject) => {
      if (this.status === 'pending') {
        this.onFulfilledCallbacks.push(() => handleFulfilled(resolve, reject));
        this.onRejectedCallbacks.push(() => handleRejected(resolve, reject));
      } else if (this.status === 'fulfilled') {
        handleFulfilled(resolve, reject);
      } else if (this.status === 'rejected') {
        handleRejected(resolve, reject);
      }
    });
  }
}

// 示例
const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise resolved');
  }, 1000);
});

promise
  .then((result) => {
    console.log(result); // 输出 'Promise resolved'
    return 'New Promise resolved';
  })
  .then((result) => {
    console.log(result); // 输出 'New Promise resolved'
  });

上述实现是一个极简的 Promise 实现,真实的 Promise 还包含一系列特性,如异步执行、链式调用、错误处理、Promise.all、Promise.race 等。这里只是为了说明基本原理,实际使用建议使用原生的 Promise 或一些成熟的 Promise 库。

十七、组合式函数的概念和应用场景

组合式函数的概念:

组合式函数是指通过将多个函数组合在一起创建新的函数。这样的新函数将原有的函数逻辑结合起来,形成一个更为复杂和高层次的功能单元。组合式函数的核心思想是将函数抽象和复用,通过组合不同的函数来构建出复杂的业务逻辑。

应用场景:

  1. 数据处理: 在数据处理过程中,可以通过组合不同的处理函数来实现复杂的数据转换、筛选、排序等操作。例如,在处理数组时,可以组合 mapfilterreduce 等函数。

    const data = [1, 2, 3, 4, 5];
    
    const double = (x) => x * 2;
    const isEven = (x) => x % 2 === 0;
    const sum = (accumulator, currentValue) => accumulator + currentValue;
    
    const result = data.map(double).filter(isEven).reduce(sum, 0);
    
  2. 状态管理: 在前端应用中,特别是使用状态管理库(如 Vuex 或 Redux)时,可以通过组合不同的 reducer 函数来处理应用的状态变化。

    const reducer1 = (state, action) => {/*...*/};
    const reducer2 = (state, action) => {/*...*/};
    const reducer3 = (state, action) => {/*...*/};
    
    const rootReducer = (state, action) => {
      return {
        subState1: reducer1(state.subState1, action),
        subState2: reducer2(state.subState2, action),
        subState3: reducer3(state.subState3, action),
      };
    };
    
  3. 函数组件: 在 React 开发中,可以通过组合不同的函数组件来构建出页面的各个部分。这种组件化的思想使得代码更易于维护和扩展。

    const Header = () => <header>Header</header>;
    const Sidebar = () => <aside>Sidebar</aside>;
    const Content = () => <main>Content</main>;
    const Footer = () => <footer>Footer</footer>;
    
    const App = () => (
      <div>
        <Header />
        <Sidebar />
        <Content />
        <Footer />
      </div>
    );
    
  4. 异步流程管理: 在处理复杂的异步流程时,可以通过组合异步操作的函数来构建出清晰的处理链。例如,在使用 Promise 或 async/await 处理异步操作时,可以将多个异步函数组合在一起。

    const fetchData = async (url) => {/*...*/};
    const processData = (data) => {/*...*/};
    const displayData = (result) => {/*...*/};
    
    const handleAsyncFlow = async () => {
      try {
        const data = await fetchData('https://example.com/api/data');
        const processedData = processData(data);
        displayData(processedData);
      } catch (error) {
        console.error('Error:', error);
      }
    };
    
    handleAsyncFlow();
    

总体来说,组合式函数的应用场景广泛,适用于许多不同的编程领域,包括函数式编程、状态管理、数据处理等。通过巧妙地组合函数,可以实现更加模块化、可维护和可复用的代码。

十八、Vite中如何配置插件

在 Vite 中配置插件主要是通过 vite.config.js 文件进行的。以下是一个简单的 Vite 插件配置的示例:

  1. 安装插件: 首先,安装你需要的插件,例如:

    npm install vite-plugin-example
    
  2. 配置插件: 在项目根目录下创建 vite.config.js 文件,并配置插件:

    // vite.config.js
    import ExamplePlugin from 'vite-plugin-example';
    
    export default {
      plugins: [
        ExamplePlugin({
          // 插件的配置项
        }),
      ],
    };
    

    这里的 vite-plugin-example 是一个示例插件,实际项目中要根据需要选择对应的插件。

  3. 配置插件的选项: 某些插件可能需要配置选项,你可以在插件的调用中传递配置项,就像上面的 ExamplePlugin 那样。

    // vite.config.js
    import Vue from '@vitejs/plugin-vue';
    import VitePluginExample from 'vite-plugin-example';
    
    export default {
      plugins: [
        Vue(),
        VitePluginExample({
          // 插件的配置项
          option1: 'value1',
          option2: 'value2',
        }),
      ],
    };
    

请确保在配置插件之前已经安装了相关的插件包,并根据插件文档提供的配置选项进行正确的配置。不同插件的配置方式可能会有所不同,具体配置参考相关插件的文档。

十九、在项目中遇到的性能问题和解决措施

在项目中遇到的性能问题通常涉及到前端页面加载、渲染、交互等方面。以下是一些常见的性能问题以及相应的解决措施:

  1. 首屏加载时间过长:

    • 问题原因: 大量资源加载、未优化的图片、过多的同步请求等。
    • 解决措施:
      • 使用懒加载(Lazy Loading)延迟加载某些资源。
      • 使用异步加载脚本,如将非关键脚本设置为异步加载。
      • 压缩和合并静态资源,减少请求次数。
      • 使用适当的图片压缩工具,选择合适的图片格式。
  2. 页面渲染卡顿:

    • 问题原因: 复杂的DOM结构、大量重绘和重排操作等。
    • 解决措施:
      • 减少不必要的DOM深度和嵌套,简化页面结构。
      • 使用CSS合并和压缩样式表,减小样式文件大小。
      • 使用硬件加速,如CSS3中的transformopacity
      • 使用requestAnimationFrame优化动画效果,避免掉帧。
  3. 网络请求过多和资源重复加载:

    • 问题原因: 页面中存在大量冗余的请求,或者某些资源被重复加载。
    • 解决措施:
      • 合并和压缩JavaScript和CSS文件。
      • 使用浏览器缓存,设置合适的缓存策略。
      • 使用CDN加速静态资源加载。
      • 检查并移除不必要的第三方库或组件。
  4. 内存泄漏:

    • 问题原因: 未及时释放不再使用的对象和事件监听。
    • 解决措施:
      • 使用工具检测内存泄漏,如Chrome DevTools的Memory面板。
      • 确保及时解绑不再需要的事件监听。
      • 使用WeakMapWeakSet存储对象,以便在不再使用时被自动垃圾回收。
  5. 大型数据量的处理:

    • 问题原因: 大量数据的前端处理导致性能下降。
    • 解决措施:
      • 使用分页加载或懒加载,减少一次性加载大量数据。
      • 前端实现简单的搜索和过滤功能,减小渲染的数据量。
      • 使用虚拟列表技术,只渲染可见区域的数据。
  6. 未优化的代码:

    • 问题原因: 未经过性能优化的JavaScript代码。
    • 解决措施:
      • 使用生产环境的打包和压缩工具。
      • 使用Tree Shaking和代码分割来减小打包体积。
      • 优化关键路径上的代码,减少不必要的计算。

在项目中解决性能问题时,需要通过性能测试工具、浏览器开发者工具以及一些第三方库(如Lighthouse)进行性能分析,找到瓶颈并有针对性地进行优化。