首頁  >  文章  >  web前端  >  JavaScript也談記憶體優化_javascript技巧

JavaScript也談記憶體優化_javascript技巧

WBOY
WBOY原創
2016-05-16 16:45:591013瀏覽

相對C/C 而言,我們所使用的JavaScript 在記憶體這一方面的處理已經讓我們在開發中更注重業務邏輯的編寫。但隨著業務的不斷複雜化,單頁應用、行動HTML5 應用程式和Node.js 程式等等的發展,JavaScript 中的記憶體問題所導致的卡頓、記憶體溢位等現像也變得不再陌生。

這篇文章將從JavaScript 的語言層面進行記憶體的使用與最佳化的探討。從大家熟悉或略有耳聞的方面,到大家大多時候不會注意到的地方,我們一一進行剖析。

1. 語言層面的記憶體管理

1.1 作用域

作用域(scope)是JavaScript 程式設計中一個非常重要的運作機制,在同步JavaScript 程式設計中它並不能充分吸引初學者的注意,但在非同步程式設計中,良好的作用域控制技能成為了JavaScript 開發者的必備技能。另外,作用域在JavaScript 記憶體管理中扮演至關重要的角色。

在JavaScript中,能形成作用域的有函數的呼叫、with語句和全域作用域。

如以下程式碼為例:

複製程式碼 程式碼如下:

var foo =>var. function() {
var local = {};
};
foo();
console.log(local); //=> undefined

var bar = function () {
local = {};
};
bar();
console.log(local); //=> {}

這裡我們定義了foo()函數和bar()函數,他們的意圖都是為了定義一個名為local的變數。但最終的結果卻截然不同。

在foo()函數中,我們使用var語句來宣告定義了一個local變量,而因為函數體內部會形成一個作用域,所以這個變數便被定義到該作用域中。而foo()函數體內並沒有做任何作用域延伸的處理,所以在該函數執行完畢後,這個local變數也隨之被銷毀。而在外層作用域中則無法存取到該變數。

而在bar()函數內,local變數並沒有使用var語句來聲明,取而代之的是直接把local當作全域變數來定義。故外層作用域可以存取到這個變數。

複製程式碼 程式碼如下:

local = {};
定義等效於
global.local = {};


1.2 作用域鏈

在JavaScript程式設計中,你一定會遇到多層函數巢狀的場景,這就是典型的作用域鏈的表示。

如以下程式碼所示:

複製程式碼 程式碼如下:

function foo( ) {
  var val = 'hello';

  function bar() {
    function baz() {
   ();
    console.log(val); //=> hello
  }
  bar();
}
foo();


根據前面關於作用域的闡述,你可能會認為這裡的程式碼所顯示的結果是world,但實際的結果卻是hello。很多初學者在這裡就會開始感到困惑了,那我們再來看看這段程式碼是怎麼運作的。

由於JavaScript 中,變數標識符的查找是從目前作用域開始向外查找,直到全域作用域為止。所以JavaScript 程式碼中對變數的存取只能向外進行,而不能逆而行之。

JavaScript也談記憶體優化_javascript技巧
baz()函數的執行在全域作用域中定義了一個全域變數val。而在bar()函數中,當val這一標識符進行存取時,按照從內到外厄德查找原則:在bar函數的作用域中沒有找到,便到上一層,即foo()函數的作用域中查找。

然而,使大家產生疑惑的關鍵就在這裡:本次標識符訪問在foo()函數的作用域中找到了符合的變量,便不會繼續向外查找,故在baz()函數中定義的全域變數val並沒有在本次變數存取中產生影響。

1.3 閉包

我們知道JavaScript 中的識別碼尋找遵循從內到外的原則。但隨著業務邏輯的複雜化,單一的傳遞順序已經遠遠無法滿足日益增加的新需求。

我們先來看看下面的程式碼:


複製程式碼 程式碼如下:

function foo() {
  var local = 'Hello';
  return function() {
    return local;
  };
  foo();
console.log(bar()); //=> Hello

這裡所展示的讓外層作用域存取內層作用域的技術就是閉包(Closure)。得益於高階函數的應用,使foo()函數的作用域得到『延伸』。

foo()函數傳回了一個匿名函數,該函數存在於foo()函數的作用域內,所以可以存取到foo()函數作用域內的local變量,並保存其參考。而因這個函數直接回傳了local變量,所以在外層作用域中便可直接執行bar()函數以獲得local變數。

閉包是JavaScript 的高階特性,我們可以藉助它來實現更多更複雜的效果來滿足不同的需求。但是要注意的是因為把帶有內部變數引用的函數帶出了函數外部,所以該作用域內的變數在函數執行完畢後的並不一定會被銷毀,直到內部變數的參考被全部解除。所以閉包的應用很容易造成記憶體無法釋放的情況。

2. JavaScript 的記憶體回收機制

這裡我將以Chrome 和Node.js 所使用的,由Google 推出的V8 引擎為例,簡要介紹一下JavaScript 的內存回收機制,更詳盡的內容可以購買我的好朋友樸靈的書《深入淺出Node.js 》進行學習,其中『記憶體控制』一章有相當詳細的介紹。

在V8 中,所有的JavaScript 物件都是透過『堆』來進行記憶體分配的。

JavaScript也談記憶體優化_javascript技巧
當我們在程式碼中宣告變數並賦值時,V8 就會在堆疊記憶體中分配一部分給這個變數。如果已申請的內存不足以儲存這個變數時,V8 就會繼續申請內存,直到堆的大小達到了V8 的內存上限為止。預設情況下,V8 的堆內存的大小上限在64位元系統中為1464MB,在32位元系統中則為732MB,即約1.4GB 和0.7GB。

另外,V8 將堆記憶體中的JavaScript 物件分代管理:新生代和老生代。新生代即存活週期較短的JavaScript 對象,如臨時變數、字串等;而老生代則為經過多次垃圾回收仍存活,存活週期較長的對象,如主控制器、伺服器物件等。

垃圾回收演算法一直是程式語言的研發中是否重要的​​一環,而V8 所使用的垃圾回收演算法主要有以下幾種:

1.Scavange 演算法:透過複製的方式進行記憶體空間管理,主要用於新生代的記憶體空間;

2.Mark-Sweep 演算法和Mark-Compact 演算法:透過標記來對堆記憶體進行整理和回收,主要用於老生代對象的檢查和回收。

PS: 更詳細的V8 垃圾回收實作可以透過閱讀相關書籍、文件和原始碼來學習。

我們再來看看JavaScript 引擎在什麼情況下會對哪些物件進行回收。

2.1 作用域與引用

初學者常常會誤認為當函數執行完畢時,在函數內部所宣告的物件就會被銷毀。但實際上這樣理解並不嚴謹和全面,容易被其導致混淆。

引用(Reference)是JavaScript 程式設計中十分重要的一個機制,但奇怪的是一般的開發者都不會刻意注意它、甚至不了解它。引用是指『程式碼對物件的存取』這種抽象關係,它與C/C 的指標有點相似,但並非同物。引用同時也是JavaScript 引擎在進行垃圾回收中最關鍵的機制。

以下面程式碼為例:


複製程式碼 程式碼如下:
/ . .....
var val = 'hello world';
function foo() {
  return function() {
    return val;
  };
}
}
  };
}
}
  };}} global.bar = foo();// ......

閱讀完這段程式碼,你能否說出這部分程式碼在執行過後,有哪些物件是依然存活的麼?

根據相關原則,這段程式碼中沒有被回收釋放的物件有val和bar(),究竟是什麼原因使他們無法被回收?

JavaScript 引擎是如何進行垃圾回收的?前面說到的垃圾回收演算法只是用在回收時的,那麼它是如何知道哪些物件可以被回收,哪些物件需要繼續生存呢?答案就是JavaScript 物件的參考。

JavaScript 程式碼中,即使是簡單的寫下一個變數名稱作為單獨一行而不做任何操作,JavaScript 引擎都會認為這是對物件的存取行為,存在了對物件的引用。為了確保垃圾回收的行為不會影響程式邏輯的運行,JavaScript 引擎就絕對不能把正在使用的物件回收,不然就亂套了。所以判斷物件是否正在使用中的標準,就是是否仍存在對該物件的引用。但事實上,這是一種妥協的做法,因為JavaScript 的引用是可以進行轉移的,那麼就有可能出現某些引用被帶到了全域作用域,但事實上在業務邏輯裡已經不需要對其進行訪問了,應該被回收,但是JavaScript 引擎仍會死板地認為程式仍然需要它。

如何用正確的姿勢使用變數、引用,正是從語言層面優化JavaScript 的關鍵所在。

3. 最佳化你的JavaScript

終於進入正題了,非常感謝你秉著耐心看到了這裡,經過上面這麼多介紹,相信你已經對JavaScript 的內存管理機制有了不錯的理解,那麼下面的技巧將會讓你如虎添翼。

3.1 善用函數

如果你有閱讀優秀JavaScript 專案的習慣的話,你會發現,很多大牛在開發前端JavaScript 程式碼的時候,常常會使用一個匿名函數在程式碼的最外層進行包裹。

複製程式碼 程式碼如下:

(function() {

(function() {
程式碼
})();
有的甚至高階一點:複製程式碼


複製程式碼

程式碼如下:
;(function(win, doc, $, undefined) {  // 主業務代碼})(window, document, jQuery);甚至連如RequireJS, SeaJS, OzJS 等前端模組化加載解決方案,都是採用類似的形式:



複製程式碼


程式碼如下:


// RequireJS
define(['jquery'], function($) {
  // 主業務碼
});
  // 主業務碼}); // SeaJSdefine('odule', ['dep', 'underscore'], function($, _) {  // 主業務碼
});

如果你說很多Node.js 開源專案的程式碼都沒有這樣處理的話,那你就錯了。 Node.js 在實際運行程式碼之前,會把每一個.js 檔案包裝,變成如下的形式:

複製程式碼

這樣做有什麼好處?我們都知道文章開始的時候就說了,JavaScript中能形成作用域的有函數的呼叫、with語句和全域作用域。而我們也知道,被定義在全域作用域的對象,很有可能是會一直存活到進程退出的,如果是一個很大的對象,那就麻煩了。例如有的人喜歡在JavaScript中做模版渲染:複製程式碼 程式碼如下:
 
  $db = mysqli_connect(伺服器、使用者、密碼、'myapp');
  $topics = mysqli_query($db, "從主題中選擇*;");
?>



 
  你是猴子請來的逗比麼?


 
    < ;/ul>
     
     

    這種程式碼在新手的作品中常常能看得到,這裡存在什麼問題呢?如果從資料庫中取得到的資料量非常大的話,導入完成模板渲染以後,資料變數便被閒置在可因為這個變數是在全域作用域中被定義的,所以 JavaScript 引擎方面不會將其還原觀點。如此該變數可能一直存在於老生代堆記憶體中,直到頁面關閉。

    但是如果我們做一些很簡單的修改,在邏輯程式碼外封裝一層函數,這樣效果就大不一樣了。當UI渲染完成之後,程式碼對資料的引用也解除了,而在最外層函數執行完畢時,JavaScript引擎就開始對其中的物件進行檢查,資料也可以被恢復。


    3.2 絕對不要定義全域變數
我們剛才也談到了,當一個變數在全域作用域中被定義時,預設情況下JavaScript 引擎不會將其回收相關事件。如此該變數就會一直存在於老生代堆記憶體中,直到頁面關閉。

那我們就一直遵循一個原則:絕對不要使用全域變數。雖然全域變數在開發中確實很省事,但是全域變數所導致的問題遠比其所帶來的方便更嚴重。

使飾品不易被恢復;

1.多人協作時容易產生幹擾;2.在作用域鏈中容易被幹擾。
3.配合上面的包裝函數,我們也可以透過包裝函數來處理『全域變數』。

3.3 手動解除變數引用

如果在業務程式碼中,一個變數已經不再需要了,那麼就可以手動解除變數引用,從而回傳。


複製程式碼

程式碼如下:

var data = { /* 一些大數據*/ } ;//等等等等data = null; 3.4 善用回調

除了使用閉包進行內部變數訪問,我們還可以使用現在十分流行的回調函數來進行業務處理。

複製程式碼
程式碼如下:

 
function getData(callback) {
  var data = 'some big data';

  callback(null, data);
}
  callback(null, data);
}
get (err, data) {
  console.log(data);

回呼函數是一種後續傳遞風格(Continuation Passing Style, CPS)的技術,這種風格的程式編寫將函數的業務重點從傳回值轉移到回呼函數。而且其相比閉包的好處也不少:


1.如果傳入的參數是基礎型別(如字串、數值),回呼函數中傳入的形參就會是複製值,業務碼使用完畢以後,更容易被回收;
2 .透過回調,我們除了可以完成同步的請求外,還可以用在非同步程式設計中,這也就是現在非常流行的一種編寫風格;

3.回呼函數本身通常也是臨時的匿名函數,一旦請求函數執行完畢,回呼函數本身的參考就會被解除,自身也被回收。

3.5 良好的閉包管理

當我們的業務需求(如循環事件綁定、私有屬性、含參回調等)一定要使用閉包時,請謹慎對待其中的細節。

循環綁定事件可謂是JavaScript 閉包入門的必修課,我們假設一個場景:有六個按鈕,分別對應六種事件,當使用者點擊按鈕時,在指定的地方輸出對應的事件。


程式碼如下:


var btns = 代碼如下:


var btns = documents = Selector btn'); // 6 elements
var output = document.querySelector('#output');
var events = [1, 2, 3, 4, 5, 6];

// Case 1
for (var i = 0; i   btns[i].onclick = function(evt) {
    output.innerText = 'Clicked ' events[ i];
  };
}

// Case 2
for (var i = 0; i   btns[i].onclick = (function(index) {
    return function(evt) {
      output.innerText = 'Clicked ' events[index];
  } 🎜>
// Case 3
for (var i = 0; i   btns[i].onclick = (function(event) {
    return function( evt) {
      output.innerText = 'Clicked ' event;
    };
  })(events[i]);
}

這裡第一個解決方案顯然是典型的循環綁定事件錯誤,這裡不細說,詳細可以參考我給一個網友的回答;而第二和第三個方案的區別就在於閉包傳入的參數。

第二個方案傳入的參數是目前循環下標,而後者則是直接傳入對應的事件物件。事實上,後者更適合在大量資料應用的時候,因為在JavaScript的函數式程式設計中,函數呼叫時傳入的參數是基本型別對象,那麼在函數體內得到的形參會就是一個複製值,這樣這個值就被當作一個局部變數定義在函數體的作用域內,在完成事件綁定之後就可以對events變數進行手動解除引用,以減輕外層作用域中的記憶體佔用了。而當某個元素被刪除時,對應的事件監聽函數、事件物件、閉包函數也隨之被銷毀回收。

3.6 記憶體不是快取

快取在業務開發中的作用舉足輕重,可以減輕時空資源的負擔。但要注意的是,不要輕易將記憶體當作快取使用。記憶體對於任何程式開發來說都是寸土寸金的東西,如果不是很重要的資源,請不要直接放在記憶體中,或是製定過期機制,自動銷毀過期快取。

4. 檢查JavaScript 的記憶體使用量

在平常的開發中,我們也可以藉助一些工具來對JavaScript 中記憶體使用情況進行分析和問題排查。

4.1 Blink / Webkit 瀏覽器

在Blink / Webkit 瀏覽器中(Chrome, Safari, Opera etc.),我們可以藉助其中的Developer Tools 的Profiles 工具來對我們的程式進行記憶體檢查。

JavaScript也談記憶體優化_javascript技巧
4.2 Node.js 中的記憶體檢查


在Node.js 中,我們可以使用node-heapdump 和node-memwatch 模組進行記憶體檢查。

程式碼如下:


var heapdump = require('heapdump'); >var fs = require('fs');
var path = require('path');
fs.writeFileSync(path.join(__dirname, 'app.pid'), process.pid);
// ...

コードをコピーします コードは次のとおりです:
ビジネスコードにnode-heapdumpを導入した後、Node.jsにデータを送信する必要があります。特定の実行時に、プロセスは SIGUSR2 シグナルを送信して、node-heapdump にヒープ メモリのスナップショットを取得させます。

コードをコピー コードは次のとおりです:
$ kill -USR2 (猫アプリ .pid)

このようにして、ファイル ディレクトリに heapdump-..heapsnapshot という名前のスナップショット ファイルが作成され、ブラウザの開発者ツールのプロファイル ツールを使用して開くことができます。それを確認してください。

5. まとめ

この記事の終わりは主に次の点です。

1. JavaScript は言語レベルでのメモリ使用量と密接に関係しています。
2. 生成された JavaScript がより効率的にメモリを使用できるようにする方法。拡張用;
4. メモリの問題が発生した場合にメモリ チェックを実行する方法。

この記事を読んで、より良い JavaScript コードを作成して、お母さんや上司を安心させられることを願っています。


陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn