通過這個簡單的技巧讓我們的 JavaScript 代碼變得異常快
秘訣:了解JavaScript 虛擬機(VM)的內部工作原理。
首先,我們來談談像 V8
這樣的JavaScript
虛擬機(VM
)。可以把它想象成我們的操作的大腦 —— 它將我們簡潔的代碼變成計算機可以理解和執行的東西。
好的、壞的代碼
讓我們深入研究一些代碼示例,看看好的、壞的和執行快的代碼。
緩慢的 JavaScript
示例:
function addProperty(obj, propName, value) {obj[propName] = value; // 這會改變對象的形狀
}
const responseObject = { user1: 1, user2: 2 };
addProperty(responseObject, 'user3', 3); // 添加新的屬性
是什么讓它變慢?
形狀更改:每次調用 addProperty
函數時,都會向對象添加一個新屬性。這會改變對象的“形狀”,即它包含的鍵變了,這反過來又會顛覆 JavaScript
引擎的優化。
添加或刪除屬性時,引擎可能必須丟棄以前的優化信息并重新開始。這種“形狀變化”就是操作緩慢的原因。
快速 JavaScript
示例:
function createObject ( a, b, c ) {
// 對象的形狀是固定的并且VM可以預測return { a, b, c };
}
const dataObject = createObject ( 212,2344,43545);
是什么讓它如此快速?
可預測的形狀:
該對象是使用一組固定的屬性創建的。創建后沒有任何變化,更容易引擎優化。
隱藏類重用:
由于每次調用 createObject
時對象的形狀都是一致的,因此 JavaScript
引擎可以重用為此形狀創建的隱藏類。這種重用允許非常快速的屬性訪問,因為引擎確切地知道每個屬性在內存中的位置。
為什么對象形狀很重要:
當我們訪問對象的屬性時,引擎不想搜索所有屬性來找到它。相反,它想要直接訪問該屬性在內存中的位置。如果對象的形狀已知,引擎可以記住每個屬性所在的位置(這稱為“內聯緩存”)。但是,如果形狀發生變化(如上面的緩慢示例),引擎必須“重新學習”屬性位置,這要慢得多。
為了獲得最佳性能,特別是在頻繁訪問屬性的代碼關鍵部分,最好:
- 創建對象時初始化所有屬性:即使某些屬性最初可能未定義。
- 避免添加或刪除屬性:這可以保持對象的形狀穩定。
- 盡可能重用對象形狀:創建始終生成具有相同屬性集的對象的工廠函數。
通過遵循這些實踐,我們可以幫助 JavaScript
引擎優化我們的代碼,從而加快執行速度。
常見的用例
當處理來自外部源的對象(例如 API
響應或 DOM
元素)時,在使用這些對象之前將它們規范化為一致的形狀對性能有益。這允許 JavaScript
引擎優化對這些對象的訪問,因為形狀(對象內的所有鍵)是可預測的并且不會改變。當我們頻繁讀取對象時,這種做法尤其有價值。
讓我們來看下面兩個常見的示例
通過 API
獲取用戶信息
慢速版本:
在慢速版本中,屬性被一一添加到對象中,這可能會導致 JavaScript
引擎由于形狀變化而取消對對象的訪問優化。
function fetchUserProfile(url) {fetch(url).then(response => response.json()).then(user => {const userProfile = {};if (user.name) {userProfile.name = user.name;}if (user.age) {userProfile.age = user.age;}if (user.email) {userProfile.email = user.email;}// ...處理更多的屬性return userProfile;});
}
快速版本:
在快速版本中,我們從一開始就創建一個具有已知形狀的對象,即使某些屬性可能未定義。這種一致性允許 JavaScript
引擎優化屬性訪問。
function fetchUserProfile(url) {return fetch(url).then(response => response.json()).then(user => {// 先定義對象中包含的所有的屬性const userProfile = {name: user.name || undefined,age: user.age || undefined,email: user.email || undefined,// ... 初始化更多屬性};return userProfile;});
}
在快速版本中,即使用戶對象不具有我們分配給 userProfile
的所有屬性,我們仍然使用其相應的值或未定義的值來定義我們期望的所有鍵。這樣,userProfile
的形狀保持一致,這有利于稍后訪問其屬性時的性能。
這種做法對于性能關鍵型應用程序至關重要,優化可以極大地提高執行速度。
如果上面的例子讓我們想起了什么,那是因為這個模式看起來像工廠模式,它遵循類似于工廠函數的原則,通過創建一個具有預定義形狀的對象,但它并不完全是這樣。在 JavaScript
中,工廠模式通常涉及構造并返回新對象的專用函數。當創建過程復雜或需要執行一些額外的設置工作時,工廠函數特別有用。
使用工廠模式
在給定的快速示例中,我們看到了一種創建具有一致形狀的對象的方法。為了使其與工廠模式更加一致,我們可以將對象創建封裝在專用函數中,如下所示:
function createUserProfile(name, age, email) {
// 通過工廠模式創建對象return {name: name || undefined,age: age || undefined,email: email || undefined,// ...};}function fetchUserProfile(url) {return fetch(url).then(response => response.json()).then(user => {return createUserProfile(user.name, user.age, user.email);});
}
在這個版本中,createUserProfile
是一個工廠函數,總是創建具有相同形狀的對象,這有利于優化。fetchUserProfile
函數使用此工廠創建一個新的 userProfile
對象。
使用DOM
現在讓我們討論另一個常見的示例,在使用 DOM
時,我們經常需要從 HTML
元素讀取信息,然后在應用程序中使用這些數據。保持對象形狀一致對于性能非常重要,尤其是當我們重復執行這些操作時。
下面的示例演示了對象形狀發生變化的慢速代碼示例,以及對象形狀可預測且一致的快速方法。
慢速代碼示例
function getUserData() {const userObject = {};const userName = document.querySelector('#input-name');if (nameElement) {userObject.name = nameElement.textContent;}const userAge = document.querySelector('#input-age');if (ageElement) {userObject.age = parseInt(ageElement.textContent);}// 每次調用此函數時,它可能會也可能不會添加新屬性// 這可能會導致對象形狀發生變化return userObject;
}
快速代碼示例
function createUserData(name = undefined, age = undefined) {
// 始終返回具有相同形狀的對象的工廠函數return { name, age };
}
function getUserData() {const userName = document.querySelector('#input-name');const userAge = parseInt(document.querySelector('#input-age')?.textContent);// 無論元素是否存在,對象的形狀都是一致的return createUserData(userName?.textContent, Number.isNaN(userAge) ? undefined : userAge);
}
在上面代碼中,createUserData
工廠函數確保返回的對象始終具有相同的形狀,這有利于 JavaScript
引擎的優化過程。getUserData
函數使用此工廠函數來創建配置文件數據對象,并通過提供 undefined
作為默認值來處理丟失的 DOM
元素,從而維護對象的形狀。
通過使用可選鏈接運算符 (?.
) 和空合并運算符 (??
),我們可以進一步細化該函數以處理 DOM
元素可能不存在的情況:
function getUserData() {const name = document.querySelector('#input-name')?.textContent ?? undefined;const ageText = document.querySelector('#input-age')?.textContent ?? undefined;const age = ageText ? parseInt(ageText) : undefined;// 對象的形狀一致return createUserData(name, age);
}
這種方法可以確保對象的形狀保持不變,即使在 DOM
中找不到某些元素,這在動態 Web
應用程序中很常見,因為有時元素尚未渲染或元素渲染順序不正確。