JavaScript 进阶

62 阅读25分钟

JavaScript 是一门单线程、基于事件循环的语言。理解 JS 的执行过程,是理解闭包、异步、Promise 的前提。

一、特性解析

单线程(Single Threaded)

  1. 含义

JavaScript 在同一时刻只有一条执行线程,也就是同一时间只能做一件事

  1. 表现

如果你写了一个长时间的计算任务,比如:

for (let i = 0; i < 1e9; i++) {
  console.log('Done');
}

这一段代码执行期间,浏览器不会响应用户操作(点击、滚动),因为 JS 的线程被占用了。

  1. 原因分析

JavaScript 的设计初衷是运行在浏览器里,而浏览器本身就有很多 UI 操作、渲染任务。如果 JS 是多线程,管理共享状态和 DOM 就复杂得多。单线程加上事件循环,让 JS 可以轻松管理异步任务

基于事件循环(Event Loop)

  1. 意思

JavaScript 使用事件循环机制来管理异步操作

  1. 核心概念
  • 调用栈(Call Stack) :存放同步代码执行上下文
  • 任务队列(Task Queue / Callback Queue) :存放异步回调,比如 setTimeoutDOM 事件回调、Promise 的 .then
  • 事件循环(Event Loop) :不停地检查调用栈是否为空,如果为空,就把队列里的任务取出来执行

例如下面这段代码:

console.log('A');

setTimeout(() => {
  console.log('B');
}, 0);

console.log('C');
  1. console.log('A') → 调用栈执行 → 输出 A
  2. setTimeout → 异步任务 → 放入任务队列
  3. console.log('C') → 调用栈执行 → 输出 C
  4. 调用栈空 → 事件循环把任务队列里的 B 回调取出来执行 → 输出 B

所以即使 setTimeout 写的是 0 毫秒,也不是立即执行,它必须等到调用栈清空才执行。

单线程 + 事件循环 = 异步魔法

  • 单线程保证了 JS 对数据结构的操作不会被并发打断
  • 事件循环允许 JS 不阻塞 UI,同时处理异步事件
  • 因此 JS 既简单(单线程),又能高效处理异步任务(事件循环)

二、执行上下文(Execution Context)

特性解析

执行上下文是 JavaScript 代码执行时的运行环境,主要包含:变量环境(Variable Environment)、词法环境(Lexical Environment)、this 绑定三部分。

变量环境

概念:

  • 变量环境是 JS 引擎用来存储变量、函数声明的内部数据结构(var/function声明)

  • 每个执行上下文(Execution Context, EC)都有一个 变量环境

  • 变量环境由两部分组成:

    • 环境记录(Environment Record) :存储实际的变量/函数
    • 外部引用(outer reference) :指向父级环境,实现作用域链

区别 变量环境 vs 词法环境

  • 变量环境主要用于执行时变量、函数存储
  • 词法环境包含 变量环境 + 用于 闭包和 let/const 的块级作用域
console.log(a);
var a = 10;
// 输出 undefined

词法环境

概念:

  • 词法环境是 ES6 引入的概念,本质是变量环境的一个增强版
  • 它是一个 变量名到变量值的映射 + 对外部作用域的引用
  • 每个函数、块(let/const)都会创建新的词法环境
  • 词法环境用于解析 标识符(变量名) ,确定作用域链

核心特性:

  • 块级作用域letconst 就是通过词法环境实现的
  • 闭包:函数能访问外部词法环境里的变量

核心:

  • 闭包原理:函数访问外部词法环境里的变量
  • Temporal Dead Zone (TDZ):letconst 在声明之前不能访问(区别于var,var可以访问,但是值为undefined)
function foo() {
  let x = 1;
  return function bar() {
    console.log(x); // 闭包访问外部词法环境
  }
}

this 绑定

概念:this 是函数执行时动态绑定的上下文对象,不在词法环境中

绑定规则:

  1. 默认绑定(Default) :普通函数 → 全局对象(严格模式下 undefined
  2. 隐式绑定(Implicit) :对象方法 → 对象本身
  3. 显式绑定(Explicit)call / apply / bind
  4. new 绑定:构造函数 → 新创建对象
  5. 箭头函数:不绑定自己的 this,取外层 this

区别 函数调用 和 方法调用:

const obj = {
  x: 10,
  foo: function() { console.log(this.x); }
}
const bar = obj.foo;
bar(); // undefined or 全局对象,注意严格模式
obj.foo(); // 10 this 指向 obj 对象本身

执行上下文的类型

全局执行上下文

当 JS 引擎开始执行代码时,第一个执行上下文就是全局执行上下文,全局上下文只有一个,是执行栈底部。

特点:

  • 变量环境:

    • var 声明会提升,初始化为 undefined
    • var 声明的全局变量 会成为全局对象的属性
    • let / const 声明的全局变量 不会成为全局对象属性
    • letconst 声明前不能访问
  • this 绑定:

    • 浏览器:绑定到 window
    • Node:绑定到 global
  • 作用域链:

    • 最顶层作用域,没有外部环境

区别 全局对象 与 全局变量

var a = 10;
console.log(window.a); // 10
let b = 20;
console.log(window.b); // undefined

函数执行上下文

每次函数调用都会创建一个新的执行上下文

组成:

  1. 变量环境:

    1. var 声明提升,初始化为 undefined
    2. 函数声明提升,整个函数体都可用
  2. 词法环境:

    1. let / const 声明
    2. 保存外部环境引用 → 形成作用域链
  3. this 绑定:

    1. 根据函数调用方式决定(四种绑定 + 箭头函数)
  4. 作用域链:

    1. 当前函数词法环境 → 外部词法环境 → … → 全局词法环境
console.log(foo); // [Function: foo]
console.log(bar); // undefined

function foo() {}
var bar = 1;

执行过程

执行上下文分为两个阶段

阶段做什么面试重点
创建阶段(Creation Phase / 编译阶段)1. 创建执行上下文(EC) 2. 创建变量环境(VE) 3. 创建词法环境(LE) 4. 函数声明提升,var 提升,绑定 this 5. 建立作用域链变量提升、函数提升、this 绑定、TDZ(暂时性死区)、作用域链
执行阶段(Execution Phase / 运行阶段)按顺序执行代码,给变量赋值,执行函数体赋值、函数调用、闭包、异步队列(事件循环)

全局执行上下文执行顺序

  1. 创建全局执行上下文(全局 VE + LE + this = window)

  2. 执行全局代码:

    1. var 提升,绑定到 window
    2. let/const 进入词法环境,不挂 window
  3. 执行阶段按顺序执行全局代码

全局上下文永远在执行栈底部

执行栈(Call Stack)

特性:

  • 先进后出
  • 当前执行的上下文总是在栈顶

流程:

  • 执行全局上下文 → 入栈
  • 调用函数 → 函数上下文入栈
  • 函数执行完 → 出栈,返回值给上一个上下文
  • 栈顶恢复执行上一个上下文

三、作用域与作用域链

作用域

作用域决定了变量和函数的可访问范围。

JavaScript 使用的是 词法作用域(Lexical Scope)变量的作用域在代码书写时就已确定,而不是运行时。

作用域类型

  1. 全局作用域
  • 代码在全局声明的变量和函数。
  • 在浏览器中,全局作用域对应 window(var 声明)或全局词法环境(let/const)。
var a = 10;
let b = 20;
console.log(a); // 10
console.log(b); // 20
  1. 函数作用域
  • 每个函数创建自己的作用域。
  • var、函数声明在函数内局部有效。
function foo() {
  var x = 1;
  console.log(x); // 1
}
console.log(x); // ReferenceError
  1. 块级作用域(let / const)
  • ES6 引入,letconst 具有块级作用域
{
  let y = 5;
  const z = 6;
}
console.log(y, z); // ReferenceError

🎯 对象字面量 ≠ 块级作用域

对象字面量 {} 不是块级作用域! 它只是创建对象的语法,不会创建新的作用域。

什么是真正的块级作用域?

块级作用域是由 { } 包裹的代码块,但仅限于特定语法:

// ✅ 这是块级作用域(if 语句)
if (true) {
  let a = 1;  // a 只在这个块内有效
}

// ✅ 这是块级作用域(for 循环)
for (let i = 0; i < 3; i++) {
  // i 只在这个块内有效
}

// ✅ 这是块级作用域(单独的块)
{
  let b = 2;  // b 只在这个块内有效
}

// ❌ 这不是块级作用域!
const obj = {
  // 这里是对象字面量,不是代码块
  x: 100,
  method() {
    // 这里是函数作用域
  }
};

🔬 深入理解:作用域 vs 对象字面量

对象字面量只是数据结构的描述

// 这段代码:
const obj = {
  x: 100,
  arrow: () => console.log(this.x)
};

// JavaScript 引擎看到的是:
// 1. 在当前作用域(假设全局)创建变量 obj
// 2. 创建一个对象,设置属性 x 和 arrow
// 3. arrow 属性的值是一个箭头函数,这个函数**定义在当前作用域**(全局)

// 相当于:
const obj = {};
obj.x = 10
obj.arrow = () => console.log(this.x);  // 箭头函数定义在全局

对比真正的块级作用域

// 块级作用域的例子
{
  let blockScoped = '只在块内';
  const innerObj = {
    x: 100,
    arrow: () => console.log(this.x)  // 这里的 this 还是外层的 this
  };
  // innerObj 在块内定义,但对象字面量本身不创建作用域
  // 箭头函数捕获的是这个块的 this(如果块在全局,就是全局 this)
}
// blockScoped 在这里访问不到 ✅

🎭 验证实验

实验1:对象字面量内能否访问块级变量?

{
  let blockVar = '我在块内';
  
  const obj = {
    x: blockVar,  // ✅ 可以访问,因为对象在块内定义
    method() {
      console.log(blockVar);  // ✅ 可以访问,闭包捕获
    }
  };
  
  console.log(obj.x); // "我在块内"
}

实验2:箭头函数 this 的捕获

// 情况1:在全局定义对象
// 浏览器环境
this.x = 'global';

const obj1 = {
  x: 'obj1',
  arrow: () => this.x
};
console.log(obj1.arrow()); // 'global'(捕获全局 this)

// 情况2:在函数内定义对象
function createObj() {
  this.x = 'function';
  
  const obj2 = {
    x: 'obj2',
    arrow: () => this.x
  };
  
  return obj2;
}

const obj2 = createObj.call({ x: 'custom' });
console.log(obj2.arrow()); // 'custom'(捕获 createObj 的 this)

// 情况3:在块内定义对象
{
  let blockThis = this;  // 块的 this 和外面一样
  
  const obj3 = {
    x: 'obj3',
    arrow: () => this.x
  };
  
  console.log(obj3.arrow()); // 还是外层的 this
}

📝 终极总结

  1. 对象字面量 { } 只是数据描述,不是代码块,不创建作用域
  2. 块级作用域由 if、for、while 或单独的 { } 创建
  3. 箭头函数的 this 捕获的是定义时所在的作用域的 this
  4. 对象字面量内部定义的东西,其作用域就是对象字面量外部的作用域

作用域链(Scope Chain)

  1. 概念
  • 作用域链是由当前执行上下文的词法环境向外层逐级查找变量的链式结构
  • 查找规则:从当前作用域开始,逐级向上查找,直到全局作用域
  • 本质上是词法环境的 outer reference 链表
  1. 作用域链的形成
  • 每个执行上下文都有自己的 词法环境(LE)
  • LE 中有 outer reference 指向外部词法环境
  • 查找变量时 → 沿着 outer reference 一直查找
let x = 10;       // 全局作用域
function foo() {
  console.log(x); // 访问 x
}
function bar() {
  let x = 20;     // bar 的局部作用域
  foo();          // 调用 foo
}
bar();            // 执行 bar

词法作用域:函数在定义时就确定了它的外部作用域。foo 定义在 全局作用域,它的作用域链为:foo LE → global LE

变量提升与 TDZ

  • var 会被提升并初始化为 undefined
  • let / const 也会提升,但存在暂时性死区(TDZ)

四、闭包(Closure)

闭包是函数与其词法作用域的组合,函数可以访问其外部作用域的变量,即使外部函数已经执行完毕

闭包 = 内函数 + 外函数的词法环境(LE)

闭包形成的条件

  1. 内部函数可以访问外部函数的变量
  2. 外部函数返回内部函数,或者将内部函数传出
  3. 内部函数被外部引用持久保存,保持对外部词法环境的引用
function outer() {
  let count = 0;
  return function inner() {
    count++;
    console.log(count);
  }
}

const fn = outer();
fn(); // 1
fn(); // 2
// inner 是闭包
// count 被 inner 持续引用,即使 outer 执行完毕也不会被垃圾回收

闭包的本质

  1. 每次函数调用都会创建新的 执行上下文(EC)词法环境(LE)
  2. 内部函数访问外部变量时,通过 作用域链 查找外部词法环境
  3. 当外部函数返回内部函数,词法环境不会被销毁,因为闭包引用了它
inner LE -> outer LE -> global LE
查找 count → outer LE 找到

闭包的常见用途

  1. 私有变量
function createCounter() {
  let count = 0;
  return {
    increment() { count++; return count; },
    decrement() { count--; return count; }
  };
}
const counter = createCounter();
console.log(counter.increment()); // 1
  1. 状态保存、延迟执行
function foo() {
  let name = 'Tom';
  setTimeout(() => console.log(name), 1000);
}
foo(); // 1s 后打印 'Tom'

function() { console.log(name); } 在 foo 内部定义 → 它的词法环境(LE)包含对 foo 的 LE 的引用,

内部函数 LE → foo LE → global LE
  1. 防抖 / 节流
  2. 柯里化
function add(x) {
  return function(y) {
    return x + y;
  }
}
console.log(add(2)(3)); // 5

闭包的风险

  • 变量无法被垃圾回收
  • 潜在内存泄漏
  • 循环与闭包陷阱
for (var i = 0; i < 3; i++) {
  setTimeout(function() { console.log(i); }, 0);
}
// 输出: 3 3 3
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 输出: 0 1 2
// 每次迭代创建新的作用域
{
  let i = 0;
  setTimeout(() => console.log(i), 0);
}
{
  let i = 1; 
  setTimeout(() => console.log(i), 0);
}
{
  let i = 2;
  setTimeout(() => console.log(i), 0);
}

五、this 指向机制 🤔

this 的指向取决于函数的调用方式

this 的本质

  • this 是函数执行时的上下文对象,与函数定义位置无关(除了箭头函数)
  • this 在执行上下文创建阶段确定

⚠️ 记住:this 绑定的是调用时的对象,不是作用域链里的变量

四种绑定规则

  1. 默认绑定

普通函数调用(非严格模式)this 指向全局对象

function foo() { console.log(this); }
foo(); // window (浏览器)
  1. 隐式绑定

对象方法调用this 指向调用它的对象

const obj = { x: 10, foo() { console.log(this.x); } };
obj.foo(); // 10
  1. 显式绑定(call / apply / bind)
function foo() { console.log(this.name); }
const obj = { name: 'Tom' };
foo.call(obj); // Tom
foo.apply(obj); // Tom
const bound = foo.bind(obj);
bound(); // Tom
  1. new 绑定

构造函数使用 new 调用:

  1. 创建一个新对象
  2. this 指向新对象
  3. 返回 this(除非显式返回对象)
function Person(name) { this.name = name; }
const p = new Person('Alice');
console.log(p.name); // Alice

箭头函数

没有自己的 this,this 继承自定义时外层作用域,不能作为构造函数,也不能用做call、apply、bind

this 指向取定义时外层作用域的 this

const obj = {
  x: 100,
  arrow: () => console.log(this.x)
};
obj.arrow(); // undefined 或 window.x

常见作用:箭头函数在回调中解决 this 指向问题

function Timer() {
  this.time = 0;
  setInterval(() => { this.time++; }, 1000);
}
Timer() // this 指向 window 或undefined
const t = new Timer(); // this 指向 t 实例

六、原型与原型链

原型:JavaScript 中每个对象都有一个内部属性 [[Prototype]],指向另一个对象,用来实现属性共享

原型链:对象在访问属性时,会沿着 [[Prototype]] 一层一层向上查找,直到 null,这条查找路径就是原型链

名称是什么面试重点
prototype函数的属性用来存放“实例共享的方法”
proto对象的属性指向构造函数的 prototype
constructor原型上的属性指回构造函数本身

函数有 prototype,实例有 proto,prototype 上有 constructor。

New 与原型

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function () {
  console.log(this.name);
};

const p = new Person('Tom');

new 的 4 个底层步骤(必背)

  1. 创建一个空对象 {}
  2. 让这个对象的 [[Prototype]] 指向 Person.prototype
  3. 执行 Person 函数,this 指向这个对象
  4. 返回这个对象

等价于下列的伪代码:

const obj = {};
obj.__proto__ = Person.prototype;
Person.call(obj, 'Tom');
return obj;

原型链查找规则

访问属性时:对象自身 → 原型 → 原型的原型 → null,这条链就叫 原型链(Prototype Chain)

JavaScript 在访问对象属性时,会先查对象自身,找不到就沿着 proto 指向的原型对象继续查找,直到 null 为止。如果查到 null 还没找到,就返回 undefined。

p 自身
↓
p.__proto__ === Person.prototypePerson.prototype.__proto__ === Object.prototypeObject.prototype.__proto__ === null

prototype & proto & constructor

  1. prototype
  • 只有函数才有
  • 用来存放实例共享的属性和方法
  1. Proto
  • 所有对象都有
  • 指向创建它的构造函数的 prototype
p.__proto__ === Person.prototype // true
  1. constructor
  • 标识“这个原型对象是由哪个构造函数创建的”
  • 常用于判断类型、修复原型链
Person.prototype = {
  sayHi() {}
};
Person.prototype.constructor === Person // false ❌ 替换了 prototype 对象
// ✅ 正确做法
Person.prototype = {
  constructor: Person,
  sayHi() {}
};

instanceof 原理

本质:Person.prototype 是否出现在 p 的原型链上

p instanceof Person // true

伪代码:

function myInstanceof(obj, Fn) {
  let proto = obj.__proto__;
  while (proto) {
    if (proto === Fn.prototype) return true;
    proto = proto.__proto__;
  }
  return false;
}

原型继承

对象不是拷贝属性,而是通过“引用另一个对象”来共享能力

JavaScript 的继承不是复制,而是通过原型链进行对象委托。所有继承方式的演进,本质都是在平衡三件事:属性独立性、方法复用性、构造函数执行成本。

ES6 的 class,只是把寄生组合继承用更易读的语法包装了。

本质

在 JS 里:

  • 每个对象都有一个 [[Prototype]](即 proto
  • 当访问属性时:自己没有 → 去原型找 → 原型的原型 → … → null

📌 继承不是复制,而是“委托(delegation)”这就是原型继承的本质

原型继承实现方式

  1. 原型链继承(最原始)

原理:子类实例的 proto 指向父类实例,属性查找沿原型链向上

function Parent() {
  this.colors = ['red', 'blue'];
}
Parent.prototype.sayHi = function () {
  console.log('hi');
};

function Child() {}
Child.prototype = new Parent();

const c1 = new Child();
  1. 构造函数继承(为了解决共享问题、传参)

原理:把父构造函数“借”过来执行,每个实例有自己的一份属性

function Parent(name) {
  this.name = name;
}
function Child(name) {
  Parent.call(this, name);
}
  1. 组合继承(经典方案)

属性:构造函数继承;方法:原型链继承

Parent 构造函数执行了 两次: Parent.call(this)、new Parent()

function Parent(name) {
  this.name = name;
}
Parent.prototype.sayHi = function () {};

function Child(name) {
  Parent.call(this, name);
}
Child.prototype = new Parent();
  1. 寄生组合继承(⭐ 最优解)

不再 new Parent()、只建立原型链关系、父构造函数只执行一次

寄生组合继承 = 构造函数继承属性 + 原型式继承方法

function inheritPrototype(child, parent) {
    // 1. 创建父类原型的副本
    const prototype = Object.create(parent.prototype);
    
    // 2. 修正 constructor 指向
    prototype.constructor = child;
    
    // 3. 赋值给子类原型
    child.prototype = prototype;
}

完整实现如下所示:

// 父类
function Animal(name) {
    this.name = name;
    this.colors = ['red', 'blue'];  // 实例属性
}

Animal.prototype.eat = function() {
    console.log(`${this.name} 在吃东西`);
};

// 子类
function Dog(name, breed) {
    Animal.call(this, name);  // 继承实例属性(只调用一次)
    this.breed = breed;
}

// 关键:寄生式继承
function inheritPrototype(child, parent) {
    // 创建父类原型的副本
    const prototype = Object.create(parent.prototype);
    // 修正 constructor
    prototype.constructor = child;
    // 赋值
    child.prototype = prototype;
}

// 执行继承
inheritPrototype(Dog, Animal);

// 子类自己的方法
Dog.prototype.bark = function() {
    console.log(`${this.name} 汪汪叫`);
};

// 测试
const dog1 = new Dog('旺财', '金毛');
const dog2 = new Dog('来福', '柴犬');

dog1.colors.push('green');
console.log(dog1.colors); // ['red', 'blue', 'green']
console.log(dog2.colors); // ['red', 'blue'] ✅ 不共享

dog1.eat();  // 旺财 在吃东西
dog2.bark(); // 来福 汪汪叫

console.log(dog1.eat === dog2.eat); // true ✅ 方法共享

Object.create 是寄生组合继承的核心,它的作用就是创建以指定对象为原型的对象

// Object.create 的手写实现
Object.myCreate = function(proto) {
    function F() {}  // 临时构造函数
    F.prototype = proto;  // 原型指向传入的对象
    return new F();  // 返回空对象,但 __proto__ 指向 proto
};

// 使用
const parent = { x: 1 };
const child = Object.myCreate(parent);
console.log(child.x); // 1(继承来的)
console.log(child.hasOwnProperty('x')); // false
  1. ES6 class extends(语法糖)

class 只是对寄生组合继承的语法封装

class Parent {
  constructor(name) {
    this.name = name;
  }
  sayHi() {}
}

class Child extends Parent {
  constructor(name) {
    super(name);
  }
}

Child.prototype.__proto__ === Parent.prototype // true
继承方式实例属性独立方法共享调用父类构造函数次数原型链是否完整
原型链继承❌共享1次
构造函数继承❌不共享1次❌没原型链
组合继承2次
寄生组合继承1次

七、JavaScript 的异步模型与Event Loop

JS 单线程:避免多线程操作 DOM 造成冲突,也是为了简化语言设计

核心组成

  1. Call Stack(调用栈):执行同步代码的地方
  2. Web APIs / Host APIs:浏览器提供 setTimeout、DOM 事件、XHR、fetch、requestAnimationFrame 等
  3. Task Queues(任务队列):至少包含宏任务队列与微任务队列(规范上是多个队列与 job queue)
  4. Event Loop 循环:如果栈空 → 取出下一个宏任务执行 → 执行完后清空微任务队列 → 渲染(如果需要) → 下一轮

宏任务与微任务

  1. 宏任务
  • script
  • setTimeout
  • setInterval
  1. 微任务
  • Promise.then / catch / finally
  • queueMicrotask
  • MutationObserver

优先级:每次宏任务执行结束后,会清空当前微任务队列(可能产生新的微任务,继续清空),随后才进行渲染/下个宏任务。

执行顺序规则

每执行完一个宏任务,都会清空所有微任务队列

  1. 从宏任务队列取一个任务并执行(把它压入调用栈)
  2. 执行过程中如果产生微任务,它们进入微任务队列
  3. 宏任务执行完(栈空)→ 执行并清空所有当前微任务(按入队顺序,微任务执行中可生成新微任务,继续清空直到为空)
  4. 浏览器可能进行渲染(Paint/Composite)
  5. 下一轮 Event Loop:回到步骤1
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
// A D C B

Node与浏览器的差别

  • Node 有 process.nextTick(更早的微任务,优先于 Promise microtasks)
  • Node 有 setImmediate(与 setTimeout(…,0) 不同,处在不同队列)
  • 浏览器没有 process.nextTick、有 queueMicrotaskMutationObserver

不同环境下微任务/宏任务细节可能不同,以规范和宿主实现为准

使用大量微任务(Promise.then)会阻塞渲染,导致 UI 卡顿。

八、Promise 与 async / await

Promise 是一个表示“未来某个异步操作结果”的对象,它有三种状态,并通过 then/catch 注册回调,以微任务的方式执行。

状态含义
pending进行中
fulfilled已成功
rejected已失败

一旦从 pending → fulfilled / rejected,状态不可逆。

Promise 的执行规则

  1. Promise 构造函数:同步执行
new Promise((resolve, reject) => {
  console.log('A');
  resolve();
  console.log('B');
}); A、B
  1. then / catch / finally:微任务
Promise.resolve().then(() => console.log('then'));
console.log('sync');
// sync then

Event Loop 中 Promise 的位置:

同步代码
↓
微任务(Promise.then / await 之后)
↓
宏任务(setTimeout / setInterval / I/O)

then 的返回规则

  • 返回普通值 → resolve
  • 返回 Promise → 等待其状态

async / await 本质

async/await 是 Promise 的语法糖,本质仍基于微任务。

  1. async 函数的返回值(Promise
async function foo() {return 10;} = function foo() {return Promise.resolve(10);}
  1. await 后的代码会被放入微任务队列
await expr; = Promise.resolve(expr).then(继续执行)
async function async1() {
  console.log('1');
  await async2();
  console.log('2');
}

async function async2() {
  console.log('3');
}

console.log('4');
async1();
console.log('5');
// 4 1 3 5 2

九、内存管理与垃圾回收

内存类型分为:

  • 栈内存:基本类型、执行上下文
  • 堆内存:对象、函数、引用类型

基本概念

  1. 根(Roots)与可达性(Reachability)

GC 从一组“根”出发(全局对象、当前执行上下文的栈引用、闭包引用等),沿引用关系把能到达的对象标记为“活着”。不能被根访问到的对象被视为垃圾,等待回收。“GC 回收不可达对象,判断依据是从根出发是否可达。”

  1. 追踪(Tracing)

现代 JS 引擎主要用追踪 GC(tracing GC) ,常见实现是 mark-and-sweep(标记-清除) 。引用计数容易受循环引用影响,因此并不是主流实现(或作为补充)。

  1. 标记-清除(Mark-and-Sweep)流程(核心步骤)
  • 标记阶段:从根开始标记所有可达对象
  • 清除阶段:遍历堆,回收未标记对象,恢复内存

垃圾回收策略

  1. 引用计数(已较少使用)
  • 每个对象维护一个“被引用次数”
  • 引用 +1,解除引用 −1
  • 计数为 0 → 立刻回收

优点:实时回收、实现简单

致命缺点:循环引用无法回收

function foo() {
  const a = {};
  const b = {};
  a.b = b;
  b.a = a;
}
即使 foo 执行完,a ↔ b 互相引用,引用计数不为 0
  1. 标记清除(主流,GC 策略基础)
  • 从根对象(Global、栈、闭包)开始标记可达对象
  • 清除未标记对象

优点:解决循环引用、实现成熟、稳定

缺点:会产生内存碎片、Stop-the-world 停顿

  1. 标记-整理 / 标记-压缩(🧓老生代常用)
  • 在标记存活对象后
  • 把它们向一侧移动,整理连续内存

优点:解决内存碎片问题、提高内存利用率

缺点:移动对象成本高

  1. 复制算法(Copying / Semi-space)(👱新生代专用)
  • 内存分成两块
  • 每次只用一半
  • 存活对象复制到另一半

优点:快速、无内存碎片

缺点:内存利用率低(50%)

  1. 分代回收(Generational GC)💡

新生代(Young Generation):复制算法,回收频繁

老生代(Old Generation):标记-清除 / 标记-整理,回收较少

分代回收是现代 JS 引擎的核心策略

执行方式优化

当出现以下场景需要考虑执行方式优化:

  • 当垃圾回收的 Stop-The-World 停顿已经影响到程序响应性、帧率或吞吐量时(增量GC、并发GC)
  • 当老生代对象变多、GC 成本上升(并发标记、并行GC)
  • 对「响应性」要求极高(UI / 实时系统)(增量GC、并发GC)
类型含义作用
增量主线程不能被长时间阻塞 需要“平滑执行”把一次 GC 拆成很多小步骤 每次只执行一点点
并发应用规模大、老生代多 停顿已经不可接受GC 线程和 JS 线程同时运行
并行多核 CPU GC 本身耗时长多个 GC 线程一起工作

内存泄漏

内存泄漏 = 不再需要的对象,仍然被引用,导致 GC 无法回收,内存持续增长。

  1. 意外的全局变量(最低级但最常见)
  • use strict
  • ESLint:no-undef
  • 所有变量必须 let / const
function foo() {
  a = 100; // 没有声明
}
  1. 未清理的定时器(setInterval / setTimeout)
  • 组件销毁时 clearInterval
  • React / Vue 生命周期中统一清理
setInterval(() => {
  doSomething();
}, 1000);
  1. 事件监听未移除(DOM 泄漏重灾区)
  • removeEventListener
  • 组件卸载时统一解绑
  • 使用事件委托(event delegation)
element.addEventListener('click', handler);
// element 被 remove,但 handler 还在
  1. 闭包持有大对象(高频隐性泄漏)
  • 缩小闭包作用域
  • 用完置 null
  • 避免在闭包中引用 DOM / 大对象
function createHandler() {
  const bigData = new Array(1000000);
  return () => {
    console.log(bigData.length);
  };
}
  1. Map / Array / Cache 无限增长
  • 使用 WeakMap / WeakSet
  • LRU 缓存
  • 定期清理策略
const cache = new Map();
cache.set(obj, data);
  1. Promise 未释放 / 长链引用
  • 任务完成后删除引用
  • 不存 Promise 本身,存结果
const pending = [];
function request() {
  pending.push(fetch(url));
}
  1. 全局状态管理滥用(SPA 常见)
  • 页面切换时清理状态
  • 分模块 store
  • 生命周期感知的状态管理
store.users.push(user);
  1. Detached DOM
  • 移除 DOM 前断开 JS 引用
  • 解绑事件
  • element = null
DOM 已从页面移除,JS 仍然持有引用

内存泄漏治理

  1. 编码层面(预防)
  • 避免隐式全局
  • 生命周期内创建,生命周期外销毁
  • 闭包中不引用 DOM / 大对象
  • 用 WeakMap 代替 Map(合适场景)
  1. 架构层面(控制)
  • 模块化状态
  • 缓存上限(size / TTL)
  • 统一资源管理(timer / listener / observer)
  1. 工具层面(排查)

Chrome DevTools

  • Memory → Heap Snapshot
  • 对比快照找增长对象
  • 看 Retainers(引用链)

内存膨胀

内存膨胀 = 内存持续升高,但对象“是合理存活的”,并非 GC 无法回收

判断指标:

现象判断
内存不降泄漏
内存下降但基线变高膨胀

区别于内存泄漏:

对比点内存泄漏内存膨胀
是否可回收❌ 不可回收✅ 可回收(但没到时机)
GC 后内存不下降会下降 / 波动
本质引用错误设计/策略问题
常见原因定时器、事件、闭包缓存、数据量、批量处理
解决方式修复引用控制规模 / 生命周期
  1. 缓存无限或过大(最常见)
const cache = {};
function load(id, data) {
  cache[id] = data;
}
  1. 大数组 / 大对象长期常驻
  2. 批量处理一次性加载大量数据
  3. 闭包 + 合理引用(但过度)
function createStore() {
  const state = hugeObject;
  return {
    get() { return state; }
  };
}
  1. 虚拟 DOM / UI 缓存过多

内存膨胀治理思路

  1. 生命周期治理(第一优先级)

什么时候创建 → 什么时候销毁

  • 页面切换清理
  • 组件卸载清理
  • 请求结束释放中间数据
  1. 容量治理(必须)
  • 缓存上限
  • 列表长度上限
  • 队列最大深度
  1. 结构优化
  • 用 stream 代替 buffer
  • 用 WeakMap 代替 Map(适合时)
  • 拆分大对象
  1. 峰值控制
  • 避免一次性加载
  • 分批、懒加载
  • 背压(Node 流)

十、class 类

class 是语法糖,本质还是构造函数 + 原型链

// 这段 class 语法
class Person {
    constructor(name) {
        this.name = name;
    }
    
    sayHi() {
        console.log(`Hi, I'm ${this.name}`);
    }
}

// 等价于 ES5 的写法
function Person(name) {
    this.name = name;
}

Person.prototype.sayHi = function() {
    console.log(`Hi, I'm ${this.name}`);
};

1. constructor 是什么

constructor 是类的构造函数/初始化方法,它做三件事:

  1. 接收参数:创建实例时传入的参数
  2. 初始化属性:给实例添加自有属性
  3. 返回实例:默认返回 this(也可以手动返回其他对象)

2. 为什么必须要有?

严格来说,constructor 不是必须的! 但你要理解它的作用:

// 情况1:不写 constructor - 会自动添加空的 constructor
class Animal {
    // 自动添加:constructor() {}
    name = '动物';  // 新语法:直接定义属性
}
const a = new Animal();  // ✅ 可以创建实例

// 情况2:写了 constructor - 用来接收参数
class Person {
    constructor(name, age) {
        this.name = name;  // 初始化实例属性
        this.age = age;
    }
}
const p = new Person('张三', 18);  // ✅ 传入参数

// 情况3:如果不需要接收参数,可以省略
class Dog {
    // 没有 constructor,但有实例属性
    type = 'dog';
    bark() {
        console.log('汪汪');
    }
}
const d = new Dog();  // ✅ 依然能创建实例

如果你需要接收初始化参数,就必须写 constructor;如果不需要,可以省略。

3. constructor 的特殊之处

class Example {
    constructor() {
        console.log('constructor 执行了');
        this.x = 1;
    }
    
    // ❌ 错误:constructor 不能重复写
    // constructor() {} 
    
    // ✅ 正确:普通方法可以叫 constructor 吗?不行!
    // constructor 是保留字,不能用作方法名
}

const e = new Example(); // 打印: constructor 执行了
console.log(e.x); // 1

3. class 的底层原理

  1. ES5 构造函数回顾
// ES5 创建对象的两种方式
function Person(name) {
    // 1. 自有属性
    this.name = name;
}

// 2. 原型方法
Person.prototype.sayHi = function() {
    console.log(`Hi, I'm ${this.name}`);
};

const p = new Person('张三');
p.sayHi(); // Hi, I'm 张三

// 检查原型链
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true
  1. class 的 ES5 等价代码
// class 语法
class Person {
    constructor(name) {
        this.name = name;
    }
    
    sayHi() {
        console.log(`Hi, I'm ${this.name}`);
    }
    
    static staticMethod() {
        console.log('静态方法');
    }
}

// 等价于 ES5
function Person(name) {
    // 确保用 new 调用
    if (!(this instanceof Person)) {
        throw new TypeError('Class constructor cannot be invoked without new');
    }
    
    this.name = name;
}

// 原型方法
Object.defineProperty(Person.prototype, 'sayHi', {
    value: function() {
        console.log(`Hi, I'm ${this.name}`);
    },
    enumerable: false,  // class 的方法不可枚举
    writable: true,
    configurable: true
});

// 静态方法
Person.staticMethod = function() {
    console.log('静态方法');
};

// 设置不可枚举的 constructor
Object.defineProperty(Person.prototype, 'constructor', {
    value: Person,
    enumerable: false,
    writable: true,
    configurable: true
});
  1. class 与构造函数的区别
// 1. class 必须用 new 调用
class Person {}
Person(); // ❌ TypeError: Class constructor Person cannot be invoked without 'new'

function Func() {}
Func(); // ✅ 可以(this 指向全局)

// 2. class 的方法不可枚举
class MyClass {
    method() {}
}
console.log(Object.keys(MyClass.prototype)); // [] ❌ 空数组

function MyFunc() {}
MyFunc.prototype.method = function() {};
console.log(Object.keys(MyFunc.prototype)); // ['method'] ✅

// 3. class 内部默认严格模式
class StrictClass {
    constructor() {
        console.log(this); // 严格模式下是实例
    }
}

// 4. class 不存在变量提升
const p = new Person(); // ❌ ReferenceError
class Person {}

// 函数有提升
const f = new Func(); // ✅ 可以
function Func() {}

4. extends 的原理

class Animal {
    constructor(name) {
        this.name = name;
    }
    
    eat() {
        console.log(`${this.name} 在吃东西`);
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);  // 调用父类构造函数
        this.breed = breed;
    }
    
    bark() {
        console.log(`${this.name} 汪汪叫`);
    }
    
    // 重写父类方法
    eat() {
        super.eat();  // 调用父类方法
        console.log('吃完还要舔舔嘴');
    }
}

const dog = new Dog('旺财', '金毛');
dog.eat(); 
// 旺财 在吃东西
// 吃完还要舔舔嘴
dog.bark(); // 旺财 汪汪叫

// 原型链
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true

5. 为什么必须调用 super()?

class Animal {
    constructor(name) {
        this.name = name;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        // ❌ 错误:必须先调用 super()
        this.breed = breed;  
        super(name);
    }
}

// 原因:子类的 this 对象必须通过父类构造函数创建
// 调用 super() 相当于执行:this = new Animal(name)

super 的两个作用

  1. 作为函数调用super() → 调用父类构造函数
  2. 作为对象使用super.method() → 调用父类原型方法

6. ES5 继承 vs ES6 继承

// ES5 组合继承
function Animal(name) {
    this.name = name;
}

Animal.prototype.eat = function() {
    console.log(`${this.name} 在吃东西`);
};

function Dog(name, breed) {
    Animal.call(this, name);  // 继承属性
    this.breed = breed;
}

// 继承方法
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
    console.log(`${this.name} 汪汪叫`);
};

// ES6 继承 - 更清晰
class Animal {
    constructor(name) {
        this.name = name;
    }
    eat() { console.log(`${this.name} 在吃东西`); }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name);
        this.breed = breed;
    }
    bark() { console.log(`${this.name} 汪汪叫`); }
}

7. 深入理解 super 的指向

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

class Child extends Parent {
    constructor() {
        super();
        this.name = 'child';
    }
    
    method() {
        // super 指向父类原型
        console.log(super.method === Parent.prototype.method); // true
        
        // 调用父类方法
        super.method();
        
        // 访问父类属性?不行!
        // console.log(super.name); // undefined ❌
        // super 只能访问原型上的方法,不能访问实例属性
    }
    
    test() {
        // 箭头函数中的 super
        const arrow = () => super.method();
        arrow(); // ✅ 可以
        
        // 普通函数中的 super
        function normal() {
            // super.method(); // ❌ 语法错误
        }
    }
}

8. class 的本质

// class 是一个特殊的函数
class MyClass {}
console.log(typeof MyClass); // "function"

// 但你不能直接调用
MyClass(); // ❌ TypeError

// 必须用 new
new MyClass(); // ✅

9. 原型链全景图

实例 dog
    ↓ __proto__
Dog.prototype
    ↓ __proto__  
Animal.prototype
    ↓ __proto__
Object.prototype
    ↓ __proto__
null

10. 属性查找顺序

class Parent {
    x = 'parent';  // 实例属性
    method() { console.log('parent'); }  // 原型方法
}

class Child extends Parent {
    x = 'child';  // 覆盖实例属性
    method() { 
        super.method();  // 调用父类原型方法
        console.log('child');
    }
}

const c = new Child();
console.log(c.x); // 'child'(先找实例属性)
c.method(); 
// parent(原型方法)
// child

// 查找过程:
// 1. c.x → 实例上有 x: 'child'
// 2. c.method → 实例上没有,去 Child.prototype 找
// 3. Child.prototype 上有 method,执行
// 4. super.method() → 去 Parent.prototype 找

Q1:class 内部如何实现私有属性?

// 目前提案:使用 #
class Person {
    #privateField = '私有';  // 私有字段
    
    #privateMethod() {       // 私有方法
        return '私有方法';
    }
    
    getPrivate() {
        return this.#privateField;
    }
}

const p = new Person();
console.log(p.#privateField); // ❌ 语法错误
console.log(p.getPrivate());   // ✅ '私有'

Q2:多重继承怎么实现?

// JavaScript 不支持多重继承,但可以用 Mixin
const FlyMixin = {
    fly() {
        console.log(`${this.name} 在飞`);
    }
};

const SwimMixin = {
    swim() {
        console.log(`${this.name} 在游泳`);
    }
};

class Animal {
    constructor(name) {
        this.name = name;
    }
}

// 混入
Object.assign(Animal.prototype, FlyMixin, SwimMixin);

const duck = new Animal('鸭子');
duck.fly();  // 鸭子 在飞
duck.swim(); // 鸭子 在游泳

11. 完整代码模板

// 类的完整形态
class MyClass {
    // 1. 静态私有字段
    static #staticPrivate = '静态私有';
    
    // 2. 私有字段
    #privateField = '私有';
    
    // 3. 公有字段
    publicField = '公有';
    
    // 4. 静态字段
    static staticField = '静态';
    
    // 5. 构造函数
    constructor(param) {
        this.param = param;  // 初始化属性
    }
    
    // 6. 私有方法
    #privateMethod() {
        return this.#privateField;
    }
    
    // 7. 原型方法
    prototypeMethod() {
        return this.#privateMethod();
    }
    
    // 8. 静态方法
    static staticMethod() {
        return this.staticField;
    }
    
    // 9. getter/setter
    get value() {
        return this.#privateField;
    }
    
    set value(val) {
        this.#privateField = val;
    }
}

// 继承
class Child extends MyClass {
    constructor(param, extra) {
        super(param);  // 必须先调用 super
        this.extra = extra;
    }
    
    // 重写方法
    prototypeMethod() {
        const parentResult = super.prototypeMethod();
        return `${parentResult} + 子类处理`;
    }
}

class 是构造函数的语法糖,通过原型链实现继承,constructor 负责初始化实例,super 用来调用父类构造函数和方法,extends 连接两个类的原型链。