摘要:將他們放在堆中是為了不影響棧的效率。接著是臨時(shí)空間函數(shù)執(zhí)行的時(shí)候,會(huì)臨時(shí)開辟一塊內(nèi)存空間,這塊內(nèi)存空間長(zhǎng)得和外面這個(gè)一樣,也有自己的棧堆,當(dāng)函數(shù)運(yùn)行完就銷毀。中的內(nèi)存第一個(gè)部分還是和上面的一樣,有棧堆運(yùn)行時(shí)環(huán)境,另外還有一個(gè)緩沖區(qū)存放。
0.前言
主要結(jié)合了內(nèi)存的概念講了js的一些的很簡(jiǎn)單、但是又不小心就犯錯(cuò)的地方。
結(jié)論:js執(zhí)行順序,先定義,后執(zhí)行,從上到下,就近原則。閉包可以讓外部訪問某函數(shù)內(nèi)部變量,而且會(huì)導(dǎo)致內(nèi)存泄漏。
在ECMAscript數(shù)據(jù)類型有基本類型和引用類型,基本類型有Undefined、Null、Boolean、Number、String,引用類型有Object,所有的的值將會(huì)是6種的其中之一(數(shù)據(jù)類型具有動(dòng)態(tài)性,沒有定義其他數(shù)據(jù)類型的必要了)
引用類型的值,也就是對(duì)象,一個(gè)對(duì)象是某個(gè)引用類型的一個(gè)實(shí)例,用new操作符創(chuàng)建也可以用字面量的方式(對(duì)象字面量創(chuàng)建var obj ={ })。ECMA里面有很多原生的引用類型,就是查文檔的時(shí)候看見的那些:Function、Number (是對(duì)于原始類型Number的引用類型)、String(是對(duì)于原始類型String的引用類型)、Date、Array、Boolean(...)、Math、RegExp等等。
在程序運(yùn)行的時(shí)候,整塊內(nèi)存可以劃分為常量池(存放基本類型的值)、棧(存放變量)、很大的堆(存放對(duì)象)、運(yùn)行時(shí)環(huán)境(函數(shù)運(yùn)行時(shí))
基本數(shù)據(jù)類型的值是直接在常量池里面可以拿到,而引用類型是拿到的是對(duì)象的引用
var a = 1; var b = "hello"; var c = a;
c = a,這種基本數(shù)據(jù)類型的復(fù)制,只是重新復(fù)制一份獨(dú)立的副本,在變量的對(duì)象上創(chuàng)建一個(gè)新的值,再把值復(fù)制到新變量分配的位置上,a、c他們自己的操作不會(huì)影響到對(duì)方。
a++;console.log(a);console.log(c)
顯然是輸出2、1
obj1和obj2,拿到的是新創(chuàng)建的對(duì)象的引用(也就是家里的鑰匙,每個(gè)人帶一把),當(dāng)操作對(duì)象的時(shí)候,對(duì)象發(fā)生改變,另一個(gè)obj訪問的時(shí)候,發(fā)現(xiàn)對(duì)象也會(huì)改。就像,家里有一個(gè)人回去搞衛(wèi)生了,另一個(gè)回家發(fā)現(xiàn)家里很干凈了。
var obj1 = new Object(); obj1.name = "obj1" var obj2 = obj1 console.log(obj2) //{name: "obj1"}
對(duì)于vue,為什么data必須是一個(gè)返回一個(gè)對(duì)象的函數(shù),也是這個(gè)道理,避免所有的vue實(shí)例共用一套data。所以對(duì)于類似于這種情況,我們可以像vue那樣處理
//data是一個(gè)對(duì)象的時(shí)候,共用一套data function D(){} D.prototype.data = {a:1,b:2} var a = new D() var b = new D() a.data.a = 666 b.data.a //666 //data是一個(gè)函數(shù)的時(shí)候,各自維護(hù)自己的data function D(){ this.data = this.data() } D.prototype.data = function () { return { a:1,b:2 } } var a = new D() var b = new D() a.data.a = 666 b.data.a //1
同樣的身為引用類型的函數(shù)也是同理
var a = function(){console.log(1)} var b = a; a = null; b();a() //b輸出1,a報(bào)錯(cuò):Uncaught TypeError: a is not a function //a指向函數(shù),b拿到和a一樣的指針,然后讓a指向空
把a(bǔ)變成null,只是切斷了a和函數(shù)之間的引用關(guān)系,對(duì)b沒有影響
2.再說順序大家常聽說的先定義后執(zhí)行,其實(shí)就是在棧中先開辟一塊內(nèi)存空間,然后在拿到他所對(duì)應(yīng)的值,基本類型去常量池,引用類型去堆拿到他的引用。大家常說的原始類型值在棧,其實(shí)就是這種效果。
在計(jì)算機(jī)的數(shù)據(jù)結(jié)構(gòu)中,棧比堆的運(yùn)算速度快,Object是一個(gè)復(fù)雜的結(jié)構(gòu)且可以擴(kuò)展:數(shù)組可擴(kuò)充,對(duì)象可添加屬性,都可以增刪改查。將他們放在堆中是為了不影響棧的效率。而是通過引用的方式查找到堆中的實(shí)際對(duì)象再進(jìn)行操作。
因此又引出另一個(gè)話題,查找值的時(shí)候先去棧查找再去堆查找。
既然都講了,棧比堆的運(yùn)算速度,堆存放的是復(fù)雜數(shù)據(jù)類型。那么簡(jiǎn)單來說,寧愿大海撈針呢還是碗里撈針呢?
3.然后到了函數(shù)先拋出一個(gè)問題
function a(){console.log(2)}; var a = function(){console.log(1)}; a()
覆蓋?那么交換的結(jié)果又是什么呢?
var a = function(){console.log(1)}; function a(){console.log(2)}; a()
都是1,然后有的人就說了,var優(yōu)先。好的,那為什么var優(yōu)先?
先定義后執(zhí)行,先去棧查找
變量提升,其實(shí)也是如此。先定義(開辟一塊內(nèi)存空間,此時(shí)值可以說是undefined)后執(zhí)行(從上到下,該賦值的就賦值,該執(zhí)行操作的就去操作),就近原則
函數(shù)聲明和函數(shù)表達(dá)式,有時(shí)候不注意,就不小心出錯(cuò)了
a(); function a(){console.log(666)}//666
另一種情況:
a(); var a = function (){console.log(666)}//a is not a function
雖然第一種方法有變量提升,不會(huì)出錯(cuò),正常來說,還是按順序?qū)懀x語句放前面。如果想嚴(yán)格要求自己,就手動(dòng)來個(gè)嚴(yán)格模式‘use strict’吧。對(duì)于框架的開發(fā),需要嚴(yán)謹(jǐn)遵守規(guī)則,所以一般會(huì)用嚴(yán)格模式。
4.接著是臨時(shí)空間函數(shù)執(zhí)行的時(shí)候,會(huì)臨時(shí)開辟一塊內(nèi)存空間,這塊內(nèi)存空間長(zhǎng)得和外面這個(gè)一樣,也有自己的棧堆,當(dāng)函數(shù)運(yùn)行完就銷毀。
4.1 eg1:var a = 10; function() { console.log(a);//undefined var a = 1; console.log(a)//1 }
宏觀來說,只有2步一和二,當(dāng)執(zhí)行第二步,就跳到函數(shù)內(nèi)部執(zhí)行②-⑧
函數(shù)外部的a=10完全就沒有關(guān)系,這里面造成undefined主要因?yàn)樽兞刻嵘鋵?shí)準(zhǔn)確的順序是:
var a console.log(a);//undefined a = 1; console.log(a)//1
為什么不出去找全局的a?
就近原則。為什么就近原則?都確定函數(shù)內(nèi)部有定義了,就不會(huì)再去外面白費(fèi)力氣。其實(shí)是,函數(shù)在自己的作用域內(nèi)找到就不會(huì)再再繼續(xù)找,類似原型鏈一樣,在構(gòu)造函數(shù)里面找到某個(gè)屬性就不會(huì)去原型找,找不到才去,再找不到就再往上。函數(shù)也是,沿著作用域鏈查找。類似的一個(gè)例子,我們用函數(shù)聲明定義一個(gè)函數(shù)f,再用一個(gè)變量g拿到這個(gè)函數(shù)的引用,然后在外面用f是訪問不了這個(gè)函數(shù)的,但是在函數(shù)內(nèi)部是能找到f這個(gè)名字的:
var g = function f(){ console.log(f) } g()//打印整個(gè)函數(shù) f()//報(bào)錯(cuò)4.2 eg2
function f(){ return function f1(){ console.log(1) } }; var res = f(); res(); f1()
res(),返回的是里面的函數(shù),如果直接f1()就報(bào)錯(cuò),因?yàn)檫@是window.f1()
函數(shù)聲明后,可以通過引用名稱查找或者內(nèi)存地址查找
局部作用域用function聲明,聲明不等于創(chuàng)建,只有調(diào)用函數(shù)的時(shí)候才創(chuàng)建
函數(shù)f有內(nèi)存地址的話,通過棧找f的內(nèi)存空間,如果找不到棧中f這個(gè)變量,就去堆中找
5.垃圾回收進(jìn)行前端開發(fā)時(shí)幾乎不需要關(guān)心內(nèi)存問題,V8限制的內(nèi)存幾乎不會(huì)出現(xiàn)用完的情況,而且我們只要關(guān)閉了瀏覽器,一切都結(jié)束。如果是node后端,后端程序往往進(jìn)行更加復(fù)雜的操作,加上長(zhǎng)期運(yùn)行在服務(wù)器不重啟,如果不關(guān)注內(nèi)存管理,積少成多就會(huì)導(dǎo)致內(nèi)存泄漏。
node中的內(nèi)存第一個(gè)部分還是和上面的一樣,有棧、堆、運(yùn)行時(shí)環(huán)境,另外還有一個(gè)緩沖區(qū)存放Buffer。你可以通過process.memoryUsage()查看node里面進(jìn)程內(nèi)存使用情況。堆中的對(duì)象,被劃分為新生代和老生代,他們會(huì)被不同的垃圾回收機(jī)制清理掉。
新生代用Scavenge算法進(jìn)行垃圾回收,利用復(fù)制的方式實(shí)現(xiàn)內(nèi)存回收的算法。
他的過程是:
將新生代的總空間一分為二,只使用其中一個(gè),另一個(gè)處于閑置,等待垃圾回收時(shí)使用。使用中的那塊空間稱為From,閑置的空間稱為To
當(dāng)觸發(fā)垃圾回收時(shí),V8將From空間中所有存活下來的對(duì)象復(fù)制到To空間。
From空間所有應(yīng)該存活的對(duì)象都復(fù)制完成后,原本的From空間將被釋放,成為閑置空間,原本To空間則成為使用中空間,也就是功能交換。
如果某對(duì)象已經(jīng)經(jīng)歷一次新生代垃圾回收而且第二次依舊存活,或者To空間已經(jīng)使用了25%,都會(huì)晉升至老生代
5.2老生代老生代利用了標(biāo)記-清除(后面又加上了標(biāo)記-整理)的方式進(jìn)行垃圾回收。
在標(biāo)記階段(周期比較大)遍歷堆中的所有對(duì)象,標(biāo)記活著的對(duì)象,在隨后的清除階段中,只清除沒有被標(biāo)記的對(duì)象。每個(gè)內(nèi)存頁有一個(gè)用來標(biāo)記對(duì)象的位圖。這個(gè)位圖另外有兩位用來標(biāo)記對(duì)象的狀態(tài),這個(gè)狀態(tài)一共有三種:未被垃圾回收器發(fā)現(xiàn)、被垃圾回收器發(fā)現(xiàn)但鄰接對(duì)象尚未全部處理、不被垃圾回收器發(fā)現(xiàn)但鄰接對(duì)象全部被處理。分別對(duì)應(yīng)著三種顏色:白、灰、黑。
遍歷的時(shí)候,主要是利用DFS。剛剛開始的時(shí)候,所有的對(duì)象都是白色。從根對(duì)象開始遍歷,遍歷過的對(duì)象會(huì)變成灰色,放入一個(gè)額外開辟的雙端隊(duì)列中。標(biāo)記階段的每次循環(huán),垃圾回收器都會(huì)從雙端隊(duì)列中取出一個(gè)對(duì)象染成黑對(duì)象,并將鄰接的對(duì)象染色為灰,然后把其鄰接對(duì)象放入雙端隊(duì)列。一直循環(huán),最后所有的對(duì)象只有黑和白,白色的將會(huì)被清理。
假設(shè)全局根對(duì)象是root,那么活對(duì)象必然是被連接在對(duì)象樹上面的,如果是死對(duì)象,比如var a = {};a=null我們創(chuàng)建了一個(gè)對(duì)象,但把他從對(duì)象樹上面切斷聯(lián)系。這樣子,DFS必然找不到他,他永遠(yuǎn)是白色。
此外,在過程中把垃圾對(duì)象刪除后,內(nèi)存空間是一塊一塊地零星散亂地分布,如果是遇到一個(gè)需要很大內(nèi)存空間的對(duì)象,需要連續(xù)一大片內(nèi)存存儲(chǔ)的對(duì)象,那就有問題了。所以還有一個(gè)整理的階段,把對(duì)象整理到在內(nèi)存上連續(xù)分布。
新生代是經(jīng)常發(fā)生的,老生代發(fā)生的周期長(zhǎng)
新生代占用的內(nèi)存小,老生代占用了大部分內(nèi)存
新生代需要把內(nèi)存分成兩塊進(jìn)行操作,老生代不需要
新生代是基于對(duì)象復(fù)制,如果對(duì)象太多,復(fù)制消耗也會(huì)很大,所以需要和老生代相互合作。老生代基于DFS,深度遍歷每一個(gè)活對(duì)象
顯然老生代花銷大,所以他的周期也長(zhǎng),但是比較徹底
6.IIFE和閉包 6.1 IIFE立即執(zhí)行函數(shù),形成一個(gè)沙盒環(huán)境,防止變量污染內(nèi)部,是做各種框架的好方法
先手寫一段假的jQuery
(function(root){ var $ = function(){ //代碼 } root.$ = $ })(this)
這樣子在內(nèi)部函數(shù)里面寫相關(guān)的表達(dá)式,我們就可以用美元符號(hào)使用jQuery(實(shí)際上jQuery第一個(gè)括號(hào)是全局環(huán)境判斷,真正的函數(shù)體放在第二個(gè)括號(hào)里面,號(hào)稱世界上最強(qiáng)的選擇器sizzle也里面)
閉包的概念各有各的說法,平時(shí)人家問閉包是什么,大概多數(shù)人都是說在函數(shù)中返回函數(shù)、函數(shù)外面能訪問到里面的變量,這些顯而易見的現(xiàn)象,或者把一些長(zhǎng)篇大論搬出來。簡(jiǎn)單來說,就是外部訪問內(nèi)部變量,而且內(nèi)部臨時(shí)開辟的內(nèi)存空間不會(huì)被垃圾回收。查找值的時(shí)候沿著作用域鏈查找,找到則停止。
對(duì)于js各種庫,是一個(gè)龐大的IIFE包裹著,如果他被垃圾回收了,我們肯定不能利用了。而我們實(shí)際上就是能利用他,就是因?yàn)樗┞读私涌冢沟萌汁h(huán)境保持對(duì)IIFE內(nèi)部的函數(shù)和變量的引用,我們才得以利用。
各種書對(duì)于閉包的解釋:
《權(quán)威指南》:函數(shù)對(duì)象通過作用域鏈相互關(guān)聯(lián)起來,函數(shù)內(nèi)部變量都可以保持在函數(shù)的作用域中,有權(quán)訪問另一個(gè)函數(shù)作用域中的變量
《忍者秘籍》:一個(gè)函數(shù)創(chuàng)建時(shí)允許自身訪問并操作該自身函數(shù)以外的變量所創(chuàng)建的作用域
《你不知道的js》:是基于詞法的作用域書寫代碼時(shí)所產(chǎn)生的結(jié)果,當(dāng)函數(shù)記住并訪問所在的詞法作用域,閉包就產(chǎn)生了
閉包的產(chǎn)生,會(huì)導(dǎo)致內(nèi)存泄漏。
前面已經(jīng)說到,js具有垃圾回收機(jī)制,如果發(fā)現(xiàn)變量被不使用將會(huì)被回收,而閉包相互引用,讓他不會(huì)被回收,一直占據(jù)著一塊內(nèi)存,長(zhǎng)期持有一塊內(nèi)存的引用,所以導(dǎo)致內(nèi)存泄漏。
var b = 10 function a(){ var b = 1 return function c(){//暴露內(nèi)部函數(shù)的接口 console.log(b) } } a()()//1,外部拿到內(nèi)部的引用,臨時(shí)開辟的內(nèi)存空間不會(huì)被回收 //改寫成IIFE形式 var b = 10 var a = (function(){ var b = 1 return function c(){ console.log(b) } })() a()//1 //改成window對(duì)象的一個(gè)引用 var b = 10 (function(){ var b = 1 window.c = function(){ console.log(b) } })() c()//1 //多個(gè)閉包 function a(){ var s = 1 return function count(){ s++ console.log(s) } } var b = a()//相當(dāng)于賦值 var c = a() b()//2 b()//3 c()//2,各自保持各自的”賦值結(jié)果”,互相不干擾 //r被垃圾回收 function a(){ var r = 1 var s = 1 return function count(){ s++ console.log(s) } } var b = a()//我們可以打個(gè)斷點(diǎn),在谷歌瀏覽器看他的調(diào)用棧,發(fā)現(xiàn)閉包里面沒有r了
對(duì)于最后一個(gè)例子,r、s并不是像一些人認(rèn)為的那樣,有閉包了,r、s都會(huì)留下,其實(shí)是r已經(jīng)被回收了。在執(zhí)行的函數(shù)時(shí)候,將會(huì)為這個(gè)函數(shù)創(chuàng)建一個(gè)上下文ctx,最開始這個(gè)ctx是空的,從上到下執(zhí)行到函數(shù)a的閉包聲明b時(shí),由于b函數(shù)依賴變量s ,因此會(huì)將 s 加入b的ctx——ctx2。a內(nèi)部所有的閉包,都會(huì)持有這個(gè)ctx2。(所以說,閉包之所以閉包,就是因?yàn)槌钟羞@個(gè)ctx)
每一個(gè)閉包都會(huì)引用其外部函數(shù)的ctx(這里是b的ctx2),讀取變量s的時(shí)候,被閉包捕捉,加入ctx中的變量,接著被分配到堆。而真正的局部變量是r ,保存在棧,當(dāng)b執(zhí)行完畢后出棧并且被垃圾回收。而a的ctx被閉包引用,如果有任何一個(gè)閉包存活,他對(duì)應(yīng)的ctx都將存活,變量也不會(huì)被銷毀。
我們也聽說一句話,盡量避免全局變量。其實(shí)也是這樣的道理,一個(gè)函數(shù)返回另一個(gè)函數(shù),也就是分別把兩個(gè)函數(shù)按順序壓入調(diào)用棧。我們知道棧是先進(jìn)后出,那全局的變量(也處于棧底),越是不能得到垃圾回收,存活的時(shí)間越長(zhǎng)。但也許全局變量在某個(gè)時(shí)候開始就沒有作用了,就不能被回收,造成了內(nèi)存泄漏。所以又引出另一個(gè)常見的注意事項(xiàng):不要過度利用閉包。用得越多,棧越深,變量越不能被回收。
瀏覽器的全局對(duì)象為window,關(guān)閉瀏覽器自然一切結(jié)束。Node中全局對(duì)象為global,如果global中有屬性已經(jīng)沒有用處了,一定要設(shè)置為null,因?yàn)橹挥械鹊匠绦蛲V惯\(yùn)行,才會(huì)銷毀。而我們的服務(wù)器當(dāng)然是長(zhǎng)期不關(guān)機(jī)的,內(nèi)存泄漏積少成多,爆內(nèi)存是早晚的事情。
Node中,當(dāng)一個(gè)模塊被引入,這個(gè)模塊就會(huì)被緩存在內(nèi)存中,提高下次被引用的速度(緩存代理)。一般情況下,整個(gè)Node程序中對(duì)同一個(gè)模塊的引用,都是同一個(gè)實(shí)例(instance),這個(gè)實(shí)例一直存活在內(nèi)存中。所以,如果任意模塊中有變量已經(jīng)不再需要,最好手動(dòng)設(shè)置為null,不然會(huì)白白占用內(nèi)存
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/94417.html
摘要:函數(shù)在執(zhí)行的時(shí)候執(zhí)行函數(shù),將當(dāng)前的變量對(duì)象由于當(dāng)前的環(huán)境是函數(shù),所以將其活動(dòng)對(duì)象作為變量對(duì)象添加到作用域鏈的前端。此時(shí),由于在執(zhí)行,而作用域鏈也存在,所以可以在作用域鏈上進(jìn)行查找,去訪問的變量。 一、現(xiàn)狀 閉包是jser繞不過的坎,一直在都在說,套用 simpson 的話來說:JavaScript中閉包無處不在,你只需要能夠識(shí)別并擁抱它。 閉包是基于詞法作用域書寫代碼時(shí)的自然結(jié)果,你甚...
摘要:將作用域賦值給變量這里的作用域是,而不是將作用域賦值給一個(gè)變量閉包返回瀏覽器中內(nèi)存泄漏問題大家都知道,閉包會(huì)使變量駐留在內(nèi)存中,這也就導(dǎo)致了內(nèi)存泄漏。 上一章我們講了匿名函數(shù)和閉包,這次我們來談?wù)勯]包中作用域this的問題。 大家都知道,this對(duì)象是在運(yùn)行時(shí)基于函數(shù)的執(zhí)行環(huán)境綁定的,如果this在全局就是[object window],如果在對(duì)象內(nèi)部就是指向這個(gè)對(duì)象,而閉包卻是在運(yùn)行...
摘要:關(guān)于循環(huán)和閉包當(dāng)循環(huán)和閉包結(jié)合在一起時(shí),經(jīng)常會(huì)產(chǎn)生讓初學(xué)者覺得匪夷所思的問題。閉包是一把雙刃劍是比較難以理解和掌握的部分,它十分強(qiáng)大,卻也有很大的缺陷,如何使用它完全取決于你自己。 在談閉包之前,我們首先要了解幾個(gè)概念: 什么是函數(shù)表達(dá)式? 與函數(shù)聲明有何不同? JavaScript查找標(biāo)識(shí)符的機(jī)制 JavaScript的作用域是詞法作用域 JavaScript的垃圾回收機(jī)制 先來...
摘要:在內(nèi)部,理所當(dāng)然能訪問到局部變量,但當(dāng)作為的返回值賦給外的全局變量時(shí),神奇的事情發(fā)生了在全局作用域中訪問到了,這就是閉包。而閉包最神奇的地方就是能在一個(gè)函數(shù)外訪問函數(shù)中的局部變量,把這些變量用閉包的形式放在函數(shù)中便能避免污染。 一、閉包是什么? 《JavaScript高級(jí)程序設(shè)計(jì)》中寫道:閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù),如果用下定義的觀點(diǎn)看,這句話就是說閉包是函數(shù),我...
本文不會(huì)過多講解基礎(chǔ)知識(shí),更多說的是在使用useRef如何能擺脫 這個(gè) 閉包陷阱 ? react hooks 的閉包陷阱 基本每個(gè)開發(fā)員都有遇見,這是很令人抓狂的。 (以下react示范demo,均為react 16.8.3 版本) 列一個(gè)具體的場(chǎng)景: functionApp(){ const[count,setCount]=useState(1); useEffect(()=...
閱讀 1726·2021-10-18 13:34
閱讀 3912·2021-09-08 10:42
閱讀 1559·2021-09-02 09:56
閱讀 1612·2019-08-30 15:54
閱讀 3135·2019-08-29 18:44
閱讀 3305·2019-08-26 18:37
閱讀 2219·2019-08-26 12:13
閱讀 460·2019-08-26 10:20