JS 閉包(內存溢出與內存泄漏)(垃圾回收機制)


1.有關閉包定義

閉包是指有權訪問另一個函數作用域中變量的函數,創建閉包的最常見的
方式就是在一個函數內創建另一個函數,通過另一個函數訪問這個函數的局部變量

閉包的特性:
    函數內再嵌套函數
    內部函數可以引用外層的參數和變量
    參數和變量不會被垃圾回收機制回收

說說你對閉包的理解

使用閉包主要是為了設計私有的方法和變量。閉包的優點是可以避免全局變量的污染,
缺點是閉包會常駐內存,會增大內存使用量,使用不當很容易造成內存泄露。在js中,
函數即閉包,只有函數才會產生作用域的概念

閉包 的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些
變量始終保持在內存中

閉包的另一個用處,是封裝對象的私有屬性和私有方法

好處:能夠實現封裝和緩存等;

壞處:就是消耗內存、不正當使用會造成內存溢出的問題

使用閉包的注意點

由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用
閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露

解決方法是,在退出函數之前,將不使用的局部變量全部刪除

閉包的定義其實很簡單:函數 A 內部有一個函數 B,函數 B 可以訪問到函數 A 中的變量,那么函數 B 就是閉包

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

閉包會產生一個很經典的問題:

多個子函數的[[scope]]都是同時指向父級,是完全共享的。因此當父級的
變量對象被修改時,所有子函數都受到影響。

解決:

變量可以通過 函數參數的形式 傳入,避免使用默認的[[scope]]向上查找
使用setTimeout包裹,通過第三個參數傳入
使用 塊級作用域,讓變量成為自己上下文的屬性,避免共享

2.閉包簡單例子
指的是有權訪問另一個函數作用域中變量的函數,
創建閉包的常見方式,就是在一個函數內部創建另一個函數。

 function f1(){
    var n=999;
    function f2(){
      alert(n); // 999
    }
  }
function f1(){
    var n=999;
    function f2(){
      alert(n); 
    }
    return f2;
  }
  var result=f1();
  result(); // 999

3.閉包的用處:

閉包可以用在許多地方。它的最大用處有兩個,一個是前面提到的可以讀取函數內部的變量,另一個就是讓這些變量的值始終保持在內存中。

function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

4.使用必閉包的問題:

由於閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題。

閉包的例子:

function outerFun()
{
 var a=0;
 function innerFun()
 {
  a++;
  alert(a);
 }
 return innerFun;  //注意這里
}
var obj=outerFun();
obj();  //結果為1
obj();  //結果為2
var obj2=outerFun();
obj2();  //結果為1
obj2();  //結果為2
function outerFun()
{
 //沒有var
 a =0;
 alert(a);  
}
var a=4;
outerFun();
alert(a);
結果為 0,0 真是奇怪,為什么呢?

作用域鏈是描述一種路徑的術語,沿着該路徑可以確定變量的值 .當執行a=0時,因
為沒有使用var關鍵字,因此賦值操作會沿着作用域鏈到var a=4;  並改變其值. 

5.閉包內的微觀世界
參考學習:https://www.cnblogs.com/goloving/p/7062212.html
  如果要更加深入的了解閉包以及函數a和嵌套函數b的關系,我們需要引入另外幾個概念:函數的執行環境(excution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數a從定義到執行的過程為例闡述這幾個概念。

1.當定義函數a的時候,js解釋器會將函數a的作用域鏈(scope chain)設置為定義a時a所在的“環境”,如果a是一個全局函數,則scope chain中只有window對象。
當執行函數a的時候,a會進入相應的執行環境(excution context)。
2.在創建執行環境的過程中,首先會為a添加一個scope屬性,即a的作用域,其值就為第1步中的scope chain。即a.scope=a的作用域鏈。
3.然后執行環境會創建一個活動對象(call object)。活動對象也是一個擁有屬性的對象,但它不具有原型而且不能通過Javascript代碼直接訪問。創建完活動對象后,把活動對象添加到a的作用域鏈的最頂端。此時a的作用域鏈包含了兩個對象:a的活動對象和window對象。
4.下一步是在活動對象上添加一個arguments屬性,它保存着調用函數a時所傳遞的參數。
5.最后把所有函數a的形參和內部的函數b的引用也添加到a的活動對象上。在這一步中,完成了函數b的的定義,因此如同第3步,函數b的作用域鏈被設置為b所被定義的環境,即a的作用域。

當在函數b中訪問一個變量的時候,搜索順序是:

先搜索自身的活動對象,如果存在則返回,如果不存在將繼續搜索函數a的活動對象,
依次查找,直到找到為止。
如果函數b存在prototype原型對象,則在查找完自身的活動對象后先查找自身的原型
對象,再繼續查找。這就是Javascript中的變量查找機制。
如果整個作用域鏈上都無法找到,則返回undefined。

函數的定義與執行。文中提到函數的作用域是在定義函數時候就已經確定,而不是在執行的時候確定

6.有關閉包經典案例
經典面試題,循環中使用閉包解決 var 定義函數的問題

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

首先因為 setTimeout 是個異步函數,所有會先把循環全部執行完畢,這時候 i 就是 6 了,所以會輸出一堆 6。
解決辦法兩種,第一種使用閉包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}

第二種就是使用 setTimeout 的第三個參數

for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}

第三種就是使用 let 定義 i 了

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}

有關內存溢出與內存泄漏



1. 內存溢出
  * 一種程序運行出現的錯誤
  * 當程序運行需要的內存超過了剩余的內存時, 就出拋出內存溢出的錯誤
2. 內存泄露
  * 占用的內存沒有及時釋放
  * 內存泄露積累多了就容易導致內存溢出
  * 常見的內存泄露:
    * 意外的全局變量
    * 沒有及時清理的計時器或回調函數
    * 閉包
// 1. 內存溢出
  var obj = {}
  for (var i = 0; i < 10000; i++) {
    obj[i] = new Array(10000000)
    console.log('-----')
  }

  // 2. 內存泄露
    // 意外的全局變量
  function fn() {
    a = new Array(10000000)
    console.log(a)
  }
  fn()

   // 沒有及時清理的計時器或回調函數
  var intervalId = setInterval(function () { //啟動循環定時器后不清理
    console.log('----')
  }, 1000)

  // clearInterval(intervalId)

    // 閉包
  function fn1() {
    var a = 4
    function fn2() {
      console.log(++a)
    }
    return fn2
  }
  var f = fn1()
  f()

  // f = null

7.js垃圾回收機制
轉載:https://www.cnblogs.com/zhwl/p/4664604.html
由於字符串、對象和數組沒有固定大小,當他們的大小已知時,才能對他們進行動態的存儲分配。JavaScript程序每次創建字符串、數組或對象時,解釋器都必須分配內存來存儲那個實體。只要像這樣動態地分配了內存,最終都要釋放這些內存以便他們能夠被再用,否則,JavaScript的解釋器將會消耗完系統中所有可用的內存,造成系統崩潰。

現在各大瀏覽器通常用采用的垃圾回收有兩種方法:標記清除、引用計數
標記清除
這是javascript中最常用的垃圾回收方式。當變量進入執行環境是,就標記這個變量為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變量所占用的內存,因為只要執行流進入相應的環境,就可能會用到他們。當變量離開環境時,則將其標記為“離開環境”。
引用計數
 另一種不太常見的垃圾回收策略是引用計數。引用計數的含義是跟蹤記錄每個值被引用的次數。當聲明了一個變量並將一個引用類型賦值給該變量時,則這個值的引用次數就是1。相反,如果包含對這個值引用的變量又取得了另外一個值,則這個值的引用次數就減1。當這個引用次數變成0時,則說明沒有辦法再訪問這個值了,因而就可以將其所占的內存空間給收回來。這樣,垃圾收集器下次再運行時,它就會釋放那些引用次數為0的值所占的內存。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM