专题知识学习:JavaScript

深拷贝与浅拷贝

基本概念

浅拷贝(shallow copy)

浅拷贝只复制对象的第一层属性,如果属性类型是基本类型,则复制其值,如果属性类型是引用类型,则复制其在内存中的地址(即引用),因此拷贝后的对象与原对象共享引用类型的属性

深拷贝(deep copy)

深拷贝会递归复制对象的所有层级,创建一个全新的对象,新对象与原对象不共享任何引用类型的属性

浅拷贝的实现方式

Object.assign()

const obj = { a: 1, b: { c: 2 } };
const shallowCopy = Object.assign({}, obj);

展开运算符

const obj = { a: 1, b: { c: 2 } };
const shallowCopy = { ...obj };

Array.prototype.slice()

const arr = [1, 2, [3, 4]];
const shallowCopy = arr.slice();

Array.prototype.concat()

const arr = [1, 2, [3, 4]];
const shallowCopy = arr.concat();

深拷贝的实现方式

JSON.parse(JSON.stringify())

最简单但有限制的深拷贝方法:

const obj = { a: 1, b: { c: 2 } };
const deepCopy = JSON.parse(JSON.stringify(obj));

局限性:

  • 不能处理函数、Symbol、undefined,会被完全删除
  • 不能处理循环引用
  • 会丢失 Date 对象的类型(转为 ISO 格式字符串)
  • 会丢失 RegExp 对象的类型(转为空对象)
  • 会丢失 Map/Set 等特殊对象(转为空对象)

需要注意的是:

  • null 是 Json 规范中的合法值,因此 Json.parse(Json.stringify())是可以正确处理 null 的
  • NaN 和 Infinity 会被转为 null,因为它们在 JavaScript 属于 Number 类型,Json 规范中没有 NaN 和 Infinity 这种值,序列化过程中会转为 null

递归实现

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 处理循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }
  
  let clone;
  
  // 处理特殊对象类型
  switch (Object.prototype.toString.call(obj)) {
    case '[object Date]':
      clone = new Date(obj.getTime());
      break;
    case '[object RegExp]':
      clone = new RegExp(obj);
      break;
    case '[object Map]':
      clone = new Map(Array.from(obj, ([key, val]) => [key, deepClone(val, hash)]));
      break;
    case '[object Set]':
      clone = new Set(Array.from(obj, val => deepClone(val, hash)));
      break;
    case '[object Array]':
      clone = obj.map(item => deepClone(item, hash));
      break;
    default:
      // 处理普通对象
      clone = Object.create(Object.getPrototypeOf(obj));
      hash.set(obj, clone);
      for (const key in obj) {
        if (obj.hasOwnProperty(key)) {
          clone[key] = deepClone(obj[key], hash);
        }
      }
  }
  
  return clone;
}

第三方工具库

  • Lodash 的 _.cloneDeep()
  • jQuery 的 $.extend(true, {}, obj)

性能考虑

深拷贝比浅拷贝更消耗性能,特别是在处理大型对象或深层嵌套结构时,实际应用中应结合需求选择合适的拷贝方式

应用场景

适合浅拷贝的场景

  • 明确知道只需要拷贝第一层属性
  • 需要快速拷贝而不关心嵌套对象的引用关系
  • 对象属性都是基本类型

适合深拷贝的场景

  • 需要完全独立的对象副本
  • 需要修改嵌套对象但又不能影响原对象
  • 处理不可变数据(如 Redux 的 state 更新)

面试常见问题

Q:如何判断一个拷贝是深拷贝还是浅拷贝?

可以通过修改拷贝对象的某个引用类型属性,查看原对象的属性是否被影响来判断

Q:如何实现一个支持循环引用的深拷贝?

使用 WeakMap 来存储已拷贝的对象,遇到相同的引用时直接返回存储的值

Q:为什么Json.parse(Json.stringify())不能处理函数?

因为 Json 格式不支持函数,在序列化过程中函数会被忽略

Q:深拷贝和浅拷贝在性能上有什么区别?

深拷贝需要递归遍历所有属性,性能开销大,特别是对于大型对象或深层嵌套结构

Q:如何实现一个支持特殊对象(如 Date、RegExp)的深拷贝?

需要在拷贝时判断对象类型,根据不同类型做特殊处理

循环引用

循环引用的定义

循环引用是指对象之间相互引用,形成闭环的情况,简单来说就是对象 A 引用了对象 B,对象 B 又直接或间接的引用了对象 A

循环引用的示例

简单循环引用

const objA = {};
const objB = { a: objA };
objA.b = objB; // 形成循环引用

自引用

const obj = {};
obj.self = obj; // 对象引用自身

复杂循环引用

const parent = { name: "Parent" };
const child = { name: "Child", parent: parent };
parent.child = child; // 形成循环引用

循环引用带来的问题

Json 序列化问题

const obj = {};
obj.self = obj;
JSON.stringify(obj); // 抛出错误: TypeError: Converting circular structure to JSON

深拷贝问题

普通的深拷贝方法(如递归拷贝)如果没有特殊处理循环引用,会导致无限递归

function naiveDeepClone(obj) {
  const clone = {};
  for (const key in obj) {
    if (typeof obj[key] === 'object') {
      clone[key] = naiveDeepClone(obj[key]);
    } else {
      clone[key] = obj[key];
    }
  }
  return clone;
}

const obj = {};
obj.self = obj;
naiveDeepClone(obj); // 栈溢出: Maximum call stack size exceeded

内存泄露问题

在老版本 IE 浏览器中,循环引用可能导致内存无法被垃圾回收机制正确释放

如何检测循环引用

使用 Json.stringify()

function hasCircularReference(obj) {
  try {
    JSON.stringify(obj);
    return false;
  } catch (e) {
    return e.message.includes('circular');
  }
}

使用 WeakMap/WeakSet

function hasCircular(obj, seen = new WeakSet()) {
  if (typeof obj !== 'object' || obj === null) return false;
  
  if (seen.has(obj)) return true;
  seen.add(obj);
  
  for (const key in obj) {
    if (hasCircular(obj[key], seen)) return true;
  }
  
  return false;
}

面试常见问题

Q:什么是循环引用?

循环引用是指对象之间相互引用形成的闭环,例如

const a = {}; 
const b = { ref: a }; 
a.ref = b; // a 引用 b,b 又引用 a

Q:循环引用会带来哪些问题?

  1. Json.stringify 会抛出异常
  2. 简单的深拷贝会导致无限递归
  3. 某些情况下可能会导致内存泄露

Q:为什么使用 WeakMap 而不是 Map 来处理循环引用?

WeakMap 的键是弱引用,不会阻止垃圾回收,更适合这种临时性的引用跟踪场景

JavaScript 中的闭包

闭包的定义

闭包(closure)是指能够访问自由变量的函数,这里的自由变量是指:

  1. 不是在该函数内部声明的
  2. 也不是作为函数参数传入的
  3. 而是在该函数定义时的作用域中存在的变量

更通俗的说:当一个函数记住并访问它所在的词法作用域,即使该函数在其词法作用域之外执行,就产生了闭包

闭包的简单示例

function outer() {
  const name = 'Alice'; // 自由变量
  
  function inner() {
    console.log(name); // 访问外部函数的变量
  }
  
  return inner;
}

const myFunc = outer();
myFunc(); // 输出 "Alice" —— 这就是闭包!

在这个例子中:

  1. inner 函数访问了 outer 函数的局部变量 name
  2. inner 函数被返回并在 outer 函数外被调用
  3. 但 inner 仍然能访问 name 变量

闭包的工作原理

闭包之所以能够工作,是因为 JavaScript 的作用域链(Scope Chain)机制:

  1. 词法作用域:函数的作用域在定义时就已经确定,而不是在运行时
  2. 作用域链:当访问一个变量时,JavaScript 会沿着定义时的作用域链查找
  3. 变量保持:即使外部函数已经执行完毕,但只要内部函数还在引用外部变量,这些外部变量就不会被垃圾回收

闭包的常见应用场景

创建私有变量

function createCounter() {
  let count = 0; // 私有变量
  
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.count); // undefined (无法直接访问)

函数工厂

function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

模块模式

const myModule = (function() {
  const privateVar = 'I am private';
  
  function privateMethod() {
    console.log(privateVar);
  }
  
  return {
    publicMethod: function() {
      privateMethod();
    }
  };
})();

myModule.publicMethod(); // "I am private"

事件处理和回调

function setupButtons() {
  const buttons = document.querySelectorAll('button');
  
  for (var i = 0; i < buttons.length; i++) {
    (function(index) {
      buttons[index].addEventListener('click', function() {
        console.log('Button ' + index + ' clicked');
      });
    })(i);
  }
}

闭包与内存管理

内存泄露风险

闭包会阻止外部函数作用域中的变量被垃圾回收,不当使用会导致内存泄露

function leakMemory() {
  const hugeArray = new Array(1000000).fill('*');
  
  return function() {
    console.log('I have access to hugeArray');
  };
}

const leakingFunction = leakMemory(); // hugeArray 不会被回收

// 为什么 return function 中没有使用 hugeArray 却还是造成了内存泄露?
// 因为闭包会保存其所在的整个词法环境(包括所有可访问的变量),而不仅仅是其实际使用的变量

避免内存泄露的方法

(1)及时解除引用

leakingFunction = null; // 解除引用,允许垃圾回收

(2)使用块级作用域变量 let/const 代替 var

function setupButtons() {
  const buttons = document.querySelectorAll('button');
  
  for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', function() {
      console.log('Button ' + i + ' clicked');
    });
  }
}

闭包的进阶理解

闭包与循环

经典的闭包面试题:

for (var i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// 输出五个 5,而不是 0,1,2,3,4

解决方法:
(1)使用IIFE(立即执行函数表达式)

for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 1000);
  })(i);
}

(2)使用 let 块级作用域

for (let i = 0; i < 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

闭包与 this 指向

闭包中的 this 需要特别注意:

const obj = {
  name: 'Alice',
  sayName: function() {
    return function() {
      console.log(this.name); // 这里的 this 指向全局或 undefined
    };
  }
};

obj.sayName()(); // 输出 undefined 或报错

解决方法:
(1)使用箭头函数

const obj = {
  name: 'Alice',
  sayName: function() {
    return () => {
      console.log(this.name); // 箭头函数继承外层 this
    };
  }
};

(2)保存 this 引用

const obj = {
  name: 'Alice',
  sayName: function() {
    const self = this;
    return function() {
      console.log(self.name);
    };
  }
};

闭包的优缺点

优点

  • 创建私有变量和方法
  • 保持变量状态
  • 实现数据封装
  • 实现函数工厂和模块模式

缺点

  • 可能导致内存泄漏
  • 过度使用可能会导致代码难以理解和调试
  • 可能影响性能(变量查找要沿着作用域链)

面试常见问题

Q:如何用闭包实现一个计数器

function createCounter() {
  let count = 0;
  return function() {
    return ++count;
  };
}

作用域/作用域链/this指向

作用域(Scope)

基本概念

JavaScript 中,作用域是指程序中定义变量的区域,它决定了变量的可访问性(变量在何处可用)

JavaScript 的作用域类型

(1)全局作用域

  • 在函数或代码块外部声明的变量
  • 在任何地方都可以访问
  • 在浏览器环境中,全局作用域是 window 对象
    var globalVar = '全局变量';
    
    function test() {
      console.log(globalVar); // 可以访问
    }

(2)函数作用域(局部作用域)

  • 在函数内部声明的变量
  • 只能在函数内部访问
    function test() {
      var localVar = '局部变量';
      console.log(localVar); // 可以访问
    }
    console.log(localVar); // 报错: localVar is not defined

(3)块级作用域(ES6+)

  • 使用 let 和 const 声明的变量
  • 只在{}代码块中有效
    if (true) {
      let blockVar = '块级变量';
      const constVar = '常量';
    }
    console.log(blockVar); // 报错
    console.log(constVar); // 报错

变量提升

  • var 声明的变量会被提升到函数/全局作用域的顶部
  • let 和 const 有暂时性死区(TDZ),不会提升
    console.log(a); // undefined (变量提升)
    var a = 1;
    
    console.log(b); // 报错: Cannot access 'b' before initialization
    let b = 2;

作用域链(Scope Chain)

基本概念

  • 当访问一个变量时,JavaScript 会从当前作用域开始查找
  • 如果当前作用域内找不到时,会向上一级作用域查找,直至全局作用域
  • 这种链式查找过程被称为作用域链

作用域链的形成

  • 每个函数在创建时都会保存其所在的作用域链
  • 函数在执行时会创建新的作用域链,并添加到作用域链前端
    var globalVar = 'global';
    
    function outer() {
      var outerVar = 'outer';
      
      function inner() {
        var innerVar = 'inner';
        console.log(innerVar); // 当前作用域
        console.log(outerVar); // 外层作用域
        console.log(globalVar); // 全局作用域
      }
      
      inner();
    }
    
    outer();

词法作用域(静态作用域)

  • JavaScript 采用词法作用域,作用域在函数定义时就确定了
  • 与调用位置无关
    var a = 'global';
    
    function foo() {
      console.log(a);
    }
    
    function bar() {
      var a = 'local';
      foo(); // 输出 "global" 而不是 "local"
    }
    
    bar();

this 指向

基本概念

  • this 是一个特殊的对象,指向当前执行上下文
  • this 的值取决于函数的调用方式

this 的绑定规则

(1)默认绑定

  • 独立函数调用时,this 指向全局对象
  • 严格模式下为 undefined
    function foo() {
      console.log(this); // 浏览器中指向 window
    }
    foo();

(2)隐士绑定

  • 作为对象方法调用时,this 指向调用对象
    const obj = {
      name: 'Alice',
      sayName: function() {
        console.log(this.name);
      }
    };
    
    obj.sayName(); // "Alice" (this 指向 obj)

(3)显示绑定

  • 使用 call、apply、bind 强制指定 this
    function greet() {
      console.log(`Hello, ${this.name}`);
    }
    
    const person = { name: 'Bob' };
    
    greet.call(person); // "Hello, Bob"

(4)new 绑定

  • 构造函数调用时,this 指向新创建的对象
    function Person(name) {
      this.name = name;
    }
    
    const p = new Person('Charlie');
    console.log(p.name); // "Charlie"

(5)箭头函数

  • 箭头函数没有自己的 this,继承外层作用域的 this
  • 无法通过 call、apply、bind 改变
    const obj = {
      name: 'Dave',
      sayName: () => {
        console.log(this.name); // 指向外层 this (可能是 window)
      }
    };
    
    obj.sayName(); // 可能输出 undefined

this 的特殊情况

(1)回调函数中的 this

const obj = {
  name: 'Eve',
  delayedGreet: function() {
    setTimeout(function() {
      console.log(this.name); // this 指向 window/undefined
    }, 100);
  }
};

obj.delayedGreet(); // 输出空或 undefined

解决方法:

// 使用箭头函数
delayedGreet: function() {
  setTimeout(() => {
    console.log(this.name); // 继承外层 this
  }, 100);
}

// 或保存 this
delayedGreet: function() {
  const self = this;
  setTimeout(function() {
    console.log(self.name);
  }, 100);
}

(2)DOM 事件处理函数

  • 事件处理函数中的 this 指向触发事件的元素
    button.addEventListener('click', function() {
      console.log(this); // 指向 button 元素
    });

三者的关系与区别

特性 作用域 作用域链 this指向
定义 变量的可访问范围 变量查找的链式结构 当前执行上下文对象
确定时机 代码编写时(词法作用域) 函数定义时 函数调用时
影响因素 变量声明位置 函数嵌套层级 调用方式
修改方式 无法动态修改 无法动态修改 call/apply/bind/new
箭头函数 遵守块级作用域 正常形成作用域链 继承外层 this

面试常见问题

Q:vary、let、const 的作用域区别?

  • var:函数作用域,会变量提升
  • let/const:块级作用域,有暂时性死区(TDZ)

Q:什么是闭包?它与作用域链有什么关系?

闭包是可以访问自由变量的函数,它通过作用域链访问外层变量,即使外层函数已经执行完毕

Q:箭头函数为什么不能用作构造函数?

箭头函数没有自己的 this,也没有 prototype 属性

Q:如何改变 this 指向?

通过 call、apply、bind、new 或箭头函数等方式

Q:以下代码的输出?

var name = 'Global';
const obj = {
  name: 'Object',
  sayName: function() {
    console.log(this.name);
  },
  sayNameArrow: () => {
    console.log(this.name);
  }
};

obj.sayName();       // ?
obj.sayNameArrow();  // ?
const fn = obj.sayName;
fn();                // ?

答案:

"Object"  (隐式绑定)
"Global"  (箭头函数继承全局 this)
"Global"  (默认绑定)

Q:全局作用域中的变量会形成闭包引用吗?

严格来说,全局变量不会形成真正的闭包引用。闭包是指函数能够访问并保持对其词法作用域(通常是外部函数作用域)变量的引用,而全局变量本身就存在于全局作用域中,任何函数都可以直接访问,不需要特殊的闭包机制来保持这些变量的存在

Q:全局作用域内使用 let 声明的变量,算块级作用域还是全局作用域?

全局作用域内使用 let 声明的变量具有双重特性:从作用范围来看它是全局的,因为在任何地方都可以访问;从绑定行为上来看它保持了块级作用域的特性,包括暂时性死区、不会成为全局对象(比如 window)的属性。这种设计既保证了变量的全局可用性,又避免了传统 var 声明变量带来的全局污染问题

事件循环和消息队列

事件循环的本质与必要性

为什么需要事件循环?

JavaScript 采用单线程模型设计,主要基于以下几点考虑:

  • DOM 操作的一致性:多线程同时操作 DOM 会带来不可预期的结果
  • 简化编程模型:避免了多线程编程中的复杂性,比如死锁、资源竞争等
  • 适合 web 应用场景:web 应用主要是 I/O 密集型而非计算密集型

而采用单线程模型设计,就意味着所有任务必须排队等待执行,如果前一个任务执行时间过长,就会阻塞后续任务。事件循环机制解决了这个问题,使得JavaScript 能够实现非阻塞的异步操作

事件循环的定义
事件循环是 JavaScript 运行时对任务调度的实现方式,它协调同步代码执行、异步回调处理、UI 渲染等操作。本质上,事件循环是一个不断检查任务队列并执行任务的循环过程

事件循环的核心组成

调用栈(Call Stack)

调用栈是存储函数调用的栈结构,遵循“后进先出”原则。当函数被调用时会被压入栈顶,执行完毕后从栈顶弹出

function foo() {
  console.log('foo');
  bar();
}

function bar() {
  console.log('bar');
}

foo();

上述代码的调用栈变化:

  1. foo()入栈
  2. console.log(‘foo’)入栈并执行,然后出栈
  3. bar()入栈
  4. console.log(‘bar’)入栈并执行,然后出栈
  5. bar()出栈
  6. foo()出栈

任务队列(Task Queue)

任务队列是一种先进先出(FIFO)的数据结构,用于存储待执行的异步任务回调,当 JavaScript 主线程执行完当前调用栈中的任务后,会从任务队列中取出下一个任务执行。任务队列分为三类:

(1)宏任务队列(MacroTask Queue)

  • script(整体代码)
  • setTimeout/setInterval
  • I/O 操作
  • UI 渲染
  • MessageChannel
  • setImmediate(Node.js)

每次事件循环只执行一个宏任务

(2)微任务队列(MicroTask Queue)

  • Promise.then/catch/finally
  • MutationObeserver
  • queueMicrotask
  • process.nextTick(Node.js,优先级最高)

每次事件循环会清空整个微任务队列

(3)其他特殊队列

  • requestAnimationFrame(浏览器)
  • requestIdleCallback(浏览器)
  • I/O 回调队列:Node.js 特有的 I/O 相关回调

Web APIs

浏览器提供的异步 API,如 DOM 点击事件、Ajax、setTimeout 等,这些 API 由浏览器的其他线程处理,完成后将回调放入任务队列

事件循环的详细工作流程

完整执行顺序

  1. 执行同步代码:从 script(整体代码)开始,依次执行所有同步任务
  2. 检查微任务队列:执行栈为空后,依次执行所有微任务,如果微任务执行过程中又产生了新的微任务,会继续执行新产生的微任务,直到微任务队列清空
  3. UI 渲染(浏览器环境):更新界面
  4. 执行一个宏任务:从宏任务队列中取出最早的一个任务执行
  5. 重复步骤 2~4,形成循环

关键特性

  • 微任务的优先级高于宏任务:每执行完一个宏任务后,会清空微任务队列
  • 微任务插队机制:微任务可以插入当前执行栈栈尾
  • 任务嵌套的处理:微任务执行过程中产生的新的微任务会在当前周期内继续执行,宏任务中产生的任务会在进入队列等待下一轮执行
  • 渲染时机:浏览器通常会在宏任务之间执行渲染

代码示例分析

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
  Promise.resolve().then(() => {
    console.log('promise in setTimeout');
  });
}, 0);

Promise.resolve().then(() => {
  console.log('promise1');
}).then(() => {
  console.log('promise2');
});

console.log('script end');

/* 输出顺序:
script start
script end
promise1
promise2
setTimeout
promise in setTimeout
*/

执行过程分析:

  1. 同步代码执行:打印 “script start“ 和 “script end“

  2. setTimeout 回调进入宏任务队列

  3. Promise.then 回调进入微任务队列

  4. 同步代码执行完毕,执行微任务队列:

    • 打印 “promise1“,第二个 then 进入微任务队列
    • 打印 “promise2“
  5. 执行宏任务队列中的 setTimeout 回调:

    • 打印 “setTimeout“
    • Promise.then 回调进入微任务队列
  6. 再次检查微任务队列,打印“promise in setTimeout”

浏览器与Node.js事件循环差异

浏览器事件循环特点

  1. 阶段简单:主要分为宏任务执行、微任务执行、UI 渲染三个阶段
  2. 任务类型:宏任务和微任务
  3. 渲染时机:在宏任务之间可能进行 UI 渲染

Node.js 事件循环特点
Node.js 使用 libuv 库实现事件循环,分为六个阶段:

  1. timers:执行 setTimeout 和 setInterval 回调
  2. pending callbacks:执行系统操作(如 TCP 错误)的回调
  3. idle,prepare:仅 Node 内部使用
  4. poll:检索新的 I/O 事件;执行与 I/O 相关的回调(除了 close、timers 和 setImmediate);可能会阻塞等待新事件
  5. check:执行 setImmediate 回调
  6. close callback:执行关闭事件的回调(如 socket.on(‘close’))

关键区别对比

特性 浏览器 Node.js
实现基础 浏览器引擎 libuv 库
阶段划分 宏任务、微任务、UI 渲染 6 个明确阶段
setImmediate 仅 IE 支持 标准 API
process.nextTick 不支持 最高优先级微任务
I/O 处理 Web APIs libuv 线程池
任务优先级 微任务优先 nextTick > 微任务

Node.js 特殊行为

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);

setImmediate(() => {
  console.log('immediate');
});

在 Node.js 中,上述代码的输出顺序是不确定的,因为受进程性能影响。但在 I/O 周期内调用时,setImmediate 总是优先于 setTimeout

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  
  setImmediate(() => {
    console.log('immediate');
  });
});
// 总是输出: immediate -> timeout

高级应用场景

Vue.nextTick 实现原理

Vue 通过微任务队列实现异步 DOM 更新。在浏览器环境中,Vue 会优先使用Promise.then,降级方案包括 mutationObeserver 和 setTimeout

async/await 的执行时机

async 函数返回 promise,await 实际上会暂停函数执行,将后续代码作为微任务

async function foo() {
  console.log(1);
  await bar();
  console.log(3); // 相当于Promise.then
}

async function bar() {
  console.log(2);
}

foo();
// 输出: 1, 2, 3

利用 MessageChannel 实现高优先级异步

MessageChannel 的回调作为宏任务,但优先级高于 setTimeout

const channel = new MessageChannel();
channel.port1.onmessage = () => {
  console.log('MessageChannel callback');
};
channel.port2.postMessage(null);

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

// 输出顺序: MessageChannel callback -> setTimeout

面试常见问题

Q:setTimeout(fn, 0)是立即执行吗?

不是,它表示”尽快执行“,但要等到:

  1. 当前同步代码执行完
  2. 所有微任务执行完
  3. 可能还要等待 UI 渲染

Q:为什么 promise 是微任务?

为了确保异步回调的高优先级执行,避免被其他宏任务延迟

Q:如何理解 “JavaScript 是单线程“?

JavaScript 只有一个主线程执行调用栈中的代码,但浏览器是多线程的(如网络请求、定时器等由其他线程处理)

JavaScript 垃圾回收机制

内存管理基础

内存生命周期

JavaScript 内存管理遵循以下生命周期:

  • 分配内存:当创建变量、函数或对象时自动分配
  • 使用内存:对内存进行读写操作
  • 释放内存:当内存不再被需要时,通过垃圾回收机制自动释放

内存分配机制

JavaScript 引擎(V8)在变量声明时自动分配内存,主要分为两类存储区域:

栈内存(Stack)

栈内存负责存储基本类型(nullundefinedSymbolNumberStringBooleanBigInt)和对象的引用地址,其特点是:

  • 空间固定(通常 1~10MB),由操作系统直接管理
  • 分配和回收速度快(通过移动栈指针实现)
  • 函数执行完毕后,局部变量自动释放

堆内存(Heap)

堆内存负责存储引用类型(对象、数组、函数等)的实际数据,其特点是:

  • 空间动态(64位系统上限约为 1.4GB),由垃圾回收器(GC)管理
  • 分配与回收成本高,需要避免内存碎片
    // 示例:内存分配
    let num = 10;          // 栈:基本类型
    let obj = { id: 1 };   // 堆:对象数据,栈中存储其引用地址

内存使用规则

  • 基本类型:直接操作栈内存中的值(按值访问)
  • 引用类型:通过栈内存中的引用地址操作堆内存中的数据(按引用访问)
    let a = { name: "Alice" };
    let b = a;  // b 复制 a 的引用地址,指向同一堆对象
    b.name = "Bob";
    console.log(a.name); // "Bob"(堆内数据被修改)

垃圾回收算法

JavaScript 主要使用两种垃圾回收算法:

标记-清除算法(Mark-and-Sweep)(主流算法)

工作原理:

  • 标记阶段:从根对象(全局对象、当前执行栈等)触发,递归标记所有可达对象
  • 清除阶段:回收所有未被标记的对象

优点:

  • 解决了循环引用的问题
  • 实现相对简单

缺点:

  • 可能造成内存碎片
  • 全堆扫描可能影响性能

内存碎片的问题可以通过:标记-整理(Mark-Compact)将存活对象移至内存一端

引用计数算法(Reference Counting)(已淘汰)

工作原理:

  • 统计对象被引用的次数,归零时释放

优点:

  • 立即回收、内存释放及时
  • 执行过程分散,没有明显停顿

缺点:

  • 无法处理循环引用
  • 计数器维护开销大

现代浏览器已不再单独使用引用计数算法

V8 引擎的垃圾回收机制

分代式垃圾回收

V8将堆内存分为两个主要区域:

新生代:

  • 存放生存时间短的对象
  • 容量小(通常 1~8MB)
  • 回收频繁
  • 采用 Scavenge 算法(一种复制算法)

老生代:

  • 存放生存时间长的对象
  • 容量大
  • 回收频率较低
  • 采用 标记-清除 和 标记-压缩 算法

回收过程详解

新生代回收过程:

  • 将新生代区域分成两个空间:From 和 To
  • 将对象分配到 From 空间
  • 当 From 空间满时,标记 From 空间内的活跃对象,并将活跃对象复制到 To 空间,执行垃圾回收清空 From 空间,同时交换 From 空间和 To 空间
  • 对象经历多次回收晋升为老生代

老生代回收过程:

  • 标记阶段:从根对象出发,采用三色标记法(白、灰、黑)标记可达对象
  • 清除/压缩阶段:清除未标记对象或压缩内存消除碎片

增量标记与惰性清理

  • 增量标记:将标记过程拆分为多个小任务,与 JavaScript 执行交替进行,避免长时间阻塞主线程
  • 惰性清理:延迟清理过程,按需清理内存,进一步减少对主线程的影响

内存泄露与排查

常见的内存泄露场景

(1)意外的全局变量

function leak() {
  leakedVar = '意外全局'; // 未使用var/let/const
  this.leakedProp = '意外绑定到全局';
}

(2)遗忘的定时器或回调

const data = fetchData();
setInterval(() => {
  process(data); // data一直被引用
}, 1000);

(3)DOM 引用

const elements = {
  button: document.getElementById('button')
};

// 即使从DOM移除,elements.button仍保留引用,需要置空 button 才能解决
document.body.removeChild(document.getElementById('button'));

(4)闭包滥用

function createClosure() {
  const largeObj = new Array(1000000).fill('*');
  
  return function() {
    // 即使不使用largeObj,它仍被保留
    console.log('closure');
  };
}

内存排查工具

Chrome DevTools:

  • Performance 面板:记录内存变化
  • Memory 面板:
    • Heap Snapshot:堆内存快照
    • Allocations On Timeline:内存分配时间线
    • Allocation Sampling:内存分配采样

Node.js 工具:

  • process.memoryUsage()
  • –inspect 标识启用调试
  • heapdump 模块生成堆快照

性能优化实践

编程最佳实践

  1. 避免意外全局变量
  2. 谨慎使用闭包
  3. 及时清除定时器和事件监听器
    const timer = setTimeout(() => {}, 1000);
    clearTimeout(timer);
  4. 避免内存密集操作
    // 不佳:频繁创建大对象
    function process() {
      const data = new Array(1000000);
      // ...
    }
    
    // 较佳:复用对象
    const dataPool = new Array(1000000);
    function process() {
      // 复用dataPool
    }

对象池模式

class ObjectPool {
  constructor(createFn) {
    this.createFn = createFn;
    this.pool = [];
  }
  
  get() {
    return this.pool.length ? this.pool.pop() : this.createFn();
  }
  
  release(obj) {
    this.pool.push(obj);
  }
}

// 使用示例
const pool = new ObjectPool(() => new Array(1000));
const arr = pool.get();
// 使用后释放
pool.release(arr);

WeakMap 和 WeakSet

WeakMap 和 WeakSet 的键是弱引用,不会阻止 GC 垃圾回收键对象

const weakMap = new WeakMap();
let obj = {};

weakMap.set(obj, 'some data');
obj = null; // 现在weakMap中的条目可以被垃圾回收

面试常见问题

Q:JavaScript 是如何管理内存的?

JavaScript 使用自动垃圾回收机制。主要采用标记-清除算法,V8引擎采用分代回收策略,将堆内存分为新生代和老生代两个区域,分别采用 Scavenge 算法和标记-清除/压缩算法

Q:什么是内存泄露?如何避免?

内存泄露是指不再被需要的内存未能正确释放。常见的内存泄露场景有:意外的全局变量、游离的 DOM、闭包滥用、未及时清除的定时器/事件监听等,可以通过弱引用、对象池、严格模式、及时清理定时器等方法避免

Q:WeakMap 和 Map 有什么区别?

WeakMap 的键必须是对象,且是弱引用,不会阻止 GC 垃圾回收器回收键对象;Map 的键可以是任意类型,且保持对键的强引用

Q:解释下 V8 引擎的回收策略?

V8 引擎采用分代回收策略,将堆内存分为新生代和老生代两个区域。新生代区域存放小对象、生存时间短的对象,老生代区域存放大对象、生存时间长的对象。新生代采用 Scavenge 算法回收,老生代采用标记-清除/压缩算法回收,采用增量标记和惰性清理策略,减少对主线程的影响

JavaScript模块化机制

模块化演进史:从混乱到标准化

原始阶段:全局命名空间污染

// math.js
function add(a, b) { return a + b; }

// app.js
function add(a, b, c) { return a + b + c; } // 命名冲突

console.log(add(1, 2)); // 3 or 6? 结果不可预测

IIFE 模式:初步封装

// 立即执行函数隔离作用域
var calculator = (function() {
  function add(a, b) { return a + b; }
  
  return {
    add: add
  };
})();

// 使用模块
calculator.add(1, 2); // 3

CommonJS:Node.js 的模块标准

// math.js
exports.add = (a, b) => a + b;
exports.PI = 3.14159;

// app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5

AMD:浏览器异步加载方案

// 使用RequireJS
define(['dep1', 'dep2'], function(dep1, dep2) {
  return {
    calculate: function() {
      return dep1.value + dep2.value;
    }
  };
});

UMD:通用模块定义

// 兼容CommonJS和AMD
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    define(['dep'], factory);
  } else if (typeof exports === 'object') {
    module.exports = factory(require('dep'));
  } else {
    root.myModule = factory(root.dep);
  }
}(this, function(dep) {
  return { /* 模块内容 */ };
}));

ES Modules:现代 JavaScript 模块标准

// math.js
export const add = (a, b) => a + b;
export const PI = 3.14159;

// app.js
import { add, PI } from './math.js';
console.log(add(PI, PI)); // 6.28318

ES Modules 核心机制深度剖析

模块加载三阶段

graph TD
    A[构建 Construction] --> B[解析模块]
    B --> C[建立依赖图]
    C --> D[实例化 Instantiation]
    D --> E[分配内存]
    E --> F[建立导入/导出链接]
    F --> G[求值 Evaluation]
    G --> H[执行顶层代码]

实时绑定原理

// counter.js
export let count = 0;
export function increment() { count++; }

// app.js
import { count, increment } from './counter.js';

console.log(count); // 0
increment();
console.log(count); // 1 (值实时更新)

循环引用解决方案

// a.js
import { b } from './b.js';
export const a = 'A';
console.log('a中获取b:', b); // 'B'

// b.js
import { a } from './a.js';
export const b = 'B';
console.log('b中获取a:', a); // undefined (但不会报错)

// ES Modules 处理循环引用的关键:
// 1. 构建阶段建立所有导出绑定
// 2. 执行阶段按顺序求值
// 3. 未初始化变量值为undefined

执行步骤:

  1. 加载阶段:
    • 解析 moduleA 发现依赖 moduleB
    • 解析 moduleB 发现依赖 moduleA → 检测到循环引用
  2. 初始化阶段:
    • 创建模块作用域和导出绑定
    • moduleA 导出:a(未初始化)
    • moduleB 导出:b(未初始化)
  3. 执行阶段:
    • 先执行 moduleB:
      • import { a } → 获取 moduleA 的 a 绑定(当前未初始化 = undefined)
      • 执行 export const b = ‘B’ → 初始化 b
      • console.log(a) → 输出 undefined
    • 再执行 moduleA:
      • import { b } → 获取已初始化的 b 值(’B’)
      • 执行 export const a = ‘A’ → 初始化 a
      • console.log(b) → 输出 ‘B’

现代模块系统核心特性对比

特性 CommonJS ES Modules
加载方式 同步加载 异步加载
模块解析时机 运行时 编译时(静态)
导出类型 值拷贝 实时绑定(引用)
循环引用处理 部分支持(可能出错) 完全支持
动态导入 require()动态语法 import()函数
顶层 await 不支持 支持
Tree shaking 困难 原生支持
严格模式 默认非严格模式 默认严格模式

高级模块化技术与实践

动态导入(Code Spliting)

// 按需加载模块
document.getElementById('loadBtn').addEventListener('click', async () => {
  const { heavyOperation } = await import('./heavyModule.js');
  heavyOperation();
});

// Webpack 自动代码分割
const LoginModal = React.lazy(() => import('./LoginModal'));

模块重导出

// utils/index.js
export { default as formatDate } from './dateUtils';
export { encrypt, decrypt } from './cryptoUtils';

// app.js
import { formatDate, encrypt } from './utils'; // 单一入口

模块联邦(微前端架构)

// app1/webpack.config.js
new ModuleFederationPlugin({
  name: 'app1',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/Button',
  },
  shared: ['react', 'react-dom'],
});

// app2/webpack.config.js
new ModuleFederationPlugin({
  remotes: {
    app1: 'app1@http://localhost:3001/remoteEntry.js',
  },
});

// app2 中使用远程模块
const RemoteButton = React.lazy(() => import('app1/Button'));

模块化工程实践

项目结构示例

src/
├── components/     // 可复用UI组件
├── features/       // 功能模块
│   ├── auth/       // 认证模块
│   │   ├── api.js
│   │   ├── store.js
│   │   └── components/
├── lib/            // 工具库
├── services/       // API服务层
└── index.js        // 应用入口

模块设计原则

  1. 单一职责原则:每个模块只解决一个问题
  2. 高内聚低耦合:模块内部紧密相连,模块间依赖最小化
  3. 明确接口:导出必要的变量/函数,隐藏内部实现细节
  4. 无副作用导入:避免在导入时执行操作(初始化除外)
  5. 稳定抽象:模块接口应向后兼容

性能优化策略

  1. 代码分割:路由级/组件级动态导入
  2. 共享依赖:避免重复打包(webpack 的 splitChunks)
  3. Tree shaking:配合 ESM 静态结构实现

常见问题与解决方案

循环引用陷阱

// 解决方案:函数导出延迟访问
// a.js
export function getA() { return a; }
export let a = 'A';

// b.js
import { getA } from './a.js';
console.log(getA()); // 'A'

默认导入与命名导入

// 模块定义
export default function main() { /*...*/ }
export const helper = () => { /*...*/ };

// 导入方式
import myMain, { helper } from './module.js';

动态路径问题

// 错误:无法静态分析
import module from `${path}/module.js`;

// 正确:使用import()函数
const module = await import(`${path}/module.js`);

浏览器跨标签通信

跨标签通信是指在不同的浏览器标签页(或窗口)之间进行数据传递和消息通信。这在现代Web应用中非常有用,比如同步多个标签页的状态、通知数据更新等。跨标签通信的几种常见方式:

  1. Broadcast Channel API
  2. SharedWorker
  3. LocalStorage 和 Storage 事件
  4. Window.postMessage
  5. Cookies 和 轮询
  6. IndexedDB 和 轮询
  7. Service Worker

Broadcast Channel API

原理

基于发布订阅模式,创建命名频道实现广播通信

代码示例

// 标签页 A(发送方)
const bc = new BroadcastChannel('app_channel');
bc.postMessage({ type: 'USER_UPDATE', data: user });

// 标签页 B(接收方)
const bc = new BroadcastChannel('app_channel');
bc.onmessage = (event) => {
  if(event.data.type === 'USER_UPDATE') {
    updateUI(event.data.data);
  }
};

优点

  • 原生支持,API 简洁
  • 支持任意同源页面(包括 iframe)

缺点

  • 不兼容 IE/Edge <79
  • 无法跨域通信
    • 适用场景:现代浏览器下的同源应用状态同步

SharedWorker

原理

共享 Web Worker 作为消息中继站

创建步骤

(1) 创建共享 Worker 文件 worker.js:

const ports = [];
onconnect = (e) => {
  const port = e.ports[0];
  ports.push(port);
  port.onmessage = (event) => {
    ports.forEach(p => {
      if(p !== port) p.postMessage(event.data);
    });
  };
};

(2) 页面代码:

// 所有标签页
const worker = new SharedWorker('worker.js');
worker.port.onmessage = (e) => handleEvent(e.data);
const broadcast = (data) => worker.port.postMessage(data);

优点

  • 真正的全局通信枢纽
  • 避免轮询性能损耗

缺点

  • 调试复杂(Chrome://inspect 调试 Worker)
  • 需要处理端口生命周期
    • 适用场景:复杂应用的多 Tab 数据同步(如金融仪表盘)

localStorage + storage 事件

原理

利用 storage 事件监听存储变化

代码示例

// 发送方
localStorage.setItem('sync_data', JSON.stringify(payload));

// 接收方
window.addEventListener('storage', (e) => {
  if(e.key === 'sync_data') {
    const data = JSON.parse(e.newValue);
    handleDataUpdate(data);
  }
});

关键细节

  • 事件在同域名其他标签页触发(当前页不触发)
  • 使用 JSON.stringify 避免对象引用问题
  • 需要清理过期数据(removeItem)
  • 适用场景:简单状态同步(用户登录状态切换)

window.postMessage

原理

通过窗口引用直接通信

代码示例

// 父页面打开子页面
const childWindow = window.open('child.html');

// 父页面向子页面发送
childWindow.postMessage({ authToken: 'xyz' }, 'https://app.com');

// 子页面接收
window.addEventListener('message', (e) => {
  if(e.origin !== 'https://app.com') return;
  console.log(e.data.authToken);
});

安全要点

  • 必须验证 event.origin
  • 敏感操作需增加消息来源白名单
  • 适用场景:跨域通信(主站与子站交互)

Service Worker 消息代理

原理

利用 Service Worker 作为消息中心

实现模式

// Service Worker 中
self.addEventListener('message', (event) => {
  event.waitUntil(
    clients.matchAll().then(all => {
      all.forEach(client => {
        if(client.id !== event.source.id) {
          client.postMessage(event.data);
        }
      });
    })
  );
});

// 页面中
navigator.serviceWorker.controller.postMessage(payload);

优势

  • 支持离线消息中转
  • 可配合 Push API 实现后台通知
  • 适用场景:PWA 应用的全栈消息系统

IndexedDB + 轮询(兼容方案)

原理

共享数据库 + 定时查询

代码示例

// 发送方
function writeUpdate(data) {
  const tx = db.transaction('updates', 'readwrite');
  tx.objectStore('updates').put({ 
    id: Date.now(), 
    data 
  });
}

// 接收方(每2秒检查)
setInterval(() => {
  const tx = db.transaction('updates', 'readonly');
  const store = tx.objectStore('updates');
  store.openCursor().onsuccess = (e) => {
    const cursor = e.target.result;
    if(cursor) {
      handleUpdate(cursor.value.data);
      cursor.delete(); // 消费后删除
    }
  };
}, 2000);

适用场景

  • 需要数据持久化的低频操作(如日志收集)

原理

共享 Cookie + 定时读取

代码示例

// 发送方
document.cookie = `sync_flag=${Date.now()}; path=/`;

// 接收方
let lastValue = '';
setInterval(() => {
  const current = getCookie('sync_flag');
  if(current !== lastValue) {
    lastValue = current;
    triggerSync();
  }
}, 1000);

缺点

  • 性能开销大(不推荐高频使用)
  • 存储空间有限(4KB)
  • 适用场景:兼容 IE8 等老旧浏览器

方案选型决策策略

graph TD
    A[需要兼容 IE?] -->|是| B(localStorage/Cookie)
    A -->|否| C[需要离线支持?]
    C -->|是| D(Service Worker)
    C -->|否| E[需要跨域?]
    E -->|是| F(window.postMessage)
    E -->|否| G[通信频率?]
    G -->高频 --> H(BroadcastChannel)
    G -->低频 --> I(SharedWorker)

面试要点

  • 同源策略:除 postMessage 外所有方案均受同源限制
  • 性能考量:避免轮询方案用于高频场景
  • 安全实践:
    • postMessage 必须验证 origin
    • localStorage 避免存储敏感数据
  • 现代方案:优先推荐 BroadcastChannel + SharedWorker
  • 异常处理:Worker 需监听 error 事件,localStorage 需 try/catch

进阶问题

Q:如何实现浏览器完全关闭后的状态同步?

需结合服务端方案(WebSocket 连接 + 本地存储恢复),在页面加载时从服务端获取最新状态。

sequenceDiagram
    participant User
    participant Browser
    participant ServiceWorker
    participant Server
    
    User->>Browser: 打开应用
    Browser->>ServiceWorker: 检查缓存状态
    ServiceWorker->>Browser: 返回缓存状态
    Browser->>User: 立即显示界面
    
    par 并行处理
        Browser->>Server: 请求最新状态
        Server->>Browser: 返回状态数据
    and
        ServiceWorker->>Server: 预加载关键资源
    end
    
    Browser->>Browser: 比较缓存与最新状态
    alt 状态有更新
        Browser->>Browser: 增量更新UI
        Browser->>ServiceWorker: 缓存新状态
    else 状态无更新
        Browser->>Browser: 保持当前状态
    end

前端路由原理

核心原理概述

前端路由的本质是通过监听 URL 的变化,在不刷新页面的情况下更新视图,主要通过两种模式实现:

  • Hash 模式
  • History 模式

Hash 模式(锚点路由)

实现原理

  • 利用 URL 中 # 后面的部分(hash)的变化不会触发页面刷新
  • 监听 hashchange 事件响应路由变化
  • 通过 location.hash 获取当前路由状态

核心代码实现

class HashRouter {
  constructor() {
    this.routes = {};
    
    // 初始化监听
    window.addEventListener('load', () => this.handleRouteChange());
    window.addEventListener('hashchange', () => this.handleRouteChange());
  }
  
  // 注册路由
  addRoute(path, callback) {
    this.routes[path] = callback || function() {};
  }
  
  // 路由变化处理
  handleRouteChange() {
    const currentHash = location.hash.slice(1) || '/';
    const routeHandler = this.routes[currentHash];
    
    if (routeHandler) {
      routeHandler();
    } else {
      // 404处理
      this.routes['/404']?.();
    }
  }
  
  // 导航方法
  navigate(path) {
    location.hash = `#${path}`;
  }
}

// 使用示例
const router = new HashRouter();
router.addRoute('/', () => renderHomePage());
router.addRoute('/about', () => renderAboutPage());
router.addRoute('/404', () => renderNotFound());

优缺点分析

优点 缺点
✅ 兼容性好(IE8+) ❌ URL中包含#不美观
✅ 无需服务器配置 ❌ SEO支持有限
✅ 实现简单 ❌ 只能使用字符串传参

History 模式(HTML5路由)

实现原理

  • 使用 HTML5 History API (pushState, replaceState)
  • 监听 popstate 事件响应路由变化
  • 通过 location.pathname 获取当前路径

核心 API

// 添加新历史记录
history.pushState(state, title, url);

// 替换当前历史记录
history.replaceState(state, title, url);

// 监听历史记录变化
window.addEventListener('popstate', (event) => {
  console.log('路由变化:', location.pathname);
});

完整实现方案

class HistoryRouter {
  constructor() {
    this.routes = {};
    this.init();
  }
  
  init() {
    // 拦截所有链接点击
    document.addEventListener('click', (e) => {
      if (e.target.tagName === 'A') {
        e.preventDefault();
        this.navigate(e.target.getAttribute('href'));
      }
    });
    
    // 监听浏览器前进/后退
    window.addEventListener('popstate', () => this.handleRouting());
    
    // 初始加载
    window.addEventListener('load', () => this.handleRouting());
  }
  
  addRoute(path, callback) {
    this.routes[path] = callback;
  }
  
  handleRouting() {
    const currentPath = location.pathname;
    const routeHandler = this.routes[currentPath] || this.routes['*'];
    
    if (routeHandler) {
      routeHandler();
    } else {
      this.navigate('/404');
    }
  }
  
  navigate(path) {
    history.pushState(null, null, path);
    this.handleRouting();
  }
  
  replace(path) {
    history.replaceState(null, null, path);
    this.handleRouting();
  }
}

优缺点对比

优点 缺点
✅ 干净的URL ❌ 需要服务器端支持
✅ 完整的路径控制 ❌ 兼容性要求(IE10+)
✅ 支持复杂参数传递 ❌ 实现复杂度更高

高级路由功能实现

动态路由匹配

// 路由配置
const routes = {
  '/user/:id': (params) => showUserProfile(params.id),
  '/product/:category/:id': (params) => showProduct(params.category, params.id)
};

// 匹配函数
function matchRoute(path) {
  const routeKeys = Object.keys(routes);
  
  for (const route of routeKeys) {
    const regex = new RegExp(
      `^${route.replace(/:\w+/g, '([^/]+)')}$`
    );
    
    const match = path.match(regex);
    if (match) {
      const params = {};
      const paramNames = [...route.matchAll(/:(\w+)/g)].map(m => m[1]);
      
      paramNames.forEach((name, i) => {
        params[name] = match[i + 1];
      });
      
      return {
        handler: routes[route],
        params
      };
    }
  }
  
  return null;
}

嵌套路由实现

// 路由配置
const routes = [
  {
    path: '/dashboard',
    component: DashboardLayout,
    children: [
      { path: '/overview', component: Overview },
      { path: '/settings', component: Settings }
    ]
  }
];

// 路由渲染组件
function RouterView({ routes }) {
  const currentPath = useCurrentPath();
  const matchedRoute = findNestedRoute(routes, currentPath);
  
  return (
    <matchedRoute.component>
      {matchedRoute.children && <RouterView routes={matchedRoute.children} />}
    </matchedRoute.component>
  );
}

路由守卫(导航守卫)

class AdvancedRouter extends HistoryRouter {
  constructor() {
    super();
    this.beforeHooks = [];
    this.afterHooks = [];
  }
  
  beforeEach(guard) {
    this.beforeHooks.push(guard);
  }
  
  afterEach(hook) {
    this.afterHooks.push(hook);
  }
  
  async navigate(to) {
    const from = location.pathname;
    
    // 执行前置守卫
    for (const guard of this.beforeHooks) {
      const result = await guard(to, from);
      if (result === false || typeof result === 'string') {
        return; // 取消导航
      }
    }
    
    // 执行导航
    history.pushState(null, null, to);
    
    // 更新视图
    this.handleRouting();
    
    // 执行后置钩子
    this.afterHooks.forEach(hook => hook(to, from));
  }
}

// 使用示例
router.beforeEach((to, from) => {
  if (to === '/admin' && !isAdmin()) {
    return '/login'; // 重定向
  }
});

路由性能优化策略

路由懒加载

const routes = {
  '/dashboard': () => import('./Dashboard.js'),
  '/admin': () => import('./AdminPanel.js')
};

async function handleRouting() {
  const path = location.pathname;
  const loader = routes[path];
  
  if (loader) {
    const module = await loader();
    module.render();
  }
}

路由预加载

// 鼠标悬停时预加载
document.querySelectorAll('a').forEach(link => {
  link.addEventListener('mouseenter', () => {
    const path = link.getAttribute('href');
    if (routes[path]) {
      routes[path]().preload();
    }
  });
});

滚动行为管理

window.addEventListener('popstate', () => {
  // 从历史记录恢复滚动位置
  const scrollPos = history.state?.scrollPosition || [0, 0];
  window.scrollTo(...scrollPos);
});

router.afterEach((to) => {
  // 记录滚动位置
  history.replaceState(
    { ...history.state, scrollPosition: [window.scrollX, window.scrollY] },
    '',
    to
  );
});

路由库实现对比

特性 原生实现 React Router Vue Router
路由模式 手动实现 支持hash/history 支持hash/history/abstract
嵌套路由 需手动实现
路由守卫 需手动实现
动态路由 需手动实现
懒加载 需手动实现
滚动行为 需手动实现
TypeScript 需手动配置

面试常见问题分析

Q:Hash模式和History模式如何选择?

  • 优先选择History模式(更友好的URL、更好的SEO支持)
  • 需要兼容IE9或以下时使用Hash模式
  • 无后端控制权时使用Hash模式(如静态托管)

Q:为什么History模式需要服务器配置?

当用户直接访问/dashboard等子路径时,服务器需要返回单页应用的入口文件(index.html),而不是尝试查找不存在的/dashboard.html文件。

Q:如何解决路由参数变化组件不刷新的问题?

// React Router 示例
<Route 
  path="/user/:id" 
  component={User} 
  key={location.pathname} // 强制重新挂载
/>

// 或监听参数变化
useEffect(() => {
  // 当id变化时重新获取数据
  fetchUserData(params.id);
}, [params.id]);

Q:如何实现路由过渡动画?

// Vue
<template>
  <transition name="fade" mode="out-in">
    <router-view></router-view>
  </transition>
</template>

<style>
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>
// React
// src/components/RouteTransition.jsx
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { useLocation, Routes, Route } from 'react-router-dom';

const RouteTransition = ({ children }) => {
  const location = useLocation();
  
  return (
    <TransitionGroup>
      <CSSTransition
        key={location.key}
        timeout={300}
        classNames="fade"
      >
        <Routes location={location}>
          {children}
        </Routes>
      </CSSTransition>
    </TransitionGroup>
  );
};

export default RouteTransition;

// src/App.jsx
import { BrowserRouter } from 'react-router-dom';
import RouteTransition from './components/RouteTransition';
import Home from './pages/Home';
import About from './pages/About';

function App() {
  return (
    <BrowserRouter>
      <RouteTransition>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </RouteTransition>
    </BrowserRouter>
  );
}

总结

前端路由是现代SPA应用的核心基础设施,理解其原理需要掌握:

  • 两种路由模式的底层实现机制
  • History API 的详细用法
  • 动态路由匹配算法
  • 路由生命周期管理(导航守卫)
  • 性能优化策略(懒加载、预加载)
  • 最佳实践选择(模式选择、参数处理)

在面试中回答路由问题时,建议结合具体场景:

  • 解释核心原理时使用技术术语(popstate/hashchange)
  • 对比不同实现方案时说明取舍原因
  • 解决实际问题时展示优化意识
  • 提到主流路由库时分析其设计哲学

Symbol

定义与描述

Symbol 是 ES6 新引入的一种原始数据类型,表示独一无二的值,Symbol 的主要用途是创建唯一标识符,用于对象属性的键,以避免属性名冲突。

创建与使用

基础创建

// 无描述创建
const sym1 = Symbol();

// 带描述创建(仅用于调试)
const sym2 = Symbol('description');
console.log(sym2.toString()); // "Symbol(description)"

全局注册表

// 创建或获取全局 Symbol
const globalSym = Symbol.for('global_key');
console.log(Symbol.keyFor(globalSym)); // "global_key"

// 跨模块访问
// module1.js
Symbol.for('shared');

// module2.js
Symbol.for('shared') === Symbol.for('shared'); // true

核心特性

唯一性

每个 Symbol 的值都是唯一的,即使它们有相同的描述

const s1 = Symbol();
const s2 = Symbol();
s1 === s2 // false

const s3 = Symbol('desc');
const s4 = Symbol('desc');
s3 === s4 // false(即使描述相同)

值不可修改

Symbol 值一旦创建不可被修改

const sym = Symbol('unique');
// 没有任何方法可以改变 sym 的值
sym.description = 'changed'; // 静默失败(非严格模式)

不可枚举性

当使用 Symbol 值作为对象属性键时,默认情况下常规方法(如 for...inObject.keys())中不可见,但可以通过 Object.getOwnPropertySymbols()Reflect.ownKeys() 获取

const obj = {
  [Symbol('key')]: 'value',
  normalKey: 'normal'
};

for (let key in obj) {
  console.log(key); // 只输出 "normalKey"
}

console.log(Object.keys(obj)); // ["normalKey"]
console.log(JSON.stringify(obj)); // {"normalKey":"normal"}

特殊运算

Symbol 类型的值不能与其他类型的值进行运算,会报错,但 Symbol 值可以转为字符串和布尔值

let s = Symbol('hello')
s + 'world' // TypeError: can't convert a Symbol value to a string
`${s} world` // TypeError: can't convert a Symbol value to a string
Number(s) // TypeError: Cannot convert a Symbol value to a number
s + 2 // TypeError: Cannot convert a Symbol value to a number

内置的 Symbol 值

JavaScript 内置的 Symbol 属性可以用于修改语言的内部行为

Symbol.hasInstance - 自定义 instanceof 行为

对象的 Symbol.hasInstance 属性指向一个内部方法,当其他对象使用 instanceof 运算符判断是否为该对象的实例时,就会调用这个内部方法

class MyObj {
  static [Symbol.hasInstance](obj) {
    return Object.prototype.toString.call(obj) === '[object Number]'
  }
}

1 instanceof MyObj // true
const obj = {}
obj instanceof MyObj // false

Symbol.iterator - 定义对象的默认迭代器

对象的 Symbol.iterator 属性指向该对象的默认遍历器方法

[...{ [Symbol.iterator]: function*() { yield 1; yield 2; } }] // [1, 2]

Symbol.toStringTag - 定制 Object.prototype.toString

对象的 Symbol.toStringTag 属性用来设定一个字符串,当其他对象调用 Object.prototype.toString 判断类型时,如果 Symbol.toStringTag 属性存在,则该属性设定的字符串会出现在 toString 方法返回的字符串中

class CustomClass {
  get [Symbol.toStringTag]() {
    return 'Custom'
  }
}
let obj = new CustomClass()
Object.prototype.toString.call(obj) // [object Custom]

({ [Symbol.toStringTag]: 'Custom' }).toString() // [object Custom]

Symbol.species - 指定衍生对象的构造函数

对象的 Symbol.species 属性指向一个构造函数,当创建衍生对象时,会使用该属性。

class MyArray extends Array {
  static get [Symbol.species]() {
    return Array
  }
}

const a = new MyArray();
const b = a.map(x => x);

b instanceof MyArray // false
b instanceof Array // true

由于创建 MyArray 时使用了 Symbol.species 属性,因此会使用 Symbol.species 返回的函数作为构造函数,衍生对象 b 就不是 MyArray 的实例

Symbol.match - 定制字符串匹配行为

对象的 Symbol.match 属性指向一个函数,当执行 str.match(MyObj),如果 MyObj 存在该属性,会调用该属性指向的函数,并将函数的返回值作为 str.match(MyObj) 的返回值

默认行为中,regex[Symbol.match] 指向 RegExp 内置的匹配方法

const regex = /foo/;
'foobar'.match(regex); // ["foo", index: 0, input: "foobar"]

通过 Symbol.match 属性可以让任何对象成为合法的 match 参数

const customMatcher = {
  [Symbol.match](str) {
    return str.includes('hello') ? 
      ['匹配成功'] : 
      null;
  }
};

'world hello'.match(customMatcher); // ["匹配成功"]
'no match'.match(customMatcher);    // null

此外,还有 Symbol.replace、Symbol.search、Symbol.split 等属性,都是用于定制 String 对应的方法

Symbol 与相关技术对比

特性 Symbol 字符串属性 WeakMap
唯一性 唯一 不唯一 唯一(键为对象)
可枚举性 不可枚举(默认) 可枚举 N/A
内存管理 全局注册 Symbol 需手动管理 自动回收 可被 GC 垃圾回收
私有性 弱私有(反射可获取) 完全公开 强私有
适用场景 协议实现/防冲突 常规数据存储 真正私有数据存储

Symbol 的核心价值

  • 提供真正唯一的标识符,解决命名冲突问题
  • 实现 JavaScript 内置协议(如迭代协议)
  • 支持元编程,改变语言内部行为
  • 为对象提供弱封装能力(非完全私有)

Set 和 Map 结构

Set

Set 的定义和基本用法

Set 是 ES6 引入的新的数据结构,是一种集合类型,类似于数组,但成员的值是唯一的,没有重复成员。Set 本身是一个构造函数,用于生成 Set 数据结构。

let set = new Set()

Set()函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化

const set = new Set([1, 2, 3, 4, 4])

Set 的核心特性

唯一性

const set = new Set();
set.add(1);
set.add(1);
set.add('1');
console.log(set.size); // 2 (1 和 '1' 是不同的值)

唯一性判断采用 SameValueZero 算法:

  • NaN 被视为相等
  • +0 和 -0 被视为相等
  • 对象引用比较(不同对象即使内容相同也被视为不同)

根据该特性可以实现数组去重:

// 去除数组的重复成员
[...new Set(array)]

值类型支持

Set 数据结构可以存储任意类型的值

const set = new Set([
  1, 
  'text',
  {name: 'obj'},
  [1, 2, 3],
  NaN,
  undefined,
  null
]);

Set 的基础 API 详解

方法/属性 描述
new Set(iterable) 构造函数
set.has(value) 检查值是否存在
set.add(value) 添加值
set.delete(value) 删除值
set.clear(value) 清空集合
set.size 获取 Set 实例的成员数量(非函数属性,不能 set.size())

Set 的迭代操作

Set 结构的实例有四个迭代方法,用于遍历成员:

  • set.values():返回键值的遍历器
  • set.keys():返回键名的遍历器
  • set.entries():返回键值对的遍历器
  • set.forEach():使用回调函数变量成员

Set 实例的遍历顺序就是成员插入的顺序,该特性可以用于保证回调函数列表的执行顺序。Set 结构实例的默认遍历器生成函数就是它的 values()方法:

const set = new Set(['a', 'b', 'c']);

// 1. for...of
for (const item of set) {
  console.log(item);
}

// 2. forEach
set.forEach((value, valueAgain) => {
  console.log(value); // 注意:两个参数相同
});

// 3. 获取迭代器
const iterator = set.values(); // 或 set[Symbol.iterator]()

Set 的高级特性与应用

性能优势

Set 的查找性能远优于数组

// 数组 vs Set 查找性能对比
const arr = [/* 10,000 个元素 */];
const set = new Set(arr);

console.time('Array');
arr.includes(9999);
console.timeEnd('Array'); // ~0.1ms

console.time('Set');
set.has(9999);
console.timeEnd('Set'); // ~0.005ms

集合运算实现

// 并集
function union(setA, setB) {
  return new Set([...setA, ...setB]);
}

// 交集
function intersection(setA, setB) {
  return new Set([...setA].filter(x => setB.has(x)));
}

// 差集
function difference(setA, setB) {
  return new Set([...setA].filter(x => !setB.has(x)));
}

对象引用管理

const users = new Set();

function trackUser(user) {
  users.add(user);
}

function untrackUser(user) {
  users.delete(user);
}

// 自动去重:同一对象不会被重复添加

Set 的实现原理

现代 JavaScript 引擎通常使用:

  • 哈希表作为底层实现
  • 使用 开放寻址法 或 链表法 解决冲突
  • 自动扩容机制(类似 Map)

WeakSet

WeakSet 的定义

WeakSet 是 ES6 引入的一种特殊集合类型,与 Set 类似,也是不重复的值的集合。

WeakSet 的核心特性

成员类型限制

// WeakSet 的成员只能是对象(null 除外)和 Symbol 值,不能是其他类型的值
const ws = new WeakSet();
ws.add(1) // 报错
ws.add(Symbol()) // 不报错

弱引用

let obj = { data: 'important' };
const weakset = new WeakSet();

weakset.add(obj);
console.log(weakset.has(obj)); // true

// 当对象不再被引用时
obj = null; 

// 垃圾回收后,对象会自动从WeakSet移除
// weakset.has(原obj) 将返回 false

不可遍历

const ws = new WeakSet();
ws.add({});
console.log(ws.size); // undefined (无size属性)

// 以下操作都会报错:
for (let item of ws) {}
ws.forEach(() => {})
[...ws]

WeakSet 的 API 详解

方法/属性 描述
new WeakSet(iterable) 构造函数
weakSet.has(value) 检查值是否存在
weakSet.add(value) 添加值
weakSet.delete(value) 删除值

WeakSet 没有 size 属性,不可遍历(即没有keys()、values()和entries()方法),因为成员都是弱引用,随时都可能消失。WeakSet 也没有 clear() 方法。

WeakSet 和 Set 的区别

特性 Set WeakSet
成员类型 任意类型 仅对象和 Symbol
可枚举性 可遍历 不可遍历
引用类型 强引用 弱引用
自动清理

Map

Map 的定义和基本用法

Map 是 ES6 引入的新的数据结构,是一种键值对集合类型(Hash结构),使用 new 关键字创建 Map 结构实例

const m = new Map()

Map 构造函数可以接受一个数组作为参数,数组成员是一个个表示键值对的数组

const items = [
  ['name', '张三'],
  ['title', 'Author']
];

const map = new Map(items);

Map 的核心特性

键的灵活性

const map = new Map();

// 支持任意类型作为键
map.set(1, 'number');
map.set('1', 'string');
map.set({id: 1}, 'object');
map.set(() => {}, 'function');

console.log(map.size); // 4

键值相等性判断

Map 和 Set 一样使用 SameValueZero 算法:

  • NaN 被视为相等
  • +0 和 -0 被视为相等
  • 对象按引用比较
    const obj = {};
    map.set(obj, 'value');
    console.log(map.get(obj)); // 'value'
    console.log(map.get({})); // undefined (不同引用)

Map 的基础 API详解

方法/属性 描述
new Map(iterable) 构造函数
map.set(key, value) 添加/更新键值对
map.get(key) 获取对应值
map.has(key) 检查键是否存在
map.delete(key) 删除键值对
map.clear() 清空集合
map.size 获取键值对的数量(非函数属性,不能 set.size())

Map 的迭代操作

Map 结构原生提供三个遍历器生成函数和一个遍历方法

  • map.values():返回键值的遍历器
  • map.keys():返回键名的遍历器
  • map.entries():返回所有成员的遍历器
  • map.forEach():遍历 Map 所有成员

和 Set 一样,Map 的遍历顺序也是插入顺序,Map 的默认遍历器生成函数是它的 entries 方法

const map = new Map([
  ['name', 'Alice'],
  ['age', 30]
]);

// 1. for...of 遍历
for (const [key, value] of map) {
  console.log(key, value);
}

// 2. forEach 遍历
map.forEach((value, key) => {
  console.log(key, value);
});

// 3. 获取迭代器
const keys = map.keys();    // 键迭代器
const values = map.values();// 值迭代器
const entries = map.entries(); // 键值对迭代器

Map 的高级特性与应用

性能优势

// 与 Object 的性能对比
const obj = {};
const map = new Map();
const n = 1000000;

// 插入性能
console.time('Object insert');
for (let i = 0; i < n; i++) obj[i] = i;
console.timeEnd('Object insert');

console.time('Map insert');
for (let i = 0; i < n; i++) map.set(i, i);
console.timeEnd('Map insert');

// 查找性能
console.time('Object lookup');
obj[n/2];
console.timeEnd('Object lookup');

console.time('Map lookup');
map.get(n/2);
console.timeEnd('Map lookup');

对象元数据存储

// 更优雅的元数据管理
const metadata = new Map();

function setMetadata(obj, data) {
  metadata.set(obj, data);
}

function getMetadata(obj) {
  return metadata.get(obj);
}

// 避免直接修改对象属性

Map 与 Object 的对比

特性 Map Object
键类型 任意值 String/Symbol
键顺序 插入顺序 复杂规则(整数属性优先)
size 属性 直接获取 需要手动计算
迭代 直接可迭代 需要Object.keys()等转换
性能 频繁增删操作更优 静态键值访问略快
序列化 需手动处理 原生支持JSON序列化
原型链污染 完全隔离 可能被污染

WeakMap

WeakMap 的定义

WeakMap 和 Map 类似,也是用于生成键值对集合

WeakMap 的核心特性

弱引用键机制

let user = { id: 123 };
const weakmap = new WeakMap();

weakmap.set(user, 'user data');
console.log(weakmap.has(user)); // true

// 当对象失去所有强引用时
user = null;

// 垃圾回收后自动移除条目
// weakmap.has(原user) 将返回 false

键类型限制

const wm = new WeakMap();

// 仅允许对象(null 除外)和 Symbol作为键
wm.set({}, 'valid');
wm.set(document.body, 'valid');
wm.set(() => {}, 'valid');
wm.set(Symbol(), 2) // 不报错

// 原始值会报错
wm.set(1, 'invalid'); // TypeError: Invalid value used as weak map key
wm.set('key', 'invalid'); // TypeError
wm.set(null, 2) // TypeError

WeakMap 的 API 详解

方法/属性 描述
new WeakMap(iterable) 构造函数
weakMap.set(key, value) 添加/更新键值对
weakMap.get(key) 获取对应值
weakMap.has(key) 检查键是否存在
weakMap.delete(key) 删除键值对

weakMap 没有 size 属性,不可遍历(即没有keys()、values()和entries()方法),因为成员都是弱引用,随时都可能消失。WeakMap 也没有 clear() 方法

WeakMap 的特殊行为与边界情况

键不可枚举

const wm = new WeakMap();
const key = {};
wm.set(key, 'secret');

// 无法通过反射获取键
Object.getOwnPropertySymbols(key); // []
Reflect.ownKeys(key);             // []

垃圾回收不确定性

let obj = {};
wm.set(obj, 'data');
obj = null;

// 无法预测何时条目会被移除
// 依赖垃圾回收器运行

不可克隆

// 无法正确克隆WeakMap
const wm1 = new WeakMap();
wm1.set({}, 'value');

const wm2 = new WeakMap(wm1); // 无效!
// 没有方法可以复制现有WeakMap的内容

WeakMap 和相关数据结构的区别

特性 WeakMap Map WeakSet
键类型 仅对象 任意值 仅对象
值类型 任意值 任意值 无值(仅存储键)
可枚举性 不可枚举 可枚举 不可枚举
内存管理 弱引用键 强引用键值 弱引用键
使用场景 私有数据/元数据存储 通用键值存储 对象存在性检查

面试常见问题

Q:请解释 Map 和 Object 的主要区别是什么?

  • 键类型不同:Map 键可以是任意类型,Object 键只能是字符串或 Symbol 值
  • 顺序保证:Map 严格按照插入顺序迭代,Object 则是数字键升序排序后其他按插入顺序
  • 大小获取:Map 可以直接用 size 属性,Object 则需要 Object.keys(obj).length
  • 性能:Map 增删改查操作更高效
  • 序列化:Map 需要手动转为数组,Object 直接支持 JSON.stringify

Q:WeakMap 和 Map 的核心区别是什么?为什么要有 WeakMap?

核心区别:

  • 键类型:Map 键可以是任意类型,WeakMap 键只能是对象和 Symbol
  • 内存管理:WeakMap 键是弱引用,不阻止 GC 垃圾回收,Map 键是强引用
  • 可访问性:WeakMap 不可遍历,Map 可以遍历所有键值

为什么要有 WeakMap?

  • 防止内存泄露:当键对象不再使用时自动清除关联值
  • 私有数据存储:实现真正无法外部访问的私有属性

Q:为什么 WeakMap 的键必须是对象?

主要基于两个核心设计目标:

  • 内存管理:只有对象存在垃圾回收机制,原始值(字符串、数字等)在 JavaScript 中永生;弱引用机制仅对需要回收的对象有意义
  • 技术实现限制:引擎底层通过对象指针实现弱引用,若允许原始值会破坏弱引用设计初衷,导致永久占用内存

Q:Set 如何保证元素的唯一性?

通过 SameValueZero 算法和底层哈希表现,Set 内部使用哈希表结构:

  1. 添加值时,计算哈希值
  2. 检查哈希表中是否存在相同哈希值
  3. 对于哈希冲突,使用”Same-value-zero”算法进行精确比较
  4. 只有新值才会被添加到集合中

Q:WeakSet 为什么没有 size 属性和遍历方法?

弱引用设计约束:成员对象都是弱引用,随时可能被回收,计算 size 可能会得到非确定的结果,同样,遍历结果也不可靠

Q:为什么 Map 要保持插入顺序而 Object 不保证?

  • 设计目标差异:Map 是专门设计的键值集合,顺序是其核心特新要求,Object 不是
  • 历史兼容性:Object 的乱序行为是历史包袱,Map 没有历史包袱,直接规范顺序保证
  • 性能取舍:Map 用额外链表维护顺序(空间换时间),Object 为优化访问速度牺牲顺序

Q:WeakSet 在垃圾回收时的行为是怎样的?

  • 自动清理:当 WeakSet 中的成员失去引用时,GC 垃圾回收器会将该对象从 WeakSet 中移除
  • 不可观测性:无法直接观测到回收时机
  • 内存影响:不会阻止其键对象被回收

Q: 如何用 Object 模拟实现一个 Map?

核心实现思路:

(1)用 Symbol 保证唯一性

const _keys = Symbol('keys');
const _values = Symbol('values');

(2)基本结构

class SimpleMap {
  constructor() {
    this[_keys] = [];   // 存储键
    this[_values] = []; // 存储值
  }
}

(3)关键方法实现

set(key, value) {
  const index = this[_keys].indexOf(key);
  if (index === -1) {
    this[_keys].push(key);
    this[_values].push(value);
  } else {
    this[_values][index] = value;
  }
}

get(key) {
  const index = this[_keys].indexOf(key);
  return index !== -1 ? this[_values][index] : undefined;
}

Q: 请手写一个简化版的 WeakMap(不考虑弱引用)

class SimpleWeakMap {
  constructor() {
    this._id = `weakmap@${Date.now()}`; // 唯一标识符
  }

  set(key, value) {
    if (typeof key !== 'object' || key === null) {
      throw TypeError('Invalid key type');
    }
    key[this._id] = value; // 直接存储到对象上
  }

  get(key) {
    return key?.[this._id];
  }

  has(key) {
    return this._id in key;
  }

  delete(key) {
    if (!this.has(key)) return false;
    delete key[this._id];
    return true;
  }
}

// 使用示例
const map = new SimpleWeakMap();
const obj = {};
map.set(obj, 'data');
console.log(map.get(obj)); // 'data'
map.delete(obj);
console.log(map.has(obj)); // false

Q: 如何用数组实现 Set 的基本功能?

class ArraySet {
  constructor() {
    this._items = [];
  }

  add(value) {
    if (!this.has(value)) {
      this._items.push(value);
    }
    return this;
  }

  has(value) {
    return this._items.includes(value); // 或 indexOf(value) !== -1
  }

  delete(value) {
    const index = this._items.indexOf(value);
    if (index > -1) {
      this._items.splice(index, 1);
      return true;
    }
    return false;
  }

  get size() {
    return this._items.length;
  }

  clear() {
    this._items = [];
  }
}

// 使用示例
const set = new ArraySet();
set.add(1).add(2).add(1);
console.log(set.size); // 2
console.log(set.has(1)); // true
set.delete(1);
console.log(set.has(1)); // false

Q: 模拟实现一个支持基本操作的 WeakSet

class SimpleWeakSet {
  constructor() {
    this._id = `weakset@${Date.now()}`; // 唯一标识符
  }

  add(obj) {
    if (typeof obj !== 'object' || obj === null) {
      throw TypeError('Invalid value used in weak set');
    }
    obj[this._id] = true; // 标记对象
  }

  has(obj) {
    return !!obj && obj[this._id] === true;
  }

  delete(obj) {
    if (!this.has(obj)) return false;
    delete obj[this._id];
    return true;
  }
}

// 使用示例
const ws = new SimpleWeakSet();
const obj = {};
ws.add(obj);
console.log(ws.has(obj)); // true
ws.delete(obj);
console.log(ws.has(obj)); // false

Proxy 和 Reflect

Proxy 详解

什么是Proxy?

Proxy 是 ES6 引入的一种元编程特性,允许你创建一个对象的代理,从而拦截和自定义该对象的基本操作(如属性访问、赋值、枚举等),Proxy 提供了一种强大的机制来控制和扩展对象的行为。

基本语法

const proxy = new Proxy(target, handler);
  • target:要代理的目标对象
  • handler:包含自定义操作的对象,handler 为空时,没有任何拦截效果,访问 Proxy 等效于访问 target

Proxy 是 ES6 原生提供的构造函数,Proxy 实例也可以用作其他对象的原型对象。需要注意的是:要想 Proxy 起作用,必须针对 Proxy 的实例(proxy)进行操作,而不是针对目标对象(target)进行操作。

常用Handler方法(Traps)

get() - 拦截属性读取

const handler = {
  get(target, property, receiver) {
    console.log(`读取属性: ${property}`);
    return Reflect.get(...arguments);
  }
};

const proxy = new Proxy({ name: 'Alice' }, handler);
console.log(proxy.name); // 输出: 读取属性: name → Alice

set() - 拦截属性设置

const handler = {
  set(target, property, value, receiver) {
    console.log(`设置属性: ${property} = ${value}`);
    return Reflect.set(...arguments);
  }
};

const proxy = new Proxy({}, handler);
proxy.age = 30; // 输出: 设置属性: age = 30

apply() - 拦截函数调用

const handler = {
  apply(target, thisArg, argumentsList) {
    console.log(`调用函数,参数: ${argumentsList}`);
    return target(...argumentsList) * 2;
  }
};

function sum(a, b) { return a + b; }

const proxy = new Proxy(sum, handler);
console.log(proxy(2, 3)); // 输出: 调用函数,参数: 2,3 → 10

has() - 拦截 in 操作符

const handler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // 隐藏私有属性
    }
    return property in target;
  }
};

const obj = { name: 'Bob', _secret: '123' };
const proxy = new Proxy(obj, handler);

console.log('name' in proxy); // true
console.log('_secret' in proxy); // false

construct() - 拦截 new 操作符

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

const handler = {
  construct(target, argumentsList, newTarget) {
    console.log(`创建实例: ${argumentsList}`);
    return new target(...argumentsList);
  }
};

const ProxyPerson = new Proxy(Person, handler);
const p = new ProxyPerson('Charlie'); // 输出: 创建实例: Charlie

完整 Handler 方法列表

方法 描述 触发操作
get(target, property, receiver) 拦截属性读取 proxy.property, proxy[‘property’]
set(target, property, value, receiver) 拦截属性设置 proxy.property = value
has(target, property) 拦截 in 操作符 property in proxy
apply(target, object, args) 拦截函数调用 proxy(…args), proxy.call(), proxy.apply()
construct(target, args) 拦截 new 操作符 new proxy(…args)
deleteProperty(target, property) 拦截 delete 操作符 delete proxy.property
ownKeys(target) 拦截对象属性枚举 Object.keys(proxy), Object.getOwnPropertyNames(proxy)
getOwnPropertyDescriptor(target, property) 拦截属性描述符获取 Object.getOwnPropertyDescriptor(proxy, property)
defineProperty(target, property, propDesc) 拦截属性定义 Object.defineProperty(proxy, property, descriptor)
getPrototypeOf(target) 拦截原型获取 Object.getPrototypeOf(proxy)
setPrototypeOf(target, proto) 拦截原型设置 Object.setPrototypeOf(proxy, prototype)
isExtensible(target) 拦截对象可扩展性检查 Object.isExtensible(proxy)
preventExtensions(target) 拦截阻止扩展操作 Object.preventExtensions(proxy)

Proxy 和 Object.defineProperty 的关系

核心概念对比

特性 Object.defineProperty Proxy
引入时间 ES5 (2009) ES6 (2015)
主要目的 定义/修改对象属性的特性 创建对象的代理,拦截基本操作
操作级别 属性级别 对象级别
拦截能力 有限(主要是 get/set) 全面(13种操作)
新增属性 无法自动捕获 可以自动捕获
返回值 修改后的对象 新创建的代理对象

功能关系详解

(1)相似之处:属性访问拦截

// 使用 Object.defineProperty
const obj1 = {};
let value = 10;

Object.defineProperty(obj1, 'count', {
  get() {
    console.log('获取 count');
    return value;
  },
  set(newValue) {
    console.log('设置 count');
    value = newValue;
  }
});

obj1.count; // 控制台: "获取 count"
obj1.count = 20; // 控制台: "设置 count"

// 使用 Proxy
const target = {};
const proxy = new Proxy(target, {
  get(target, prop) {
    console.log(`获取 ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`设置 ${prop}${value}`);
    target[prop] = value;
    return true;
  }
});

proxy.name; // 控制台: "获取 name"
proxy.age = 30; // 控制台: "设置 age 为 30"

(2)互补关系:Proxy 扩展了 defineProperty 的能力

核心区别

  • 拦截范围不同:defineProperty 只能拦截属性的读写操作(属性级别),无法拦截属性新增、删除等操作,Proxy 可以拦截整个对象的多种操作(对象级别)
  • Proxy 返回修对象,defineProperty 修改原对象

this 问题

Proxy 代理目标对象时,目标对象内部的 this 会指向 Proxy 代理

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

此外,有些原生对象的内部属性,只有通过正确的 this 才能拿到,所以 Proxy 无法代理这些原生对象的属性

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

实际应用场景

数据验证

const validator = {
  set(target, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('年龄必须是整数');
      }
      if (value < 0 || value > 150) {
        throw new RangeError('年龄必须在0-150之间');
      }
    }
    return Reflect.set(...arguments);
  }
};

const person = new Proxy({}, validator);
person.age = 30; // 正常
person.age = 'thirty'; // 抛出类型错误

自动格式化

const formatter = {
  get(target, prop) {
    const value = Reflect.get(...arguments);
    return typeof value === 'number' ? `$${value.toFixed(2)}` : value;
  }
};

const prices = new Proxy({ apple: 1.2, banana: 0.8 }, formatter);
console.log(prices.apple); // $1.20
console.log(prices.banana); // $0.80

自动填充对象

const autoFill = {
  get(target, prop) {
    if (!(prop in target)) {
      target[prop] = new Proxy({}, autoFill);
    }
    return target[prop];
  }
};

const obj = new Proxy({}, autoFill);
obj.a.b.c = 'value';
console.log(obj.a.b.c); // 'value'

函数节流

const throttle = (fn, delay) => {
  let lastCall = 0;
  return new Proxy(fn, {
    apply(target, thisArg, args) {
      const now = Date.now();
      if (now - lastCall < delay) {
        console.log('调用太频繁,被节流');
        return;
      }
      lastCall = now;
      return Reflect.apply(target, thisArg, args);
    }
  });
};

const expensiveFn = () => console.log('执行操作');
const throttledFn = throttle(expensiveFn, 1000);

// 测试
throttledFn(); // 执行操作
throttledFn(); // 调用太频繁,被节流
setTimeout(throttledFn, 1100); // 执行操作

注意事项

  1. Reflect API:通常与 Reflect 对象配合使用,确保正确的 this 绑定
  2. 性能影响:Proxy 操作比直接访问属性慢,避免在性能关键路径上过度使用
  3. 目标对象不可变:Proxy 代理不会改变目标对象本身
  4. this 绑定:Proxy 内部方法中的 this 指向 handler 对象
  5. 可撤销代理:可以使用 Proxy.revocable() 创建可撤销的代理
    const { proxy, revoke } = Proxy.revocable({}, {});
    revoke(); // 此后对代理的任何操作都会抛出错误

Reflect 详解

什么是Reflect?

Reflect 是 ES6 引入的一个内置对象,它提供了一组静态方法用于操作对象,这些方法与 Proxy 的拦截方法一一对应。Reflect 的设计目的是为了:

(1)将一些明显属于语言内部的方法转移到 JavaScript 代码层,比如 Object.defineProperty 未来只能通过 Reflect 对象访问

(2)提供更合理的返回值(用布尔值代替抛出异常)

// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

(3)统一操作对象的函数式 API,将对象操作都变成函数式行为

// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true

(4)为 Proxy 提供默认行为的基础方法,让 Proxy 对象可以更方便的调用对应的 Reflect 方法,完成默认行为

var obj = {}
var obj2 = {}
var proxyHandler = {
  set(target, property, value) {
    // todo
  }
}
var reflectHandler = {
  set(target, property, value) {
    return Reflect.set(...arguments)
  }
}
var proxyObj = new Proxy(obj, proxyHandler)
var reflectObj = new Proxy(obj2, reflectHandler)

proxyObj.a = 1
reflectObj.a = 1
console.log(proxyObj) // {}
console.log(reflectObj) // {a: 1}

Reflect 核心方法详解

Reflect.get(target, propertyKey[, receiver])

  • 获取对象属性的值
  • 等同于 target[propertyKey]
  • receiver 参数可以改变 getter 中的 this 指向
    const obj = { x: 1, y: 2 };
    console.log(Reflect.get(obj, 'x')); // 1
    
    // 改变 getter 中的 this
    const obj2 = {
      foo: 1,
      get bar() {
        return this.foo;
      }
    };
    const receiver = { foo: 2 };
    console.log(Reflect.get(obj2, 'bar', receiver)); // 2

Reflect.set(target, propertyKey, value[, receiver])

  • 设置对象属性的值
  • 等同于 target[propertyKey] = value
  • 返回布尔值表示是否设置成功
    const obj = {};
    Reflect.set(obj, 'name', 'Alice');
    console.log(obj.name); // 'Alice'
    
    // 改变 setter 中的 this
    const obj2 = {
      set prop(value) {
        return this.bar = value;
      }
    };
    const receiver = {};
    Reflect.set(obj2, 'prop', 10, receiver);
    console.log(receiver.bar); // 10

Reflect.has(target, propertyKey)

  • 判断对象是否包含某属性
  • 等同于 propertyKey in target
    const obj = { name: 'Bob' };
    console.log(Reflect.has(obj, 'name')); // true
    console.log(Reflect.has(obj, 'toString')); // true (继承属性)

Reflect.deleteProperty(target, propertyKey)

  • 删除对象的某属性
  • 等同于 delete target[propertyKey]
  • 返回布尔值表示是否删除成功
    const obj = { x: 1, y: 2 };
    Reflect.deleteProperty(obj, 'x');
    console.log(obj); // { y: 2 }

Reflect.construct(target, argumentsList[, newTarget])

  • 调用构造函数创建实例
  • 等同于 new target(…args)
    class Person {
      constructor(name) {
        this.name = name;
      }
    }
    
    const p = Reflect.construct(Person, ['Alice']);
    console.log(p.name); // 'Alice'

Reflect.apply(target, thisArgument, argumentsList)

  • 调用函数
  • 等同于 Function.prototype.apply.call(target, thisArgument, argumentsList)
    function greet(name) {
      return `Hello, ${name}!`;
    }
    
    const result = Reflect.apply(greet, null, ['Alice']);
    console.log(result); // 'Hello, Alice!'

Reflect.defineProperty(target, propertyKey, attributes)

  • 定义或修改对象属性
  • 等同于 Object.defineProperty()
  • 返回布尔值表示是否成功
    const obj = {};
    const success = Reflect.defineProperty(obj, 'name', {
      value: 'Bob',
      writable: true,
      enumerable: true,
      configurable: true
    });
    console.log(success); // true
    console.log(obj.name); // 'Bob'

Reflect.getOwnPropertyDescriptor(target, propertyKey)

  • 获取属性描述符
  • 等同于 Object.getOwnPropertyDescriptor()
    const obj = { name: 'Alice' };
    const desc = Reflect.getOwnPropertyDescriptor(obj, 'name');
    console.log(desc.value); // 'Alice'
    console.log(desc.enumerable); // true

Reflect.getPrototypeOf(target)

  • 获取对象的原型对象
  • 等同于 Object.getPrototypeOf()
    const obj = {};
    console.log(Reflect.getPrototypeOf(obj) === Object.prototype); // true

Reflect.setPrototypeOf(target, prototype)

  • 设置对象的原型对象
  • 等同于 Object.setPrototypeOf()
  • 返回布尔值表示是否设置成功
    const obj = {};
    const proto = { foo: 'bar' };
    Reflect.setPrototypeOf(obj, proto);
    console.log(obj.foo); // 'bar'

Reflect.isExtensible(target)

  • 判断对象是否可扩展
  • 等同于 Object.isExtensible()
    const obj = {};
    console.log(Reflect.isExtensible(obj)); // true
    Object.preventExtensions(obj);
    console.log(Reflect.isExtensible(obj)); // false

Reflect.preventExtensions(target)

  • 阻止对象扩展
  • 等同于 Object.preventExtensions()
  • 返回布尔值表示是否成功
    const obj = { x: 1 };
    Reflect.preventExtensions(obj);
    obj.y = 2; // 静默失败或严格模式下报错
    console.log(obj); // { x: 1 }

Reflect.ownKeys(target)

  • 获取对象所有自有属性键(包括 Symbol 和不可枚举属性)
  • 等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
    const obj = {
      [Symbol('id')]: 123,
      name: 'Alice',
      age: 30
    };
    
    Object.defineProperty(obj, 'hidden', {
      value: 'secret',
      enumerable: false
    });
    
    console.log(Reflect.ownKeys(obj)); 
    // ['name', 'age', 'hidden', Symbol(id)]

为什么使用 Reflect?

除了上面讲过的 Reflect 的几个设计目的外,还包括下面的原因:

支持 receiver 参数

Reflect 的 get/set 方法支持 receiver 参数,可改变 getter/setter 中的 this 指向:

const obj = {
  _value: 0,
  get value() {
    return this._value;
  }
};

const receiver = { _value: 42 };
console.log(Reflect.get(obj, 'value', receiver)); // 42

Reflect和Object方法的区别

操作 Object 方法 Reflect 方法 主要区别
获取属性描述符 getOwnPropertyDescriptor getOwnPropertyDescriptor 相同
定义属性 defineProperty defineProperty Reflect 返回布尔值而非对象
获取原型 getPrototypeOf getPrototypeOf Reflect 参数非对象会抛出错误
设置原型 setPrototypeOf setPrototypeOf Reflect 返回布尔值
扩展性检查 isExtensible isExtensible Reflect 参数非对象会抛出错误
阻止扩展 preventExtensions preventExtensions Reflect 返回布尔值
属性枚举 keys + getOwnPropertyNames ownKeys Reflect 返回所有键,包括不可枚举和 Symbol
函数调用 - apply 无直接对应方法
构造函数调用 - construct 无直接对应方法
属性存在检查 - has 对应 in 操作符
属性删除 - deleteProperty 对应 delete 操作符

Promise、Iterator/Generator、Async/Await

Promise:异步编程的基石

核心概念

  • 状态机:Promise 是一个状态机,包含三种状态:
    • Pending(进行中)
    • Fulfilled(已成功)
    • Rejected(已失败)
  • 不可逆性:状态一旦改变(从 Pending → Fulfilled 或 Pending → Rejected)就不可逆转,会一直保持这个结果,这时就称为 resolved(已定型)

基本用法

const promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    const success = Math.random() > 0.5;
    success ? resolve('操作成功') : reject(new Error('操作失败'));
  }, 1000);
});

promise
  .then(result => console.log(result))
  .catch(error => console.error(error))
  .finally(() => console.log('无论成功失败都会执行'));

Promise 静态方法

方法 描述 示例
Promise.resolve() 创建已解决的 Promise Promise.resolve(42)
Promise.reject() 创建已拒绝的 Promise Promise.reject(new Error())
Promise.all() 所有 Promise 成功时才成功 Promise.all([p1, p2])
Promise.race() 第一个完成的 Promise 决定结果 Promise.race([p1, p2])
Promise.allSettled() 所有 Promise 完成后返回结果 Promise.allSettled([p1, p2])
Promise.any() 任一 Promise 成功即成功 Promise.any([p1, p2])

Promise 链式调用

fetch('/api/data')
  .then(response => {
    if (!response.ok) throw new Error('网络错误');
    return response.json();
  })
  .then(data => processData(data))
  .then(processed => saveData(processed))
  .catch(error => handleError(error));

实现原理

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = null;
    this.reason = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      const handleFulfilled = () => {
        try {
          typeof onFulfilled === 'function'
            ? resolve(onFulfilled(this.value))
            : resolve(this.value);
        } catch (error) {
          reject(error);
        }
      };

      const handleRejected = () => {
        try {
          typeof onRejected === 'function'
            ? resolve(onRejected(this.reason))
            : reject(this.reason);
        } catch (error) {
          reject(error);
        }
      };

      if (this.state === 'fulfilled') {
        setTimeout(handleFulfilled, 0);
      } else if (this.state === 'rejected') {
        setTimeout(handleRejected, 0);
      } else {
        this.onFulfilledCallbacks.push(() => setTimeout(handleFulfilled, 0));
        this.onRejectedCallbacks.push(() => setTimeout(handleRejected, 0));
      }
    });
  }
}

Iterator 和 Generator:可迭代协议与生成器

Iterator(迭代器)

  • 迭代器协议:任何实现了 next() 方法的对象
    • next() 返回 { value, done } 对象
  • 可迭代协议:实现了 Symbol.iterator 方法的对象
    // 自定义迭代器
    const counter = {
      [Symbol.iterator]() {
        let count = 1;
        return {
          next() {
            return count <= 5 
              ? { value: count++, done: false } 
              : { done: true };
          }
        };
      }
    };
    
    // 使用迭代器
    for (const num of counter) {
      console.log(num); // 1, 2, 3, 4, 5
    }

Generator(生成器)

  • 函数生成器:使用 function* 定义的生成器函数
  • 执行控制:通过 yield 暂停执行,通过 next() 恢复执行
    function* idGenerator() {
      let id = 1;
      while (true) {
        const reset = yield id++;
        if (reset) id = 1;
      }
    }
    
    const gen = idGenerator();
    console.log(gen.next().value); // 1
    console.log(gen.next().value); // 2
    console.log(gen.next(true).value); // 1(重置)

高级生成器特性

(1) 双向通信

function* twoWayGenerator() {
  const name = yield 'What is your name?';
  yield `Hello, ${name}!`;
}

const gen = twoWayGenerator();
console.log(gen.next().value); // "What is your name?"
console.log(gen.next('Alice').value); // "Hello, Alice!"

(2) 错误处理

function* errorGenerator() {
  try {
    yield 'Start';
    throw new Error('Something wrong');
  } catch (e) {
    yield `Caught: ${e.message}`;
  }
}

const gen = errorGenerator();
console.log(gen.next().value); // "Start"
console.log(gen.next().value); // "Caught: Something wrong"

Async/Await:异步编程的终极解决方案

本质与原理

  • 语法糖:async/await 是 Generator + Promise 的语法糖
  • 执行过程:
    • async 函数返回一个 Promise
    • await 等待 Promise 解析(暂停执行但不阻塞主线程)
    • 通过隐式的生成器控制执行流程

基本用法

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) throw new Error('用户数据获取失败');
    
    const userData = await response.json();
    const posts = await fetch(`/api/posts?userId=${userId}`).then(r => r.json());
    
    return { ...userData, posts };
  } catch (error) {
    console.error('数据加载失败:', error);
    throw error;
  }
}

// 使用
fetchUserData(123)
  .then(data => console.log(data))
  .catch(error => console.error(error));

实现原理(基于 Generator)

function asyncGenerator(generatorFunc) {
  return function(...args) {
    const gen = generatorFunc.apply(this, args);
    
    return new Promise((resolve, reject) => {
      function step(key, arg) {
        let result;
        try {
          result = gen[key](arg);
        } catch (error) {
          return reject(error);
        }
        
        const { value, done } = result;
        if (done) {
          return resolve(value);
        }
        
        return Promise.resolve(value).then(
          val => step('next', val),
          err => step('throw', err)
        );
      }
      
      step('next');
    });
  };
}

// 使用
const myAsync = asyncGenerator(function* () {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
});

myAsync().then(console.log); // 3

最佳实践

(1) 并行优化

// 顺序执行(慢)
async function sequentialFetch() {
  const user = await fetch('/user');
  const posts = await fetch('/posts');
  return { user, posts };
}

// 并行执行(快)
async function parallelFetch() {
  const [user, posts] = await Promise.all([
    fetch('/user'),
    fetch('/posts')
  ]);
  return { user, posts };
}

(2) 循环中的 await

// 错误方式(顺序执行)
async function processArray(array) {
  for (const item of array) {
    await processItem(item); // 每次循环都等待
  }
}

// 正确方式(并行处理)
async function processArrayFast(array) {
  await Promise.all(array.map(item => processItem(item)));
}

(3) 错误处理模式

// 方式1:try/catch
async function tryCatchExample() {
  try {
    const result = await asyncOperation();
    return result;
  } catch (error) {
    handleError(error);
  }
}

// 方式2:Promise.catch
async function catchExample() {
  const result = await asyncOperation().catch(handleError);
  return result;
}

// 方式3:高阶函数封装
function asyncHandler(promise) {
  return promise.then(data => [null, data]).catch(err => [err]);
}

async function handlerExample() {
  const [error, data] = await asyncHandler(asyncOperation());
  if (error) handleError(error);
  return data;
}

面试常见问题

Q:Promise 和 Async/Await 的主要区别是什么?

  • Promise 是对象,async/await 是语法
  • async/await 使异步代码看起来像同步代码
  • async/await 的错误处理使用 try/catch 更直观
  • async/await 更容易实现复杂的控制流

Q:Generator 函数在异步编程中的角色是什么?

  • 提供暂停和恢复执行的能力
  • 是 async/await 的实现基础
  • 适合实现自定义迭代逻辑
  • 可用于实现协程(coroutine)和状态机

Q:如何处理多个异步操作的错误?

async function handleMultiple() {
  try {
    const [a, b] = await Promise.all([
      taskA().catch(e => ({ error: e })),
      taskB().catch(e => ({ error: e }))
    ]);
    
    if (a.error) console.error('A失败:', a.error);
    if (b.error) console.error('B失败:', b.error);
    
    if (!a.error && !b.error) {
      console.log('全部成功:', a, b);
    }
  } catch (error) {
    console.error('全局错误:', error);
  }
}

Q:如何取消一个正在进行的异步操作

在异步操作结果响应之前主动 reject 掉(setTimeout)

function cancellableAsync(task) {
  let cancel;
  const promise = new Promise((resolve, reject) => {
    cancel = () => reject(new Error('操作取消'));
    task(resolve, reject);
  });
  return { promise, cancel };
}

// 使用
const { promise, cancel } = cancellableAsync((resolve, reject) => {
  setTimeout(() => resolve('完成'), 3000);
});

promise
  .then(console.log)
  .catch(e => console.log(e.message)); // "操作取消"

// 2秒后取消
setTimeout(cancel, 2000);