JavaScript 核心知识点面试题整理

151 阅读28分钟

一、执行上下文 / 作用域链 / 闭包

(一)介绍一下 JavaScript 的执行上下文

执行上下文是 JavaScript 代码执行的环境,包含变量环境、词法环境等信息,主要分三类:

  • 全局执行上下文:程序运行时创建的基础环境,浏览器关联 windowglobalThis 规范后更通用),一个程序仅 1 个。

  • 函数执行上下文:调用函数时创建,管理函数内代码执行(含参数、内部变量等 ),调用 N 次函数则创建 N 个。

  • Eval 执行上下文eval 函数调用时创建(因安全、性能问题,开发中极少用 )。

生命周期分创建阶段(确定 this、词法环境、变量环境 )和执行阶段(变量赋值、函数执行等 ),由 ** 执行栈(调用栈)** 管理顺序,栈底是全局上下文,栈顶是当前执行的函数上下文。

(二)介绍一下 JavaScript 的作用域链

作用域链是变量查找机制,由 ** 当前执行上下文的词法环境 + 所有父级执行上下文的词法环境(父级作用域)** 依次组成。

  • 访问变量时,先在自身词法环境找,找不到则沿作用域链向上级作用域查找,直至全局作用域。

  • 作用域链与 ** 函数定义时的词法作用域(代码书写位置决定的作用域关系 )** 相关,函数定义时记录所在词法作用域,调用时据此构建作用域链。

示例:

js

function outer() {  
  let outerVar = ' outer ';  
  function inner() {  
    let innerVar = ' inner ';  
    console.log(outerVar + innerVar); // 访问 outerVar 时,沿作用域链从 inner → outer 查找  
  }  
  inner();  
}  
outer();  

(三)介绍一下 JavaScript 的闭包是什么及应用场景

1. 闭包定义

函数与其周围的 ** 词法环境(作用域)** 形成的封闭结构,让内层函数能访问外层函数作用域的变量,且外层函数执行结束后,其变量因内层函数引用不会被销毁。常见形式是 “函数内返回另一个函数,且返回函数访问外层变量”。

示例:

js

function createCounter() {  
  let count = 0;  
  return function () {  
    return ++count;  
  };  
}  
const counter = createCounter();  
console.log(counter()); // 1(count 因闭包被保留 )  
console.log(counter()); // 2  

2. 应用场景

  • 模块模式:实现私有变量 / 方法,暴露有限接口。

    js

    const module = (function () {  
      let privateVar = 'secret';  
      function privateFn() {  
        console.log(privateVar);  
      }  
      return {  
        publicFn() {  
          privateFn();  
        }  
      };  
    })();  
    module.publicFn(); // 访问私有逻辑  
    
  • 防抖节流:闭包保存定时器标识、上次执行时间等状态(以防抖为例 )。

    js

    function debounce(fn, delay) {  
      let timer = null;  
      return function (...args) {  
        clearTimeout(timer);  
        timer = setTimeout(() => {  
          fn.apply(this, args);  
        }, delay);  
      };  
    }  
    
  • 函数柯里化:逐步传参,利用闭包缓存中间参数。

    js

    function curryAdd(a) {  
      return function (b) {  
        return a + b;  
      };  
    }  
    const add5 = curryAdd(5);  
    console.log(add5(3)); // 8  
    

二、this/call/apply/bind

(一)介绍一下 JavaScript 里的 this

this 是函数执行时自动绑定的关键字,指向函数执行时的关联对象,其指向由调用方式决定:

  • 全局环境(非严格模式)this 指向全局对象(浏览器 window、Node.js global );严格模式下,全局环境 this 为 undefined

  • 函数调用:普通函数调用(非箭头函数 ),this 指向全局对象(严格模式为 undefined );若函数作为对象方法调用,this 指向调用方法的对象。

    js

    const obj = {  
      name: 'obj',  
      sayName() {  
        console.log(this.name);  
      }  
    };  
    obj.sayName(); // this 指向 obj  
    const fn = obj.sayName;  
    fn(); // 普通调用,非严格模式下 this 指向 window  
    
  • 构造函数调用new 调用函数时,this 指向新创建的实例对象

    js

    function Person(name) {  
      this.name = name;  
    }  
    const person = new Person('Tom');  
    console.log(person.name); // Tom  
    
  • 箭头函数:不绑定自身 this,继承外层作用域的 this(定义时确定,无法通过 call 等改变 )。

    js

    const obj = {  
      name: 'obj',  
      sayName: () => {  
        console.log(this.name); // 外层是全局,非严格模式下 this 指向 window  
      }  
    };  
    obj.sayName();  
    

(二)如何改变 this 指向

主要方法:

  • call 方法:调用函数时,第一个参数设为 this 指向对象,后续参数逐个传递

    js

    function fn(a, b) {  
      console.log(this, a, b);  
    }  
    const obj = { name: 'obj' };  
    fn.call(obj, 1, 2); // this 指向 obj  
    
  • apply 方法:第一个参数是 this 指向对象,第二个参数是包含函数参数的数组(或类数组)

    js

    fn.apply(obj, [1, 2]); // 效果同 call,参数传数组  
    
  • bind 方法:创建新函数,this 绑定到传入对象,后续调用新函数时传参(可分两次传参:bind 时传部分,调用时传剩余 )。

    js

    const boundFn = fn.bind(obj, 1);  
    boundFn(2); // this 指向 obj,参数 1、2  
    
  • 箭头函数:利用其 this 继承外层作用域的特性,间接固定 this(如定时器回调用箭头函数,避免 this 指向变化 )。

    js

    const obj = {  
      name: 'obj',  
      init() {  
        setTimeout(() => {  
          console.log(this.name); // this 继承 init 的 this(即 obj )  
        }, 1000);  
      }  
    };  
    obj.init();  
    

(三)call 和 apply 有什么区别

  • 参数传递

    • call:逐个传参,形式 function.call(thisArg, arg1, arg2, ...)
    • apply:传参数数组(或类数组 ),形式 function.apply(thisArg, [arg1, arg2, ...])
  • 使用场景

    • call:参数明确、数量少,或需逐个传参时用。
    • apply:处理数组参数(如 Math.max.apply(null, arr),ES6 后可用 Math.max(...arr) 替代 )更方便。

(四)如何实现 call 和 apply

1. 实现 call

思路:将函数设为传入对象的 “临时方法”,调用后删除,让函数执行时 this 指向该对象。

js

Function.prototype.myCall = function (context = window, ...args) {  
  context = context || window;  
  const fnKey = Symbol('fn'); // 用 Symbol 避免属性名冲突  
  context[fnKey] = this; // 函数挂载到对象  
  const result = context[fnKey](...args); // 执行函数  
  delete context[fnKey]; // 删除临时方法  
  return result;  
};  

// 测试  
function test(a, b) {  
  console.log(this.name, a, b);  
}  
const obj = { name: 'testObj' };  
test.myCall(obj, 1, 2);  

2. 实现 apply

思路类似 call,仅参数处理为数组。

js

Function.prototype.myApply = function (context = window, args = []) {  
  context = context || window;  
  const fnKey = Symbol('fn');  
  context[fnKey] = this;  
  const result = context[fnKey](...args); // 展开数组参数  
  delete context[fnKey];  
  return result;  
};  

// 测试  
test.myApply(obj, [1, 2]);  

(五)如何实现一个 bind

思路:bind 返回新函数,调用时 this 指向绑定对象;需处理构造函数调用(用 new 时,this 应指向实例,而非绑定对象 )

js

Function.prototype.myBind = function (context = window) {  
  const self = this;  
  const args = Array.prototype.slice.call(arguments, 1); // 截取 bind 时的参数  

  const boundFn = function () {  
    const callArgs = Array.prototype.slice.call(arguments); // 调用时的参数  
    // 若用 new 调用 boundFn,this 指向实例;否则指向 context  
    return self.apply(this instanceof boundFn ? this : context, args.concat(callArgs));  
  };  

  // 维护原型关系:让 boundFn 作为构造函数时,实例继承原函数原型  
  const Fn = function () {};  
  Fn.prototype = this.prototype;  
  boundFn.prototype = new Fn();  

  return boundFn;  
};  

// 测试  
function Person(name) {  
  this.name = name;  
}  
const boundPerson = Person.myBind({});  
const p = new boundPerson('Tom');  
console.log(p.name); // Tom(new 调用时,this 指向实例 )  

三、原型 / 继承

(一)介绍一下 JavaScript 的原型

在 JavaScript 中,原型(Prototype)  是实现对象继承和属性共享的核心机制,每个函数(除箭头函数)都有一个 prototype 属性(显式原型 ),它是一个对象,包含了可以由特定类型的所有实例共享的属性和方法;每个对象(通过 new 构造函数创建或对象字面量等方式创建)都有一个 __proto__ 属性(隐式原型 ,部分环境下为 [[Prototype]] 内部槽 ),指向其构造函数的 prototype

  • 构造函数与原型关系

js

function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function () {
  console.log(this.name);
};
const person1 = new Person('Tom');
// person1.__proto__ 指向 Person.prototype 
console.log(person1.__proto__ === Person.prototype); // true 
  • 原型作用:实现属性和方法的继承与共享,避免每个实例重复创建相同方法,节省内存;同时是原型链查找机制的基础,当访问对象属性 / 方法时,若自身没有,会沿着 __proto__ 构成的原型链向上查找。

(二)原型链是什么

原型链(Prototype Chain)  是 JavaScript 中对象属性和方法查找的链式结构,由对象的 __proto__ 属性串联而成。

  • 查找机制:当访问对象的某个属性或方法时,先在对象自身的属性中查找;若找不到,就沿着 __proto__ 指向的原型对象继续查找;如果原型对象也没有,就去原型对象的原型(即 Object.prototype ,因为所有对象最终都继承自 Object )中查找,直到找到或到达原型链末端(Object.prototype.__proto__ 为 null )。

js

function Person() {}
Person.prototype.age = 18;
const p = new Person();
console.log(p.age); // 自身无 age,沿原型链找到 Person.prototype.age → 18 
console.log(p.toString()); // 原型链最终找到 Object.prototype.toString 
  • 原型链终点Object.prototype.__proto__ 的值是 null,代表原型链的末端,若一直找不到属性,最终返回 undefined 。

(三)如何利用原型实现继承

1. 原型链继承

将子类构造函数的 prototype 指向父类实例,让子类实例能通过原型链访问父类属性和方法。

js

function Parent() {
  this.parentProp = 'parent value';
}
Parent.prototype.parentMethod = function () {
  console.log(this.parentProp);
};
function Child() {
  this.childProp = 'child value';
}
// 子类原型指向父类实例,建立原型链
Child.prototype = new Parent(); 
Child.prototype.constructor = Child; // 修复 constructor 指向
const child = new Child();
child.parentMethod(); // 访问父类方法 

缺点:父类实例属性变为子类原型属性,多个子类实例操作引用类型父类属性会相互影响;创建子类实例时无法向父类构造函数传参(一次性初始化父类属性 )。

2. 构造函数继承(盗用构造函数 / 对象冒充 )

在子类构造函数中调用父类构造函数,通过 call 或 apply 改变 this 指向,让父类构造函数为子类实例添加属性。

js

function Parent(name) {
  this.name = name;
  this.sayName = function () {
    console.log(this.name);
  };
}
function Child(name, age) {
  Parent.call(this, name); // 调用父类构造函数,为子类实例添加 name 等属性 
  this.age = age;
}
const child = new Child('Tom', 18);
child.sayName(); // Tom 

优点:解决了原型链继承中父类引用类型属性共享问题,能向父类传参;缺点:父类原型上的方法无法被继承,每个子类实例都要重复创建父类实例方法,浪费内存。

3. 组合继承(原型链继承 + 构造函数继承 )

结合前两种方式,用原型链继承实现方法继承(共享 ),用构造函数继承实现属性继承(私有 )。

js

function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function () {
  console.log(this.name);
};
function Child(name, age) {
  Parent.call(this, name); // 构造函数继承属性 
  this.age = age;
}
Child.prototype = new Parent(); // 原型链继承方法 
Child.prototype.constructor = Child;
const child = new Child('Tom', 18);
child.sayName(); // 访问父类原型方法 
console.log(child.age); // 子类自身属性 

优点:既实现属性私有(避免实例间干扰 ),又实现方法共享;缺点:调用了两次父类构造函数(new Parent() 和 Parent.call ),父类原型会被多余地添加父类实例属性(虽然后续子类实例属性会覆盖 )。

4. 寄生组合继承(优化组合继承 )

通过创建空对象作为中间媒介,避免重复调用父类构造函数,是较优的继承方式。

js

function Parent(name) {
  this.name = name;
}
Parent.prototype.sayName = function () {
  console.log(this.name);
};
function Child(name, age) {
  Parent.call(this, name); 
  this.age = age;
}
// 创建空函数,避免重复调用父类构造
const Fn = function () {}; 
Fn.prototype = Parent.prototype;
Child.prototype = new Fn(); 
Child.prototype.constructor = Child;
const child = new Child('Tom', 18);
child.sayName(); 

优点:只调用一次父类构造函数,原型链简洁,避免父类原型被多余属性污染,是常用的继承实现方式。

四、Promise

(一)Promise 是什么

Promise 是 JavaScript 中用于处理异步操作的对象,解决了回调地狱(嵌套回调导致代码可读性差、难以维护 )问题,提供更优雅的异步流程控制方式。

  • 三种状态

    • pending(进行中 ):初始状态,异步操作未完成。
    • fulfilled(已成功 ):异步操作完成,会触发 then 里的成功回调。
    • rejected(已失败 ):异步操作出错,会触发 then 里的失败回调或 catch 回调,状态一旦改变(pending → fulfilled 或 pending → rejected )就不可再变。
  • 基本用法

js

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    const success = true;
    if (success) {
      resolve('操作成功'); // 状态变为 fulfilled 
    } else {
      reject('操作失败'); // 状态变为 rejected 
    }
  }, 1000);
});
promise.then(
  (result) => {
    console.log(result); // 成功回调 
  },
  (error) => {
    console.log(error); // 失败回调 
  }
);
  • 作用:链式调用(then 方法返回新 Promise,可继续链式调用 )、错误捕获(catch 统一捕获链中错误 )、并行 / 串行控制(Promise.allPromise.race 等静态方法 )。

(二)如何实现一个 Promise(Promise A+)

实现需遵循 Promise/A+ 规范 ,核心包含状态管理、then 方法实现、异步处理(微任务 )、解决循环调用等。以下是简化版实现:

js

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(executor) {
  let self = this;
  self.status = PENDING;
  self.value = undefined; // 成功值 
  self.reason = undefined; // 失败原因 
  self.onFulfilledCallbacks = []; // 保存成功回调(异步时)
  self.onRejectedCallbacks = []; // 保存失败回调(异步时)

  function resolve(value) {
    if (self.status === PENDING) {
      self.status = FULFILLED;
      self.value = value;
      // 异步完成后,执行保存的成功回调 
      self.onFulfilledCallbacks.forEach((cb) => cb()); 
    }
  }

  function reject(reason) {
    if (self.status === PENDING) {
      self.status = REJECTED;
      self.reason = reason;
      // 异步完成后,执行保存的失败回调 
      self.onRejectedCallbacks.forEach((cb) => cb()); 
    }
  }

  try {
    executor(resolve, reject); // 执行构造函数传入的函数 
  } catch (e) {
    reject(e); // 捕获错误,转为 reject 
  }
}

// 实现 then 方法,遵循 Promise/A+ 规范
MyPromise.prototype.then = function (onFulfilled, onRejected) {
  // 处理默认参数(规范要求,若不是函数,透传值 )
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (v) => v; 
  onRejected = typeof onRejected === 'function' ? onRejected : (e) => { throw e }; 

  let self = this;
  return new MyPromise((resolve, reject) => {
    // 封装成功回调执行逻辑
    function handleFulfilled() {
      // 异步执行(模拟微任务,实际可结合环境用 queueMicrotask 等 )
      setTimeout(() => { 
        try {
          let x = onFulfilled(self.value);
          // 解决 x(可能是 Promise 等,需递归解析 )
          resolvePromise(newPromise, x, resolve, reject); 
        } catch (e) {
          reject(e);
        }
      });
    }

    // 封装失败回调执行逻辑
    function handleRejected() {
      setTimeout(() => {
        try {
          let x = onRejected(self.reason);
          resolvePromise(newPromise, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      });
    }

    if (self.status === FULFILLED) {
      handleFulfilled();
    } else if (self.status === REJECTED) {
      handleRejected();
    } else if (self.status === PENDING) {
      // 异步情况,先保存回调 
      self.onFulfilledCallbacks.push(handleFulfilled); 
      self.onRejectedCallbacks.push(handleRejected);
    }
  });
};

// 解析 Promise 返回值(处理各种情况,如 x 是 Promise、thenable 等 )
function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) { // 循环引用,报错 
    return reject(new TypeError('Chaining cycle detected for promise')); 
  }
  if (x instanceof MyPromise) { // x 是 Promise,等待其状态改变 
    x.then((value) => {
      resolvePromise(promise, value, resolve, reject);
    }, reject);
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { // x 是对象或函数(可能是 thenable )
    let then;
    try {
      then = x.then;
    } catch (e) {
      return reject(e);
    }
    if (typeof then === 'function') { // 是 thenable,尝试调用 then 方法 
      let called = false;
      try {
        then.call(
          x,
          (y) => {
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } catch (e) {
        if (called) return;
        reject(e);
      }
    } else { // 普通对象,直接 resolve 
      resolve(x);
    }
  } else { // x 是普通值,直接 resolve 
    resolve(x);
  }
}

// 可扩展实现 catch、finally、all、race 等方法,如 catch 方法:
MyPromise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);
};

说明:实际完整实现需严格遵循规范,处理微任务调度(如用 queueMicrotask 替代 setTimeout 更接近原生行为 )、更多边界情况等,但上述代码体现了 Promise 核心的状态管理、then 链式调用和异步解析逻辑。

(三)async await

async/await 是基于 Promise 的语法糖,让异步代码更接近同步代码的写法,增强可读性和可维护性。

  • 基本用法

    • async 函数:返回一个 Promise,函数内 return 的值会被包装成 Promise 的 resolved 值;若函数内抛出错误,会使返回的 Promise 变为 rejected 。

    • await:只能用在 async 函数内,等待一个 Promise 解决(resolved 或 rejected ),暂停函数执行,直到 Promise 状态改变,然后返回其 resolved 值(或抛出 rejected 错误,可被 try...catch 捕获 )。

js

async function asyncFn() {
  try {
    const result = await new Promise((resolve) => {
      setTimeout(() => resolve('异步结果'), 1000);
    });
    console.log(result); // 1 秒后输出“异步结果” 
  } catch (error) {
    console.log(error);
  }
}
asyncFn();
  • 优势:避免了 Promise 链式调用的 .then 嵌套,代码逻辑更线性;用 try...catch 统一捕获错误,比 Promise 的 catch 更贴近同步代码的错误处理习惯。

五、深浅拷贝(进阶)

(一)如何实现深拷贝

1. 递归 + 类型判断(完善版 )

递归遍历对象 / 数组的每一层,根据数据类型(对象、数组、正则、日期等 )创建新实例,实现真正的深度复制。

js

function deepClone(target, map = new WeakMap()) {
  // 处理原始值(非对象类型直接返回)
  if (target === null || typeof target !== 'object') return target; 
  // 处理循环引用(用 WeakMap 缓存已拷贝对象,避免无限递归)
  if (map.has(target)) return map.get(target); 

  // 区分具体对象类型,创建对应新实例
  let cloneTarget;
  const constructor = target.constructor;
  switch (constructor) {
    case Object:
      cloneTarget = {};
      break;
    case Array:
      cloneTarget = [];
      break;
    case RegExp:
      cloneTarget = new RegExp(target);
      break;
    case Date:
      cloneTarget = new Date(target);
      break;
    // 可扩展处理其他内置对象(如 Set、Map 等)
    default:
      cloneTarget = new constructor(target); 
  }

  // 缓存当前拷贝关系
  map.set(target, cloneTarget); 

  // 遍历对象/数组的可枚举属性,递归拷贝
  for (const key in target) {
    if (Object.prototype.hasOwnProperty.call(target, key)) {
      cloneTarget[key] = deepClone(target[key], map); 
    }
  }

  return cloneTarget;
}

// 测试
const obj = { 
  a: 1, 
  b: { c: 2 }, 
  d: [3, { e: 4 }], 
  r: /\d+/, 
  date: new Date() 
};
obj.self = obj; // 循环引用
const clone = deepClone(obj);
console.log(clone.b === obj.b); // false(深拷贝,嵌套对象独立)
console.log(clone.self === clone); // true(正确处理循环引用)

2. 利用 MessageChannel(浏览器环境,处理大对象 )

MessageChannel 可实现异步深拷贝(利用其 postMessage 自动克隆数据的特性 ),适合拷贝复杂且无函数、特殊对象(如正则、日期 )的场景,不过是异步的。

js

function deepCloneAsync(target) {
  return new Promise((resolve) => {
    const channel = new MessageChannel();
    channel.port2.onmessage = (event) => resolve(event.data);
    channel.port1.postMessage(target);
  });
}

// 测试(异步使用)
const obj = { a: 1, b: { c: 2 } };
deepCloneAsync(obj).then((clone) => {
  console.log(clone); // 深拷贝后的对象
});

3. Lodash 的 _.cloneDeep(实际开发推荐 )

Lodash 库提供的 _.cloneDeep 方法,内部处理了各种数据类型(包括循环引用、特殊对象 ),稳定且易用。

js

import _ from 'lodash';
const obj = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(obj);

(二)实现深拷贝需要注意哪些问题

  1. 循环引用:对象属性引用自身(如 obj.self = obj ),若不处理会导致递归无限调用,栈溢出。需用缓存(如 WeakMap )记录已拷贝对象,避免重复处理。

  2. 特殊对象类型

    • 内置对象(正则 RegExp、日期 DateSetMap 等 ):直接赋值会复制引用,需手动创建新实例(如 new RegExp(target) )。
    • 函数:一般场景下深拷贝函数意义不大(函数拷贝后作用域可能变化 ),可选择直接引用或忽略(根据需求决定是否复制函数 )。
  3. 原型链属性for...in 遍历默认包含原型链属性,需用 Object.prototype.hasOwnProperty.call 过滤,避免拷贝无关属性。

  4. 性能问题:深拷贝递归遍历所有层级,嵌套过深或数据量大时,会占用较多内存和时间,需根据场景权衡(如大对象用 MessageChannel 或拆分拷贝 )。

(三)如何解决循环引用的问题

核心思路是记录已拷贝的对象,在递归过程中检查当前对象是否已处理过,若已处理则直接返回缓存的拷贝结果,避免重复递归。

1. 用 WeakMap 缓存(推荐 )

WeakMap 的键是弱引用,不影响垃圾回收,适合存储对象缓存。

js

function deepClone(target, map = new WeakMap()) {
  if (target === null || typeof target !== 'object') return target;
  if (map.has(target)) return map.get(target); // 命中缓存,返回已拷贝对象

  const cloneTarget = // ... 创建新实例(同前所述)
  
  map.set(target, cloneTarget); // 缓存当前拷贝关系
  // 递归拷贝属性...
  return cloneTarget;
}

2. 用数组模拟缓存(不推荐,性能差 )

原理类似,但数组查找效率低(需遍历匹配对象 ),适合简单场景或学习理解。

js

function deepClone(target, cache = []) {
  if (target === null || typeof target !== 'object') return target;
  
  // 检查是否已拷贝过该对象
  const cached = cache.find((item) => item.target === target);
  if (cached) return cached.clone; 

  const cloneTarget = // ... 创建新实例
  
  cache.push({ target, clone: cloneTarget }); // 记录到缓存
  // 递归拷贝属性...
  return cloneTarget;
}

六、事件机制 / Event Loop

(一)如何实现一个事件的发布订阅

发布订阅模式(观察者模式 )让对象间解耦,事件发布者无需知道订阅者细节,只需触发事件;订阅者可自主订阅感兴趣的事件。

1. 基础实现(类 + 方法 )

js

class EventEmitter {
  constructor() {
    // 存储事件与回调的映射:{ eventName: [callback1, callback2] }
    this.events = new Map(); 
  }

  // 订阅事件
  on(eventName, callback) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, []);
    }
    this.events.get(eventName).push(callback);
    return this; // 支持链式调用
  }

  // 发布事件(触发回调)
  emit(eventName, ...args) {
    const callbacks = this.events.get(eventName);
    if (callbacks) {
      callbacks.forEach((cb) => cb(...args));
    }
    return this;
  }

  // 取消订阅(移除指定回调,若未传 callback 则移除所有 )
  off(eventName, callback) {
    const callbacks = this.events.get(eventName);
    if (callbacks) {
      if (callback) {
        this.events.set(
          eventName, 
          callbacks.filter((cb) => cb !== callback)
        );
      } else {
        this.events.delete(eventName);
      }
    }
    return this;
  }

  // 订阅一次,触发后自动取消订阅
  once(eventName, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(eventName, wrapper); // 触发后移除
    };
    this.on(eventName, wrapper);
    return this;
  }
}

// 测试
const emitter = new EventEmitter();
const callback = (data) => console.log('事件触发:', data);

emitter.on('test', callback);
emitter.emit('test', 'Hello'); // 输出“事件触发: Hello”

emitter.once('onceTest', (data) => console.log('一次性事件:', data));
emitter.emit('onceTest', 'Hi'); // 输出“一次性事件: Hi”
emitter.emit('onceTest', 'Hi'); // 无输出(已自动取消订阅)

emitter.off('test', callback);
emitter.emit('test', 'Hello'); // 无输出(已取消订阅)

2. 扩展优化

  • 支持异步触发:可通过 setTimeout 或 Promise 包装 emit 逻辑,让回调异步执行。
  • 错误处理:在 emit 时捕获回调错误,避免影响其他订阅者(如 try...catch 包裹回调执行 )。

(二)介绍一下事件循环(Event Loop)

事件循环是 JavaScript 处理异步任务(如定时器、网络请求、DOM 渲染 )的机制,让单线程的 JS 能非阻塞地执行代码,核心是协调调用栈(Call Stack)、任务队列(Task Queue,含宏任务、微任务 )、渲染队列的执行顺序。

1. 核心流程(简化版 )

  1. 调用栈:执行同步代码,栈底是全局执行上下文,栈顶是当前执行的函数。

  2. 任务队列:分为宏任务(Macro Task)  和微任务(Micro Task)

    • 宏任务:setTimeoutsetIntervalDOM 渲染script 标签代码、I/O 操作等。
    • 微任务:Promise.then/catch/finallyMutationObserverqueueMicrotask 等。
  3. 执行顺序

    • 同步代码执行完毕(调用栈清空 )→ 执行所有微任务(清空微任务队列 )→ 执行一个宏任务 → 执行所有微任务 → …(循环往复,即事件循环 )。
    • 每次宏任务执行后,可能触发 DOM 渲染(浏览器环境 )。

2. 示例验证

js

console.log('同步1');

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

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

console.log('同步2');

// 执行顺序:
// 同步1 → 同步2 → 微任务: Promise.then → 宏任务: setTimeout 

3. 关键点

  • 微任务优先于宏任务:同一轮事件循环中,微任务队列清空后才会执行下一个宏任务。
  • 调用栈与任务队列分离:异步任务(如 setTimeout )的回调不会立即执行,而是先进入任务队列,等待调用栈清空后再执行。

(三)宏任务和微任务有什么区别

对比项宏任务(Macro Task)微任务(Micro Task)
包含类型setTimeoutsetIntervalDOM 渲染scriptI/O 等Promise.then/catch/finallyMutationObserverqueueMicrotask 等
执行时机一轮事件循环中,微任务全部执行完后,执行一个宏任务同步代码执行完后,立即执行所有微任务(当前宏任务执行阶段结束后 )
优先级低于微任务高于宏任务
对渲染的影响宏任务执行后可能触发 DOM 渲染(浏览器环境 )微任务执行在渲染前,可修改 DOM 且不触发多次渲染(性能更优 )

示例

js

console.log('同步');

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

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

// 输出顺序:同步 → 微任务 → 宏任务 

总结:微任务更 “紧急”,用于处理需要立即完成的异步逻辑(如 Promise 回调 );宏任务处理耗时或非紧急任务(如定时器、DOM 渲染 ),事件循环通过这种区分,保证了异步代码的有序执行和页面渲染的效率。

七、函数式编程

(一)介绍一下 JavaScript 中的函数式编程

函数式编程(Functional Programming,简称 FP )是一种编程范式,将计算视为数学函数的组合,强调纯函数不可变性函数是一等公民等特性,核心思想是用函数构建清晰、可维护、可复用的代码逻辑。

1. 核心特性

  • 函数是一等公民:函数可以作为参数传递、作为返回值返回、赋值给变量,如:

js

// 函数作为参数
const add = (a, b) => a + b;
const calculate = (fn, a, b) => fn(a, b);
console.log(calculate(add, 2, 3)); // 5 

// 函数作为返回值
const createAdder = (x) => (y) => x + y;
const add5 = createAdder(5);
console.log(add5(3)); // 8 
  • 纯函数:相同输入始终产生相同输出,无副作用(不修改外部状态、不依赖外部可变状态 )。

js

// 纯函数(输出仅由输入决定,无副作用)
const pureAdd = (a, b) => a + b; 

// 非纯函数(依赖外部可变变量 count,输出受其影响 )
let count = 0;
const impureAdd = (a) => a + count++; 
  • 不可变性:数据一旦创建,就不能修改,如需变化则返回新数据。

js

// 数组不可变操作(原数组不变,返回新数组 )
const arr = [1, 2, 3];
const newArr = arr.map((item) => item * 2); 
console.log(arr); // [1, 2, 3]
console.log(newArr); // [2, 4, 6] 
  • 声明式编程:关注 “做什么” 而非 “怎么做”,常用 mapfilterreduce 等高阶函数。

js

// 声明式(用 filter 描述要筛选偶数,不关心遍历过程 )
const numbers = [1, 2, 3, 4];
const evens = numbers.filter((num) => num % 2 === 0); 

// 命令式(需手动遍历、判断、push,关注过程 )
const evensCmd = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    evensCmd.push(numbers[i]);
  }
}

2. 常用概念与工具

  • 高阶函数:接收函数作为参数或返回函数的函数,如 mapfilterreducecompose(函数组合 )、curry(函数柯里化 )等。

  • 函数柯里化:将多参数函数转化为单参数函数序列,逐步接收参数并返回新函数,直到参数齐全执行计算。

js

const curryAdd = (a) => (b) => (c) => a + b + c;
const add1 = curryAdd(1);
const add1And2 = add1(2);
console.log(add1And2(3)); // 6 
  • 函数组合:将多个函数组合成一个新函数,前一个函数的输出作为后一个函数的输入。

js

const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const add = (x) => x + 1;
const multiply = (x) => x * 2;
const composedFn = compose(multiply, add);
console.log(composedFn(3)); // (3 + 1) * 2 = 8 
  • ** functor(函子 )**:包含值和映射函数的容器,用于处理副作用、异常等,如 Maybe 函子处理空值安全:

js

class Maybe {
  constructor(value) {
    this.value = value;
  }
  map(fn) {
    return this.value ? new Maybe(fn(this.value)) : new Maybe(null);
  }
}
const maybeValue = new Maybe(5);
const result = maybeValue.map((x) => x * 2).map((x) => x + 3);
console.log(result.value); // 13 

const maybeNull = new Maybe(null);
const nullResult = maybeNull.map((x) => x * 2);
console.log(nullResult.value); // null(安全处理,无报错 )

3. 优势与适用场景

  • 优势:代码更简洁、可复用性高、便于测试(纯函数易 Mock 和验证 )、减少副作用带来的 Bug、适合处理复杂数据变换逻辑。
  • 适用场景:数据处理与转换(如数组操作、函数管道 )、状态管理(Redux 中大量使用函数式思想,纯 reducer 函数 )、前端框架的响应式数据处理等。

(二)函数式编程有哪些核心原则,在 JavaScript 中如何体现

函数式编程的核心原则及 JavaScript 中的体现:

1. 纯函数(Pure Functions)

  • 原则:输入相同则输出相同,无副作用(不修改外部状态、不依赖外部可变数据 )。

  • JavaScript 体现

js

// 纯函数(计算依赖输入,不影响外部 )
const pureFn = (a, b) => a + b; 

// 非纯函数(修改外部数组,有副作用 )
let externalArr = [1, 2];
const impureFn = (x) => externalArr.push(x); 

2. 不可变性(Immutability)

  • 原则:数据创建后不可修改,变化时返回新数据。

  • JavaScript 体现

js

// 数组不可变操作(用 concat 替代 push )
const arr = [1, 2];
const newArr = arr.concat(3); // 原数组 [1,2] 不变,返回 [1,2,3] 

// 对象不可变操作(用扩展运算符或 Object.assign )
const obj = { a: 1 };
const newObj = { ...obj, b: 2 }; // 原对象不变,新对象 {a:1, b:2} 

3. 函数是一等公民(First-Class Functions)

  • 原则:函数可作为参数、返回值、赋值给变量,与其他数据类型同等对待。

  • JavaScript 体现

js

// 函数作为参数(回调函数 )
const arr = [1, 2];
const doubled = arr.map((x) => x * 2); 

// 函数作为返回值(柯里化 )
const createAdder = (x) => (y) => x + y;
const add5 = createAdder(5);

4. 声明式编程(Declarative Programming)

  • 原则:描述 “做什么”,而非 “怎么做”,强调结果导向。

  • JavaScript 体现

js

// 声明式(用 filter 描述筛选条件 )
const numbers = [1, 2, 3];
const evens = numbers.filter((x) => x % 2 === 0); 

// 命令式(需手动控制循环、判断 )
const evensCmd = [];
for (let i = 0; i < numbers.length; i++) {
  if (numbers[i] % 2 === 0) {
    evensCmd.push(numbers[i]);
  }
}

5. 避免副作用(Side Effects)

  • 原则:减少对外部环境的依赖和修改,如避免直接操作 DOM、全局变量等(实际开发中需平衡,可通过封装副作用到边界处理 )。

  • JavaScript 体现

js

// 有副作用(直接操作 DOM )
const badFn = () => {
  document.body.innerHTML = '<div>Hello</div>';
};

// 副作用封装(通过参数传入,便于测试和控制 )
const goodFn = (domEl) => {
  domEl.innerHTML = '<div>Hello</div>';
};

6. 函数组合与管道(Function Composition & Pipelining)

  • 原则:将多个简单函数组合成复杂逻辑,前一个函数输出作为后一个函数输入。

  • JavaScript 体现

js

// 函数组合(从右到左执行 )
const compose = (...fns) => (x) => fns.reduceRight((acc, fn) => fn(acc), x);
const add = (x) => x + 1;
const multiply = (x) => x * 2;
const composed = compose(multiply, add);
console.log(composed(3)); // (3 + 1) * 2 = 8 

// 函数管道(从左到右执行 )
const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
const piped = pipe(add, multiply);
console.log(piped(3)); // (3 + 1) * 2 = 8 

这些原则让函数式编程在 JavaScript 中能写出更简洁、可维护、可测试的代码,尤其在复杂数据处理、状态管理(如 Redux )场景中广泛应用。

八、Service Worker

(一)介绍一下 Service Worker,它的作用和使用场景是什么

Service Worker 是运行在浏览器后台的独立线程(与网页主线程分离 ),基于 Web Worker 实现,主要用于离线缓存网络代理后台同步等,是 PWA(渐进式 Web 应用 )的核心技术之一。

1. 核心作用

  • 离线缓存:拦截网络请求,根据缓存策略返回缓存内容(如缓存静态资源、API 响应 ),让网页离线可访问。
  • 网络代理:控制页面的网络请求,可修改请求、响应,实现自定义缓存逻辑、请求重定向等。
  • 后台同步:在页面关闭后,仍能在后台完成异步任务(如发送离线时积累的表单数据 )。
  • 推送通知:配合 Web Push 实现浏览器推送通知,即使页面未打开也能接收消息。

2. 使用场景

  • PWA 应用:实现离线可用、添加到主屏幕、推送通知等特性,提升 Web 应用体验,接近原生 App。
  • 静态资源缓存:缓存 HTML、CSS、JS、图片等静态文件,减少重复请求,加快页面加载(尤其弱网环境 )。
  • API 响应缓存:缓存接口数据,离线时返回缓存内容,或在网络恢复后更新缓存。
  • 后台任务:如定时同步数据、上传日志、处理离线操作队列等。

3. 基本使用流程

js

// 1. 注册 Service Worker(一般在页面入口脚本 )
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/service-worker.js')
      .then((registration) => {
        console.log('Service Worker 注册成功,作用域:', registration.scope);
      })
      .catch((error) => {
        console.log('Service Worker 注册失败:', error);
      });
  });
}

// 2. service-worker.js(编写缓存、拦截逻辑 )
self.addEventListener('fetch', (event) => {
  // 拦截 fetch 请求,自定义响应逻辑
  event.respondWith(
    caches.match(event.request) // 先查缓存
      .then((response) => {
        if (response) {
          return response; // 有缓存则返回
        }
        // 无缓存则发网络请求
        return fetch(event.request); 
      })
  );
});

self.addEventListener('install', (event) => {
  // 安装阶段缓存静态资源
  event.waitUntil(
    caches.open('my-cache-v1')
      .then((cache) => {
        return cache.addAll([
          '/',
          '/index.html',
          '/styles.css',
          '/app.js'
        ]);
      })
  );
});

4. 注意事项

  • 运行环境:需在 HTTPS 环境(本地开发可使用 localhost ),因为涉及拦截网络请求、访问敏感数据。
  • 生命周期:有 install(安装 )、activate(激活 )、fetch(拦截请求 )、sync(后台同步 )、push(推送 )等事件,需合理处理各阶段逻辑。
  • 缓存策略:需设计清晰的缓存更新、失效机制(如版本控制、缓存优先 / 网络优先策略 ),避免缓存过时或占用过多空间。

(二)Service Worker 的生命周期是怎样的,关键事件有哪些

Service Worker 有独立的生命周期,与网页的加载、关闭解耦,核心阶段和事件如下:

1. 生命周期阶段

  • 注册(Registration) :页面通过 navigator.serviceWorker.register() 触发,浏览器下载、解析 service-worker.js,进入 install 阶段。
  • 安装(Install) :触发 install 事件,可在此阶段缓存静态资源。若缓存成功,进入 waiting 状态(等待激活 );若失败,注册失败。
  • 激活(Activate) :旧 Service Worker 控制的页面全部关闭后,新 Service Worker 进入 activate 阶段,触发 activate 事件,可在此清理旧缓存、初始化状态。激活后,开始控制页面的网络请求(fetch 事件 )。
  • 运行(Running) :激活后,监听 fetchsyncpush 等事件,处理网络请求、后台任务等。
  • 终止(Termination) :空闲时会被浏览器终止(节省资源 ),但事件触发时会重新启动执行逻辑。

2. 关键事件

事件触发时机常用用途
installService Worker 注册后,首次下载、解析完成时触发缓存静态资源(如 HTML、CSS、JS ),调用 event.waitUntil() 等待缓存完成
activateService Worker 激活时触发(旧实例关闭,新实例开始控制页面 )清理旧版本缓存、初始化状态,调用 event.waitUntil() 处理异步操作(如删除旧缓存 )
fetch页面发起网络请求(fetch API、浏览器加载资源 )时触发拦截请求,自定义响应逻辑(从缓存取或发网络请求 ),实现离线缓存、请求重定向等
sync注册的后台同步任务触发时(如 registration.sync.register('taskName') )处理离线时积累的任务(如发送表单数据 ),网络恢复后执行
push服务器通过 Web Push 发送推送消息时触发(需配合推送订阅 )接收并展示推送通知,即使页面未打开
message主线程通过 serviceWorker.postMessage() 发消息时触发与主线程通信,接收指令或传递数据

3. 示例:处理 install 和 activate 事件

js

// service-worker.js
self.addEventListener('install', (event) => {
  // 强制跳过等待,让新 Service Worker 立即激活(可选,需谨慎 )
  // self.skipWaiting(); 
  event.waitUntil(
    caches.open('my-cache-v1')
      .then((cache) => {
        return cache.addAll([
          '/',
          '/index.html',
          '/styles.css',
          '/app.js'
        ]);
      })
  );
});

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys()
      .then((cacheNames) => {
        // 删除旧版本缓存
        return Promise.all(
          cacheNames.filter((cacheName) => cacheName !== 'my-cache-v1')
            .map((cacheName) => caches.delete(cacheName))
        );
      })
  );
});

理解 Service Worker 的生命周期和事件,才能合理设计缓存策略、后台任务,避免缓存冲突、更新不及时等问题,是实现 PWA 离线功能的核心。

九、Web Worker

(一)介绍一下 Web Worker,它的作用和使用场景是什么

Web Worker 是 HTML5 引入的特性,让 JavaScript 能在独立于主线程的后台线程中运行脚本,实现多线程并发(主线程负责 UI 交互,Worker 线程处理耗时任务 ),避免耗时操作阻塞主线程(导致页面卡顿、无响应 )。

1. 核心作用

  • 耗时任务 offload:将计算密集型任务(如大数据排序、复杂算法 )、I/O 操作(如大量数据解析 )放到 Worker 线程,不阻塞主线程的渲染、交互。
  • 并行计算:利用多线程同时处理任务,提升整体效率(如分块处理数据 )。
  • 线程隔离:Worker 线程有独立的全局上下文(self 替代 window ),与主线程通过消息传递通信,避免变量污染。

2. 使用场景

  • 数据处理:排序、过滤、解析大型 JSON 数据、图片