JavaScript核心知识点详解

好的,我们来详细扩展说明这几个核心的 JavaScript 知识点。


1. 事件循环 (Event Loop)

事件循环是 JavaScript 处理异步操作的核心机制。它允许单线程的 JavaScript 执行非阻塞的 I/O 操作。

宏任务 vs. 微任务

  • 宏任务: 由浏览器/Node.js 环境发起的任务。例如:
    • setTimeout, setInterval
    • setImmediate (Node.js 特有)
    • I/O 操作 (如读取文件、网络请求)
    • UI 渲染 (浏览器)
    • 整体的 script 代码本身
  • 微任务: 由 JavaScript 自身发起的任务,通常与“承诺”相关。例如:
    • Promise.then(), Promise.catch(), Promise.finally()
    • process.nextTick (Node.js 特有,优先级最高)
    • MutationObserver (浏览器)

执行规则 (浏览器)

  1. 执行一个宏任务(通常是最老的或最先入队的),例如执行全局的脚本。
  2. 执行过程中遇到微任务,就将其放入微任务队列;遇到宏任务,就将其放入宏任务队列。
  3. 当前宏任务执行完毕后,立即清空整个微任务队列(执行所有微任务)。
  4. 进行 UI 渲染(如果需要)。
  5. 从宏任务队列中取出下一个宏任务,开始新一轮的循环。

核心:一个宏任务 → 所有微任务 → (渲染) → 下一个宏任务。

Node.js 与浏览器的差异

Node.js 的事件循环基于 libuv 库,其阶段划分比浏览器更复杂。

Node.js 事件循环阶段 (简化版):

  1. Timers: 执行 setTimeoutsetInterval 的回调。
  2. Pending callbacks: 执行延迟到下一个循环的 I/O 回调。
  3. Idle, Prepare: 内部使用。
  4. Poll: 检索新的 I/O 事件;执行 I/O 相关的回调(如读取文件)。必要时会在此阶段阻塞
  5. Check: 执行 setImmediate 的回调。
  6. Close callbacks: 执行关闭事件的回调(如 socket.on('close', ...))。

process.nextTick() 在一个阶段结束后、下一个阶段开始前执行,它拥有一个独立的队列,优先级高于微任务

主要差异总结:

特性 浏览器 Node.js
阶段 相对简单(宏任务、微任务、渲染) 分为多个明确阶段(Timers, Poll, Check等)
setImmediate 不支持 支持,在 Check 阶段执行
process.nextTick 不支持 支持,优先级最高(在各阶段间执行)
微任务执行时机 在每个宏任务之后 在事件循环的每个阶段之后

复杂异步代码执行顺序示例

console.log('1. Script Start'); // 同步代码,宏任务1开始

setTimeout(() => {
  console.log('2. setTimeout'); // 回调是宏任务
  Promise.resolve().then(() => {
    console.log('3. Promise inside setTimeout'); // 微任务
  });
}, 0);

Promise.resolve()
  .then(() => {
    console.log('4. Promise 1'); // 微任务
  })
  .then(() => {
    console.log('5. Promise 2'); // 微任务
  });

console.log('6. Script End'); // 同步代码,宏任务1结束

// 预期输出顺序:
// 1. Script Start
// 6. Script End
// 4. Promise 1
// 5. Promise 2
// 2. setTimeout
// 3. Promise inside setTimeout

执行流程图示:

flowchart TD A["开始执行宏任务 (主脚本)"] --> B["输出 'Script Start'"] B --> C["遇到 setTimeout<br>将其回调放入宏任务队列"] C --> D["遇到 Promise.resolve().then<br>将其回调放入微任务队列"] D --> E["输出 'Script End'<br>主脚本(宏任务)执行完毕"] E --> F["清空微任务队列"] F --> G["执行第一个Promise回调<br>输出 'Promise 1'"] G --> H["执行then返回的新Promise回调<br>输出 'Promise 2'"] H --> I["微任务队列清空"] I --> J["取出并执行下一个宏任务(setTimeout回调)"] J --> K["输出 'setTimeout'"] K --> L["遇到Promise.resolve().then<br>将其回调放入微任务队列"] L --> M["setTimeout回调(宏任务)执行完毕"] M --> N["清空微任务队列"] N --> O["执行Promise回调<br>输出 'Promise inside setTimeout'"] O --> P[结束]

2. 内存管理与垃圾回收

JavaScript 的内存分配和释放是自动的,这个过程称为垃圾回收 (Garbage Collection)。

V8 垃圾回收机制

V8 采用了分代式垃圾回收,将内存分为两个“代”:

  1. 新生代 (New Space)

    • 存放生存时间短的对象(如局部变量)。
    • 使用 Scavenge 算法(一种复制算法),速度快。
    • 分为 From-SpaceTo-Space。垃圾回收时,将存活对象从 From 复制到 To,然后清空 From。经历多次回收仍存活的对象会被移到老生代。
  2. 老生代 (Old Space)

    • 存放生存时间较长或较大的对象(如全局变量、闭包变量)。
    • 使用 标记-清除 (Mark-Sweep)标记-压缩 (Mark-Compact) 算法。
    • 标记-清除:从根对象(全局变量、当前调用栈)开始,标记所有可达对象。然后遍历整个堆,清除未被标记的对象。
    • 标记-压缩:在标记-清除的基础上,将存活的对象向一端移动,解决内存碎片问题。

引用计数是一种旧的算法(无法处理循环引用,已基本被淘汰)。

常见内存泄漏场景

  1. 意外的全局变量:在非严格模式下,给未声明的变量赋值会创建全局变量。
    function foo() {
      bar = 'leak'; //  window.bar = 'leak'
      this.baz = 'leak'; // 如果foo是普通函数,this指向window
    }
    
  2. 遗忘的定时器或回调函数
    const data = getHugeData();
    setInterval(() => {
      const node = document.getElementById('Node');
      if(node) {
        node.innerHTML = JSON.stringify(data); // data 一直被定时器引用,无法释放
      }
    }, 1000);
    
  3. 脱离DOM的引用:保存了DOM节点的引用,即使节点已从DOM树移除。
    const elements = {
      button: document.getElementById('myButton')
    };
    document.body.removeChild(document.getElementById('myButton'));
    // 由于 elements.button 仍引用着该DOM节点,它无法被回收
    
  4. 闭包:闭包会保留其词法作用域中变量的引用。如果不慎引用了不再需要的大对象,就会导致泄漏。
    function outer() {
      const bigData = new Array(1000000);
      return function inner() {
        console.log('I have a big data reference'); // bigData 一直被inner函数引用
      };
    }
    

使用 Chrome DevTools 排查内存泄漏

  1. Performance 面板:录制一段时间内的性能,观察 JS Heap 内存曲线是否持续攀升而不下降。
  2. Memory 面板
    • Heap Snapshot:拍摄堆内存快照,查看对象分布,比较多次快照找出增多的对象。
    • Allocation instrumentation on timeline:实时查看内存分配的时间线,定位分配内存的函数。
    • Allocation sampling:采样内存分配,统计哪个函数分配了最多的内存。

3. 原型与继承

JavaScript 使用原型链来实现继承。

原型链

  • 每个函数都有一个 prototype 属性(原型对象)。
  • 每个对象都有一个 __proto__ 属性(指向其构造函数的原型对象)。
  • 当访问一个对象的属性时,如果对象自身没有,就会通过 __proto__ 向上查找其原型对象,直到找到或到达链条尽头 (null)。
function Parent() {
  this.name = 'Parent';
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child() {}
// 关键:继承 Parent 的原型
Child.prototype = new Parent(); // 或 Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child; // 修复构造函数指向

const child = new Child();
child.sayName(); // 'Parent'
// 查找路径:child -> child.__proto__ (Parent实例) -> Parent实例.__proto__ (Parent.prototype)

Class 语法糖的本质

ES6 的 class 本质上是基于原型的继承的语法糖,更清晰易读。

class Parent {
  constructor() {
    this.name = 'Parent';
  }
  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent { // extends 关键字建立了原型链
  constructor() {
    super(); // 相当于调用 Parent.call(this)
  }
}

// 其原型链结构与上面的函数实现完全相同

继承的多种实现及优缺点

  1. 原型链继承

    • 优点:简单。
    • 缺点:所有子类实例共享原型属性(引用类型会互相影响);无法向父类传参。
  2. 构造函数继承

    function Child() {
      Parent.call(this); // “借调”父类构造函数
    }
    
    • 优点:解决了共享属性和传参问题。
    • 缺点:方法都在构造函数中定义,无法复用;无法继承父类原型上的方法。
  3. 组合继承(最常用)

    function Child() {
      Parent.call(this); // 继承实例属性
    }
    Child.prototype = new Parent(); // 继承原型方法
    Child.prototype.constructor = Child;
    
    • 优点:融合两者优点,是 JavaScript 中最常用的继承模式。
    • 缺点:调用了两次父类构造函数。
  4. 原型式继承 (Object.create())

    • 优点:无需创建构造函数,基于现有对象创建新对象。
    • 缺点:与原型链继承类似,属性共享。
  5. 寄生组合式继承(最优解)

    function inheritPrototype(Child, Parent) {
      const prototype = Object.create(Parent.prototype); // 创建父类原型的副本
      prototype.constructor = Child;
      Child.prototype = prototype;
    }
    function Child() {
      Parent.call(this);
    }
    inheritPrototype(Child, Parent);
    
    • 优点:只调用一次父类构造函数,避免了在子类原型上创建不必要的属性,效率最高。extends 的原理与此类似。

4. ES6+ 核心特性

  1. Promise

    • 作用:解决“回调地狱”,提供更优雅的异步编程方式。
    • 状态pending(进行中)、fulfilled(已成功)、rejected(已失败)。状态一旦改变就不可逆。
    • API.then(), .catch(), .finally(), Promise.all(), Promise.race(), Promise.resolve(), Promise.reject().
  2. Async/Await

    • 作用:以同步代码的书写方式处理异步操作,是 Promise 的语法糖。
    • async 函数总是返回一个 Promise。
    • await 后面可以跟一个 Promise,它会暂停函数执行,等待 Promise 解决后再继续。
  3. Generator

    • 作用:可以暂停和恢复的函数。用 function* 声明,使用 yield 暂停。
    • 是 Async/Await 实现的基础。
    function* gen() {
      yield 'hello';
      yield 'world';
    }
    const g = gen();
    g.next(); // { value: 'hello', done: false }
    
  4. Module (ES Module)

    • 作用:语言层面的模块化方案,取代 CommonJS、AMD 等。
    • export:导出模块。
    • import:导入模块。
    • 静态化,编译时就能确定依赖关系,支持 Tree Shaking。
  5. 箭头函数

    • 语法更简洁:(params) => { expression }
    • 没有自己的 this,其 this 继承自外层词法作用域(定义时决定)。
    • 没有 arguments 对象,不能用作构造函数(不能 new)。
  6. 解构赋值

    • 从数组或对象中提取值,对变量进行赋值。
    // 数组
    const [a, b] = [1, 2];
    // 对象
    const { name, age } = { name: 'Alice', age: 30 };
    // 函数参数
    function foo({ id, data }) { ... }
    
posted @ 2025-10-10 13:36  阿木隆1237  阅读(16)  评论(0)    收藏  举报