裸辭后的第二個月開始準備找工作,今天是第三天目前還沒有面試,現在的行情是一言難盡,都在瘋狂的壓價。
下邊是今天復習的個人筆記
一、事件循環
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 不熟悉時,優先考慮更直觀的參數傳遞方式(如對象、默認參數)。
柯里化的核心價值在于將 “不變的部分” 與 “變化的部分” 分離,通過函數的 “預配置” 提高代碼的靈活性和復用性。實際開發中,可從小規模場景(如工具函數、配置類函數)開始嘗試,逐步判斷是否符合項目需求。