
JavaScript 不提供任何記憶體管理作業。相反,記憶體由 JavaScript VM 透過記憶體回收過程管理,該過程稱為垃圾收集。 【相關推薦:javascript學習教學】
既然我們不能強制的垃圾回收,那我們怎麼知道它能正常運作?我們對它又了解多少呢?
- 腳本執行在此過程中暫停
- 它為不可存取的資源釋放記憶體
- 它是不確定的
- 它不會一次檢查整個內存,而是在多個週期中運行
- 它是不可預測的,但它會在必要時執行
##這是否意味著無需擔心資源和內存分配問題?當然不是。如果我們一不小心,可能會產生一些記憶體洩漏。

什麼是記憶體洩漏?
記憶體洩漏是軟體無法回收的已分配的記憶體區塊。
Javascript 提供了一個垃圾收集程序,但這並不代表我們就能避免記憶體洩漏。為了符合垃圾收集的條件,該物件必須不被其他地方引用。如果持有未使用的資源的引用,這將會阻止這些資源被回收。這就是所謂的
無意識的記憶體保持。
洩漏記憶體可能會導致垃圾收集器更頻繁地運作。由於這個過程會阻止腳本的運行,它可能會讓我們程式卡起來,這麼一卡,挑剔的用戶肯定會注意到,一用不爽了,那這個產品離下線的日子就不完了。更嚴重可能會讓整個應用程式奔潰,那就gg了。
如何防止記憶體洩漏? 主要還是我們應該避免保留不必要的資源。來看看一些常見的場景。
1.計時器的監聽
setInterval() 方法重複呼叫函數或執行程式碼片段,每次呼叫之間有固定的時間延遲。它會傳回一個時間間隔
ID,該
ID唯一地標識時間間隔,因此您可以稍後透過呼叫
clearInterval() 來刪除它。
我們建立一個元件,它呼叫一個回調函數來表示它在
x個循環之後完成了。我在這個例子中使用React,但這適用於任何FE框架。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import React, { useRef } from 'react' ;
const Timer = ({ cicles, onFinish }) => {
const currentCicles = useRef(0);
setInterval(() => {
if (currentCicles.current >= cicles) {
onFinish();
return ;
}
currentCicles.current++;
}, 500);
return (
<p>Loading ...</p>
);
}
export default Timer;
|
登入後複製
一看,好像沒啥問題。不急,我們再建立一個觸發這個定時器的元件,並分析其記憶體效能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import React, { useState } from 'react' ;
import styles from '../styles/Home.module.css'
import Timer from '../components/Timer' ;
export default function Home() {
const [showTimer, setShowTimer] = useState();
const onFinish = () => setShowTimer(false);
return (
<p>
{showTimer ? (
<timer></timer>
): (
<button> setShowTimer(true)}>
Retry
</button>
)}
</p>
)
}
|
登入後複製
在
Retry 按鈕上點擊幾次後,這是使用Chrome Dev Tools取得記憶體使用的結果:

當我們點擊重試按鈕時,可以看到分配的記憶體越來越多。這說明之前分配的記憶體沒有被釋放。計時器仍然在運作而不是被替換。
怎麼解決這個問題?
setInterval 的回傳值是一個間隔 ID,我們可以用它來取消這個間隔。在這種特殊情況下,我們可以在元件卸載後呼叫
clearInterval。
1 2 3 4 5 6 7 8 9 10 11 | useEffect(() => {
const intervalId = setInterval(() => {
if (currentCicles.current >= cicles) {
onFinish();
return ;
}
currentCicles.current++;
}, 500);
return () => clearInterval(intervalId);
}, [])
|
登入後複製
有時,在寫程式碼時,很難發現這個問題,最好的方式,還是要把元件抽象化。
這裡使用的是React,我們可以把所有這些邏輯都包裝在一個自訂的 Hook 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import { useEffect } from 'react' ;
export const useTimeout = (refreshCycle = 100, callback) => {
useEffect(() => {
if (refreshCycle {
callback();
}, refreshCycle);
return () => clearInterval(intervalId);
}, [refreshCycle, setInterval, clearInterval]);
};
export default useTimeout;
|
登入後複製
現在需要使用
setInterval時,都可以這樣做:
1 2 3 | const handleTimeout = () => ...;
useTimeout(100, handleTimeout);
|
登入後複製
現在你可以使用這個
useTimeout Hook,而不必擔心記憶體被洩露,這也是抽象化的好處。
2.事件監聽
Web API提供了大量的事件監聽器。在前面,我們討論了
setTimeout。現在來看看
addEventListener。
在這個例子中,我們建立一個鍵盤快速鍵功能。由於我們在不同的頁面上有不同的功能,所以將創建不同的快捷鍵功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function homeShortcuts({ key}) {
if (key === 'E' ) {
console.log( 'edit widget' )
}
}
document.addEventListener( 'keyup' , homeShortcuts);
function settingsShortcuts({ key}) {
if (key === 'E' ) {
console.log( 'edit setting' )
}
}
document.addEventListener( 'keyup' , settingsShortcuts);
|
登入後複製
看起來還是很好,除了在執行第二個
addEventListener 時沒有清理之前的
keyup。這段程式碼不是取代我們的
keyup 監聽器,而是將增加另一個
callback。這意味著,當一個鍵被按下時,它將觸發兩個函數。
要清除先前的回調,我們需要使用
removeEventListener :
1 | document.removeEventListener(‘keyup’, homeShortcuts);
|
登入後複製
重構上面的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function homeShortcuts({ key}) {
if (key === 'E' ) {
console.log( 'edit widget' )
}
}
document.addEventListener( 'keyup' , homeShortcuts);
function settingsShortcuts({ key}) {
if (key === 'E' ) {
console.log( 'edit setting' )
}
}
document.removeEventListener( 'keyup' , homeShortcuts);
document.addEventListener( 'keyup' , settingsShortcuts);
|
登入後複製
根據經驗,當使用來自全域物件的工具時,需要灰常小心。
3.Observers
Observers 是一個瀏覽器的 Web API功能,很多開發者都不知道。如果你想檢查HTML元素的可見性或大小的變化,這個就很強大了。
IntersectionObserver
接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport
)交叉状态的方法。祖先元素与视窗(viewport
)被称为根(root
)。
尽管它很强大,但我们也要谨慎的使用它。一旦完成了对对象的观察,就要记得在不用的时候取消它。
看看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const ref = ...
const visible = (visible) => {
console.log(`It is ${visible}`);
}
useEffect(() => {
if (!ref) {
return ;
}
observer.current = new IntersectionObserver(
(entries) => {
if (!entries[0].isIntersecting) {
visible(true);
} else {
visbile(false);
}
},
{ rootMargin: `-${header.height}px` },
);
observer.current.observe(ref);
}, [ref]);
|
登入後複製
上面的代码看起来不错。然而,一旦组件被卸载,观察者会发生什么?它不会被清除,那内存可就泄漏了。我们怎么解决这个问题呢?只需要使用 disconnect
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | const ref = ...
const visible = (visible) => {
console.log(`It is ${visible}`);
}
useEffect(() => {
if (!ref) {
return ;
}
observer.current = new IntersectionObserver(
(entries) => {
if (!entries[0].isIntersecting) {
visible(true);
} else {
visbile(false);
}
},
{ rootMargin: `-${header.height}px` },
);
observer.current.observe(ref);
return () => observer.current?.disconnect();
}, [ref]);
|
登入後複製

4. Window Object
向 Window 添加对象是一个常见的错误。在某些场景中,可能很难找到它,特别是在使用 Window Execution上下文中的this
关键字。看看下面的例子:
1 2 3 4 5 6 7 8 9 | function addElement(element) {
if (!this.stack) {
this.stack = {
elements: []
}
}
this.stack.elements.push(element);
}
|
登入後複製
它看起来无害,但这取决于你从哪个上下文调用addElement
。如果你从Window Context调用addElement,那就会越堆越多。
另一个问题可能是错误地定义了一个全局变量:
1 2 | var a = 'example 1' ;
b = 'example 2' ;
|
登入後複製
要防止这种问题可以使用严格模式:
通过使用严格模式,向JavaScript编译器暗示,你想保护自己免受这些行为的影响。当你需要时,你仍然可以使用Window。不过,你必须以明确的方式使用它。
严格模式是如何影响我们前面的例子:
- 对于
addElement
函数,当从全局作用域调用时,this
是未定义的
- 如果没有在一个变量上指定
const | let | var
,你会得到以下错误:
1 | Uncaught ReferenceError: b is not defined
|
登入後複製
5. 持有DOM引用
DOM节点也不能避免内存泄漏。我们需要注意不要保存它们的引用。否则,垃圾回收器将无法清理它们,因为它们仍然是可访问的。
用一小段代码演示一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | const elements = [];
const list = document.getElementById( 'list' );
function addElement() {
list.innerHTML = '' ;
const pElement= document.createElement( 'p' );
const element = document.createTextNode(`adding element ${elements.length}`);
pElement.appendChild(element);
list.appendChild(pElement);
elements.push(pElement);
}
document.getElementById( 'addElement' ).onclick = addElement;
|
登入後複製
注意,addElement
函数清除列表 p
,并将一个新元素作为子元素添加到它中。这个新创建的元素被添加到 elements
数组中。
下一次执行 addElement
时,该元素将从列表 p
中删除,但是它不适合进行垃圾收集,因为它存储在 elements
数组中。
我们在执行几次之后监视函数:

在上面的截图中看到节点是如何被泄露的。那怎么解决这个问题?清除 elements
数组将使它们有资格进行垃圾收集。
总结
在这篇文章中,我们已经看到了最常见的内存泄露方式。很明显,JavaScript本身并没有泄漏内存。相反,它是由开发者方面无意的内存保持造成的。只要代码是整洁的,而且我们不忘自己清理,就不会发生泄漏。
了解内存和垃圾回收在JavaScript中是如何工作的是必须的。一些开发者得到了错误的意识,认为由于它是自动的,所以他们不需要担心这个问题。
作者: Jose Granja
原文:https://betterprogramming.pub/5-common-javascript-memory-mistakes-c8553972e4c2
(学习视频分享:web前端)
以上是5個常見的JavaScript記憶體錯誤的詳細內容。更多資訊請關注PHP中文網其他相關文章!