從閉包函式的變數自增的角度 – 解析js垃圾回收機制

pengliheng發表於2019-02-25

GitHub

前言

感覺每一道都可以深入研究下去,單獨寫一篇文章,包括不限於閉包,原型鏈,從url輸入到頁面展示過程,頁面優化,react和vue的價值等等。

程式碼實現

const times = (()=>{
  var times = 0;
  return () => times++;
})()
console.log(
  times(),
  times(),
  times(),
  times()
) // 0,1,2,3,複製程式碼

原理

因為times變數一直被引用,沒有被回收,所以,每次自增1。

更簡單的實現方式,一行程式碼實現閉包

const times = ((times = 0)=> () => times++)()
console.log(
  times(),
  times(),
  times(),
  times()
) // 0,1,2,3複製程式碼

這並非閉包地專利, 變數放在閉包外部同樣可以實現阻止變數地垃圾回收機制

let time = 0
const times = ()=>{
	let time = 10
	return function(){
		return time++
	} 
}// 根據JavaScript作用域鏈地規則,閉包內部沒有,就從外面拿變數

const a = times();  // times函式只被執行了1次,產生了一個變數 time
console.log(
	a(),          // 而times返回的匿名函式卻被執行了5次
	a(),          // 而times返回的匿名函式卻被執行了5次
	a(),          // 而times返回的匿名函式卻被執行了5次   其中的差別相差非常遠
	a(),          // 而times返回的匿名函式卻被執行了5次
	a()           // 而times返回的匿名函式卻被執行了5次
) // 0,1,2,3複製程式碼

深入寫下去之前,先放出類似的程式碼

同樣的執行,我把函式執行時間放到了前面,自增失敗

const times = ((times = 0)=> () => times++)()();  匿名函式只被執行了一次,同時返回函式再次執行一次
console.log(
  times,   // 得到匿名函式返回值, 函式只有配合()才會被執行一次麼,此處
  times,   // 此處沒有函式被執行
  times,   // 因此列印值為四個零
  times
); // 0,0,0,0複製程式碼

同樣的執行,我把閉包函式執行時間放到了後面,同樣自增失敗

const times = ((times = 0)=> () => times++); time相當於宣告式函式
console.log(
  times()(),    // 此處外部函式執行一次,產生times變數,返回的函式再執行一次times引用次數為0
  times()(),    // 此處外部函式執行一次,產生times變數,返回的函式再執行一次
  times()(),    // 此處外部函式執行一次,產生times變數,返回的函式再執行一次
  times()()
); // 0,0,0,0複製程式碼

函式[1,2,3,4,4].entires()會返回一個迭代器,一下程式碼同樣實現了類似自增1的效果
image

const arr = [1,2,3,3,5,6,4,78].entries()
console.log(
  arr2.next().value,
  arr2.next().value,
  arr2.next().value,
  arr2.next().value,
  arr2.next().value
); // [0, 1], [1, 2], [2, 3], [3, 3], [4, 5]  迭代器返回值, 【index,value】複製程式碼

JavaScript辣雞回收機制

按照JavaScript裡垃圾回收的機制,是從root(全域性物件)開始尋找這個物件的引用是否可達,如果引用鏈斷裂,那麼這個物件就會回收。換句話說,所有物件都是point關係。引用鏈就是所謂的指標關係。
當const的過程中,宣告的那個函式會被壓入呼叫棧,執行完畢,又沒有其他地方引用它,那就會被釋放。這個瀏覽器端,挺難的,但是在nodejs端,就可以用process.memoryUsage()呼叫檢視記憶體使用情況。

{ 
  rss: 23560192,         // 所有記憶體佔用,包括指令區和堆疊。
  heapTotal: 10829824,   // "堆"佔用的記憶體,包括用到的和沒用到的。
  heapUsed: 4977904,     // 用到的堆的部分。同時也是判斷記憶體是否洩露的標準。
  external: 8608         // V8 引擎內部的 C++ 物件佔用的記憶體。
}複製程式碼
如果你想要引用,又不想影響垃圾回收機制,那就用WeakMap,WeakSet這種弱引用吧,es6的新屬性。

從引用次數來解釋為什麼變數times沒有被回收

const timeFunc = ((time = 0)=> () => time++)
var b = timeFunc();   // time 變數引用次數+1,不能被回收
console.log(b());
console.log(b());
console.log(b());複製程式碼
// 真的非常神奇,需要滿足2個條件
// 1.變數命名於返回函式外部,函式函式內部。
// 2.返回函式引用外部變數,導致外部變數無法觸發垃圾回收機制。因為引用次數>1
let timeFunc = (time = 0)=>{
  return (() => time++)()
}
var b = timeFunc();  // b變數接受的是timeFunc返回的函式,由於返回函式內部有引用外部變數,故
console.log(b)
console.log(b)複製程式碼

JavaScript中的記憶體簡介(如果缺少必須的基礎知識,想要深入瞭解下去,也是比較難的吧)

像C語言這樣的高階語言一般都有低階的記憶體管理介面,比如 malloc()和free()。另一方面,JavaScript建立變數(物件,字串等)時分配記憶體,並且在不再使用它們時“自動”釋放。 後一個過程稱為垃圾回收。這個“自動”是混亂的根源,並讓JavaScript(和其他高階語言)開發者感覺他們可以不關心記憶體管理。 這是錯誤的。

閉包的本質

JavaScript閉包的形成原理是基於函式變數作用域鏈的規則 和 垃圾回收機制的引用計數規則。
JavaScript閉包的本質是記憶體洩漏,指定記憶體不釋放
(不過根據記憶體洩漏的定義是無法使用,無法回收來說,這不是記憶體洩漏,由於只是無法回收,但是可以使用,為了使用,不讓系統回收)
JavaScript閉包的用處,私有變數,獲取對應值等,。。

記憶體生命週期

不管什麼程式語言,記憶體生命週期基本是一致的:

  • 分配你所需要的記憶體
  • 使用分配到的記憶體(讀、寫)
  • 不需要時將其釋放歸還

在所有語言中第一和第二部分都很清晰。最後一步在底層語言中很清晰,但是在像JavaScript 等上層語言中,這一步是隱藏的、透明的。

為了不讓程式設計師操心(真的是操碎了心),JavaScript自動完成了記憶體分配工作。

var n = 123;   // 給數值變數分配記憶體
var s = "azerty"; // 給字串變數分配記憶體

var obj = {
	a: 1,
	b: null
};  // 給物件以及其包含的值分配記憶體

var arr = [1,null,"abra"]; // 給函式(可呼叫的物件)分配記憶體

function f(a){
	return a+2
} // 給函式(可呼叫物件)分配記憶體

// 為函式表示式也分配一段記憶體
document.body.addEventListener(`scroll`, function (){
	console.log(`123`)
},false)複製程式碼

有些函式呼叫之後會返回一個物件

var data = new Date();
var a = document.createElement(`div`);複製程式碼

有些方法是分配新變數或者新物件

var s1 = `azerty`;   // 由於字串屬於引用,所以JavaScript不會為他分配新的記憶體
var s2 = `s.substr(0,3)`; // s2是一個新的字串

var a = ["ouais ouais", "nan nan"];
var a2 = ["generation", "nan nan"];
var a3 = a.concat(a2); 
// 新陣列有四個元素,是 a 連線 a2 的結果複製程式碼

命名變數的過程其實是對記憶體的寫入和釋放

辣雞回收

如上文所述,記憶體是否仍然被需要是無法判斷的,下面將介紹垃圾回收演算法以及垃圾回收的侷限性

引用

辣雞回收演算法主要依賴於引用的概念。在記憶體管理的環境中,如果一個物件有訪問另一個物件的許可權,那麼對於屬性屬於顯示引用,對於原型鏈屬於隱式引用。

引用計數垃圾收集

下面是最簡單的垃圾回收演算法。此演算法把“物件是否被需要”簡單定義為“該物件沒有被其他物件引用到”。

var o = {
	a: {
		b: 2
	}
};
// 兩個物件被建立,一個作為另一個的屬性被引用,另一個被分配給變數o
// 很顯然,沒有一個可以被作為辣雞收集

var o2 = o; // o2變數是第二個對“這個物件”

o = 1;   // 現在這個物件的原始引用o被o2替換了

var oa = o2.a; // 引用“這個物件”的a屬性
                 // 現在,“這個物件”有兩個引用了,一個是o2,一個是oa

o2 = `yo`;  // 最初的物件現在已經是零引用了
		// 它可以被垃圾回收了
		// 然而他的屬性a還在被呼叫,所以不能回收

oa = null;	// a屬性的那個物件現在也是零引用了
		// 它可以被垃圾回收了複製程式碼

相關文章