一、执行上下文 / 作用域链 / 闭包
(一)介绍一下 JavaScript 的执行上下文
执行上下文是 JavaScript 代码执行的环境,包含变量环境、词法环境等信息,主要分三类:
-
全局执行上下文:程序运行时创建的基础环境,浏览器关联
window(globalThis规范后更通用),一个程序仅 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.jsglobal);严格模式下,全局环境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.all、Promise.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);
(二)实现深拷贝需要注意哪些问题
-
循环引用:对象属性引用自身(如
obj.self = obj),若不处理会导致递归无限调用,栈溢出。需用缓存(如WeakMap)记录已拷贝对象,避免重复处理。 -
特殊对象类型:
- 内置对象(正则
RegExp、日期Date、Set、Map等 ):直接赋值会复制引用,需手动创建新实例(如new RegExp(target))。 - 函数:一般场景下深拷贝函数意义不大(函数拷贝后作用域可能变化 ),可选择直接引用或忽略(根据需求决定是否复制函数 )。
- 内置对象(正则
-
原型链属性:
for...in遍历默认包含原型链属性,需用Object.prototype.hasOwnProperty.call过滤,避免拷贝无关属性。 -
性能问题:深拷贝递归遍历所有层级,嵌套过深或数据量大时,会占用较多内存和时间,需根据场景权衡(如大对象用
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. 核心流程(简化版 )
-
调用栈:执行同步代码,栈底是全局执行上下文,栈顶是当前执行的函数。
-
任务队列:分为宏任务(Macro Task) 和微任务(Micro Task) :
- 宏任务:
setTimeout、setInterval、DOM 渲染、script标签代码、I/O操作等。 - 微任务:
Promise.then/catch/finally、MutationObserver、queueMicrotask等。
- 宏任务:
-
执行顺序:
- 同步代码执行完毕(调用栈清空 )→ 执行所有微任务(清空微任务队列 )→ 执行一个宏任务 → 执行所有微任务 → …(循环往复,即事件循环 )。
- 每次宏任务执行后,可能触发 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) |
|---|---|---|
| 包含类型 | setTimeout、setInterval、DOM 渲染、script、I/O 等 | Promise.then/catch/finally、MutationObserver、queueMicrotask 等 |
| 执行时机 | 一轮事件循环中,微任务全部执行完后,执行一个宏任务 | 同步代码执行完后,立即执行所有微任务(当前宏任务执行阶段结束后 ) |
| 优先级 | 低于微任务 | 高于宏任务 |
| 对渲染的影响 | 宏任务执行后可能触发 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]
-
声明式编程:关注 “做什么” 而非 “怎么做”,常用
map、filter、reduce等高阶函数。
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. 常用概念与工具
-
高阶函数:接收函数作为参数或返回函数的函数,如
map、filter、reduce、compose(函数组合 )、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) :激活后,监听
fetch、sync、push等事件,处理网络请求、后台任务等。 - 终止(Termination) :空闲时会被浏览器终止(节省资源 ),但事件触发时会重新启动执行逻辑。
2. 关键事件
| 事件 | 触发时机 | 常用用途 |
|---|---|---|
install | Service Worker 注册后,首次下载、解析完成时触发 | 缓存静态资源(如 HTML、CSS、JS ),调用 event.waitUntil() 等待缓存完成 |
activate | Service 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 数据、图片