<<github 加星 Taimili.com 艾米莉 >> 前端面试题-JavaScript高级篇

0 阅读8分钟

一、 V8引擎工作原理与垃圾回收 (GC)

理解JavaScript的执行环境是高级优化的前提。

V8引擎核心流程

Taimili 艾米莉 ( 一款专业的 GitHub star 管理和github 加星涨星工具taimili.com )

艾米莉 是一款优雅便捷的 GitHub star 管理和github 加星 涨星工具,基于 PHP & javascript 构建, 能对github star fork follow watch 刷星管理和提升,最适合github 的深度用户

WX20251021-210346@2x.png

  1. 解析 (Parsing) : V8将JavaScript源代码解析成抽象语法树 (AST)
  2. 解释 (Interpretation) : Ignition (V8的解释器) 将AST转换成字节码并执行。同时,Ignition会收集分析信息,用于后续的优化。
  3. 编译 (Compilation) : 对于被频繁执行的代码(热点代码),TurboFan (V8的优化编译器) 会介入,利用分析信息将字节码编译成高度优化的机器码,以提升执行效率。这个过程被称为JIT (Just-In-Time) 编译。如果优化的假设失败(如函数参数类型改变),会进行去优化 (Deoptimization) ,回退到字节码执行。

垃圾回收 (Garbage Collection)

V8采用分代回收 (Generational Collection)的策略,将堆内存分为新生代 (New Generation)老生代 (Old Generation)

新生代 (Scavenger算法)

  • 空间小,存活对象少。采用Scavenger算法,将空间一分为二(From-Space 和 To-Space)。
  • 回收时,将From-Space中的存活对象复制到To-Space,然后清空From-Space。最后,From-Space和To-Space角色互换。
  • 对象若经历多轮回收仍存活,则被**晋升 (Promotion)**到老生代。

老生代

  • 空间大,存活对象多。采用标记-清除算法。
  • 标记阶段: 从根对象(如全局对象)开始,遍历所有可达对象并打上标记。
  • 清除阶段: 清除非标记对象所占用的内存。
  • 整理阶段 : 为解决内存碎片化问题,在清除后,会将所有存活对象向一端移动,形成连续的内存空间。

代码示例 (导致内存泄漏的场景):

高级开发者需要能够识别并解释内存泄漏。闭包引用了已分离的DOM节点是典型案例。

js
 体验AI代码助手
 代码解读
复制代码
function createLeakingElement() {
  const container = document.getElementById('container');
  const detachedElement = document.createElement('div');
  detachedElement.textContent = 'This is a potentially leaking element.';
  container.appendChild(detachedElement);

  // 关键:一个外部可访问的函数,通过闭包持有了对 detachedElement 的引用
  const leakingClosure = function() {
    // 即使 detachedElement 从DOM树中移除,只要 leakingClosure 存在,
    // detachedElement 就不会被GC回收。
    console.log(detachedElement.textContent);
  };

  // 从DOM中移除元素
  container.removeChild(detachedElement);

  // 返回这个闭包
  return leakingClosure;
}

// globalLeaker 现在持有了对 detachedElement 的间接引用
// 即使它在DOM中已不可见,它依然存在于内存中
window.globalLeaker = createLeakingElement();

// 只要 window.globalLeaker 不被设为 null,这块内存就永远无法被回收

二、 事件循环 (Event Loop)

高级面试会深入到Node.js环境,考察对Event Loop各阶段的理解。

  • 浏览器 vs. Node.js: 两者模型相似,但Node.js的事件循环有更明确的阶段划分。

  • Node.js 事件循环的六个阶段:

    1. timers: 执行 setTimeout()setInterval() 的回调。
    2. pending callbacks: 执行上一轮循环中延迟到本轮执行的I/O回调。
    3. idle, prepare: 仅内部使用。
    4. poll: 核心阶段。检索新的I/O事件;执行与I/O相关的回调。如果队列不为空,会遍历执行;如果为空,会在此阻塞等待,直到有新的I/O事件或到达 timers 设定的阈值。
    5. check: 执行 setImmediate() 的回调。
    6. close callbacks: 执行如 socket.on('close', ...) 的回调。
  • process.nextTick() 与微任务 (Micro-task) :

    • process.nextTick() 有自己独立的队列,其优先级高于所有微任务。
    • 在一个阶段执行完毕后,事件循环会立即清空 nextTick 队列,然后才清空微任务队列,之后才进入下一个阶段。

代码示例 (Node.js环境下):

js
 体验AI代码助手
 代码解读
复制代码
const fs = require('fs');

console.log('1. Script Start');

// Timers 阶段
setTimeout(() => {
  console.log('7. setTimeout');
}, 0);

// Check 阶段
setImmediate(() => {
  console.log('8. setImmediate');
});

// Micro-task
Promise.resolve().then(() => {
  console.log('5. Promise.then');
});

// process.nextTick 队列 (最高优先级)
process.nextTick(() => {
  console.log('4. process.nextTick');
});

// I/O 操作,其回调将在 Poll 阶段执行
fs.readFile(__filename, () => {
  console.log('6. I/O (readFile) callback');

  // I/O回调内部的调度
  setTimeout(() => console.log('11. I/O -> setTimeout'), 0);
  setImmediate(() => console.log('9. I/O -> setImmediate'));
  process.nextTick(() => console.log('10. I/O -> nextTick'));
});

console.log('2. Script End');
console.log('3. Poll phase may start here...');

// 理论输出顺序:
// 1. Script Start
// 2. Script End
// 3. Poll phase may start here...
// 4. process.nextTick
// 5. Promise.then
// 6. I/O (readFile) callback
// 10. I/O -> nextTick
// 9. I/O -> setImmediate
// 7. setTimeout
// 8. setImmediate
// 11. I/O -> setTimeout
// (注意:9, 7, 8, 11 的确切顺序可能因I/O耗时和系统调度而有细微变化,但基本规律如此)

三、 高级性能优化

Tree Shaking (摇树优化)

原理

  • 依赖ES Modules (import/export) 的静态结构,在编译时分析代码,移除未被实际引用的“死代码”(dead-code)。

实践

  • Webpack, Rollup等现代打包工具在生产模式下默认开启。开发者需保证代码遵循ESM规范,并避免有副作用的模块导入。

Code Splitting (代码分割)

目的

  • 将巨大的单体bundle分割成多个小块(chunks),按需加载,以减小首屏加载体积,提升用户体验。

策略

  1. 按路由分割: 每个页面或路由对应一个chunk。
  2. 按组件分割: 对于非首屏、或需要交互才出现的大型组件(如弹窗、图表)进行懒加载。
  3. 公共库分离 (Vendor Splitting) : 将不常变动的第三方库(如React, Lodash)打包成独立的vendor chunk,利用浏览器缓存。

利用浏览器渲染路径

  • 关键渲染路径 : 优化CSS加载(内联关键CSS)、减少阻塞渲染的脚本、使用 async/defer
  • 硬件加速: 尽量使用 transformopacity 属性进行动画,它们能被提升到单独的合成层(Compositor Layer),由GPU处理,避免触发重排(Reflow)和重绘(Repaint)。

代码示例 (React中的代码分割)

jsx
 体验AI代码助手
 代码解读
复制代码
import React, { Suspense, lazy } from 'react';

// 使用 React.lazy 和动态 import() 来实现组件的懒加载
const HeavyComponent = lazy(() => import('./components/HeavyComponent'));
const AnotherLazyComponent = lazy(() => import('./components/AnotherLazyComponent'));

function App() {
  const [showHeavy, setShowHeavy] = React.useState(false);

  return (
    <div>
      <h1>My App</h1>
      <button onClick={() => setShowHeavy(true)}>Load Heavy Component</button>

      {/* 
        Suspense 组件用于在懒加载组件下载和解析期间,显示一个fallback UI。
        只有当 showHeavy 为 true 时,浏览器才会去请求 HeavyComponent.js。
      */}
      <Suspense fallback={<div>Loading...</div>}>
        {showHeavy && <HeavyComponent />}
        
        {/* 假设这是另一个需要懒加载的组件 */}
        {/* <AnotherLazyComponent /> */}
      </Suspense>
    </div>
  );
}

四、 内存管理与诊断

内存泄漏的常见原因

  1. 意外的全局变量: 未经声明的变量被赋值,成为全局对象的属性。
  2. 遗忘的定时器或回调: setInterval 未被清除,其回调函数及其闭包环境无法被回收。
  3. 分离的DOM节点引用: 如第一节的代码示例。
  4. 闭包的滥用: 闭包会使其外部函数的作用域持续存在,如果作用域中包含大量数据,则可能造成内存占用过高。

诊断工具 (Chrome DevTools)

  • Performance Monitor: 实时监控CPU使用率、JS堆大小、DOM节点数等。

  • Memory Tab:

    • Heap Snapshot (堆快照) : 拍摄堆内存的快照,用于分析对象分布、查找分离的DOM树、定位内存泄漏。
    • Allocation Instrumentation on Timeline: 记录内存分配的时间线,用于定位是哪个函数或操作导致了频繁的内存分配或内存激增。

代码示例 (遗忘的定时器):

js
 体验AI代码助手
 代码解读
复制代码
class PulsingDot {
  constructor() {
    this.size = 0;
    this.isGrowing = true;

    // 定时器通过闭包持有了对 this (PulsingDot实例) 的引用
    this.intervalId = setInterval(() => {
      if (this.isGrowing) {
        this.size += 1;
        if (this.size >= 10) this.isGrowing = false;
      } else {
        this.size -= 1;
        if (this.size <= 0) this.isGrowing = true;
      }
    }, 100);
  }

  // 必须提供一个销毁方法来清除定时器
  destroy() {
    clearInterval(this.intervalId);
    console.log('PulsingDot destroyed and interval cleared.');
  }
}

let dot = new PulsingDot();

// 假设在某个时间点,我们不再需要这个 dot 实例
dot = null;

// 问题:虽然 dot 变量被设为 null,但 PulsingDot 实例无法被回收,
// 因为 setInterval 的回调函数仍然持有对它的引用,定时器还在不停地运行。
// 正确做法:在销毁对象前,调用 dot.destroy()。

五、 软件设计模式

高级开发者应能将设计模式思想融入日常编码,以构建可维护、可扩展的系统。

  • 单例模式 (Singleton) : 确保一个类只有一个实例,并提供一个全局访问点。
  • 观察者模式 (Observer / Pub/Sub) : 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知并自动更新。
  • 工厂模式 (Factory) : 定义一个用于创建对象的接口,让子类决定实例化哪一个类。
  • 装饰器模式 (Decorator) : 动态地给一个对象添加一些额外的职责。
  • 代理模式 (Proxy) : 为其他对象提供一种代理以控制对这个对象的访问。

代码示例 (观察者模式/发布-订阅):

js
 体验AI代码助手
 代码解读
复制代码
class EventBus {
  constructor() {
    this.listeners = {};
  }

  // 订阅
  on(eventName, callback) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName].push(callback);
  }

  // 取消订阅
  off(eventName, callback) {
    if (!this.listeners[eventName]) return;
    this.listeners[eventName] = this.listeners[eventName].filter(
      listener => listener !== callback
    );
  }

  // 发布
  emit(eventName, ...args) {
    if (!this.listeners[eventName]) return;
    this.listeners[eventName].forEach(listener => {
      try {
        listener(...args);
      } catch (e) {
        console.error(`Error in listener for event "${eventName}":`, e);
      }
    });
  }
}

// --- 使用场景 ---
const bus = new EventBus();

function onUserLogin(userData) {
  console.log('Analytics Service: User logged in', userData.name);
}

function updateNavbar(userData) {
  console.log('UI Service: Updating navbar for', userData.name);
}

bus.on('user:login', onUserLogin);
bus.on('user:login', updateNavbar);

// 某处登录成功后...
bus.emit('user:login', { id: 1, name: 'Mickey' });

// 用户退出时,可以取消订阅
// bus.off('user:login', onUserLogin);