JavaScript-重新学习-20260407

3 阅读8分钟

JavaScript

一、数据类型与类型检测

1. JavaScript 的数据类型及检测方法

问题:JavaScript 数据类型有哪些?基本数据类型有哪些?值类型与引用类型的区别是什么?JavaScript 数据类型检测方法有哪些?typeof 运算符返回值有哪些?typeof 与 instanceof 的区别及适用场景?

答案

JavaScript 数据类型(共 8 种):

  • 基本类型(值类型):Number、String、Boolean、Undefined、Null、Symbol(ES6)、BigInt(ES2020)
  • 引用类型:Object(包含 Array、Function、Date、RegExp、Map、Set、WeakMap、WeakSet 等)

值类型 vs 引用类型对比表

对比项值类型引用类型
存储位置栈内存堆内存,栈中存储引用地址
复制行为复制值本身复制引用地址
比较方式比较值是否相等比较引用地址是否相同
可变性不可变(值本身)可变(对象属性可变)

数据类型检测方法

  1. typeof:返回类型字符串,能识别基本类型(除 null 外)和 function

    typeof 42; // "number"
    typeof 'hello'; // "string"
    typeof true; // "boolean"
    typeof undefined; // "undefined"
    typeof null; // "object"(历史遗留问题)
    typeof {}; // "object"
    typeof []; // "object"
    typeof function() {}; // "function"
    typeof Symbol(); // "symbol"
    typeof 10n; // "bigint"
    
  2. instanceof:检测对象是否为某个构造函数的实例

    [] instanceof Array; // true
    ({}) instanceof Object; // true
    function() {} instanceof Function; // true
    
  3. Object.prototype.toString.call():最准确的类型检测方法

    Object.prototype.toString.call(42); // "[object Number]"
    Object.prototype.toString.call('hello'); // "[object String]"
    Object.prototype.toString.call(null); // "[object Null]"
    Object.prototype.toString.call(undefined); // "[object Undefined]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call({}); // "[object Object]"
    Object.prototype.toString.call(function() {}); // "[object Function]"
    
  4. Array.isArray():判断是否为数组

    Array.isArray([]); // true
    Array.isArray({}); // false
    

typeof vs instanceof 对比表

特性typeofinstanceof
作用判断基本类型和函数判断对象是否为某个类的实例
返回值字符串布尔值
对 null 的判断"object"(错误)false(正确)
对数组的判断"object"(错误)true(正确)
适用场景基本类型检测、函数检测对象类型检测、继承关系检测

补充说明

  • typeof null === "object" 是 JavaScript 设计初期的历史遗留错误
  • instanceof 通过原型链查找,跨 iframe 或跨 realm 时会失效
  • 推荐使用 Object.prototype.toString.call() 进行精确类型判断
  • 基本类型值不可变,对值的修改会创建新值
  • 引用类型变量存储的是堆内存地址,复制时复制的是地址

二、类型转换

2. 类型转换规则与方法

问题:数据类型转换方式有哪些?JavaScript 隐式类型转换规则是什么?显式类型转换的方法有哪些?

答案

隐式类型转换(自动转换)触发场景

  1. 算术运算+-*/%
  2. 比较运算==!=<><=>=
  3. 逻辑运算!&&||
  4. 条件判断ifwhilefor 条件表达式
  5. 属性访问obj[prop] 中的 prop 如果不是字符串会被转成字符串

常用显式转换方法

// 1. 转为数字
Number('123'); // 123
parseInt('123px', 10); // 123(推荐始终指定基数)
parseFloat('3.14px'); // 3.14
+'123'; // 123(一元加操作符)
~~'123'; // 123(双按位非)

// 2. 转为字符串
String(123); // "123"
(123).toString(); // "123"
123 + ''; // "123"
`${123}`; // "123"

// 3. 转为布尔值
Boolean(0); // false
!!'hello'; // true(双非操作符)

特殊值的转换规则

// 空值转换
Number(''); // 0
Number('   '); // 0
Number(null); // 0
Number(undefined); // NaN
Boolean(null); // false
Boolean(undefined); // false
Boolean(''); // false
Boolean(0); // false
Boolean(NaN); // false
Boolean([]); // true(所有对象都为 true)
Boolean({}); // true

补充说明

  • parseIntparseFloat 会从字符串开头解析数字直到遇到非数字字符
  • 使用 Number() 转换空字符串或空白字符串会得到 0,而 parseInt 会得到 NaN
  • 数组转数字:空数组为 0,单个元素的数组看元素值,多个元素的数组为 NaN
  • 对象转数字会先调用 valueOf() 方法,如果没有返回基本类型再调用 toString()

三、空值与特殊值

3. null、undefined 与 NaN

问题nullundefined 的区别是什么?NaN 是什么?如何检测?isNaN() 的作用及实现原理?

答案

null vs undefined 对比表

特性nullundefined
含义表示"空值",表示一个对象指针为空表示"未定义",变量声明但未赋值
类型objectundefined
转换为数字0NaN
使用场景主动赋空值,如对象初始化变量未赋值、函数无返回值、访问对象不存在的属性
全等比较null === null 为 trueundefined === undefined 为 true
历史渊源设计缺陷导致 typeof null === "object"变量声明默认值

NaN(Not a Number)特性

  • NaN 是 Number 类型的一个特殊值,表示非数字
  • NaN 与任何值(包括自身)比较都返回 false:NaN === NaN 为 false
  • NaN 参与任何数学运算结果都是 NaN

检测 NaN 的方法

  1. isNaN() 全局函数:先尝试转换为数字,再判断是否为 NaN

    isNaN(NaN); // true
    isNaN('123'); // false(字符串"123"可转为数字123)
    isNaN('hello'); // true(字符串"hello"转数字为NaN)
    
  2. Number.isNaN()(ES6):严格判断,只有值确实是 NaN 才返回 true

    Number.isNaN(NaN); // true
    Number.isNaN('hello'); // false(字符串不是NaN)
    Number.isNaN(123); // false
    
  3. 利用 NaN 不等于自身的特性

    function isNaN(value) {
      return value !== value;
    }
    

补充说明

  • 推荐使用 Number.isNaN() 而不是全局的 isNaN(),避免隐式转换带来的误判
  • null == undefined 为 true,但 null === undefined 为 false
  • 判断变量是否为 undefined 时,使用 typeof variable === "undefined" 可避免变量未声明时报错

四、相等性比较

4. ===== 的区别

问题===== 的区别及使用场景?

答案

对比表

操作符名称比较方式是否进行类型转换
==抽象相等值相等是,进行隐式类型转换
===严格相等值和类型都相等否,不进行类型转换

== 的类型转换规则

  1. 类型相同:直接比较值
  2. 类型不同
    • null 和 undefined 相等:null == undefined 为 true
    • 字符串和数字:将字符串转为数字再比较
    • 布尔值和其他类型:将布尔值转为数字(true→1,false→0)再比较
    • 对象和基本类型:调用对象的 valueOf()toString() 方法转为基本类型后比较

示例

'' == 0; // true(空字符串转数字为0)
'0' == 0; // true(字符串"0"转数字为0)
false == 0; // true(false转数字为0)
true == 1; // true(true转数字为1)
null == undefined; // true
[] == 0; // true(空数组转字符串为"",再转数字为0)
[1] == 1; // true(数组[1]转字符串为"1",再转数字为1)

推荐使用场景

  • 始终使用 ===:避免隐式转换带来的意外结果
  • 唯一使用 == 的情况:判断变量是否为 null 或 undefined 时可以使用 value == null(等价于 value === null || value === undefined

补充说明

  • 严格相等 === 不仅检查值相等,还检查类型相同,更安全可预测
  • 对象的比较是比较引用地址,不是比较内容
  • 使用 Object.is()(ES6)可以处理 NaN === NaN 为 false 和 -0 === +0 为 true 的特殊情况

五、作用域与作用域链

5. 作用域、作用域链与变量提升

问题:JavaScript 作用域类型有哪些?什么是作用域和作用域链?什么是变量提升?

答案

作用域类型

  1. 全局作用域:在函数外部或代码块外部声明的变量
  2. 函数作用域:在函数内部声明的变量(ES5)
  3. 块级作用域:在 {} 内声明的变量,使用 letconst(ES6)

作用域链

  • 函数在定义时就会创建自己的作用域链,包含自身的作用域和所有父级作用域
  • 查找变量时,从当前作用域开始,逐级向上查找,直到全局作用域
  • 作用域链在函数定义时确定,与调用位置无关(词法作用域)

变量提升(Hoisting)

  • 变量声明提升:使用 var 声明的变量会被提升到作用域顶部,但赋值不提升
  • 函数声明提升:函数声明整体提升到作用域顶部
  • let/const 提升:也存在提升,但存在暂时性死区(TDZ),在声明前访问会报错

代码示例

// 变量提升示例
console.log(a); // undefined(变量声明提升)
var a = 10;

// 相当于
var a;
console.log(a); // undefined
a = 10;

// 函数提升示例
foo(); // "Hello"
function foo() {
  console.log('Hello');
}

// let/const 暂时性死区
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;

补充说明

  • 使用 letconst 可以避免变量提升带来的问题
  • 函数表达式不会被提升,只有函数声明会被提升
  • 优先使用 letconst 替代 var,利用块级作用域减少错误

六、函数相关概念

6. 函数声明与表达式、arguments、this 指向

问题:函数声明与函数表达式的区别?arguments 对象是什么?JavaScript 中 this 的指向规则?

答案

函数声明 vs 函数表达式对比表

特性函数声明函数表达式
语法function fn() {}const fn = function() {}
提升整体提升(函数名和函数体)变量声明提升,函数体不提升
命名必须有函数名可以是匿名函数或有名函数
使用场景通用函数定义回调函数、IIFE、赋值给变量

arguments 对象

  • 函数内部可用的类数组对象,包含调用时传入的所有参数
  • length 属性,可通过索引访问:arguments[0]arguments[1]
  • 不是真正的数组,但有 callee 属性指向函数自身
  • ES6 中可使用剩余参数 ...args 替代

this 指向规则(优先级从高到低):

  1. new 绑定:通过 new 调用构造函数,this 指向新创建的对象
  2. 显式绑定:通过 callapplybind 指定 this
  3. 隐式绑定:作为对象方法调用,this 指向调用对象
  4. 默认绑定:普通函数调用,非严格模式指向 window/global,严格模式为 undefined
  5. 箭头函数:没有自己的 this,继承外层作用域的 this

代码示例

// 1. new 绑定
function Person(name) {
  this.name = name;
}
const p = new Person('Alice'); // this 指向 p

// 2. 显式绑定
function sayHello() {
  console.log(`Hello, ${this.name}`);
}
const obj = { name: 'Bob' };
sayHello.call(obj); // this 指向 obj

// 3. 隐式绑定
const user = {
  name: 'Charlie',
  greet() {
    console.log(`Hi, ${this.name}`);
  }
};
user.greet(); // this 指向 user

// 4. 默认绑定
function show() {
  console.log(this); // 严格模式:undefined,非严格模式:window
}
show();

// 5. 箭头函数
const outer = {
  name: 'David',
  inner: () => {
    console.log(this.name); // this 指向外层作用域的 this(可能是 window)
  }
};

补充说明

  • 箭头函数的 this 在定义时确定,不会因调用方式改变
  • 使用 bind 会创建新函数,this 永久绑定,无法再次修改
  • 事件处理函数中的 this 通常指向触发事件的 DOM 元素

七、函数方法

7. callapplybind 的区别

问题callapplybind 的区别及使用场景?

答案

对比表

方法参数传递返回值执行时机使用场景
call参数逐个传递函数执行结果立即执行借用方法、改变 this 指向
apply参数以数组传递函数执行结果立即执行参数数量不确定、传递数组
bind参数逐个传递返回新函数延迟执行事件回调、预设参数

代码示例

function introduce(age, city) {
  console.log(`我叫${this.name}${age}岁,来自${city}`);
}

const person = { name: '张三' };

// 1. call - 立即执行,参数逐个传递
introduce.call(person, 25, '北京');

// 2. apply - 立即执行,参数数组传递
introduce.apply(person, [25, '北京']);

// 3. bind - 返回新函数,延迟执行
const boundFunction = introduce.bind(person, 25);
boundFunction('北京'); // 执行:我叫张三,25岁,来自北京

// bind 预设参数(局部应用)
const introduceFromBeijing = introduce.bind(null, 25, '北京');
introduceFromBeijing.call(person); // 我叫张三,25岁,来自北京

手写实现

// 手写 call
Function.prototype.myCall = function(context = window, ...args) {
  context.fn = this; // this 指向调用 myCall 的函数
  const result = context.fn(...args);
  delete context.fn;
  return result;
};

// 手写 apply
Function.prototype.myApply = function(context = window, args = []) {
  context.fn = this;
  const result = context.fn(...args);
  delete context.fn;
  return result;
};

// 手写 bind
Function.prototype.myBind = function(context, ...presetArgs) {
  const fn = this;
  return function(...args) {
    return fn.call(context, ...presetArgs, ...args);
  };
};

补充说明

  • apply 在传递数组参数时比 call 更方便,如 Math.max.apply(null, [1, 2, 3])
  • bind 常用于事件处理、定时器等需要保持 this 指向的场景
  • 多次调用 bind 只有第一次有效

八、变量声明

8. varletconst 的区别

问题varletconst 的区别?

答案

对比表

特性varletconst
作用域函数作用域块级作用域块级作用域
变量提升提升且初始化为 undefined提升但存在暂时性死区提升但存在暂时性死区
重复声明允许不允许不允许
全局属性成为 window 对象的属性不会成为 window 属性不会成为 window 属性
初始值可不初始化(undefined)可不初始化必须初始化
重新赋值可以可以不可以(但对象属性可修改)

代码示例

// 作用域差异
{
  var a = 1;
  let b = 2;
  const c = 3;
}
console.log(a); // 1(var 没有块级作用域)
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined

// 暂时性死区
console.log(x); // undefined(var 提升)
var x = 10;

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;

// 重复声明
var z = 1;
var z = 2; // 允许

let w = 1;
let w = 2; // SyntaxError: Identifier 'w' has already been declared

// const 必须初始化
const p; // SyntaxError: Missing initializer in const declaration

// const 对象属性可修改
const obj = { name: 'Alice' };
obj.name = 'Bob'; // 允许
obj = { name: 'Charlie' }; // TypeError: Assignment to constant variable

补充说明

  • 优先使用 const,除非需要重新赋值
  • 使用 let 替代 var,利用块级作用域
  • 暂时性死区(TDZ)提高了代码可预测性,避免声明前使用变量

九、闭包

9. 闭包的理解与应用

问题:什么是闭包?闭包的用途和缺点?闭包的典型应用场景有哪些?

答案

闭包定义

  • 函数在定义时会创建作用域链,包含自身的作用域和所有父级作用域
  • 当函数在其词法作用域外执行时,依然可以访问其词法作用域中的变量,形成闭包

闭包的优缺点

优点

  1. 封装私有变量:模拟私有方法和私有变量
  2. 数据持久化:函数执行后变量不会被垃圾回收
  3. 模块化:创建模块模式,实现信息隐藏
  4. 函数工厂:创建具有特定配置的函数

缺点

  1. 内存泄漏:闭包中的变量不会被垃圾回收,可能导致内存占用过高
  2. 性能影响:闭包的作用域链较长,变量查找速度较慢

典型应用场景

  1. 封装私有变量

    function createCounter() {
      let count = 0; // 私有变量
      return {
        increment() { count++; },
        decrement() { count--; },
        getValue() { return count; }
      };
    }
    const counter = createCounter();
    counter.increment();
    console.log(counter.getValue()); // 1
    console.log(counter.count); // undefined(无法直接访问)
    
  2. 回调函数和事件处理

    function setupClickHandler(buttonId) {
      const button = document.getElementById(buttonId);
      let clickCount = 0;
      
      button.addEventListener('click', function() {
        clickCount++;
        console.log(`按钮被点击了 ${clickCount} 次`);
      });
    }
    
  3. 函数工厂

    function createMultiplier(factor) {
      return function(x) {
        return x * factor;
      };
    }
    const double = createMultiplier(2);
    const triple = createMultiplier(3);
    console.log(double(5)); // 10
    console.log(triple(5)); // 15
    
  4. 模块模式

    const myModule = (function() {
      let privateVar = 0;
      
      function privateMethod() {
        return privateVar;
      }
      
      return {
        publicMethod() {
          privateVar++;
          return privateMethod();
        }
      };
    })();
    
    console.log(myModule.publicMethod()); // 1
    

内存泄漏防范

// 及时解除引用
function createHandler() {
  const largeData = new Array(1000000).fill('data');
  return function() {
    console.log('handled');
  };
}

const handler = createHandler();
// 使用后设置 handler = null 解除引用

补充说明

  • 闭包在现代 JavaScript 开发中无处不在,理解闭包是理解 JavaScript 核心概念的关键
  • 合理使用闭包,避免不必要的内存占用
  • 使用模块化(ES6 Modules)是更好的封装方式,但闭包仍有其应用场景

十、原型与原型链

10. 原型链、继承与 new 操作符

问题:什么是原型和原型链?JavaScript 继承的实现方式有哪些?prototype__proto__ 的区别?constructor 属性的作用?ES5 和 ES6 继承的区别?instanceof 的原理及手动实现?new 操作符的执行过程?

答案

原型链基本概念

  • 每个函数都有 prototype 属性(原型对象)
  • 每个对象都有 __proto__ 属性(原型链指针)
  • 对象的 __proto__ 指向其构造函数的 prototype
  • 查找属性时,先查找自身,再沿 __proto__ 链向上查找

prototype vs __proto__ vs constructor

function Person(name) {
  this.name = name;
}
const p = new Person('Alice');

// 关系图:
// p.__proto__ === Person.prototype
// Person.prototype.constructor === Person
// Person.__proto__ === Function.prototype
// Person.prototype.__proto__ === Object.prototype
// Object.prototype.__proto__ === null

new 操作符的执行过程

  1. 创建一个空对象 obj
  2. 设置 obj.__proto__ 指向构造函数的 prototype
  3. 将构造函数的 this 绑定到 obj
  4. 执行构造函数,为 obj 添加属性
  5. 如果构造函数返回对象,则返回该对象;否则返回 obj

手写 new

function myNew(constructor, ...args) {
  const obj = Object.create(constructor.prototype); // 步骤1-2
  const result = constructor.apply(obj, args); // 步骤3-4
  return result instanceof Object ? result : obj; // 步骤5
}

继承方式对比

继承方式优点缺点
原型链继承简单1. 引用类型属性被所有实例共享 2. 不能向父类传参
构造函数继承可向父类传参,引用类型不共享方法都在构造函数中定义,无法复用
组合继承结合两者优点1. 父类构造函数被调用两次 2. 子类原型中有多余属性
寄生组合继承最优方案实现稍复杂
ES6 class 继承语法简洁,语义清晰本质仍是原型继承的语法糖

寄生组合继承(最佳实践)

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

function Child(name, age) {
  Parent.call(this, name); // 继承实例属性
  this.age = age;
}

// 使用 Object.create 建立原型链,避免调用 Parent 构造函数
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child; // 修复 constructor 指向

Child.prototype.sayAge = function() {
  console.log(this.age);
};

const child1 = new Child('Tom', 10);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // Tom
child1.sayAge(); // 10

ES6 class 继承

class Parent {
  constructor(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
  }
  
  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name); // 必须调用 super
    this.age = age;
  }
  
  sayAge() {
    console.log(this.age);
  }
}

const child = new Child('Alice', 12);
child.sayName(); // Alice
child.sayAge(); // 12

instanceof 原理

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

补充说明

  • ES6 class 是语法糖,本质仍然是基于原型的继承
  • 推荐使用 ES6 class 语法,代码更清晰易维护
  • 理解原型链对于调试和性能优化非常重要

十一、事件机制

11. 事件冒泡、捕获、委托与事件流

问题:什么是事件冒泡和事件捕获?什么是事件委托?如何阻止事件冒泡?如何阻止默认行为?preventDefaultstopPropagation 的区别?DOM 事件流的三个阶段?DOM0、DOM2、DOM3 级事件的区别?addEventListener 的参数?哪些事件是不冒泡的?mouseentermouseover 的区别?如何用原生 JS 给一个按钮绑定多个 onclick 事件?

答案

DOM 事件流三个阶段

  1. 捕获阶段:从 window → document → ... → 目标元素
  2. 目标阶段:到达目标元素
  3. 冒泡阶段:从目标元素 → ... → document → window

事件绑定方式对比

方式特点移除方式事件流
DOM0:onclick简单,覆盖前一个赋值为 null冒泡阶段
DOM2:addEventListener可添加多个,可控制阶段removeEventListener可选捕获或冒泡
DOM3:新增事件类型DOMContentLoaded同上同上

代码示例

// 1. DOM0 级事件
element.onclick = function() { console.log('click 1'); };
element.onclick = function() { console.log('click 2'); }; // 覆盖前一个

// 2. DOM2 级事件
element.addEventListener('click', handler1);
element.addEventListener('click', handler2); // 可添加多个

function handler1() { console.log('handler1'); }
function handler2() { console.log('handler2'); }

// 移除事件
element.removeEventListener('click', handler1);

事件委托

  • 将事件监听器添加到父元素,利用事件冒泡处理子元素事件
  • 优点:减少事件监听器数量,提高性能;动态添加的子元素无需重新绑定
// 传统方式:为每个 li 绑定事件
const items = document.querySelectorAll('li');
items.forEach(item => {
  item.addEventListener('click', function(e) {
    console.log(e.target.textContent);
  });
});

// 事件委托:只需一个事件监听器
const list = document.querySelector('ul');
list.addEventListener('click', function(e) {
  if (e.target.tagName === 'LI') {
    console.log(e.target.textContent);
  }
});

// 动态添加元素也能正常工作
const newItem = document.createElement('li');
newItem.textContent = 'New Item';
list.appendChild(newItem); // 无需重新绑定事件

事件方法对比

  • event.preventDefault():阻止默认行为(如链接跳转、表单提交)
  • event.stopPropagation():阻止事件继续传播(冒泡或捕获)
  • event.stopImmediatePropagation():阻止事件传播并阻止同一元素的其他监听器执行

不冒泡的事件

  • focusblurloadunloadmouseentermouseleaveresizescroll

mouseenter vs mouseover

  • mouseenter:不冒泡,鼠标进入元素时触发一次
  • mouseover:冒泡,鼠标进入元素或其子元素时触发

绑定多个 onclick 事件

// 使用 addEventListener
const button = document.getElementById('btn');

button.addEventListener('click', function() {
  console.log('第一个事件');
});

button.addEventListener('click', function() {
  console.log('第二个事件');
});

// 或使用事件处理器组合
function handler1() { console.log('事件1'); }
function handler2() { console.log('事件2'); }

button.onclick = function() {
  handler1();
  handler2();
};

补充说明

  • 推荐使用 addEventListener,功能更强大,不覆盖已有事件
  • 事件委托是性能优化的重要手段,尤其适合动态内容
  • 注意事件处理函数中 this 的指向(箭头函数和普通函数不同)

十二、异步编程

12. 异步编程方式与事件循环

问题:JavaScript 异步编程的方式有哪些?什么是回调地狱?Promise 是什么?有哪些状态?Promise.all 的作用?Promise.race 的作用?async/await 的原理?async/await 与 Promise 的关系?Generator 函数?如何手写实现一个 Promise?事件循环(Event Loop)机制?宏任务与微任务有哪些?setTimeoutPromiseasync/await 的执行差异?

答案

异步编程演进

  1. 回调函数setTimeoutfs.readFile
  2. Promise(ES6):解决回调地狱
  3. Generator(ES6):可暂停执行的函数
  4. async/await(ES7):基于 Promise 的语法糖,同步方式写异步代码

事件循环(Event Loop)机制

  • JavaScript 是单线程,通过事件循环实现异步
  • 执行栈 → 微任务队列 → 宏任务队列
  • 每次从宏任务队列取一个任务执行,然后执行所有微任务

宏任务 vs 微任务

类型示例执行时机
宏任务setTimeoutsetIntervalsetImmediateI/O、UI 渲染每次事件循环执行一个
微任务Promise.then/catch/finallyprocess.nextTickMutationObserver每个宏任务执行后清空微任务队列

执行顺序示例

console.log('1'); // 同步

setTimeout(() => {
  console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务
});

console.log('4'); // 同步

// 输出顺序:1 → 4 → 3 → 2

Promise 状态

  • pending:初始状态
  • fulfilled:操作成功完成
  • rejected:操作失败

Promise 静态方法

// 1. Promise.all:所有成功或一个失败
Promise.all([p1, p2, p3])
  .then(values => console.log(values))
  .catch(error => console.error(error));

// 2. Promise.race:第一个完成(成功或失败)
Promise.race([p1, p2, p3])
  .then(value => console.log(value))
  .catch(error => console.error(error));

// 3. Promise.allSettled:所有完成(无论成功失败)
Promise.allSettled([p1, p2, p3])
  .then(results => console.log(results));

// 4. Promise.any:第一个成功
Promise.any([p1, p2, p3])
  .then(value => console.log(value))
  .catch(errors => console.error(errors));

async/await

  • async 函数返回 Promise
  • await 等待 Promise 解决,只能用在 async 函数中
  • 本质是 Generator + Promise 的语法糖
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
}

// 等价于
function fetchData() {
  return fetch('/api/data')
    .then(response => response.json())
    .catch(error => {
      console.error('Error:', error);
      throw error;
    });
}

Generator 函数

function* generator() {
  const a = yield 1;
  const b = yield a + 2;
  return b + 3;
}

const gen = generator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next(5)); // { value: 7, done: false }(a = 5)
console.log(gen.next(10)); // { value: 13, done: true }(b = 10)

手写 Promise(简化版):

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    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 {
          const result = onFulfilled ? onFulfilled(this.value) : this.value;
          resolve(result);
        } catch (error) {
          reject(error);
        }
      };
      
      const handleRejected = () => {
        try {
          const result = onRejected ? onRejected(this.reason) : this.reason;
          reject(result);
        } 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));
      }
    });
  }
  
  catch(onRejected) {
    return this.then(null, onRejected);
  }
  
  static resolve(value) {
    return new MyPromise(resolve => resolve(value));
  }
  
  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

补充说明

  • 理解事件循环是 JavaScript 异步编程的核心
  • async/await 让异步代码更易读,但本质上仍是 Promise
  • 注意 Promise 的错误处理,避免未捕获的异常

十三、手写代码题

13. 手写常用函数实现

手写规则:提供完整可运行的代码,包含中文注释,涵盖边界情况和错误处理。

1. 手写 Promise(见上文)
2. 手写 Promise.all
Promise.myAll = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Arguments must be an array'));
    }
    
    const results = new Array(promises.length);
    let completedCount = 0;
    
    if (promises.length === 0) {
      return resolve(results);
    }
    
    promises.forEach((promise, index) => {
      Promise.resolve(promise).then(
        value => {
          results[index] = value;
          completedCount++;
          if (completedCount === promises.length) {
            resolve(results);
          }
        },
        reason => {
          reject(reason);
        }
      );
    });
  });
};

// 测试
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);

Promise.myAll([p1, p2, p3])
  .then(values => console.log(values)) // [1, 2, 3]
  .catch(error => console.error(error));
3. 手写 Promise.race
Promise.myRace = function(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Arguments must be an array'));
    }
    
    if (promises.length === 0) {
      return; // Promise 永远处于 pending 状态
    }
    
    promises.forEach(promise => {
      Promise.resolve(promise).then(resolve, reject);
    });
  });
};
4. 手写深拷贝
function deepClone(obj, hash = new WeakMap()) {
  // 基础类型直接返回
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }
  
  // 处理 Date
  if (obj instanceof Date) {
    return new Date(obj.getTime());
  }
  
  // 处理 RegExp
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }
  
  // 处理 Array
  if (Array.isArray(obj)) {
    return obj.map(item => deepClone(item, hash));
  }
  
  // 防止循环引用
  if (hash.has(obj)) {
    return hash.get(obj);
  }
  
  // 处理普通对象
  const cloneObj = Object.create(Object.getPrototypeOf(obj));
  hash.set(obj, cloneObj);
  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  
  return cloneObj;
}

// 测试
const obj = {
  name: 'Alice',
  age: 25,
  hobbies: ['reading', 'coding'],
  address: {
    city: 'Beijing',
    street: 'Main St'
  },
  birthDate: new Date('1998-01-01'),
  regex: /test/g,
  sayHello: function() {
    console.log('Hello');
  }
};

// 循环引用
obj.self = obj;

const cloned = deepClone(obj);
console.log(cloned !== obj); // true
console.log(cloned.address !== obj.address); // true
console.log(cloned.hobbies !== obj.hobbies); // true
console.log(cloned.sayHello === obj.sayHello); // true(函数共享)

// 测试日期和正则
console.log(cloned.birthDate.getTime() === obj.birthDate.getTime()); // true
console.log(cloned.regex.test('test')); // true
5. 手写防抖(debounce)
function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoked = false;
  
  return function(...args) {
    const context = this;
    
    // 清除之前的定时器
    if (timer) {
      clearTimeout(timer);
    }
    
    // 立即执行模式
    if (immediate && !isInvoked) {
      fn.apply(context, args);
      isInvoked = true;
    }
    
    // 设置新的定时器
    timer = setTimeout(() => {
      timer = null;
      if (!immediate) {
        fn.apply(context, args);
      }
      isInvoked = false;
    }, delay);
  };
}

// 测试
const expensiveOperation = debounce(function(searchTerm) {
  console.log(`搜索:${searchTerm}`);
}, 300);

// 快速连续输入只会执行一次
document.getElementById('search').addEventListener('input', (e) => {
  expensiveOperation(e.target.value);
});
6. 手写节流(throttle)
function throttle(fn, delay, options = {}) {
  const { leading = true, trailing = true } = options;
  let timer = null;
  let lastCallTime = 0;
  let lastArgs = null;
  let lastContext = null;
  
  function execute() {
    if (lastArgs) {
      fn.apply(lastContext, lastArgs);
      lastArgs = null;
      lastContext = null;
      lastCallTime = Date.now();
    }
    timer = null;
  }
  
  return function(...args) {
    const now = Date.now();
    const remaining = delay - (now - lastCallTime);
    
    lastArgs = args;
    lastContext = this;
    
    if (!lastCallTime && !leading) {
      lastCallTime = now;
    }
    
    if (remaining <= 0 || remaining > delay) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      execute();
    } else if (!timer && trailing) {
      timer = setTimeout(execute, remaining);
    }
  };
}

// 测试
const handleScroll = throttle(function() {
  console.log('滚动事件', Date.now());
}, 1000);

window.addEventListener('scroll', handleScroll);
7. 手写 call/apply/bind(见上文)
8. 手写 new(见上文)
9. 手写 instanceof(见上文)
10. 手写数组 map
Array.prototype.myMap = function(callback, thisArg) {
  if (this == null) {
    throw new TypeError('this is null or not defined');
  }
  
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  
  const O = Object(this);
  const len = O.length >>> 0;
  const result = new Array(len);
  
  for (let i = 0; i < len; i++) {
    if (i in O) {
      result[i] = callback.call(thisArg, O[i], i, O);
    }
  }
  
  return result;
};

// 测试
const arr = [1, 2, 3];
const doubled = arr.myMap(num => num * 2);
console.log(doubled); // [2, 4, 6]
11. 手写数组 filter
Array.prototype.myFilter = function(callback, thisArg) {
  if (this == null) {
    throw new TypeError('this is null or not defined');
  }
  
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  
  const O = Object(this);
  const len = O.length >>> 0;
  const result = [];
  
  for (let i = 0; i < len; i++) {
    if (i in O) {
      if (callback.call(thisArg, O[i], i, O)) {
        result.push(O[i]);
      }
    }
  }
  
  return result;
};

// 测试
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.myFilter(num => num % 2 === 0);
console.log(evens); // [2, 4]
12. 手写数组 reduce
Array.prototype.myReduce = function(callback, initialValue) {
  if (this == null) {
    throw new TypeError('this is null or not defined');
  }
  
  if (typeof callback !== 'function') {
    throw new TypeError(callback + ' is not a function');
  }
  
  const O = Object(this);
  const len = O.length >>> 0;
  
  if (len === 0 && initialValue === undefined) {
    throw new TypeError('Reduce of empty array with no initial value');
  }
  
  let accumulator = initialValue !== undefined ? initialValue : O[0];
  let startIndex = initialValue !== undefined ? 0 : 1;
  
  for (let i = startIndex; i < len; i++) {
    if (i in O) {
      accumulator = callback(accumulator, O[i], i, O);
    }
  }
  
  return accumulator;
};

// 测试
const arr = [1, 2, 3, 4];
const sum = arr.myReduce((acc, cur) => acc + cur, 0);
console.log(sum); // 10
13. 手写数组 flat
Array.prototype.myFlat = function(depth = 1) {
  if (depth < 1) {
    return this.slice();
  }
  
  return this.reduce((acc, cur) => {
    if (Array.isArray(cur)) {
      acc.push(...cur.myFlat(depth - 1));
    } else {
      acc.push(cur);
    }
    return acc;
  }, []);
};

// 测试
const nested = [1, [2, [3, [4, 5]]]];
console.log(nested.myFlat()); // [1, 2, [3, [4, 5]]]
console.log(nested.myFlat(2)); // [1, 2, 3, [4, 5]]
console.log(nested.myFlat(Infinity)); // [1, 2, 3, 4, 5]
14. 手写发布订阅模式
class EventEmitter {
  constructor() {
    this.events = new Map();
  }
  
  on(event, listener) {
    if (!this.events.has(event)) {
      this.events.set(event, new Set());
    }
    this.events.get(event).add(listener);
    return this;
  }
  
  off(event, listener) {
    if (this.events.has(event)) {
      const listeners = this.events.get(event);
      listeners.delete(listener);
      if (listeners.size === 0) {
        this.events.delete(event);
      }
    }
    return this;
  }
  
  emit(event, ...args) {
    if (this.events.has(event)) {
      const listeners = this.events.get(event);
      listeners.forEach(listener => {
        try {
          listener.apply(this, args);
        } catch (error) {
          console.error(`Error in event listener for ${event}:`, error);
        }
      });
    }
    return this;
  }
  
  once(event, listener) {
    const onceWrapper = (...args) => {
      listener.apply(this, args);
      this.off(event, onceWrapper);
    };
    return this.on(event, onceWrapper);
  }
}

// 测试
const emitter = new EventEmitter();

// 订阅事件
emitter.on('message', (msg) => {
  console.log(`收到消息:${msg}`);
});

emitter.once('greet', (name) => {
  console.log(`你好,${name}!`);
});

// 发布事件
emitter.emit('message', 'Hello World'); // 收到消息:Hello World
emitter.emit('greet', 'Alice'); // 你好,Alice!
emitter.emit('greet', 'Bob'); // 不执行(once 只执行一次)
15. 手写柯里化
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}

// 测试
function add(a, b, c) {
  return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

// 实际应用:创建特定功能的函数
const add5 = curriedAdd(5);
const add5And10 = add5(10);
console.log(add5And10(15)); // 30
16. 手写 LRU 缓存
class LRUCache {
  constructor(capacity) {
    this.capacity = capacity;
    this.cache = new Map();
  }
  
  get(key) {
    if (!this.cache.has(key)) {
      return -1;
    }
    
    // 将访问的元素移到最前面(最近使用)
    const value = this.cache.get(key);
    this.cache.delete(key);
    this.cache.set(key, value);
    
    return value;
  }
  
  put(key, value) {
    // 如果 key 已存在,先删除
    if (this.cache.has(key)) {
      this.cache.delete(key);
    }
    
    // 如果容量已满,删除最久未使用的(Map 的第一个键)
    if (this.cache.size >= this.capacity) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    // 添加新元素
    this.cache.set(key, value);
  }
  
  // 辅助方法:查看缓存内容
  toString() {
    return Array.from(this.cache.entries())
      .map(([k, v]) => `${k}:${v}`)
      .join(' -> ');
  }
}

// 测试
const cache = new LRUCache(2);
cache.put(1, 'A');
cache.put(2, 'B');
console.log(cache.get(1)); // 'A'
cache.put(3, 'C'); // 容量已满,删除键 2
console.log(cache.get(2)); // -1(已被删除)
console.log(cache.get(3)); // 'C'
cache.put(4, 'D'); // 删除键 1
console.log(cache.get(1)); // -1
console.log(cache.get(3)); // 'C'
console.log(cache.get(4)); // 'D'

// 输出顺序变化
console.log(cache.toString()); // "3:C -> 4:D"

补充说明:手写代码时要注意边界条件、错误处理、性能优化和代码可读性。在实际面试中,能够手写这些基础函数并解释原理,能显著提升面试表现。


总结

JavaScript涵盖了 117 道题目中的核心考点,通过合并相似问题、提供详细解释、完整代码示例和实用补充说明,形成了系统化的知识体系。建议结合实际编码练习加深理解,重点关注高频考点和手写代码题。