這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、面向對象 一般使用字面量的形式直接創建對象,但是這種創建方式對於創建大量相似對象的時候,會產生大量的重覆代碼。但 js和一般的面向對象的語言不同,在 ES6 之前它沒有類的概念。但是可以使用函數來進行模擬,從而產生出可復用的對象創建方 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
一、面向對象
一般使用字面量的形式直接創建對象,但是這種創建方式對於創建大量相似對象的時候,會產生大量的重覆代碼。但 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時,說明這個變數已經沒有價值,因此,在在機回收期下次再運行時,這個變數所占有的記憶體空間就會被釋放出來。
- 這種方法會引起迴圈引用的問題:例如:
obj1
和obj2
通過屬性進行相互引用,兩個對象的引用次數都是2。當使用迴圈計數時,由於函數執行完後,兩個對象都離開作用域,函數執行結束,obj1
和obj2
還將會繼續存在,因此它們的引用次數永遠不會是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 元素的引用,而後面這個元素被刪除,由於一直保留了對這個元素的引用,所以它也無法被回收。
●閉包:不合理的使用閉包,從而導致某些變數一直被留在記憶體當中。