常备面试题

54 阅读25分钟

一、基础核心知识

1. HTML/CSS

  1. HTML5的新特性有哪些?

    • 新的语义元素(如 <article>, <section>, <header>, <footer>
    • 原生支持视频和音频(<video>, <audio> 标签)
    • 本地存储(localStorage 和 sessionStorage)
    • Canvas API,用于绘图
  2. 如何使用CSS实现响应式布局?

    • 使用媒体查询(@media)针对不同屏幕尺寸应用不同样式。
    • 使用Flexbox和CSS Grid进行灵活布局。
    • 使用百分比和相对单位(如 emrem)设置宽度和高度。
  3. 解释CSS盒模型及其组成部分。

    • 盒模型由四个部分组成:内容(content)、内边距(padding)、边框(border)、外边距(margin)。
    • 可以使用 box-sizing 属性控制盒模型的计算方式。
  4. 盒模型(标准盒模型 vs 怪异盒模型)?

    • 标准盒模型:width 和 height 只包含内容区域。
    • 怪异盒模型:width 和 height 包含内容、内边距和边框。
    • 切换方式:box-sizing: content-box(标准)或 border-box(怪异)。
  5. CSS3 新特性:掌握 Flexbox 和 Grid 布局,了解动画(@keyframes)、过渡(transition)、媒体查询(@media)等。

  6. Flex 布局和 Grid 布局的应用场景?

    • Flex 布局:适合一维布局,即在一个方向上(行或列)排列元素。常用于导航栏、卡片布局等。
    • Grid 布局:适合二维布局,即在行和列两个方向上排列元素。常用于复杂的网格布局,如仪表盘、复杂的表单布局等。提供了更好的控制。
  7. BFC(块级格式化上下文)是什么?如何触发?

  • BFC:是页面上的一个独立的渲染区域,内部的元素不会影响到外部的元素。BFC可以包裹浮动元素,防止父容器高度塌陷。BFC内的元素外边距不会与外部元素的外边距重叠。常见的触发方式包括:

    • float 不为 none
    • position 为 absolute 或 fixed
    • display 为 inline-blocktable-cellflex 等
    • overflow 不为 visible
    • contain:值为layoutcontentpaint
  • 应用场景

    1. 清除浮动:防止父容器高度塌陷。
    2. 避免外边距重叠:阻止相邻元素的外边距合并。
    3. 自适应两栏布局:利用BFC特性实现布局。
  1. 响应式设计的实现方案(媒体查询、REM/VW 单位)?

    • 媒体查询@media (max-width: 768px) { ... }
    • REM/VWrem 基于根元素字体大小,vw 基于视口宽度。
  2. CSS 选择器优先级计算规则?

    • 优先级:!important > 内联样式 > ID 选择器 > 类/伪类/属性选择器 > 元素/伪元素选择器 > 通配符。

2. JavaScript

  1. 事件循环(Event Loop)机制,宏任务与微任务的区别?

    • 事件循环:JavaScript 单线程通过事件循环处理异步任务。
    • 宏任务setTimeoutsetIntervalI/O 等。
    • 微任务Promise.thenMutationObserver 等。微任务在当前执行栈清空后、下一个宏任务前执行。
  2. 闭包的作用与使用场景?如何避免内存泄漏?

    • 闭包:函数内部可以访问外部作用域(函数)的变量 ,常用于数据私有化。

    • 使用场景:模块化、私有变量、回调函数。

    • 避免内存泄漏:及时释放不再使用的闭包引用。

      function createCounter() {
        let count = 0;
        return function() {
          count++;
          return count;
        };
      }
      const counter = createCounter();
      
  3. ES6+ 新特性(箭头函数、Promise、async/await、Proxy 等)?

    • 箭头函数() => {},没有自己的 this
    • Promise:异步编程,thencatchfinally
    • async/await:基于 Promise 的语法糖,使异步代码更易读。
    • Proxy:拦截对象操作,如 getset
    const fetchData = async () => {
      const response = await fetch('url');
      const data = await response.json();
      return data;
    };
    
  4. 原型链与继承的实现方式?

    • 原型链:每个对象都有一个 __proto__ 指向其原型对象,原型又可以指向更上层的原型。。
    • 继承:通过 prototype 和 __proto__ 实现。
    • 构造函数继承:通过构造函数创建实例。
    function Animal(name) {
      this.name = name;
    }
    Animal.prototype.speak = function() {
      console.log(this.name + ' makes a noise.');
    };
    
    function Dog(name) {
      Animal.call(this, name); // 继承属性
    }
    Dog.prototype = Object.create(Animal.prototype); // 继承方法
    Dog.prototype.constructor = Dog;
    
  5. 防抖(Debounce)与节流(Throttle)的实现原理?

    • 防抖:多次触发只执行最后一次。

      function debounce(fn, delay) {
        let timer;
        return function(...args) {
          clearTimeout(timer);
          timer = setTimeout(() => fn.apply(this, args), delay);
        };
      }
      
    • 节流:多次触发按固定频率执行。

      function throttle(fn, wait) {
        let lastTime = 0;
        return function(...args) {
          const now = Date.now();
          if (now - lastTime >= wait) {
            lastTime = now;
            fn.apply(this, args);
          }
        };
      }
      
  6. 什么是事件委托?

    • 事件委托是将事件处理程序绑定到父元素,而不是子元素。这样可以减少内存消耗和提高性能,尤其在动态添加子元素时。
  7. 如何处理异步编程?请举例说明Promise和async/await的用法。

    • Promise是处理异步操作的对象,表示一个可能还未完成但将来会完成的操作。例如:
    const fetchData = () => {
      return new Promise((resolve, reject) => {
        // 异步操作
      });
    };
    
    • async/await使得异步代码看起来更像同步代码:

    const fetchData = async () => {
      const data = await fetch('url');
      const json = await data.json();
      return json;
    };
    

3. 浏览器与网络

  1. 浏览器渲染流程(关键渲染路径)?

    • 浏览器渲染过程包括解析HTML生成DOM树、解析CSS生成CSSOM树、构建渲染树、布局(calculate layout)、绘制(paint)和合成(composite)。
    • 步骤:HTML 解析 -> CSSOM 构建 -> Render Tree -> Layout -> Paint -> Composite。
  2. HTTP 缓存机制(强缓存、协商缓存)?

    • 强缓存Cache-ControlExpires 缓存有效时直接使用。
    • 协商缓存Last-ModifiedETag 请求时验证缓存是否有。
  3. 跨域问题的解决方案(CORS、JSONP、代理)?

    • CORS:服务器设置 Access-Control-Allow-Origin
    • JSONP:通过 <script> 标签跨域请求。
    • 代理:服务器代理请求。
  4. HTTPS 的工作原理及与 HTTP 的区别?

    • HTTP是超文本传输协议,数据以明文形式传输。HTTPS是HTTP的安全版本,使用SSL/TLS加密数据,提供安全性和完整性。
    • HTTPS:HTTP + SSL/TLS 加密 ;提供数据加密、身份验证和数据完整性。
    • 区别:HTTPS 更安全,默认端口 443;HTTP 使用端口 80。
  5. 从输入 URL 到页面加载完成的整个过程?

    • 步骤:DNS 解析 -> TCP 连接 -> HTTP 请求 -> 服务器响应 -> 浏览器渲染。
      • DNS 解析:将域名解析为 IP 地址。

      • 建立 TCP 连接:与服务器建立连接。

      • 发送 HTTP 请求:请求页面资源。

      • 服务器响应:服务器返回 HTML、CSS、JS 等资源。

      • 浏览器渲染:解析并渲染页面,执行 JS,处理 CSS。

  6. 什么是跨域请求?如何解决跨域问题?

  • 跨域请求是指在不同源之间进行的HTTP请求。可以通过CORS(跨源资源共享)、JSONP或代理服务器解决。
  • 如何解决跨域问题?
    1. CORS(跨源资源共享)

      • 服务器可以通过设置 HTTP 头部来允许跨域请求。常用的头部包括:

        • Access-Control-Allow-Origin: 指定允许的源。
        • Access-Control-Allow-Methods: 指定允许的 HTTP 方法(如 GET、POST)。
        • Access-Control-Allow-Headers: 指定允许的请求头。
      • 例如:

        Access-Control-Allow-Origin: https://example.com
        
    2. JSONP(JSON with Padding)

      • JSONP 是一种通过 <script> 标签进行跨域请求的技术。服务器返回一个 JavaScript 函数调用,而不是 JSON 数据。这种方式只支持 GET 请求。
    3. 代理服务器

      • 使用代理服务器将请求转发到目标服务器。前端代码请求代理服务器,代理服务器再请求实际资源,从而避免跨域问题。
    4. 使用 iframe 和 postMessage

      • 在不同源之间使用 <iframe> 进行通信,利用 window.postMessage 方法安全地传递数据。
    5. WebSocket

      • WebSocket 协议不受同源策略限制,可以进行跨域通信。

二、框架相关

1. React

  1. 虚拟 DOM 的作用及 Diff 算法原理?

    • 虚拟 DOM:提高性能,通过更新虚拟 DOM 而不是直接操作真实 DOM;轻量级的 DOM 副本,提高渲染性能。
    • Diff 算法:通过比较新旧虚拟 DOM,找到最小的变更并更新真实 DOM。
  2. React Hooks 的使用场景(useState、useEffect、自定义 Hook)?

    • useState:管理组件状态。
    • useEffect:处理副作用,如数据获取、订阅。
    • 自定义 Hook:复用逻辑。
    import { useState, useEffect } from 'react';
    
    function useFetch(url) {
      const [data, setData] = useState(null);
      useEffect(() => {
        fetch(url).then(res => res.json()).then(setData);
      }, [url]);
      return data;
    }
    
  3. 组件通信方式(Props、Context、Redux 等)?

    • Props:父组件向子组件传递数据。
    • Context:跨层级组件通信;在组件树中共享数据,避免多层传递 props。
    • Redux:全局状态管理;集中管理应用状态。
  4. React 性能优化手段(Memo、useCallback、懒加载)?

    • Memo:缓存组件;避免不必要的渲染。。
    • useCallback:缓存回调函数,减少子组件的渲染。
    • 懒加载React.lazy 和 Suspense;按需加载组件,减少初始加载时间。
  5. React Fiber 架构的设计目标?

    • 目标:提高渲染性能,支持异步渲染;提升 React 的性能和可扩展性,支持异步渲染,优化更新过程。
  6. React的生命周期方法有哪些?

    • 主要生命周期方法包括 componentDidMount, componentDidUpdate, componentWillUnmount,以及钩子函数如 useEffect
  7. 解释Redux的工作原理和基本概念。

    • Redux是一个状态管理库,使用单一的状态树(store)来存储应用的状态,使用 action 来描述状态变化,reducer 处理这些变化并返回新的状态。

2. Vue

  1. 响应式原理(Object.defineProperty vs Proxy)?

    • Object.defineProperty:Vue2 使用,劫持对象属性;通过 getter/setter 拦截属性访问和修改。
    • Proxy:Vue3 使用,劫持整个对象;使用代理对象,能够拦截更多操作,如数组方法。
  2. Vue 生命周期钩子及使用场景?

    • created:实例创建后(数据初始化),适合数据获取。。
    • mounted:DOM 挂载后,适合操作 DOM。
    • updated:数据更新后,适合执行依赖数据的操作。
    • destroyed:实例销毁后;清理定时器,取消网络请求,解除事件监听,清理外部库实例,数据清理。
  3. Vuex 和 Pinia 的状态管理机制?

    • Vuex:集中式状态管理,statemutationsactionsgetters;使用 store 管理全局状态。
    • Pinia:轻量级状态管理,更简洁的 API;支持 Composition API。
  4. Vue3 的 Composition API 优势?

    • 优势:提供更灵活的逻辑复用,更灵活的组织代码,更好的 TypeScript 支持,简化复杂组件的状态管理。
  5. 虚拟 DOM 与 Diff 算法的优化点?

    • 优化:静态节点提升、事件缓存、动态节点标记。通过标记更新、节点复用、只更新变化的部分,减少不必要的 DOM 操作
  6. Vue中的计算属性和侦听器有什么区别?

    • 计算属性是基于依赖的 getter,缓存其值,只有在相关依赖变化时重新计算。而侦听器用于响应数据的变化,可以执行异步操作或开销大的操作。

3. 通用框架问题

MVVM 模式的理解?

  • MVVM:Model-View-ViewModel,数据驱动视图;分离 UI 与业务逻辑,使用双向数据绑定。

前端路由的实现原理(Hash vs History API)?

  • Hash:通过 # 实现,兼容性好; 通过 URL 的 hash 部分实现路由,简单,但不友好。
  • History API:通过 pushState 和 replaceState 实现,URL 更美观。

SSR(服务端渲染)的优缺点及实现方案?

  • 优点:SEO 友好,首屏加载快。
  • 缺点:服务器压力大,开发复杂度高。
  • 实现方案:Next.js(React)、Nuxt.js(Vue)支持 SSR。。

三、算法与手写代码

1. 常见算法题

  1. 数组去重、扁平化、排序(手写快排/归并)。

    • 去重[...new Set(arr)]

      const uniqueArray = arr => [...new Set(arr)];
      
    • 扁平化arr.flat(Infinity)

      const flatten = arr => arr.flat(Infinity);
      
    • 快排

      //方法一
      function quickSort(arr) {
        // 如果数组长度小于或等于 1,直接返回该数组(因为它已经是有序的)
        if (arr.length <= 1) return arr;
      
        // 选择第一个元素作为基准(pivot)
        const pivot = arr[0];
        // 初始化左侧数组,用于存放小于基准的元素
        const left = [];
        // 初始化右侧数组,用于存放大于或等于基准的元素
        const right = [];
      
        // 遍历数组,从第二个元素开始
        for (let i = 1; i < arr.length; i++) {
          // 如果当前元素小于基准,将其放入左侧数组
          if (arr[i] < pivot) {
            left.push(arr[i]);
          } 
          // 否则,将其放入右侧数组
          else {
            right.push(arr[i]);
          }
        }
      
        // 递归调用 quickSort,对左侧和右侧数组进行排序
        // 最后合并排序后的左侧数组、基准元素和排序后的右侧数组
        return [...quickSort(left), pivot, ...quickSort(right)];
      }
      //方法二
      const quickSort = arr => {
        // 如果数组长度小于或等于 1,直接返回该数组(因为它已经是有序的)
        if (arr.length <= 1) return arr;
      
        // 选择最后一个元素作为基准(pivot)
        const pivot = arr[arr.length - 1];
      
        // 使用 filter 方法创建一个新数组,包含所有小于基准的元素
        const left = arr.filter(x => x < pivot);
      
        // 使用 filter 方法创建一个新数组,包含所有大于基准的元素
        const right = arr.filter(x => x > pivot);
      
        // 递归调用 quickSort,对左侧和右侧数组进行排序
        // 将排序后的左侧数组、基准元素和排序后的右侧数组合并并返回
        return [...quickSort(left), pivot, ...quickSort(right)];
      };
      
  2. 链表操作(反转、环检测)。

    1. 反转链表

      反转链表的过程是将链表中的节点顺序反转。以下是使用迭代方法反转链表的示例:

      function reverseList(head) {
          let prev = null;       // 初始化前一个节点为 null
          let curr = head;       // 当前节点从头节点开始
      
          while (curr) {
              const next = curr.next; // 保存下一个节点
              curr.next = prev;       // 反转当前节点的指针
              prev = curr;            // 移动 prev 指针到当前节点
              curr = next;            // 移动 curr 指针到下一个节点
          }
          return prev; // 返回新的头节点
      }
      
      class ListNode {
          constructor(value = 0, next = null) {
              this.value = value;
              this.next = next;
          }
      }
      
      function reverseLinkedList(head) {
          let prev = null;
          let current = head;
      
          while (current) {
              let nextNode = current.next; // 保存下一个节点
              current.next = prev;         // 反转当前节点的指针
              prev = current;              // 移动 prev 指针
              current = nextNode;          // 移动 current 指针
          }
          return prev; // 返回新的头节点
      }
      
      // 示例用法
      const head = new ListNode(1, new ListNode(2, new ListNode(3)));
      const reversedHead = reverseLinkedList(head);
      
    2. 环检测

      环检测可以使用快慢指针的方法。以下是环检测的实现:

      function reverseList(head) {
          let prev = null;       // 初始化前一个节点为 null
          let curr = head;       // 当前节点从头节点开始
      
          while (curr) {
              const next = curr.next; // 保存下一个节点
              curr.next = prev;       // 反转当前节点的指针
              prev = curr;            // 移动 prev 指针到当前节点
              curr = next;            // 移动 curr 指针到下一个节点
          }
          return prev; // 返回新的头节点
      }
      // 示例用法
      const node1 = new ListNode(1);
      const node2 = new ListNode(2);
      const node3 = new ListNode(3);
      node1.next = node2;
      node2.next = node3;
      node3.next = node1; // 创建环
      
      console.log(hasCycle(node1)); // 输出: true
      
    • 总结

      • 反转链表:通过迭代改变指针方向。

      • 环检测:使用快慢指针判断是否有相遇。

  3. 二叉树遍历(前序、中序、后序)。

    • 前序

      //按顺序访问节点,顺序为:根 -> 左子树 -> 右子树。
      function preorderTraversal(root) {
          const result = [];
      
          function traverse(node) {
              if (!node) return; // 如果节点为空,返回
              result.push(node.val); // 访问当前节点
              traverse(node.left);   // 遍历左子树
              traverse(node.right);  // 遍历右子树
          }
      
          traverse(root); // 从根节点开始遍历
          return result;  // 返回结果数组
      }
      //简化版本;将递归函数与主函数合并,稍微简化代码结构:
      //通过立即调用函数表达式(IIFE)来减少代码量。
      function preorderTraversal(root) {
          const result = [];
          (function traverse(node) {
              if (node) {
                  result.push(node.val);
                  traverse(node.left);
                  traverse(node.right);
              }
          })(root); // 立即调用函数
          return result;
      }
      
  4. 动态规划(爬楼梯、背包问题)。

    • 爬楼梯:每次可以爬 1 或 2 级台阶,求到达第 n 级台阶的方法总数。

      /* 使用动态规划,时间复杂度为 O(n),空间复杂度为 O(1)(简化版) */
      
      function climbStairs(n) {
        // 如果楼梯的台阶数小于或等于 2,直接返回 n
        // 因为只有 1 或 2 台阶时,走法分别为 1 和 2 种
        if (n <= 2) return n;
      
        // 初始化动态规划数组 dp,其中 dp[i] 表示到达第 i 台阶的方式总数
        let dp = [1, 2]; // dp[1] = 1 种方式,dp[2] = 2 种方式
      
        // 从第 3 台阶开始计算到达每个台阶的方式
        for (let i = 3; i <= n; i++) {
          // 到达第 i 台阶的方式等于到达第 (i-1) 台阶和第 (i-2) 台阶的方式之和
          dp[i] = dp[i - 1] + dp[i - 2];
        }
      
        // 返回到达第 n 台阶的方式总数
        return dp[n];
      }
      // 进一步简化为仅使用两个变量,不需要数组:
      
      function climbStairs(n) {
        // 如果楼梯的台阶数小于或等于 2,直接返回 n
        // 因为只有 1 或 2 台阶时,走法分别为 1 和 2 种
        if (n <= 2) return n;
      
        // 初始化变量 a 和 b
        // a: 表示到达第 (i-2) 台阶的方式数
        // b: 表示到达第 (i-1) 台阶的方式数
        let a = 1, b = 2;
      
        // 从第 3 台阶开始计算到达每个台阶的方式
        for (let i = 3; i <= n; i++) {
          // 更新 a 和 b
          // a 变为 b(即 dp[i-2] 变为 dp[i-1])
          // b 变为 a + b(即 dp[i] = dp[i-1] + dp[i-2])
          [a, b] = [b, a + b];
        }
      
        // 返回到达第 n 台阶的方式总数(即 b)
        return b;
      }
      
    • 背包问题:给定一组物品,每个物品有重量和价值,求在不超过最大重量的情况下,能获得的最大价值。

      /* 采用一维数组动态规划,时间复杂度为 O(n * capacity)。 */
    function knapsack(weights, values, capacity) {
      // 获取物品的数量
      const n = weights.length;
    
      // 创建一个长度为 capacity + 1 的数组 dp,用于存储每个容量下的最大价值
      const dp = Array(capacity + 1).fill(0);
    
      // 遍历每个物品
      for (let i = 0; i < n; i++) {
        // 从背包的最大容量开始,向下遍历到当前物品的重量
        // 这样可以确保每个物品只被计算一次
        for (let w = capacity; w >= weights[i]; w--) {
          // 更新 dp[w],选择是当前容量下的最大价值
          // 比较不选当前物品和选当前物品的价值
          dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
        }
      }
    
      // 返回在给定容量下的最大价值
      return dp[capacity];
    }
    

2. 手写代码

  1. 实现 Promise(包含 all、race 等方法)。

    • Promise

        class MyPromise {
            constructor(executor) {
              // 初始化状态为 pending,表示 Promise 尚未完成
              this.status = 'pending';
              // 存储成功时的值
              this.value = undefined;
              // 存储失败时的原因
              this.reason = undefined;
              // 存储成功回调函数的数组
              this.onResolvedCallbacks = [];
              // 存储失败回调函数的数组
              this.onRejectedCallbacks = [];
      
              // resolve 函数用于将 Promise 状态从 pending 改为 fulfilled
              const resolve = value => {
                // 只有在状态为 pending 时才能改变状态
                if (this.status === 'pending') {
                  this.status = 'fulfilled'; // 更新状态为 fulfilled
                  this.value = value; // 保存成功的值
                  // 执行所有成功的回调函数
                  this.onResolvedCallbacks.forEach(fn => fn());
                }
              };
      
              // reject 函数用于将 Promise 状态从 pending 改为 rejected
              const reject = reason => {
                // 只有在状态为 pending 时才能改变状态
                if (this.status === 'pending') {
                  this.status = 'rejected'; // 更新状态为 rejected
                  this.reason = reason; // 保存失败的原因
                  // 执行所有失败的回调函数
                  this.onRejectedCallbacks.forEach(fn => fn());
                }
              };
      
              // 执行传入的 executor 函数,并传递 resolve 和 reject 函数
              executor(resolve, reject);
            }
      
            then(onFulfilled, onRejected) {
              // 如果 Promise 已经成功,直接调用成功回调
              if (this.status === 'fulfilled') {
                onFulfilled(this.value);
              }
              // 如果 Promise 已经失败,直接调用失败回调
              if (this.status === 'rejected') {
                onRejected(this.reason);
              }
              // 如果 Promise 仍在 pending 状态,将回调函数存入数组
              if (this.status === 'pending') {
                this.onResolvedCallbacks.push(() => {
                  onFulfilled(this.value);
                });
                this.onRejectedCallbacks.push(() => {
                  onRejected(this.reason);
                });
              }
            }
      
            static all(promises) {
              return new MyPromise((resolve, reject) => {
                let result = []; // 用于存储所有成功的结果
                let count = 0; // 计数器,跟踪成功的 Promise 数量
                promises.forEach((promise, index) => {
                  promise.then(value => {
                    result[index] = value; // 保存每个 Promise 的成功结果
                    count++; // 计数器加一
                    // 当所有 Promise 都成功时,调用 resolve
                    if (count === promises.length) resolve(result);
                  }, reject); // 如果有任何一个 Promise 失败,调用 reject
                });
              });
            }
      
            static race(promises) {
              return new MyPromise((resolve, reject) => {
                promises.forEach(promise => {
                  // 只要有一个 Promise 成功或失败,就调用相应的 resolve 或 reject
                  promise.then(resolve, reject);
                });
              });
            }
        }
      
  2. 手写防抖(Debounce)与节流(Throttle)。

    • 防抖(Debounce):防抖是指在一定时间内,只有最后一次触发的事件才会被执行。如果在这个时间内再次触发事件,之前的调用会被取消。

      function debounce(func, delay) {
        let timer;
        return function(...args) {
          const context = this;
          clearTimeout(timer); // 清除之前的定时器
          timer = setTimeout(() => {
            func.apply(context, args); // 在指定延迟后调用函数
          }, delay);
        };
      }
      //使用
      const handleResize = debounce(() => {
        console.log('Window resized');
      }, 300);
      
      window.addEventListener('resize', handleResize);
      
    • 节流(Throttle):防抖是指在一定时间内,只有最后一次触发的事件才会被执行。如果在这个时间内再次触发事件,之前的调用会被取消。

    function throttle(func, limit) {
      let lastFunc;
      let lastRan;
      return function(...args) {
        const context = this;
        if (!lastRan) {
          func.apply(context, args); // 立即执行
          lastRan = Date.now();
        } else {
          clearTimeout(lastFunc); // 清除之前的定时器
          lastFunc = setTimeout(() => {
            if ((Date.now() - lastRan) >= limit) {
              func.apply(context, args); // 在限制时间内执行
              lastRan = Date.now();
            }
          }, limit - (Date.now() - lastRan));
        }
      };
    }
    使用
    const handleScroll = throttle(() => {
      console.log('Scrolling...');
    }, 1000);
    
    window.addEventListener('scroll', handleScroll);
    
  3. 实现深拷贝(处理循环引用)。

    • 深拷贝

      function deepClone(obj, map = new WeakMap()) {
        // 如果 obj 是基本类型或 null,直接返回
        if (obj === null || typeof obj !== 'object') return obj;
      
        // 如果 obj 已经被克隆过,直接返回克隆的对象以处理循环引用
        if (map.has(obj)) return map.get(obj);
      
        // 创建一个新对象或数组
        const clone = Array.isArray(obj) ? [] : {};
      
        // 将原对象和克隆对象的映射存入 WeakMap
        map.set(obj, clone);
      
        // 处理 Date 对象
        if (obj instanceof Date) {
          return new Date(obj);
        }
      
        // 处理 RegExp 对象
        if (obj instanceof RegExp) {
          return new RegExp(obj);
        }
      
        // 遍历对象的每个属性进行递归克隆
        for (const key in obj) {
          // 确保只处理对象自身的属性
          if (obj.hasOwnProperty(key)) {
            clone[key] = deepClone(obj[key], map);
          }
        }
      
        return clone; // 返回深拷贝后的对象
      }
      
    // 使用
    
    const original = {
       name: 'Alice',
       age: 30,
       hobbies: ['reading', 'traveling'],
       meta: {
         created: new Date(),
       },
     };
    
     // 添加循环引用
     original.self = original;
    
     const cloned = deepClone(original);
    
     console.log(cloned);
     console.log(cloned.self === cloned); // true, 验证循环引用处理
     ```
    
  4. 手写 call/apply/bind。

     ```
     Function.prototype.myCall = function (context, ...args) {
       context = context || globalThis; // 如果没有提供上下文,使用 globalThis
       context.fn = this; // 将函数赋值给上下文的临时属性
       const result = context.fn(...args); // 调用函数并传入参数
       delete context.fn; // 删除临时属性
       return result; // 返回结果
     };
    
    Function.prototype.myApply = function (context, args) {
       context = context || globalThis; // 如果没有提供上下文,使用 globalThis
       context.fn = this; // 将函数赋值给上下文的临时属性
       const result = context.fn(...(args || [])); // 调用函数并传入参数数组
       delete context.fn; // 删除临时属性
       return result; // 返回结果
    };
    
    Function.prototype.myBind = function (context, ...args) {
       const fn = this; // 保存原函数
       return function (...newArgs) {
         return fn.myCall(context, ...args, ...newArgs); // 返回一个新函数,调用原函数
       };
     };
     ```
    

    使用实例

    function greet(greeting, punctuation) {
     return `${greeting}, ${this.name}${punctuation}`;
    }
    
    const person = { name: 'Alice' };
    
    console.log(greet.myCall(person, 'Hello', '!')); // "Hello, Alice!"
    console.log(greet.myApply(person, ['Hi', '.'])); // "Hi, Alice."
    const boundGreet = greet.myBind(person, 'Hey');
    console.log(boundGreet('!')); // "Hey, Alice!"
    
    • 总结

      • myCallmyApply 方法用于改变函数的执行上下文,并传入参数。
      • myBind 方法用于创建一个新的函数,固定上下文并允许后续参数传递。
  5. 实现一个发布-订阅模式(EventEmitter)。

    • EventEmitter

      class EventEmitter {
        constructor() {
          this.events = {}; // 存储事件及其监听器的对象
        }
      
        // 注册事件监听器
        on(event, listener) {
          if (!this.events[event]) { // 如果事件不存在,初始化为数组
            this.events[event] = []; // 创建事件数组
          }
          this.events[event].push(listener); // 将监听器添加到事件数组
        }
      
        // 触发事件
        emit(event, ...args) {
          if (this.events[event]) { // 检查事件是否有监听器
            this.events[event].forEach(listener => { // 遍历所有监听器
              listener(...args); // 调用监听器并传入参数
            });
          }
        }
      
        // 移除事件监听器
        off(event, listener) {
          if (!this.events[event]) return; // 如果事件不存在,直接返回
          this.events[event] = this.events[event].filter(l => l !== listener); // 过滤掉要移除的监听器
        }
      
        // 只触发一次的监听器
        once(event, listener) {
          const wrapper = (...args) => { // 包装函数以实现一次性调用
            listener(...args); // 调用原监听器
            this.off(event, wrapper); // 移除包装函数
          };
          this.on(event, wrapper); // 注册包装函数
        }
      }
      
      // 使用示例
      const emitter = new EventEmitter();
      
      function responseToEvent(data) {
        console.log(`Received: ${data}`); // 打印接收到的数据
      }
      
      emitter.on('event1', responseToEvent); // 注册事件监听器
      emitter.emit('event1', 'Hello, World!'); // 触发事件,输出 "Received: Hello, World!"
      emitter.off('event1', responseToEvent); // 移除事件监听器
      emitter.emit('event1', 'This will not be received'); // 不会有输出
      
      emitter.once('event2', (data) => {
        console.log(`Once: ${data}`); // 只会打印一次
      });
      
      emitter.emit('event2', 'First'); // 输出 "Once: First"
      emitter.emit('event2', 'Second'); // 不会有输出
      

四、性能优化 & 前端工程化

1.性能优化

  1. 如何优化前端页面的加载速度?

    • 使用延迟加载(lazy loading)、压缩和合并资源、使用CDN、优化图片和使用浏览器缓存。
  2. 什么是懒加载(Lazy Loading)?如何实现?

    • 懒加载是指仅在需要时才加载资源。可通过 Intersection Observer API 或者在路由中实现动态导入。
  3. 解释CDN的作用及其优势。

    • CDN(内容分发网络)将资源分发到全球各地的多个服务器,减少加载时间和带宽消耗,提高可用性和冗余性。
  4. 如何减少 JavaScript 的执行时间?

    1. 避免不必要的计算

      • 将经常使用的计算结果缓存到变量中,避免重复计算。
    2. 使用 Web Workers

      • 将 CPU 密集型任务放在 Web Worker 中执行,避免阻塞主线程。
    3. 减少 DOM 操作

      • 批量更新 DOM,尽量减少对 DOM 的访问次数。
      • 使用文档片段(DocumentFragment)进行批量插入。
    4. 使用防抖和节流

      • 对于频繁触发的事件(如滚动、输入),使用防抖或节流技巧优化性能。
  5. 如何优化 CSS 选择器?

    1. 简化选择器

      • 避免使用过于复杂的选择器,尽量使用类选择器和 ID 选择器。
      • 避免在选择器中使用通配符(如 *)和后代选择器(如 div p)。
    2. 使用 CSS 预处理器

      • 使用 SCSS 或 LESS 等预处理器,使样式更清晰易维护,减少选择器的复杂性。
    3. 避免过多的嵌套

      • CSS 嵌套层级过深会导致选择器的性能下降,保持选择器的扁平化。
  6. Web Vitals

  • 了解核心 Web 指标

    1. LCP(Largest Contentful Paint)

      • 衡量页面加载速度,指用户看到的最大内容元素(如图像、视频或文本块)渲染所需的时间。

      • 优化方法:

        • 使用优化的图像格式(如 WebP)。
        • 预加载关键资源,确保快速加载。
    2. FID(First Input Delay)

      • 测量用户首次与页面交互(如点击链接、按钮)到浏览器能够响应的延迟。

      • 优化方法:

        • 减少 JavaScript 的执行时间,优化事件处理函数。
        • 使用 Web Workers 将重任务移出主线程。
    3. CLS(Cumulative Layout Shift)

      • 测量页面布局的稳定性,指在加载过程中可见内容的意外偏移。

      • 优化方法:

        • 为图像和视频设置明确的宽高属性,避免加载时内容位置变化。
        • 使用占位符或骨架屏,确保内容在加载过程中保持位置稳定。

2. 前端工程化

  1. 模块化

    1. CommonJS

      • 定义:CommonJS 是一套规范,主要用于服务器端的模块化,最典型的实现是 Node.js。

      • 导出与导入

        • 导出:使用 module.exportsexports
        • 导入:使用 require() 函数。
      • 特点

        • 同步加载:适合服务器端,因为模块在文件系统中直接读取。
        • 运行时加载:模块在执行时加载,无法进行静态分析。
    2. AMD(Asynchronous Module Definition)

      • 定义:AMD 是一种异步模块定义规范,适用于浏览器端,最典型的实现是 RequireJS。

      • 导出与导入

        • 导出:使用 define() 函数。
        • 导入:通过 require() 函数。
      • 特点

        • 异步加载:模块在需要时才加载,不会阻塞其他代码执行。
        • 适合浏览器环境,能显著提升加载速度。
    3. ES6 Module

      • 定义:ES6 模块是 JavaScript 原生支持的模块化方案,通过 importexport 关键字实现。

      • 导出与导入

        • 导出:使用 export 关键字。
        • 导入:使用 import 语法。
      • 特点

        • 静态加载:模块在编译时确定依赖关系,支持静态分析。
        • 支持异步加载:可以与动态 import() 结合,实现按需加载。

    区别总结

    特性CommonJSAMDES6 Module
    加载方式同步异步异步(静态)
    导出方式module.exportsdefine()export
    导入方式require()require()import
    使用场景Node.js浏览器浏览器及 Node.js
  2. 打包工具

    • Webpack 基本配置

      • 安装

        npm install --save-dev webpack webpack-cli
        
      • 基本配置(webpack.config.js)

      const path = require('path');
      
      module.exports = {
        entry: './src/index.js', // 入口文件
        output: {
          filename: 'bundle.js', // 输出文件名
          path: path.resolve(__dirname, 'dist'), // 输出路径
        },
        module: {
          rules: [
            {
              test: /.js$/, // 处理 JavaScript 文件
              exclude: /node_modules/,
              use: {
                loader: 'babel-loader', // 使用 Babel 转换
              },
            },
          ],
        },
        mode: 'development', // 开发模式
      };
      
    • 优化打包性能

    1. 代码分割

    • 使用 SplitChunksPlugin 实现代码分割,按需加载模块。

    optimization: {
      splitChunks: {
        chunks: 'all',
      },
    },
    
    1. Tree Shaking

      • 确保使用 ES6 模块化,Webpack 会自动去除未使用的代码。
      • package.json 中设置 "sideEffects": false 来标记不需要副作用的文件。
    2. 压缩与优化

      • 使用 TerserPlugin 压缩 JavaScript 代码。
      • 配置 MiniCssExtractPlugin 提取 CSS 文件,减少重复加载。
  3. 版本控制

    • Git 基本操作

      1. 初始化仓库
      git init
      
      1. 常用命令

        • 添加文件:git add <file>
        • 提交更改:git commit -m "commit message"
        • 查看状态:git status
        • 查看历史:git log
    • 分支管理

      • 创建分支
      git branch <branch-name>
      
      • 切换分支
      git checkout <branch-name>
      
      • 合并分支

      git merge <branch-name>
      
    • 合并冲突

      • 当两个分支修改同一文件的同一部分时,合并会产生冲突。

      • Git 会标记冲突位置,需要手动解决。

      • 解决后,标记为已解决:

      git add <file>
      git commit -m "Resolved merge conflict"
      
    • Rebase

      • 定义:将一个分支的更改应用到另一个分支的基础上,保持提交历史的整洁。

      • 使用

      git checkout <feature-branch>
      git rebase <base-branch>
      
      • 注意:在与他人分享的分支上要谨慎使用 rebase
  4. CI/CD

    • 持续集成(CI)和持续部署(CD)

      • 持续集成(CI) :频繁地将代码集成到主干,通过自动化测试验证集成的有效性。
      • 持续部署(CD) :将经过测试的代码自动部署到生产环境,确保代码始终可用。
    • 配置 CI/CD 流程

      1. 选择 CI/CD 工具:如 GitHub Actions、Travis CI、CircleCI 等。
      2. 编写配置文件(例如 .github/workflows/ci.yml):
      name: CI
      
      on:
        push:
          branches:
            - main
      
      jobs:
        build:
          runs-on: ubuntu-latest
          steps:
            - name: Checkout code
              uses: actions/checkout@v2
      
            - name: Set up Node.js
              uses: actions/setup-node@v2
              with:
                node-version: '14'
      
            - name: Install dependencies
              run: npm install
      
            - name: Run tests
              run: npm test
      
            - name: Build
              run: npm run build
      

五、项目经验与系统设计

1. 项目深挖

  1. 你做过最复杂的项目是什么?技术难点如何解决?

    • 项目描述:描述项目的功能、技术栈。

    • 技术难点:如性能优化、状态管理、跨域问题等,具体解决方案。

      • 示例:一个实时聊天应用,技术难点包括 WebSocket 连接管理、消息队列处理、性能优化等。
  2. 如何优化首屏加载速度?(具体指标与工具)

    • 优化手段:代码分割、懒加载、CDN、图片优化、服务端渲染。
    • 工具:Lighthouse、Webpack Bundle Analyzer。 进行性能监测。
  3. 如何设计一个组件库?需要考虑哪些问题?

    • 考虑点:组件复用性、可定制性、文档、测试、版本管理。

    • 实现方式:使用 Storybook、Rollup 等工具进行构建和展示。

  4. 是否处理过内存泄漏?如何定位和解决?

    • 定位:使用 Chrome DevTools 的 Memory 面板或使用 Chrome DevTools 的内存快照。
    • 解决:及时释放不再使用的引用,避免循环引用(清理不再需要的事件监听、定时器等)。

2. 系统设计题

  1. 设计一个实时聊天功能(WebSocket、消息队列)。

    • 方案:使用 WebSocket 实现实时通信,消息队列处理高并发消息。

    • 技术选型:Node.js 后端、Redis 作为消息队列。

  2. 如何实现一个前端埋点监控系统?

    • 设计思路:收集用户行为数据(点击、浏览等),发送到后端。

    • 实现方式:使用 fetchnavigator.sendBeacon 或 XMLHttpRequest 发送日志,后端存储和分析。

  3. 设计一个支持拖拽的可视化搭建平台。

    • 设计思路:使用 HTML5 的 Drag and Drop API 或第三方库(如 React DnD);结合 React 或 Vue 实现组件拖拽和布局。

    • 实现方式:定义可拖拽的组件,管理状态以记录布局变化。

六、工程化与工具

  1. Webpack 的构建流程及常用优化手段(Tree Shaking、Code Splitting)?

    • 构建流程:入口 -> 加载器 -> 插件 -> 输出。(解析依赖图,编译、打包、输出文件)
    • 优化手段Tree Shaking 去除未使用代码,Code Splitting 分割代码(按需加载模块,减少初始加载时间。)。
  2. Babel 的作用及插件开发原理?

    • 作用:将 ES6+ 代码转换为 ES5(将现代 JavaScript 转换为兼容性更好的版本)。
    • 插件开发:通过 AST(抽象语法树)转换代码。
  3. 如何配置 ESLint 和 Prettier 保证代码规范?

    • ESLint:配置规则,使用 .eslintrc 文件。

    • Prettier:配置格式化规则,使用 .prettierrc 文件。

    • 集成:使用 eslint-config-prettier 解决冲突。

    • 配置.eslintrc 和 .prettierrc 文件,结合 husky 和 lint-staged 实现提交前检查。

  4. CI/CD 在前端项目中的应用?

    • 定义:持续集成和持续交付,自动化测试和部署。

    • 工具:使用 GitHub Actions、Travis CI 等实现自动化流程。

    • 应用:自动化测试、构建、部署,常用工具如 Jenkins、GitLab CI、GitHub Actions。

七、综合能力

  1. 如何学习新技术?最近关注的前端趋势?

    1. 学习新技术的策略
      • 学习方法:官方文档、开源项目、技术博客、实践项目。

      • 定期阅读技术博客和文档:关注一些知名的前端开发博客(如 CSS-Tricks、Smashing Magazine、MDN)和官方文档(如 React、Vue、Angular 的文档)。

      • 在线课程和视频:利用平台(如 Udemy、Coursera、YouTube)上的课程进行系统学习。

      • 实践项目:通过构建小项目或参与开源项目来巩固所学技术,确保理论与实践结合。

      • 社区参与:加入开发者社区(如 Stack Overflow、GitHub、Discord),参与讨论,向他人学习,分享经验。

    2. 最近关注的前端趋势
      • 前端趋势:WebAssembly、Progressive Web Apps(PWA)、Serverless、微前端。
      • 微前端架构:一种将大型应用拆分成多个小型、独立可部署的应用的架构。
      • Server Components:React 及其他框架中的服务端组件,简化数据获取和页面渲染。
      • TypeScript 的普及:越来越多的项目和库采用 TypeScript,提高代码的可维护性和可读性。
      • Jamstack 架构:将前端与后端解耦,使用静态生成、API 和 CDN 来构建快速、可扩展的应用。
  2. 遇到技术分歧时如何与团队沟通?

    • 沟通方式:基于数据和事实,尊重他人意见,寻求共识。

    • 保持开放的心态

      • 尊重不同的观点,理解团队成员的立场和考虑因素。
      • 以建设性的方式表达自己的意见,避免情绪化。
    • 数据驱动的讨论

      • 使用数据和实际案例支持自己的观点,展示技术选择的优缺点。
      • 进行 A/B 测试或原型验证不同方案的效果,提供实证依据。
    • 寻求共识

      • 在讨论中寻找共同点,明确团队的目标和需求,促成团队达成一致。
      • 如果可能,提出折衷方案,结合不同意见形成最佳解决方案。
    • 文档化和总结

      • 将讨论结果和决策过程记录下来,确保团队成员都能查阅。
      • 通过文档促进透明度,减少未来类似问题的发生。
  3. 未来的职业规划?

    • 规划:深入前端技术,学习后端知识,向全栈发展,或专注于前端架构、性能优化等领域。

    • 短期目标(1-2年)

      • 深入掌握当前使用的前端框架(如 React 或 Vue),提升开发技能。
      • 学习 TypeScript、GraphQL 等新技术,增强技术栈的广度。
      • 参与开源项目,贡献代码,提升自己的影响力和技术认可度。
    • 中期目标(3-5年)

      • 担任技术领导或架构师角色,负责技术方向和架构设计。
      • 深入了解前端性能优化和用户体验设计,提升产品质量。
      • 可能考虑转型为全栈开发,拓宽自己的职业发展空间。
    • 长期目标(5年以上)

      • 成为行业内的技术专家,分享经验,撰写技术文章或演讲。
      • 参与技术社区或开设技术课程,帮助其他开发者成长。
      • 如果有兴趣,探索创业机会,建立自己的产品或公司。

八、准备建议

  1. 针对性复习:根据目标公司 JD(Job Description)调整重点(如 React/Vue 偏向)。

  2. 实战演练

    • 刷题平台:LeetCode(算法)、Codewars(JavaScript)。
    • 模拟面试:使用 Pramp 或找同行模拟。
  3. 项目复盘:梳理过往项目,用 STAR 法则(情境、任务、行动、结果)总结亮点。

  4. 资源推荐

    • 书籍:《JavaScript 高级程序设计》《React 设计原理》。
    • 文档:MDN、React/Vue 官方文档。
    • 博客:掘金、Medium 上的技术文章。