1. 程式人生 > 其它 >JavaScript知識總結 終結篇--面向物件,垃圾回收與記憶體洩漏

JavaScript知識總結 終結篇--面向物件,垃圾回收與記憶體洩漏

這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助

一、面向物件

一般使用字面量的形式直接建立物件,但是這種建立方式對於建立大量相似物件的時候,會產生大量的重複程式碼。但 js和一般的面向物件的語言不同,在 ES6 之前它沒有類的概念。但是可以使用函式來進行模擬,從而產生出可複用的物件建立方式,常見的有以下幾種:

(1)第一種是工廠模式,工廠模式的主要工作原理是用函式來封裝建立物件的細節,從而通過呼叫函式來達到複用的目的。但是它有一個很大的問題就是創建出來的物件無法和某個型別聯絡起來,它只是簡單的封裝了複用程式碼,而沒有建立起物件和型別間的關係。

(2)第二種是建構函式模式。js 中每一個函式都可以作為建構函式,只要一個函式是通過 new 來呼叫的,那麼就可以把它稱為建構函式。執行建構函式首先會建立一個物件,然後將物件的原型指向建構函式的 prototype 屬性,然後將執行上下文中的 this 指向這個物件,最後再執行整個函式,如果返回值不是物件,則返回新建的物件。因為 this 的值指向了新建的物件,因此可以使用 this 給物件賦值。建構函式模式相對於工廠模式的優點是,所建立的物件和建構函式建立起了聯絡,因此可以通過原型來識別物件的型別。但是建構函式存在一個缺點就是,造成了不必要的函式物件的建立,因為在 js 中函式也是一個物件,因此如果物件屬性中如果包含函式的話,那麼每次都會新建一個函式物件,浪費了不必要的記憶體空間,因為函式是所有的例項都可以通用的。

(3)第三種模式是原型模式,因為每一個函式都有一個 prototype 屬性,這個屬性是一個物件,它包含了通過建構函式建立的所有例項都能共享的屬性和方法。因此可以使用原型物件來新增公用屬性和方法,從而實現程式碼的複用。這種方式相對於建構函式模式來說,解決了函式物件的複用問題。但是這種模式也存在一些問題,一個是沒有辦法通過傳入引數來初始化值,另一個是如果存在一個引用型別如 Array 這樣的值,那麼所有的例項將共享一個物件,一個例項對引用型別值的改變會影響所有的例項。

(4)第四種模式是組合使用建構函式模式和原型模式,這是建立自定義型別的最常見方式。因為建構函式模式和原型模式分開使用都存在一些問題,因此可以組合使用這兩種模式,通過建構函式來初始化物件的屬性,通過原型物件來實現函式方法的複用。這種方法很好的解決了兩種模式單獨使用時的缺點,但是有一點不足的就是,因為使用了兩種不同的模式,所以對於程式碼的封裝性不夠好。

(5)第五種模式是動態原型模式,這一種模式將原型方法賦值的建立過程移動到了建構函式的內部,通過對屬性是否存在的判斷,可以實現僅在第一次呼叫函式時對原型物件賦值一次的效果。這一種方式很好地對上面的混合模式進行了封裝。

(6)第六種模式是寄生建構函式模式,這一種模式和工廠模式的實現基本相同,我對這個模式的理解是,它主要是基於一個已有的型別,在例項化時對例項化的物件進行擴充套件。這樣既不用修改原來的建構函式,也達到了擴充套件物件的目的。它的一個缺點和工廠模式一樣,無法實現物件的識別。

2. 物件繼承的方式有哪些?

(1)第一種是以原型鏈的方式來實現繼承,但是這種實現方式存在的缺點是,在包含有引用型別的資料時,會被所有的例項物件所共享,容易造成修改的混亂。還有就是在建立子型別的時候不能向超型別傳遞引數。

(2)第二種方式是使用借用建構函式的方式,這種方式是通過在子型別的函式中呼叫超型別的建構函式來實現的,這一種方法解決了不能向超型別傳遞引數的缺點,但是它存在的一個問題就是無法實現函式方法的複用,並且超型別原型定義的方法子型別也沒有辦法訪問到。

(3)第三種方式是組合繼承,組合繼承是將原型鏈和借用建構函式組合起來使用的一種方式。通過借用建構函式的方式來實現型別的屬性的繼承,通過將子型別的原型設定為超型別的例項來實現方法的繼承。這種方式解決了上面的兩種模式單獨使用時的問題,但是由於我們是以超型別的例項來作為子型別的原型,所以呼叫了兩次超類的建構函式,造成了子型別的原型中多了很多不必要的屬性。

(4)第四種方式是原型式繼承,原型式繼承的主要思路就是基於已有的物件來建立新的物件,實現的原理是,向函式中傳入一個物件,然後返回一個以這個物件為原型的物件。這種繼承的思路主要不是為了實現創造一種新的型別,只是對某個物件實現一種簡單繼承,ES5 中定義的 Object.create() 方法就是原型式繼承的實現。缺點與原型鏈方式相同。

(5)第五種方式是寄生式繼承,寄生式繼承的思路是建立一個用於封裝繼承過程的函式,通過傳入一個物件,然後複製一個物件的副本,然後物件進行擴充套件,最後返回這個物件。這個擴充套件的過程就可以理解是一種繼承。這種繼承的優點就是對一個簡單物件實現繼承,如果這個物件不是自定義型別時。缺點是沒有辦法實現函式的複用。

(6)第六種方式是寄生式組合繼承,組合繼承的缺點就是使用超型別的例項做為子型別的原型,導致添加了不必要的原型屬性。寄生式組合繼承的方式是使用超型別的原型的副本來作為子型別的原型,這樣就避免了建立不必要的屬性。

二、垃圾回收與記憶體洩漏

1. 瀏覽器的垃圾回收機制

(1)垃圾回收的概念

垃圾回收:JavaScript程式碼執行時,需要分配記憶體空間來儲存變數和值。當變數不在參與執行時,就需要系統收回被佔用的記憶體空間,這就是垃圾回收。

回收機制

  • Javascript 具有自動垃圾回收機制,會定期對那些不再使用的變數、物件所佔用的記憶體進行釋放,原理就是找到不再使用的變數,然後釋放掉其佔用的記憶體。
  • JavaScript中存在兩種變數:區域性變數和全域性變數。全域性變數的生命週期會持續要頁面解除安裝;而區域性變數宣告在函式中,它的生命週期從函式執行開始,直到函式執行結束,在這個過程中,區域性變數會在堆或棧中儲存它們的值,當函式執行結束後,這些區域性變數不再被使用,它們所佔有的空間就會被釋放。
  • 不過,當局部變數被外部函式使用時,其中一種情況就是閉包,在函式執行結束後,函式外部的變數依然指向函式內部的區域性變數,此時區域性變數依然在被使用,所以不會回收。

(2)垃圾回收的方式

瀏覽器通常使用的垃圾回收方法有兩種:標記清除,引用計數。

1)標記清除

  • 標記清除是瀏覽器常見的垃圾回收方式,當變數進入執行環境時,就標記這個變數“進入環境”,被標記為“進入環境”的變數是不能被回收的,因為他們正在被使用。當變數離開環境時,就會被標記為“離開環境”,被標記為“離開環境”的變數會被記憶體釋放。
  • 垃圾收集器在執行的時候會給儲存在記憶體中的所有變數都加上標記。然後,它會去掉環境中的變數以及被環境中的變數引用的標記。而在此之後再被加上標記的變數將被視為準備刪除的變數,原因是環境中的變數已經無法訪問到這些變量了。最後。垃圾收集器完成記憶體清除工作,銷燬那些帶標記的值,並回收他們所佔用的記憶體空間。

2)引用計數

  • 另外一種垃圾回收機制就是引用計數,這個用的相對較少。引用計數就是跟蹤記錄每個值被引用的次數。當聲明瞭一個變數並將一個引用型別賦值給該變數時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變數又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變為0時,說明這個變數已經沒有價值,因此,在在機回收期下次再執行時,這個變數所佔有的記憶體空間就會被釋放出來。
  • 這種方法會引起迴圈引用的問題:例如: obj1obj2通過屬性進行相互引用,兩個物件的引用次數都是2。當使用迴圈計數時,由於函式執行完後,兩個物件都離開作用域,函式執行結束,obj1obj2還將會繼續存在,因此它們的引用次數永遠不會是0,就會引起迴圈引用。
function fun() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

這種情況下,就要手動釋放變數佔用的記憶體:

 obj1.a =  null
 obj2.a =  null

3)減少垃圾回收
雖然瀏覽器可以進行垃圾自動回收,但是當代碼比較複雜時,垃圾回收所帶來的代價比較大,所以應該儘量減少垃圾回收。
對陣列進行優化:在清空一個數組時,最簡單的方法就是給其賦值為[ ],但是與此同時會建立一個新的空物件,可以將陣列的長度設定為0,以此來達到清空陣列的目的。
對object進行優化:物件儘量複用,對於不再使用的物件,就將其設定為null,儘快被回收。
對函式進行優化:在迴圈中的函式表示式,如果可以複用,儘量放在函式的外面。

2. 哪些情況會導致記憶體洩漏
以下四種情況會造成記憶體的洩漏:
意外的全域性變數:由於使用未宣告的變數,而意外的建立了一個全域性變數,而使這個變數一直留在記憶體中無法被回收。
被遺忘的計時器或回撥函式:設定了 setInterval 定時器,而忘記取消它,如果迴圈函式有對外部變數的引用的話,那麼這個變數會被一直留在記憶體中,而無法被回收。
脫離 DOM 的引用:獲取一個 DOM 元素的引用,而後面這個元素被刪除,由於一直保留了對這個元素的引用,所以它也無法被回收。
閉包:不合理的使用閉包,從而導致某些變數一直被留在記憶體當中。

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文件,大家一起討論學習,一起進步。