裸辞后的第二个月开始准备找工作,今天是第三天目前还没有面试,现在的行情是一言难尽,都在疯狂的压价。
下边是今天复习的个人笔记
一、事件循环
JavaScript 的事件循环(Event Loop)是其实现异步编程的关键机制。
从原理上讲,JavaScript 是单线程语言,只有一个主线程来执行代码,这意味着同一时间只能做一件事。但为了实现异步操作(比如处理用户交互、网络请求等),引入了事件循环机制。
事件循环涉及到几个重要概念:
- 调用栈(Call Stack): 是一种数据结构,用于记录函数的调用关系。函数调用时入栈,执行完毕后出栈。
- 任务队列(Task Queue): 也叫消息队列,用于存放异步操作的回调函数。当异步操作完成时,对应的回调函数会被放入任务队列。
- 宏任务(Macrotask): 包括 script (整体代码)、setTimeout、setInterval、setImmediate(Node.js 环境)、requestAnimationFrame 等。
- 微任务(Microtask): 包括 Promise 的 then、catch、finally,MutationObserver 等。
事件循环的执行过程大致如下:
- 首先执行调用栈中的同步任务。
- 当遇到异步任务时,异步任务会被挂起,不会阻塞主线程,继续执行同步任务。
- 当同步任务执行完毕后,开始处理微任务队列,依次执行微任务队列中的任务。
- 微任务执行完毕后,开始执行宏任务队列中的任务,每执行一个宏任务,就会检查并执行微任务队列。
- 重复上述过程,不断循环,这就是事件循环。
例如:
console.log('start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise then');
});
console.log('end');
在这段代码中,首先 console.log('start')
和 console.log('end')
作为同步任务在调用栈中依次执行。setTimeout 是宏任务,会被放到宏任务队列。Promise.resolve().then()
是微任务,会被放到微任务队列。当同步任务执行完后,开始执行微任务队列中的 Promise
的 then
回调,打印 Promise then
,最后执行宏任务队列中的 setTimeout
回调,打印 setTimeout
。
二、Promise.all 和 Promise.race
Promise.all 和 Promise.race ,它们都是 Promise 的静态方法,在处理多个 Promise 时非常有用,以下是它们的详细介绍:
Promise.all
- 它接受一个包含多个
Promise
对象的可迭代对象(比如数组)作为参数。 - 只有当传入的所有
Promise
都成功时,Promise.all 才会返回一个成功的Promise
,其结果是一个包含所有Promise
结果的数组,顺序和传入的Promise
顺序一致。 - 只要有一个
Promise
失败,Promise.all
就会立即返回一个失败的Promise
,失败原因就是第一个失败的Promise
的原因。
例如:
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); //输出: [1, 2, 3]
})
.catch((error) => {
console.error(error);
});
有失败的Promise
:
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(new Error('Promise 2 failed'));
const promise3 = Promise.resolve(3);
Promise.all([promise1, promise2, promise3])
.then((values) => {
console.log(values); // 不会输出
})
.catch((error) => {
console.error(error.message); // 输出: Promise 2 failed
});
Promise.race
- 同样接受一个包含多个
Promise
对象的可迭代对象作为参数。 - 只要其中一个
Promise
率先改变状态(无论是成功还是失败),Promise.race
就会返回这个Promise
的结果或原因。
例如:
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Promise 1 resolved');
}, 2000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Promise 2 failed'));
}, 1000);
});
Promise.race([promise1, promise2])
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error.message); // Promise 2 failed
});
简单来说,Promise.all
强调所有 Promise
都成功,Promise.race
则关注谁先改变状态。
纯JS实现Promise,并集成all和race
下面是一个简单实现 Promise 并添加 all 和 race 方法的代码示例,解释了其基本原理和实现思路:
// 自定义Promise类
function MyPromise(executor) {
this.status = 'pending';
this.value = null;
this.reason = null;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (val) => {
if (this.status === 'pending') {
this.status = 'fulfilled';
this.value = val;
this.onResolvedCallbacks.forEach(callback => callback(this.value));
}
};
const reject = (err) => {
if (this.status === 'pending') {
this.status ='rejected';
this.reason = err;
this.onRejectedCallbacks.forEach(callback => callback(this.reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
// Promise.prototype.then方法实现
MyPromise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function'? onFulfilled : value => value;
onRejected = typeof onRejected === 'function'? onRejected : reason => { throw reason };
let nextPromise;
if (this.status === 'fulfilled') {
nextPromise = new MyPromise((resolve, reject) => {
try {
const x = onFulfilled(this.value);
resolvePromise(nextPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
});
}
if (this.status ==='rejected') {
nextPromise = new MyPromise((resolve, reject) => {
try {
const x = onRejected(this.reason);
resolvePromise(nextPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
});
}
if (this.status === 'pending') {
nextPromise = new MyPromise((resolve, reject) => {
this.onResolvedCallbacks.push((value) => {
try {
const x = onFulfilled(value);
resolvePromise(nextPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
});
this.onRejectedCallbacks.push((reason) => {
try {
const x = onRejected(reason);
resolvePromise(nextPromise, x, resolve, reject);
} catch (error) {
reject(error);
}
});
});
}
return nextPromise;
};
// 辅助函数,处理then方法中返回值的逻辑
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
if (x instanceof MyPromise) {
x.then(resolve, reject);
} else if (typeof x === 'object' || typeof x === 'function') {
if (x === null) {
return resolve(x);
}
let called = false;
try {
const then = x.then;
if (typeof then === 'function') {
then.call(
x,
(y) => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
(r) => {
if (called) return;
called = true;
reject(r);
}
);
} else {
resolve(x);
}
} catch (error) {
if (called) return;
called = true;
reject(error);
}
} else {
resolve(x);
}
}
// 实现Promise.all方法
MyPromise.all = function (promises) {
return new MyPromise((resolve, reject) => {
const result = [];
let count = 0;
if (promises.length === 0) {
resolve(result);
} else {
promises.forEach((p, index) => {
MyPromise.resolve(p)
.then((value) => {
result[index] = value;
count++;
if (count === promises.length) {
resolve(result);
}
})
.catch((error) => {
reject(error);
});
});
}
});
};
// 实现Promise.race方法
MyPromise.race = function (promises) {
return new MyPromise((resolve, reject) => {
promises.forEach((p) => {
MyPromise.resolve(p)
.then((value) => {
resolve(value);
})
.catch((error) => {
reject(error);
});
});
});
};
使用自定义的 MyPromise
:
// 使用示例
const promise1 = new MyPromise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const promise2 = new MyPromise((resolve, reject) => {
setTimeout(() => {
reject(new Error('Promise 2 failed'));
}, 500);
});
// 使用then方法
promise1
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error);
});
// 使用all方法
MyPromise.all([promise1, promise2])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.error(error);
});
// 使用race方法
MyPromise.race([promise1, promise2])
.then((value) => {
console.log(value);
})
.catch((error) => {
console.error(error);
});
在上述代码中,首先定义了一个 MyPromise 类,实现了基本的 Promise 功能,包括 then 方法。然后添加了 all 和 race 静态方法,分别用于按顺序处理多个 Promise(all)和谁先有结果就返回谁(race)。resolvePromise 函数则处理了 then 方法中返回值的复杂逻辑,确保遵循 Promise/A+ 规范。
三、闭包
闭包是面试中常见的一个考点,复习也是很有必要的,在工作中使用闭包的场景很多比如在Vue和React组件就是个大闭包,还有防抖节流等函数的封装等等。
1. 什么是闭包?
闭包是指函数和与其相关的词法环境的组合。当一个内部函数在其外部函数返回后仍然能访问外部函数的变量时,就创建了闭包。
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
const closureFn = outer();
closureFn(); // 1
closureFn(); // 2
在这个例子中,inner
函数形成了闭包,即使 outer
函数已经执行完毕,inner
函数依然可以访问 outer
函数作用域内的 count
变量。
2. 闭包有什么作用?
- 数据私有性: 可以隐藏变量,通过闭包,外部代码无法直接访问函数内部的变量,只能通过闭包返回的函数来操作这些变量,实现数据的封装。
- 状态保存: 闭包能记住创建时的状态,比如在计数器的例子中,每次调用闭包函数,都能记住上次 count 的值并进行操作。
- 柯里化: 闭包是实现函数柯里化的基础,柯里化可以将多参数函数转化为一系列单参数函数,提高函数的复用性和灵活性。柯里化后边会延伸描述
3. 闭包可能会带来什么问题?
- 内存泄漏: 如果闭包函数一直存在,并且引用了一些大的对象或不再需要的变量,这些变量不会被垃圾回收机制回收,可能会导致内存占用过高,出现内存泄漏。例如:
function createBigObject() { const bigArray = new Array(1000000).fill(0); return function () { // 闭包函数一直存在,bigArray无法被回收 console.log('closure'); }; } const leakyClosure = createBigObject();
- 变量的值不是预期的: 在循环中使用闭包时,如果不注意,可能会得到意外的结果。比如:
可以通过立即执行函数或使用const functions = []; for (var i = 0; i < 5; i++) { functions.push(() => { console.log(i); }); } functions.forEach(fn => fn()); // 输出 5 5 5 5 5,因为这里的i是最后循环结束时的值
let
关键字来解决这个问题。如下:const functions = []; for (let i = 0; i < 5; i++) { functions.push(() => { console.log(i); }); } functions.forEach(fn => fn()); // 输出 0 1 2 3 4
4. 如何避免闭包导致的内存泄漏?
当闭包不再使用时,手动将闭包函数赋值为 null
,这样相关的变量就可以被垃圾回收机制回收。例如:
function createClosure() {
let data = { a: 1 };
return function () {
console.log(data.a);
};
}
let closureFn = createClosure();
// 使用闭包函数
closureFn();
// 不再使用闭包时,将其赋值为null
closureFn = null;
5. 实际开发中,不专门手动将闭包引用的变量置为null
实际开发中,闭包导致的内存泄漏本质是 “外部引用未正确释放”,而非闭包语法本身的问题。现代 GC 机制和框架已能处理大部分场景,手动置空闭包变量既不现实也无必要。开发者的核心任务是:
- 正确管理外部依赖(移除事件监听、清除定时器、避免不合理的全局引用);
- 依赖框架的生命周期钩子处理副作用,让闭包随上下文自然释放。
只有在极端或不规范的场景下,才需针对性地手动清理,但这也应优先通过切断外部引用来实现,而非直接操作闭包内部的变量。
四、柯里化
**柯里化(Currying)**是一种在函数式编程中广泛使用的技术,它允许你将一个多参数函数转换为一系列单参数函数。以下从定义、原理、用途、示例等方面详细介绍柯里化。
定义与原理
- 定义: 柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。简单来说,就是将一个多参数函数拆分成多个单参数函数。
- 原理: 利用闭包的特性,让函数记住之前传入的参数,当参数数量达到原函数所需的参数数量时,执行原函数逻辑。
用途
参数复用: 当多次调用同一个函数,并且传递的参数大部分相同时,使用柯里化可以复用这些相同的参数。
延迟计算: 可以在需要的时候再传入剩余的参数进行计算,而不是一次性传入所有参数。
动态创建函数: 根据不同的参数动态生成不同的函数。
示例
以下是一个简单的 JavaScript 示例,展示如何实现柯里化:
// 定义一个普通的加法函数
function add(a, b) {
return a + b;
}
// 实现柯里化函数
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function (...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
}
};
}
// 将 add 函数进行柯里化
const curriedAdd = curry(add);
// 使用柯里化函数
const add5 = curriedAdd(5);
console.log(add5(3)); // 输出 8
代码解释
- curry 函数: 接受一个函数 func 作为参数,返回一个新的函数 curried。
- curried 函数: 接受任意数量的参数 args,如果 args 的长度大于或等于原函数 func 的参数长度,则直接调用 func 并返回结果;否则,返回一个新的函数,该函数会将之前的参数和新传入的参数合并后再次调用 curried 函数。
- curriedAdd 函数: 是 add 函数柯里化后的结果。通过 curriedAdd(5) 得到一个新的函数 add5,这个函数记住了之前传入的参数 5,当调用 add5(3) 时,将 5 和 3 相加并返回结果。
实际应用场景
- **日志记录:**在日志记录时,通常会有一些固定的参数(如日志级别、日志来源等),可以使用柯里化来复用这些参数。
function log(level, source, message) { console.log(`[${level}] [${source}] ${message}`); } const curriedLog = curry(log); const errorLog = curriedLog('ERROR'); const appErrorLog = errorLog('App'); appErrorLog('Something went wrong!'); // 输出 [ERROR] [App] Something went wrong!
- 事件处理: 在处理事件时,可能需要传递一些额外的参数,可以使用柯里化来动态创建事件处理函数。
在这个示例中,通过柯里化创建了一个带有固定消息的事件处理函数<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> </head> <body> <button id="myButton">Click me</button> <script> function handleClick(message, event) { console.log(`${message}: ${event.type}`); } const curriedHandleClick = curry(handleClick); const clickWithMessage = curriedHandleClick('Button clicked'); const button = document.getElementById('myButton'); button.addEventListener('click', clickWithMessage); </script> </body> </html>
clickWithMessage
,当按钮被点击时,会输出相应的日志信息。
不推荐使用柯里化的场景
- 简单一次性调用: 若函数仅调用一次,且参数无复用可能,直接调用普通函数更高效(如
sum(1, 2, 3)
无需柯里化)。 - 参数顺序依赖强或易混淆: 柯里化要求严格按参数顺序传参,若参数含义不明确(如
log('ERROR', 'App', '消息')
中 ‘App’ 可能是来源或消息),可能导致调用时参数错位。 - 追求极致性能的场景: 虽然现代引擎优化较好,但柯里化涉及闭包和多层函数嵌套,在极端高频调用(如循环内)时可能存在微小性能损耗(需实测验证)
总结
- 用: 当需要参数复用、延迟计算、函数组合或动态生成定制函数,且代码风格兼容函数式思维时。
- 慎: 当参数逻辑复杂、可读性可能受损,或团队对 FP 不熟悉时,优先考虑更直观的参数传递方式(如对象、默认参数)。
柯里化的核心价值在于将 “不变的部分” 与 “变化的部分” 分离,通过函数的 “预配置” 提高代码的灵活性和复用性。实际开发中,可从小规模场景(如工具函数、配置类函数)开始尝试,逐步判断是否符合项目需求。