Vanson's Eternal Blog

JavaScript夯实基础

Web javascript.png
Published on
/44 mins read/---

高级

JS单线程语言

单线程设计的好处

  • 简单和直观

    • 好处:JavaScript 的单线程模型使得语言本身更加简单和直观。开发者不需要关心多线程之间的竞态条件、死锁等复杂的并发问题,这简化了编程模型和语言的学习曲线。
    • 示例:对于初学者来说,理解单线程的执行顺序比理解多线程的并发执行更容易。
  • 易于控制

    • 好处:由于单线程,JavaScript 可以更容易地管理和控制代码的执行顺序和状态变化。所有的操作都是顺序执行的,没有并发执行带来的不确定性,这有助于避免意外的副作用和错误。
    • 示例:在单线程中,变量的值变化是可预测的,因为不会出现多个线程同时修改同一个变量的情况。
  • 节省内存资源

    • 好处:在浏览器中,每个标签页或窗口都是一个单独的进程或线程。如果 JavaScript 是多线程的,每个标签页可能需要额外的内存来管理多个线程,而单线程模型可以节省内存资源。
    • 示例:单线程模型减少了内存占用,使得浏览器可以同时打开更多标签页而不会过度消耗系统资源。
  • 避免同步问题

    • 好处:在多线程编程中,需要处理同步问题(如竞态条件、死锁、数据一致性等),这对于开发者来说是一个复杂和容易出错的部分。单线程模型避免了这些问题,因为不需要考虑多个线程同时访问和修改共享状态的情况。
    • 示例:在单线程中,开发者不需要使用锁或其他同步机制来保护共享数据。
  • 更好的响应性

    • 好处:JavaScript 在浏览器中通常用于处理用户交互和响应用户操作。单线程模型确保所有的事件和操作都按照其发生的顺序进行处理,这样可以保证 UI 的响应性和一致性。
    • 示例:用户点击按钮后,事件处理函数会立即执行,不会被其他线程打断,从而保证了用户体验的流畅性。

设计成单线程的原因

  • 适应应用场景
    • 原因:JavaScript 最初被设计成单线程主要是为了适应其主要的应用场景:浏览器中的交互式网页。在浏览器中,JavaScript 负责处理用户交互、DOM 操作和网络请求等任务,这些任务不需要多线程并发执行,而是按照事件驱动的方式处理即可。
    • 示例:网页中的大多数任务(如点击按钮、输入文本、发送网络请求)都是顺序执行的,不需要并发处理。
  • 简化实现和优化性能
    • 原因:JavaScript 的单线程模型有助于避免因为多线程带来的复杂性和性能开销,特别是在早期浏览器性能较弱的情况下,单线程可以简化实现和优化执行效率。
    • 示例:早期浏览器的 JavaScript 引擎性能有限,单线程模型减少了实现复杂度,使得引擎可以更高效地执行代码。

怎么解决单线程问题

异步编程

  • 回调函数:通过回调函数处理异步操作,避免阻塞主线程。
  • Promise:提供更优雅的异步编程方式,支持链式调用和错误处理。
  • async/await:使异步代码看起来像同步代码,提高可读性和易用性。

事件循环和任务队列

  • 事件循环:JavaScript 的事件循环机制确保了即使在单线程环境下,也可以处理多个异步任务。
  • 宏任务和微任务队列:通过任务队列管理异步任务,确保任务按顺序执行。

Web Workers

  • Web Workers:允许在后台线程中运行 JavaScript 代码,从而实现真正的多线程处理。虽然 Web Workers 仍然运行在独立的线程中,但它们可以与主线程通信,从而实现复杂的并发任务。

js运行需要什么

JavaScript 引擎

JavaScript 引擎是解释和执行 JavaScript 代码的核心组件。不同的浏览器和运行环境有不同的 JavaScript 引擎实现,其中最常见的包括:

  • V8:由 Google 开发,用于 Chrome 浏览器和 Node.js。
  • SpiderMonkey:由 Mozilla 开发,用于 Firefox 浏览器。
  • JavaScriptCore:由苹果开发,用于 Safari 浏览器和 WebKit。 这些引擎负责将 JavaScript 代码解析、编译成可执行代码,并执行代码逻辑。

宿主环境(Host Environment)

JavaScript 是一种脚本语言,它依赖于宿主环境提供的运行时环境和各种 API。常见的宿主环境包括:

  • 浏览器环境:在浏览器中运行的 JavaScript 可以访问浏览器提供的 DOM、BOM、Fetch API 等 Web API。
  • Node.js 环境:在服务器端运行的 JavaScript,可以访问操作系统的文件系统、网络等功能,还能使用 Node.js 提供的内置模块和第三方模块。

Web API

在浏览器环境中,JavaScript 可以通过 Web API 访问浏览器提供的各种功能和服务,例如:

  • DOM API:用于操作和修改 HTML 文档结构。
  • XHR / Fetch API:用于进行网络请求和获取数据。
  • Canvas 和 WebGL:用于绘制图形和实现复杂的图形渲染。
  • Local Storage 和 Session Storage:用于在客户端持久化存储数据等。

运行时

JavaScript 运行时指的是 JavaScript 引擎与宿主环境和其他 API 的整体集合。它提供了一个完整的环境,使得 JavaScript 能够与外部环境进行交互和操作,从而实现丰富的应用程序功能

浏览器下, JS 的进程和线程

进程

  • 浏览器主进程(Browser Process)

    • 作用:这是浏览器的核心进程,负责管理浏览器的界面(如标签页、地址栏、书签栏等),并协调其他进程的工作。
    • 示例:控制浏览器窗口的显示和关闭。
  • 渲染进程(Rendering Process)

    • 作用:每个标签页通常都有一个独立的渲染进程,负责解析和执行 HTML、CSS 和 JavaScript,以及渲染页面内容。
    • 示例:加载网页内容并显示给用户。
  • 插件进程(Plugin Process)

    • 作用:负责运行浏览器插件,如 Flash 等。
    • 示例:运行网页中的 Flash 动画

线程

  • 主线程(Main Thread)

    • 作用:在渲染进程中,主线程负责解析 HTML、执行 JavaScript 代码,管理 DOM 和 CSSOM,并处理用户交互事件(如点击、滚动等)。
    • 示例:执行页面中的 JavaScript 脚本,更新页面的 DOM 结构。
  • 事件线程(Event Thread)

    • 作用:事件线程负责处理异步事件,例如处理鼠标点击、键盘输入、网络请求的回调等。在浏览器中,事件线程通常由 Event Loop 来管理。
    • 示例:当用户点击按钮时,事件线程会将事件放入事件队列,等待主线程处理。
  • 定时器线程(Timer Thread)

    • 作用:负责管理定时器(如 setTimeout、setInterval)的计时和触发。
    • 示例:设置一个 setTimeout,定时器线程会在指定时间后将回调函数放入事件队列。
  • 网络线程(Network Thread)

    • 作用:负责处理网络请求和响应。
    • 示例:当页面发起一个 HTTP 请求时,网络线程会处理请求和接收响应。

JavaScript 执行的核心位置

总体来说,JavaScript 代码的执行主要发生在浏览器的渲染进程中的主线程中。这个线程负责执行 JavaScript 代码、更新 DOM、处理用户输入等。事件循环(Event Loop)则是负责管理异步操作和事件的执行顺序,确保 JavaScript 在单线程的执行环境下能够处理并发的事件和任务。

JavaScript事件循环机制

核心概念

  • 调用栈(Call Stack):同步代码执行
  • 任务队列(Task Queue):宏任务(setTimeout, setInterval, I/O)
  • 微任务队列(Microtask Queue):Promise, MutationObserver, process.nextTick

js-task-2025-08-06-11-08-21

执行顺序

同步代码 > 所有微任务 > 一个宏任务 > 所有微任务

console.log('1');
 
setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => console.log('3'));
}, 0);
 
Promise.resolve().then(() => {
  console.log('4');
  setTimeout(() => console.log('5'), 0);
});
 
console.log('6');
 
// 输出顺序:1 6 4 2 3 5

实现Promise A+规范

class MyPromise {
  /**
   * 构造函数,接收一个执行器函数(executor)
   * @param {Function} executor - 包含resolve和reject两个参数的函数
   */
  constructor(executor) {
    // 初始状态为pending
    this.state = 'pending';
    // 存储成功或失败的值
    this.value = undefined;
    // 存储成功回调函数数组
    this.onFulfilledCallbacks = [];
    // 存储失败回调函数数组
    this.onRejectedCallbacks = [];
 
    /**
     * 定义resolve函数
     * @param {any} value - 要解析的值
     */
    const resolve = (value) => {
      // 如果解析的值是Promise,则等待这个Promise解决
      if (value instanceof MyPromise) {
        return value.then(resolve, reject);
      }
      
      // 只有pending状态可以转换为fulfilled
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        // 执行所有成功回调
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };
 
    /**
     * 定义reject函数
     * @param {any} reason - 拒绝原因
     */
    const reject = (reason) => {
      // 只有pending状态可以转换为rejected
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.value = reason;
        // 执行所有失败回调
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };
 
    // 立即执行executor函数
    try {
      // 传入resolve和reject函数
      executor(resolve, reject);
    } catch (err) {
      // 如果执行器抛出错误,直接reject
      reject(err);
    }
  }
 
  /**
   * then方法,接收两个可选参数:onFulfilled和onRejected
   * @param {Function} onFulfilled - 成功回调
   * @param {Function} onRejected - 失败回调
   * @returns {MyPromise} 返回一个新的Promise
   */
  then(onFulfilled, onRejected) {
    // 创建新的Promise实例以实现链式调用
    const promise2 = new MyPromise((resolve, reject) => {
      /**
       * 处理成功情况的函数
       */
      const handleFulfilled = () => {
        // 使用queueMicrotask确保异步执行
        queueMicrotask(() => {
          try {
            // 如果onFulfilled是函数则执行它,否则直接传递值
            const x = typeof onFulfilled === 'function' 
              ? onFulfilled(this.value)
              : this.value;
            // 解析返回的值
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            // 如果回调函数抛出错误,reject新Promise
            reject(err);
          }
        });
      };
 
      /**
       * 处理失败情况的函数
       */
      const handleRejected = () => {
        // 使用queueMicrotask确保异步执行
        queueMicrotask(() => {
          try {
            // 如果onRejected是函数则执行它,否则直接传递原因
            const x = typeof onRejected === 'function' 
              ? onRejected(this.value)
              : this.value;
            // 解析返回的值
            this.resolvePromise(promise2, x, resolve, reject);
          } catch (err) {
            // 如果回调函数抛出错误,reject新Promise
            reject(err);
          }
        });
      };
 
      // 根据当前状态决定如何处理
      if (this.state === 'fulfilled') {
        // 如果已经是fulfilled状态,异步执行回调
        handleFulfilled();
      } else if (this.state === 'rejected') {
        // 如果已经是rejected状态,异步执行回调
        handleRejected();
      } else {
        // 如果是pending状态,将回调函数存入数组
        this.onFulfilledCallbacks.push(handleFulfilled);
        this.onRejectedCallbacks.push(handleRejected);
      }
    });
 
    return promise2;
  }
 
  /**
   * 解析Promise返回值的方法
   * @param {MyPromise} promise2 - 新的Promise实例
   * @param {any} x - then回调返回的值
   * @param {Function} resolve - 新Promise的resolve函数
   * @param {Function} reject - 新Promise的reject函数
   */
  resolvePromise(promise2, x, resolve, reject) {
    // 如果返回的Promise和then返回的Promise相同,抛出循环引用错误
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise'));
    }
 
    // 如果x是Promise实例,则等待它的结果
    if (x instanceof MyPromise) {
      x.then(resolve, reject);
    } 
    // 如果x是对象或函数,可能是一个thenable对象
    else if (typeof x === 'object' && x !== null || typeof x === 'function') {
      let then;
      try {
        // 尝试获取x的then方法
        then = x.then;
      } catch (err) {
        // 如果获取then方法时出错,reject
        return reject(err);
      }
 
      // 如果then是函数,则认为x是thenable对象
      if (typeof then === 'function') {
        let called = false; // 防止多次调用
        try {
          // 调用then方法
          then.call(
            x,
            // 成功回调
            y => {
              if (called) return;
              called = true;
              // 递归解析y
              this.resolvePromise(promise2, y, resolve, reject);
            },
            // 失败回调
            r => {
              if (called) return;
              called = true;
              reject(r);
            }
          );
        } catch (err) {
          if (called) return;
          reject(err);
        }
      } else {
        // 普通对象,直接resolve
        resolve(x);
      }
    } else {
      // 基本类型值,直接resolve
      resolve(x);
    }
  }
 
  /**
   * catch方法 - 错误处理快捷方式
   * @param {Function} onRejected - 失败回调
   * @returns {MyPromise} 返回一个新的Promise
   */
  catch(onRejected) {
    return this.then(null, onRejected);
  }
 
  /**
   * finally方法 - 无论成功失败都会执行
   * @param {Function} callback - 回调函数
   * @returns {MyPromise} 返回一个新的Promise
   */
  finally(callback) {
    return this.then(
      // 成功情况
      value => MyPromise.resolve(callback()).then(() => value),
      // 失败情况
      reason => MyPromise.resolve(callback()).then(() => { throw reason })
    );
  }
 
  /**
   * 静态resolve方法
   * @param {any} value - 要解析的值
   * @returns {MyPromise} 返回一个已解决的Promise
   */
  static resolve(value) {
    // 如果已经是Promise实例,直接返回
    if (value instanceof MyPromise) {
      return value;
    }
    // 否则创建新的已解决Promise
    return new MyPromise(resolve => resolve(value));
  }
 
  /**
   * 静态reject方法
   * @param {any} reason - 拒绝原因
   * @returns {MyPromise} 返回一个已拒绝的Promise
   */
  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
 
  /**
   * 静态all方法
   * @param {Iterable} promises - 可迭代的Promise集合
   * @returns {MyPromise} 返回一个新的Promise
   */
  static all(promises) {
    return new MyPromise((resolve, reject) => {
      const results = []; // 存储结果
      let completed = 0;  // 完成计数器
      
      // 空数组直接resolve
      if (promises.length === 0) {
        return resolve(results);
      }
 
      // 遍历所有Promise
      promises.forEach((promise, index) => {
        // 确保每个值都是Promise
        MyPromise.resolve(promise).then(
          // 成功处理
          value => {
            results[index] = value; // 按索引存储结果
            completed++;
            // 全部完成时resolve
            if (completed === promises.length) {
              resolve(results);
            }
          },
          // 任何一个失败立即reject
          reject
        );
      });
    });
  }
 
  /**
   * 静态race方法
   * @param {Iterable} promises - 可迭代的Promise集合
   * @returns {MyPromise} 返回一个新的Promise
   */
  static race(promises) {
    return new MyPromise((resolve, reject) => {
      // 遍历所有Promise
      promises.forEach(promise => {
        // 确保每个值都是Promise
        MyPromise.resolve(promise).then(
          // 第一个解决或拒绝的结果决定race的结果
          resolve,
          reject
        );
      });
    });
  }
 
  /**
   * 静态allSettled方法
   * @param {Iterable} promises - 可迭代的Promise集合
   * @returns {MyPromise} 返回一个新的Promise
   */
  static allSettled(promises) {
    return new MyPromise(resolve => {
      const results = []; // 存储结果
      let completed = 0;  // 完成计数器
      
      // 空数组直接resolve
      if (promises.length === 0) {
        return resolve(results);
      }
 
      // 遍历所有Promise
      promises.forEach((promise, index) => {
        // 确保每个值都是Promise
        MyPromise.resolve(promise).then(
          // 成功处理
          value => {
            results[index] = { status: 'fulfilled', value };
            completed++;
            // 全部完成时resolve
            if (completed === promises.length) {
              resolve(results);
            }
          },
          // 失败处理
          reason => {
            results[index] = { status: 'rejected', reason };
            completed++;
            // 全部完成时resolve
            if (completed === promises.length) {
              resolve(results);
            }
          }
        );
      });
    });
  }
 
  /**
   * 静态any方法
   * @param {Iterable} promises - 可迭代的Promise集合
   * @returns {MyPromise} 返回一个新的Promise
   */
  static any(promises) {
    return new MyPromise((resolve, reject) => {
      const errors = []; // 存储所有错误
      let rejected = 0;  // 拒绝计数器
      
      // 空数组直接reject
      if (promises.length === 0) {
        return reject(new AggregateError([], 'All promises were rejected'));
      }
 
      // 遍历所有Promise
      promises.forEach((promise, index) => {
        // 确保每个值都是Promise
        MyPromise.resolve(promise).then(
          // 任何一个成功立即resolve
          resolve,
          // 失败处理
          reason => {
            errors[index] = reason; // 存储错误
            rejected++;
            // 全部失败时reject
            if (rejected === promises.length) {
              reject(new AggregateError(errors, 'All promises were rejected'));
            }
          }
        );
      });
    });
  }
}
  • 状态管理机制:
    • Promise有三种不可逆状态: pending → fulfilled/rejected
    • 状态一旦改变就不能再变,确保Promise的确定性
  • 异步执行保证:
    • 使用queueMicrotask实现微任务队列(比setTimeout优先级更高)
    • 符合Promise/A+规范的2.2.4条要求回调必须异步执行
  • 链式调用实现:
    • 每个then都返回新Promise,形成调用链
    • 通过resolvePromise方法处理各种返回值(Promise/thenable/普通值)
  • 错误处理流程:
    • 执行器错误 → 直接reject
    • 回调错误 → 捕获并传递给下一个Promise
    • 通过catch方法提供便捷的错误处理
  • 静态方法设计:
    • all: 等待所有成功或第一个失败
    • race: 采用第一个解决的结果
    • allSettled: 等待所有Promise完成
    • any: 采用第一个成功的结果或所有失败
  • 边缘情况处理:
    • 循环引用检测
    • thenable对象处理
    • 空数组处理
    • 值穿透实现

符合 Promise A+ 规范的重要特性

  • then 方法可以被同一个 promise 调用多次:通过回调数组实现多个回调的注册
  • then 方法必须返回一个 promise:每次调用 then 都创建并返回新的 Promise
  • 值的穿透:当 then 的参数不是函数时,值会穿透到下一个 then
  • 错误冒泡:链式调用中,前面的错误会被后面的 catch 捕获

带缓存的函数记忆化工具

  • 支持自定义resolver处理复杂参数
  • 使用Map而非普通对象存储缓存
  • 保留原函数的this绑定
  • 提供缓存清除接口
function memoize(fn, resolver) {
  // 使用WeakMap保存缓存,键为this对象时更安全
  const cache = new Map();
  
  const memoized = function(...args) {
    // 生成缓存key:使用resolver或直接JSON.stringify
    const key = resolver 
      ? resolver.apply(this, args)
      : JSON.stringify(args);
    
    // 检查缓存
    if (cache.has(key)) {
      return cache.get(key);
    }
    
    // 执行原函数并缓存结果
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
  
  // 允许手动清除缓存
  memoized.clear = () => cache.clear();
  
  return memoized;
}
 
// 测试用例
const add = (a, b) => {
  console.log('Calculating...');
  return a + b;
};
 
const memoizedAdd = memoize(add);
console.log(memoizedAdd(1, 2)); // Calculating... 3
console.log(memoizedAdd(1, 2)); // 3 (from cache)
 
// 带对象参数的案例
const getUser = memoize(
  (userId, obj) => ({...obj, id: userId}),
  (userId, obj) => `user-${userId}-${obj.lang}`
);
 
console.log(getUser(1, {lang: 'en'})); // Calculating...
console.log(getUser(1, {lang: 'en'})); // From cache

异步任务调度器

  • 使用队列管理等待任务
  • 通过running计数器控制并发
  • 返回Promise保持链式调用
  • 任务完成后自动触发下一个
class Scheduler {
  constructor(concurrency) {
    this.max = concurrency;
    this.queue = [];
    this.running = 0;
  }
 
  add(promiseCreator) {
    return new Promise((resolve, reject) => {
      // 包装任务以处理执行和计数
      const task = () => {
        this.running++;
        promiseCreator()
          .then(resolve)
          .catch(reject)
          .finally(() => {
            this.running--;
            this.next();
          });
      };
      
      // 根据当前运行数量决定立即执行或入队
      if (this.running < this.max) {
        task();
      } else {
        this.queue.push(task);
      }
    });
  }
 
  next() {
    if (this.queue.length > 0 && this.running < this.max) {
      const task = this.queue.shift();
      task();
    }
  }
}
 
// 测试用例
const timeout = (time) => new Promise(r => setTimeout(r, time));
 
const scheduler = new Scheduler(2);
const addTask = (time, order) => {
  scheduler.add(() => timeout(time))
    .then(() => console.log(order));
};
 
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 输出:2 3 1 4

深度对象比较

  • 处理7种基本类型的比较
  • 特殊对象(Date/RegExp)的单独处理
  • 数组与对象的递归比较
  • 属性顺序不影响比较结果
  • 循环引用检测(可扩展实现)
function isEqual(obj1, obj2) {
  // 基本类型直接比较
  if (obj1 === obj2) return true;
  
  // 处理null/undefined
  if (obj1 == null || obj2 == null) {
    return obj1 === obj2;
  }
  
  // 类型不同直接返回false
  if (Object.prototype.toString.call(obj1) !== 
      Object.prototype.toString.call(obj2)) {
    return false;
  }
  
  // 处理Date/RegExp等特殊对象
  if (obj1 instanceof Date) return +obj1 === +obj2;
  if (obj1 instanceof RegExp) return obj1.toString() === obj2.toString();
  
  // 处理原始值包装对象
  if (typeof obj1 !== 'object') return obj1 === obj2;
  
  // 处理数组
  if (Array.isArray(obj1)) {
    if (obj1.length !== obj2.length) return false;
    return obj1.every((item, i) => isEqual(item, obj2[i]));
  }
  
  // 处理普通对象
  const keys1 = Object.keys(obj1);
  const keys2 = Object.keys(obj2);
  
  if (keys1.length !== keys2.length) return false;
  
  return keys1.every(key => {
    // 递归比较每个属性
    return isEqual(obj1[key], obj2[key]);
  });
}
 
// 测试用例
console.log(isEqual(
  { a: 1, b: { c: [1,2,3], d: new Date('2020-01-01') } },
  { a: 1, b: { c: [1,2,3], d: new Date('2020-01-01') } }
)); // true
 
console.log(isEqual(
  { a: 1, b: { c: [1,2,3] } },
  { a: 1, b: { c: [1,2,4] } }
)); // false

内存泄漏常见场景及检测方法

内部函数长期被引用

把大数组保存在外层作用域,内部函数长期被引用,导致大数组无法释放。

<body>
  <button id="create">创建闭包泄漏</button>
  <button id="release">释放闭包</button>
  <script>
    let closureLeak;                // ① 全局变量始终引用闭包
    document.getElementById('create').onclick = () => {
      const big = new Array(1e6).fill('leak');  // ~8 MB
      closureLeak = function () {                // ② 内部函数被长期持有
        console.log(big.length);                 // ③ big 无法被 GC
      };
    };
    document.getElementById('release').onclick = () => {
      closureLeak = null;          // ④ 解除引用 → 稍后 GC
    };
  </script>
</body>

泄漏原因

  • big 被内部函数捕获,而内部函数又挂在全局变量 closureLeak 上 → V8 认为仍在使用。
  • 如果 closureLeak 一直不置空,哪怕页面切走 这 8 MB 也不会释放。

未清理的 DOM 引用

用 JS 对象缓存 DOM,节点从文档移除后,缓存仍引用导致无法回收。

<ul id="list"></ul>
<button id="add">新增 li</button>
<button id="clear">清空 DOM</button>
<script>
  const cache = {};                   // ① 手动缓存
  let counter = 0;
  document.getElementById('add').onclick = () => {
    const li = document.createElement('li');
    li.textContent = 'item ' + (++counter);
    cache[counter] = li;              // ② 强引用
    document.getElementById('list').appendChild(li);
  };
  document.getElementById('clear').onclick = () => {
    document.getElementById('list').innerHTML = ''; // ③ 节点脱离文档
    // cache 未清理 → 节点仍存活
  };
</script>

泄漏原因:

  • 节点虽然不在 DOM 树,但 cache 对象仍强引用它们 → 无法 GC。
  • 如果 cache 是全局变量或模块级变量,泄漏将伴随整个页面生命周期。
document.getElementById('clear').onclick = () => {
  document.getElementById('list').innerHTML = '';
  Object.keys(cache).forEach(k => delete cache[k]); // 解除引用
};

用 Chrome DevTools 检测内存泄漏

  1. 打开 DevTools → Memory → 选择「Heap snapshot」
  2. 操作前拍一次快照「Snapshot 1」
  3. 触发泄漏动作(如点击「创建闭包泄漏」10 次)
  4. 再拍「Snapshot 2」,然后对比:
  • Summary 视图 → 选择「Objects allocated between Snapshot 1 and 2」
  • 过滤关键词:(string)、HTMLLIElement、(closure)
  • 展开构造函数 → 看 Retained Size(保留大小),如果持续增长即可确认泄漏。
  1. 进阶:Allocation instrumentation on timeline
  • DevTools → Memory → Allocation instrumentation on timeline → 开始录制 → 操作页面 → 停止。
  • 红色柱形表示未释放分配,可直接定位到代码行。

大型单页应用的首屏加载速度?

  • 代码分割与懒加载策略:减少初始加载的JavaScript体积
    • 动态导入:使用 import() 语法实现路由级和组件级分割
    • 路由懒加载:配合React Router/Vue Router按需加载路由
    • 第三方库分割:通过Webpack的 splitChunks 分离第三方依赖
    • 组件级懒加载:非关键组件(如弹窗、Tab内容)延迟加载
  • 预加载关键资源:优先加载影响首屏的核心资源
    • 资源优先级标记
      • <link rel="preload" href="critical.css" as="style">
      • <link rel="preload" href="main.js" as="script">
      • <link rel="prefetch" href="next-page-chunk.js">
    • Webpack魔法注释:预加载异步模块 import(/* webpackPreload: true */ 'AnalyticsModule');
    • 关键CSS内联:将首屏CSS直接内嵌到HTML中,避免请求阻塞
    • 字体预加载 <link rel="preload" href="font.woff2" as="font" crossorigin>
  • SSR与CSR结合方案
    • 静态SSG+动态CSR:
      • 使用Next.js/Nuxt.js对静态页面预渲染(SSG)
      • 动态内容通过客户端 hydration 接管
    • 流式SSR:分块传输HTML(如React 18的 renderToPipeableStream)
    • 部分SSR:仅对首屏关键部分服务端渲染(如导航栏、骨架屏)
    • Edge CDN渲染:利用Cloudflare Workers/Vercel Edge Functions就近渲染
  • 其他关键优化
    • CDN加速静态资源:压缩并分发JS/CSS/图片
    • 缓存策略:强缓存(Cache-Control: max-age=31536000)与版本哈希
    • 图片优化:
      • 使用WebP/AVIF格式
      • 响应式图片(srcset)
      • SVG雪碧图替代图标字体
    • 减少主线程工作:
      • Web Worker处理复杂计算
      • 避免同步布局抖动

高性能无限滚动列表

import React, {
  useState,
  useRef,
  useEffect,
  useCallback,
  useMemo,
} from "react";
 
interface VirtualListProps<T> {
  data: T[]; // 数据源
  renderItem: (item: T, index: number) => React.ReactNode; // 单项渲染函数
  estimatedItemHeight?: number; // 预估行高(动态高度必传)
  bufferSize?: number; // 缓冲区大小
  className?: string;
}
 
/**
 * 高性能虚拟滚动列表
 * 核心功能:
 * 1. 仅渲染可视区域内的元素
 * 2. 支持动态高度(自动测量)
 * 3. Intersection Observer优化滚动性能
 * 4. DOM节点回收复用
 */
function VirtualList<T>({
  data,
  renderItem,
  estimatedItemHeight = 50,
  bufferSize = 3,
  className,
}: VirtualListProps<T>) {
  const containerRef = useRef<HTMLDivElement>(null);
  const itemRefs = useRef<Map<number, HTMLDivElement>>(new Map()); // 存储已渲染的DOM节点
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 0 });
  const [itemPositions, setItemPositions] = useState<number[]>([]); // 每个元素的位置缓存
 
  // 1. 初始化位置缓存(基于预估高度)
  const initPositions = useCallback(() => {
    const positions = [0];
    for (let i = 1; i <= data.length; i++) {
      positions[i] = positions[i - 1] + estimatedItemHeight;
    }
    setItemPositions(positions);
  }, [data.length, estimatedItemHeight]);
 
  // 2. 动态更新元素位置(实际测量高度)
  const updateItemPosition = useCallback(
    (index: number, height: number) => {
      if (Math.abs(itemPositions[index + 1] - itemPositions[index] - height) < 1) {
        return; // 高度无变化时不更新
      }
 
      setItemPositions((prev) => {
        const newPositions = [...prev];
        const diff = height - (newPositions[index + 1] - newPositions[index]);
        for (let i = index + 1; i <= data.length; i++) {
          newPositions[i] += diff;
        }
        return newPositions;
      });
    },
    [data.length, itemPositions]
  );
 
  // 3. 计算当前可视区域
  const calculateVisibleRange = useCallback(() => {
    if (!containerRef.current) return;
 
    const { scrollTop, clientHeight } = containerRef.current;
    const startIdx = findNearestIndex(itemPositions, scrollTop);
    const endIdx = findNearestIndex(itemPositions, scrollTop + clientHeight);
 
    setVisibleRange({
      start: Math.max(0, startIdx - bufferSize),
      end: Math.min(data.length - 1, endIdx + bufferSize),
    });
  }, [itemPositions, bufferSize, data.length]);
 
  // 4. 使用Intersection Observer监听可视区域变化
  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting || entry.intersectionRatio > 0) {
            calculateVisibleRange();
          }
        });
      },
      {
        root: containerRef.current,
        rootMargin: `${bufferSize * estimatedItemHeight}px`,
        threshold: 0.1,
      }
    );
 
    // 观察底部哨兵元素
    const sentinel = document.createElement("div");
    sentinel.style.position = "absolute";
    sentinel.style.bottom = "0";
    sentinel.style.height = "1px";
    containerRef.current?.appendChild(sentinel);
    observer.observe(sentinel);
 
    return () => {
      observer.disconnect();
      containerRef.current?.removeChild(sentinel);
    };
  }, [calculateVisibleRange, bufferSize, estimatedItemHeight]);
 
  // 5. 初始化位置和可视区域
  useEffect(() => {
    initPositions();
    calculateVisibleRange();
  }, [initPositions, calculateVisibleRange]);
 
  // 6. 渲染可视区域内的元素
  const visibleItems = useMemo(() => {
    const items = [];
    for (let i = visibleRange.start; i <= visibleRange.end; i++) {
      items.push(
        <div
          key={i}
          ref={(node) => {
            if (node) {
              itemRefs.current.set(i, node);
              // 动态高度测量
              if (estimatedItemHeight > 0) {
                const realHeight = node.getBoundingClientRect().height;
                updateItemPosition(i, realHeight);
              }
            } else {
              itemRefs.current.delete(i);
            }
          }}
          style={{
            position: "absolute",
            top: `${itemPositions[i]}px`,
            width: "100%",
          }}
        >
          {renderItem(data[i], i)}
        </div>
      );
    }
    return items;
  }, [
    visibleRange,
    data,
    renderItem,
    itemPositions,
    updateItemPosition,
    estimatedItemHeight,
  ]);
 
  return (
    <div
      ref={containerRef}
      className={`virtual-list ${className}`}
      style={{
        height: "100%",
        overflowY: "auto",
        position: "relative",
        transform: "translateZ(0)", // GPU加速
      }}
      onScroll={calculateVisibleRange}
    >
      {/* 撑开容器高度的占位元素 */}
      <div
        style={{
          height: `${itemPositions[data.length] || 0}px`,
          position: "relative",
        }}
      >
        {visibleItems}
      </div>
    </div>
  );
}
 
/** 二分查找最近的元素索引(性能关键) */
function findNearestIndex(arr: number[], value: number): number {
  let low = 0;
  let high = arr.length - 1;
  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    if (arr[mid] === value) return mid;
    if (arr[mid] < value) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  return low;
}
 
export default React.memo(VirtualList);
 
  • 虚拟列表核心逻辑
    • 可视区域计算:通过 containerRef.current.scrollTop 和 clientHeight 确定当前视口位置
    • 二分查找优化:findNearestIndex 快速定位起始/结束索引(时间复杂度 O(log n))
    • 绝对定位:每个元素通过 top 定位到正确位置,避免重排
  • 动态高度处理
    • 双高度策略:
      • 初始化使用 estimatedItemHeight 估算位置
      • 渲染后通过 getBoundingClientRect() 获取实际高度并修正位置
    • 差值传播:当某个元素高度变化时,递归更新后续所有元素的位置
  1. 性能优化手段
优化技术实现方式效果
Intersection Observer监听底部哨兵元素,替代滚动事件减少主线程压力
GPU加速transform: translateZ(0) 强制开启GPU层平滑滚动
缓冲区bufferSize 扩展渲染范围,防止快速滚动白屏提升用户体验
DOM节点复用itemRefs Map保存已渲染节点,避免重复创建减少内存占用

完整工作流程

  • 初始化:根据预估高度生成初始位置数组 [0, h1, h1+h2, ...]
  • 滚动触发:计算新 visibleRange → 渲染新元素 → 回收旧节点
  • 高度修正:对新渲染元素进行实际测量 → 更新位置缓存 → 触发重计算
  • 渲染优化:复用已有DOM → GPU加速滚动 → 异步分批渲染

WebAssembly

WebAssembly(简称 WASM)是一个二进制指令格式,它让 C/C++/Rust/Go 等语言编译的产物能在浏览器或任何 WASM 运行时里以接近原生速度安全地运行,并与 JavaScript 无缝互调。

wasm-2025-08-06-10-43-31

维度描述
字节码.wasm 文件,体积小、可流式编译,比 JS 解析快 10-30 倍
性能接近原生(80%-90%),可利用 SIMD、多线程、64 位内存
安全沙盒运行在浏览器或 Node.js 的线性内存 + Capability 模型里,无越界访问
语言无关任何能编译到 LLVM 的语言(C/C++、Rust、Go、AssemblyScript、C#、Swift…)皆可
JS 互操作通过 import/export 与 JS 共享内存、函数调用,零拷贝

应用场景

  • 视频解码:FFmpeg编译为WASM处理H.264
  • 密码学:SHA-3算法比JS快20倍
  • 游戏物理引擎:Box2D/Ammo.js实现碰撞检测

JS预编译

全局预编译

脚本加载时

  • 变量声明提升:var 声明被提升,初始值为 undefined
  • 函数声明提升:整个函数体被提升
  • 函数优先:同名时函数覆盖变量
console.log(a);     // 输出: undefined (变量提升)
var a = 1;
function a() {}     // 函数声明提升并覆盖变量a
console.log(a);     // 输出: 1 (赋值覆盖函数)
 
// 预编译后的等效代码:
/*
function a() {}
var a;
console.log(a); // 此时a是函数
a = 1;
console.log(a);
*/

函数预编译

函数调用前

  • 创建 AO(Activation Object)
  • 形参和变量声明提升到AO,值为 undefined
  • 实参赋值给形参
  • 函数声明提升并覆盖同名变量
function test(b) {
  console.log(b);    // 输出: function b() {} (函数声明覆盖形参)
  var b = 2;
  console.log(b);    // 输出: 2 (赋值覆盖)
  function b() {}    // 提升到AO顶部
}
test(1);
 
// 预编译后的等效代码:
/*
function test(b) {
  function b() {}    // 函数声明提升
  var b;             // 已存在同名函数,忽略
  b = 1;             // 形参赋值
  console.log(b);    // 输出函数
  b = 2;             // 重新赋值
  console.log(b);
}
*/

ES6+ 的影响

let/const:存在暂时性死区(TDZ),不会提升

console.log(c); // ReferenceError
let c = 3;

函数表达式:不会提升

foo(); // TypeError: foo is not a function
var foo = function() {};

面试题:

var x = 1;
function foo(
  x,
  y = function() { x = 2; }
) {
  var x = 3;
  y();
  console.log(x); // 输出: 3
}
foo();
console.log(x);   // 输出: 1
 
/*
解析:
1. 函数参数作用域与函数体作用域分离
2. y()修改的是参数x(=2),而内部var x=3是新的局部变量
3. 最外层的x未被影响
*/

手写

实现数组的map方法

function myMap(arr, callback) {
  // 检查参数是否正确
  if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function') {
      return [];
  } else {
      let result = [];
      for (let i = 0, len = arr.length; i < len; i++) {
          result.push(callback(arr[i], i, arr))
      }
      return result
  }
}

实现instanceof

const _instanceof = (target, Fn) => {
    if ((typeof target !== 'object' && typeof target !== 'function') || target === null)
        return false;
 
    let proto = target.__proto__
    while (true) {
        if (proto === null) return false
        if (proto === Fn.prototype) return true
        proto = proto.__proto__
    }
}
 
function A() {}
const a = new A()
console.log(_instanceof(a, A)) // true
console.log(_instanceof(1, A)) // false

实现call方法

 

实现apply方法

var name = '公众号:前端技术营';
var obj = {
    name: '张三'
};
 
function fn(a, b, c) {
    console.log(`${a} ${b} ${c} ${this.name}`);
};
// 模拟apply
Function.prototype.myApply = function(){
  let target = arguments[0]|| window || global;
  target.fn = this;
  let args = Array.from(arguments)[1];
  let result = target.fn(...args);
  delete target.fn;
  return result;
}
 
fn.myApply(obj, ['my', 'name', 'is']); // my name is 张三
fn.myApply(null, ['my', 'name', 'is']); // my name is 公众号:前端技术营
fn.myApply(undefined, ['my', 'name', 'is']); // my name is 公众号:前端技术营

实现bind方法

var name = '公众号:前端技术营';
var obj = {
    name: '张三'
};
 
function fn(a, b, c) {
    console.log(`${a} ${b} ${c} ${this.name}`);
};
// 模拟call
Function.prototype.myBind = function(context) {
    const _this = this
    const args = [...arguments].slice(1);
    // 返回一个函数
    return function F() {
        // 因为返回了一个函数,我们可以 new F(),所以需要判断
        if (this instanceof F) {
            // 忽略传入的this
            return new _this(...args, ...arguments)
        }
        // 直接调用,将两边的参数拼接起来
 
        return _this.apply(context, args.concat(...arguments))
    }
}
 
const fn1 = fn.myBind(obj, 'my', 'name', 'is');
fn1(); // my name is 张三
 
const fn2 = fn.myBind(null, 'my', 'name', 'is');
fn2(); // my name is 公众号:前端技术营
 
const fn3 = fn.myBind(undefined, 'my', 'name', 'is');
fn3(); // my name is 公众号:前端技术营

实现new关键字

function myNew() {
    // 创建一个空对象
    let obj = new Object();
    // shift删除数组第一个元素,并返回一个元素。原有arguments数组第一个参数是构造函数,返回值为构造函数。
    const Constructor = [].shift.call(arguments);
    // 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
    obj.__proto__ = Constructor.prototype;
    // 执行构造函数的方法
    const ret = Constructor.apply(obj, arguments);
    // 返回构造函数中有返回值且返回值是个引用类型,没有返回值则默认返回这个对象
    return typeof ret === 'object' ? ret : obj;
}
 
function Person(name, age) {
    this.name = name;
    this.age = age
    // 模拟有返回值且是个对象
    return {
        name: name,
        address: '上海市浦东新区'
    };
}
 
const a = myNew(Person, '公众号:前端技术营', 18);
console.log(a.name); // 公众号:前端技术营
console.log(a.address); // 上海市浦东新区
console.log(a.age); // undefined

实现函数柯里化

把固定 n 个参数的函数拆成 n 个嵌套的一元函数,让调用可以逐次传参,最后一次性返回结果。

function curry(fn, args) {
  // 获取函数需要的参数长度
  let length = fn.length;
 
  args = args || [];
 
  return function() {
    let subArgs = args.slice(0);
 
    // 拼接得到现有的所有参数
    for (let i = 0; i < arguments.length; i++) {
      subArgs.push(arguments[i]);
    }
 
    // 判断参数的长度是否已经满足函数所需参数的长度
    if (subArgs.length >= length) {
      // 如果满足,执行函数
      return fn.apply(this, subArgs);
    } else {
      // 如果不满足,递归返回科里化的函数,等待参数的传入
      return curry.call(this, fn, subArgs);
    }
  };
}
 
// es6 实现
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

实现object.create()方法

function objectCreate(obj){
  function F(){};  // 构造函数
  F.prototype = obj; // obj 作为构造函数原型的属性
  return new F(); // 返回一个实例对象 就像 `let ff = new F();  return ff`
}

数组去重

// 1.利用Set
const res = Array.from(new Set(arr));
 
// 2.两层for循环+splice
const unique = arr => {
  let len = arr.length;
  for (let i = 0; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] === arr[j]) {
        arr.splice(j, 1);
        len--;
        j--;
      }
    }
  }
  return arr;
}
 
// 3.利用indexOf
const unique = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
  }
  return res;
}
 
// 4.利用include
const unique = arr => {
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!res.includes(arr[i])) res.push(arr[i]);
  }
  return res;
}
 
// 5.利用filter
const unique = arr => {
  return arr.filter((item, index) => {
    return arr.indexOf(item) === index;
  });
}
 
// 6.利用Map
const unique = arr => {
  const map = new Map();
  const res = [];
  for (let i = 0; i < arr.length; i++) {
    if (!map.has(arr[i])) {
      map.set(arr[i], true);
      res.push(arr[i]);
    }
  }
  return res;
}

数组求和

// 1.利用for循环
function getSum(arr){    
  let sum = 0;
  for(var i = 0;i<arr.length;i++){
      sum += arr[i]; 
  }
  return sum ;
}
 
// 2.利用reduce
const sum = arr.reduce(function (prev, cur) {
    return prev + cur;
}, 0);
 
// 3.利用forEach
function getSum(arr) {
  let sum = 0;
  arr.forEach((val) => {
    sum += val;
  });
  
  return sum;
};
 
// 4.使用递归实现数组求和
function sum(arr) {
  if (arr.length === 0) {
    return 0;
  }
  return arr[0] + sum(arr.slice(1));
}

xxx

 

xxx

 

参考:

https://mp.weixin.qq.com/s/xtyrAj1qtkUFLdb75dShtw

https://mp.weixin.qq.com/s/39zJtMHUFSTAfhPXEXDvIA

https://mp.weixin.qq.com/s/x2xjF_6_O399EBxxwYqtQw

https://mp.weixin.qq.com/s/nkUw6qPJGFRHQuYLmsXKWQ

https://mp.weixin.qq.com/s/rLuGgS-HL2s3GUeJOk_F9w

https://mp.weixin.qq.com/s/Nyy_ByYUoiEsWuO-bDdydw

https://mp.weixin.qq.com/s/yPtwrW1yNZsE7r0QRs8lGg