iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Modern Web

前端常見問題攻略系列 第 8

JS 記憶體釋放機制及驗證

  • 分享至 

  • xImage
  •  

作用域

JavaScript 的變數有作用域的範圍,意思是指「宣告的變數有作用的區域限制」,如果超出了作用域則變數無法再被取得,這樣的做法優點為:

  • 可以避免所有變數轉為全域變數
  • 有效限制變數的作用區域

而變數作用域也會依據宣告的方式不同,產生不同的作用域:

  • 未宣告:全域變數
  • var 宣告:作用域在 函式
  • letconst 宣告:作用域在 {}

var 宣告的變數

函式內宣告的變數作用域就會限制在該函式的執行堆疊中,因此外部會無法直接取得該作用域內的變數。以下範例來說 fn 函式內的變數在全域環境終究無法查看值。

function fn() {
  var a = 1;
}
fn();
console.log(a); // 無法取得 fn 函式內的 a 變數

因此,會很常見使用 "立即函式" 來限制變數的作用域,主要是避免全域變數的產生。

(function() {
  var b = 1;
})();
console.log(b); // 無法取得 fn 函式內的 b 變數

let、const 宣告的變數

ES6 以後所新增的 letconst 作用域則與過去不同,改用 {} 作為作用域限制的方式,這讓 for 迴圈及部分的語法避免產生多餘變數來影響作用域(案例參考)。

var 不同的是 const 所定義的變數作用域限制在 {} 之中。因此此範例中的變數 c 可在外部取得值,d 則無法取得。

{
  var c = 1;
  const d = 1;
}
console.log(c); // 1
console.log(d); // Uncaught ReferenceError: d is not defined,無法取得 d 的變數

記憶體與釋放機制

每當我們新增一個變數則會產生一個記憶體位置來除存值,以便於接下來程式運作時可以反覆的使用同一個值。

如以下範例中,就會開啟一個 a 的空間來儲存數字 1 的值。

var a = 1

流程如下:

  1. 產生一個記憶體空間來存放 a 變數,此時還沒有賦予值(概念可參考:[Hoisting]https://ithelp.ithome.com.tw/upload/images/20200923/20083608BAIK6XSOVk.png
  2. a 指派一個值。
    https://ithelp.ithome.com.tw/upload/images/20200923/20083608pJb4KSw6Bd.png

因此所有的變數都會佔用記憶體空間,除此之外物件、陣列的屬性以及函式參數也會使用相同的概念進行佔用。呼叫一個函式時,每一個函式作用域也都會反覆的進行記憶體佔用。隨著應用程式越來越複雜的情況下,如果持續佔用記憶體沒有適當的釋放,那麼系統可能會無法負荷。

JavaScript 引擎具有記憶體回收的機制,會釋放不再使用的變數記憶體,基本概念為:「沒有任何的參考指向它」時則會釋放記憶體。

MDN:collectible if there are zero references pointing to it.

記憶體釋放的驗證

本段使用一個範例來說明及驗證記憶體釋放的機制,首先,我們使用一段函式來產生非常長的字串,長字串會佔用大量的記憶體空間。

randomString 函式呼叫後會回傳很長的字串:

function randomString(length) {
  var result = '';
  var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  var charactersLength = characters.length;
  for (var i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
}
案例一:使變數維持可參考的狀態(不會釋放記憶體)

案例一:定義一個全域變數 demoData,此變數會持續維持可被參考的狀態。

var demoData = []; // 全域變數
function getData() {
  for (let i = 0; i < 1000; i++) {
    demoData.push(randomString(5000))
  }
}
getData()
console.log(demoData); // 可參考 demoData 值

執行此段範例以後,進入 Chrome 中的 Memory 分頁,此功能可以擷取當下網頁應用程式所佔用的記憶體狀況。接下來按下 "Take snapshot"。
https://ithelp.ithome.com.tw/upload/images/20200923/20083608dqZCAXd2p9.png

可以看到目前執行完此範例的當下佔用了 6.2MB 的記憶體空間(注意:任何瀏覽器環境、插件都會影響佔用的記憶體狀況)。
https://ithelp.ithome.com.tw/upload/images/20200923/20083608TCxtKAko88.png

案例一:使變數無法再次被參考(執行後釋放記憶體)

案例二:將變數限制作用域,使變數無法再被外部參考。

此段程式碼中依然會執行此函式,也會將值加入變數中,但外部無法再次參考 demoData 的值。

function getData() {
  var demoData = []; // 區域變數
  for (var i = 0; i < 1000; i++) {
    demoData.push(randomString(5000))
  }
}
getData();

接下來回到 Memory 分頁按下 "Take snapshot" 重新取得記憶體的狀態,接下來會得到與上方不同的結果,此次僅占用 1.2MB 的記憶體(其中 5MB 被釋放掉了)

https://ithelp.ithome.com.tw/upload/images/20200923/20083608iZTrP1QO67.png

結語

以目前範例,我們得知了作用域以及記憶體之間的關係,而記憶體管理也是前端工程師必要學習的一環(除了掌控用量,還要在必要時保留不被釋放),往後的章節還會介紹到如何透過保留函式記憶體的特性,建構特有的函式設計模式。


上一篇
呼叫函式時,到底有多少個參數 / 變數可供使用?
下一篇
JavaScript 一級函式 (First Class Functions)
系列文
前端常見問題攻略30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言