浅谈js的内存与闭包

本文来自于个人github

0.前言

主要结合了内存的概念讲了js的一些的很简单、可是又不当心就犯错的地方。 结论:js执行顺序,先定义,后执行,从上到下,就近原则。闭包可让外部访问某函数内部变量,并且会致使内存泄漏。javascript

1.先说类型

在ECMAscript数据类型有基本类型和引用类型,基本类型有Undefined、Null、Boolean、Number、String,引用类型有Object,全部的的值将会是6种的其中之一(数据类型具备动态性,没有定义其余数据类型的必要了) 引用类型的值,也就是对象,一个对象是某个引用类型的一个实例,用new操做符建立也能够用字面量的方式(对象字面量建立var obj ={ })。ECMA里面有不少原生的引用类型,就是查文档的时候看见的那些:Function、Number (是对于原始类型Number的引用类型)、String(是对于原始类型String的引用类型)、Date、Array、Boolean(...)、Math、RegExp等等。 在程序运行的时候,整块内存能够划分为常量池(存放基本类型的值)、栈(存放变量)、很大的堆(存放对象)、运行时环境(函数运行时)前端

1

基本数据类型的值是直接在常量池里面能够拿到,而引用类型是拿到的是对象的引用vue

var a = 1;
var b = 'hello';
var c = a;
复制代码

c = a,这种基本数据类型的复制,只是从新复制一份独立的副本,在变量的对象上建立一个新的值,再把值复制到新变量分配的位置上,a、c他们本身的操做不会影响到对方。java

a++;console.log(a);console.log(c)
复制代码

显然是输出二、1node

obj1和obj2,拿到的是新建立的对象的引用(也就是家里的钥匙,每一个人带一把),当操做对象的时候,对象发生改变,另外一个obj访问的时候,发现对象也会改。就像,家里有一我的回去搞卫生了,另外一个回家发现家里很干净了。git

var obj1 = new Object();
obj1.name = 'obj1'
var obj2 = obj1
console.log(obj2)  //{name: "obj1"}
复制代码

2

对于vue,为何data必须是一个返回一个对象的函数,也是这个道理,避免全部的vue实例共用一套data。因此对于相似于这种状况,咱们能够像vue那样处理github

//data是一个对象的时候,共用一套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是一个函数的时候,各自维护本身的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
复制代码

一样的身为引用类型的函数也是同理算法

var a = function(){console.log(1)}
var b = a;
a = null;
b();a()
//b输出1,a报错:Uncaught TypeError: a is not a function
//a指向函数,b拿到和a同样的指针,而后让a指向空
复制代码

把a变成null,只是切断了a和函数之间的引用关系,对b没有影响后端

2.再说顺序

你们常据说的先定义后执行,其实就是在栈中先开辟一块内存空间,而后在拿到他所对应的值,基本类型去常量池,引用类型去堆拿到他的引用。你们常说的原始类型值在栈,其实就是这种效果。 数组

3

2.1 为何引用类型值要放在堆中,而原始类型值要放在栈

在计算机的数据结构中,栈比堆的运算速度快,Object是一个复杂的结构且能够扩展:数组可扩充,对象可添加属性,均可以增删改查。将他们放在堆中是为了避免影响栈的效率。而是经过引用的方式查找到堆中的实际对象再进行操做。 所以又引出另外一个话题,查找值的时候先去栈查找再去堆查找。

2.2 为何先去栈查找再去堆查找

既然都讲了,栈比堆的运算速度,堆存放的是复杂数据类型。那么简单来讲,宁愿大海捞针呢仍是碗里捞针呢?

3.而后到了函数

先抛出一个问题

function a(){console.log(2)};
var a  = function(){console.log(1)};
a()
复制代码

覆盖?那么交换的结果又是什么呢?

var a  = function(){console.log(1)};
function a(){console.log(2)};
a()
复制代码

都是1,而后有的人就说了,var优先。好的,那为何var优先?

4

先定义后执行,先去栈查找

变量提高,其实也是如此。先定义(开辟一块内存空间,此时值能够说是undefined)后执行(从上到下,该赋值的就赋值,该执行操做的就去操做),就近原则 函数声明和函数表达式,有时候不注意,就不当心出错了

a(); function a(){console.log(666)}//666
复制代码

另外一种状况:

a(); var a = function (){console.log(666)}//a is not a function
复制代码

虽然第一种方法有变量提高,不会出错,正常来讲,仍是按顺序写,定义语句放前面。若是想严格要求本身,就手动来个严格模式‘use strict’吧。对于框架的开发,须要严谨遵照规则,因此通常会用严格模式。

4.接着是临时空间

函数执行的时候,会临时开辟一块内存空间,这块内存空间长得和外面这个同样,也有本身的栈堆,当函数运行完就销毁。

4.1 eg1:

var a = 10;
function() {
console.log(a);//undefined
var a = 1;
console.log(a)//1
}
复制代码

宏观来讲,只有2步一和二,当执行第二步,就跳到函数内部执行②-⑧

5
函数外部的a=10彻底就没有关系,这里面形成undefined主要由于变量提高,其实准确的顺序是:

var a
console.log(a);//undefined
a = 1;
console.log(a)//1
复制代码

为何不出去找全局的a? 就近原则。为何就近原则?都肯定函数内部有定义了,就不会再去外面白费力气。实际上是,函数在本身的做用域内找到就不会再再继续找,相似原型链同样,在构造函数里面找到某个属性就不会去原型找,找不到才去,再找不到就再往上。函数也是,沿着做用域链查找。相似的一个例子,咱们用函数声明定义一个函数f,再用一个变量g拿到这个函数的引用,而后在外面用f是访问不了这个函数的,可是在函数内部是能找到f这个名字的:

var g = function f(){
   console.log(f)
    }
g()//打印整个函数
f()//报错
复制代码

4.2 eg2

function f(){
return function f1(){
       console.log(1)
   }
};
var res = f();
res();
f1()
复制代码

res(),返回的是里面的函数,若是直接f1()就报错,由于这是window.f1()

6

  • 函数声明后,能够经过引用名称查找或者内存地址查找
  • 局部做用域用function声明,声明不等于建立,只有调用函数的时候才建立
  • 函数f有内存地址的话,经过栈找f的内存空间,若是找不到栈中f这个变量,就去堆中找

5.垃圾回收

进行前端开发时几乎不须要关心内存问题,V8限制的内存几乎不会出现用完的状况,并且咱们只要关闭了浏览器,一切都结束。若是是node后端,后端程序每每进行更加复杂的操做,加上长期运行在服务器不重启,若是不关注内存管理,聚沙成塔就会致使内存泄漏。 node中的内存第一个部分仍是和上面的同样,有栈、堆、运行时环境,另外还有一个缓冲区存放Buffer。你能够经过process.memoryUsage()查看node里面进程内存使用状况。堆中的对象,被划分为新生代和老生代,他们会被不一样的垃圾回收机制清理掉。

5.1新生代

新生代用Scavenge算法进行垃圾回收,利用复制的方式实现内存回收的算法。 他的过程是:

  • 将新生代的总空间一分为二,只使用其中一个,另外一个处于闲置,等待垃圾回收时使用。使用中的那块空间称为From,闲置的空间称为To
  • 当触发垃圾回收时,V8将From空间中全部存活下来的对象复制到To空间。
  • From空间全部应该存活的对象都复制完成后,本来的From空间将被释放,成为闲置空间,本来To空间则成为使用中空间,也就是功能交换。
  • 若是某对象已经经历一次新生代垃圾回收并且第二次依旧存活,或者To空间已经使用了25%,都会晋升至老生代

1

5.2老生代

老生代利用了标记-清除(后面又加上了标记-整理)的方式进行垃圾回收。 在标记阶段(周期比较大)遍历堆中的全部对象,标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。每一个内存页有一个用来标记对象的位图。这个位图另外有两位用来标记对象的状态,这个状态一共有三种:未被垃圾回收器发现、被垃圾回收器发现但邻接对象还没有所有处理、不被垃圾回收器发现但邻接对象所有被处理。分别对应着三种颜色:白、灰、黑。

遍历的时候,主要是利用DFS。刚刚开始的时候,全部的对象都是白色。从根对象开始遍历,遍历过的对象会变成灰色,放入一个额外开辟的双端队列中。标记阶段的每次循环,垃圾回收器都会从双端队列中取出一个对象染成黑对象,并将邻接的对象染色为灰,而后把其邻接对象放入双端队列。一直循环,最后全部的对象只有黑和白,白色的将会被清理。 假设全局根对象是root,那么活对象必然是被链接在对象树上面的,若是是死对象,好比var a = {};a=null咱们建立了一个对象,但把他从对象树上面切断联系。这样子,DFS必然找不到他,他永远是白色。 此外,在过程当中把垃圾对象删除后,内存空间是一块一块地零星散乱地分布,若是是遇到一个须要很大内存空间的对象,须要连续一大片内存存储的对象,那就有问题了。因此还有一个整理的阶段,把对象整理到在内存上连续分布。

5.3 对比

  • 新生代是常常发生的,老生代发生的周期长
  • 新生代占用的内存小,老生代占用了大部份内存
  • 新生代须要把内存分红两块进行操做,老生代不须要
  • 新生代是基于对象复制,若是对象太多,复制消耗也会很大,因此须要和老生代相互合做。老生代基于DFS,深度遍历每个活对象
  • 显然老生代花销大,因此他的周期也长,可是比较完全

6.IIFE和闭包

6.1 IIFE

当即执行函数,造成一个沙盒环境,防止变量污染内部,是作各类框架的好方法 先手写一段假的jQuery

(function(root){
 var $ = function(){
//代码
}
root.$ = $
})(this)
复制代码

这样子在内部函数里面写相关的表达式,咱们就能够用美圆符号使用jQuery(实际上jQuery第一个括号是全局环境判断,真正的函数体放在第二个括号里面,号称世界上最强的选择器sizzle也里面)

7

6.2闭包

闭包的概念各有各的说法,平时人家问闭包是什么,大概多数人都是说在函数中返回函数、函数外面能访问到里面的变量,这些显而易见的现象,或者把一些长篇大论搬出来。简单来讲,就是外部访问内部变量,并且内部临时开辟的内存空间不会被垃圾回收。查找值的时候沿着做用域链查找,找到则中止。 对于js各类库,是一个庞大的IIFE包裹着,若是他被垃圾回收了,咱们确定不能利用了。而咱们实际上就是能利用他,就是由于他暴露了接口,使得全局环境保持对IIFE内部的函数和变量的引用,咱们才得以利用。 各类书对于闭包的解释: 《权威指南》:函数对象经过做用域链相互关联起来,函数内部变量均可以保持在函数的做用域中,有权访问另外一个函数做用域中的变量 《忍者秘籍》:一个函数建立时容许自身访问并操做该自身函数之外的变量所建立的做用域 《你不知道的js》:是基于词法的做用域书写代码时所产生的结果,当函数记住并访问所在的词法做用域,闭包就产生了 闭包的产生,会致使内存泄漏。 前面已经说到,js具备垃圾回收机制,若是发现变量被不使用将会被回收,而闭包相互引用,让他不会被回收,一直占据着一块内存,长期持有一块内存的引用,因此致使内存泄漏。

var b = 10
function a(){
	var b = 1
	return function c(){//暴露内部函数的接口
		console.log(b)
	}
}
a()()//1,外部拿到内部的引用,临时开辟的内存空间不会被回收

//改写成IIFE形式
var b = 10
var a = (function(){
	var b = 1
	return function c(){
		console.log(b)
	}
})()
a()//1

//改为window对象的一个引用
var b = 10
(function(){
	var b = 1
	window.c =  function(){
		console.log(b)
	}
})()
c()//1

//多个闭包
function a(){
	var s = 1
	return function count(){
		s++
		console.log(s)
	}
}
var b = a()//至关于赋值
var c = a()
b()//2
b()//3
c()//2,各自保持各自的”赋值结果”,互相不干扰

//r被垃圾回收
function a(){
        var r = 1
	var s = 1
	return function count(){
		s++
		console.log(s)
	}
}
var b = a()//咱们能够打个断点,在谷歌浏览器看他的调用栈,发现闭包里面没有r了
复制代码

对于最后一个例子,r、s并非像一些人认为的那样,有闭包了,r、s都会留下,实际上是r已经被回收了。在执行的函数时候,将会为这个函数建立一个上下文ctx,最开始这个ctx是空的,从上到下执行到函数a的闭包声明b时,因为b函数依赖变量s ,所以会将 s 加入b的ctx——ctx2。a内部全部的闭包,都会持有这个ctx2。(因此说,闭包之因此闭包,就是由于持有这个ctx) 每个闭包都会引用其外部函数的ctx(这里是b的ctx2),读取变量s的时候,被闭包捕捉,加入ctx中的变量,接着被分配到堆。而真正的局部变量是r ,保存在栈,当b执行完毕后出栈而且被垃圾回收。而a的ctx被闭包引用,若是有任何一个闭包存活,他对应的ctx都将存活,变量也不会被销毁。

image

咱们也据说一句话,尽可能避免全局变量。其实也是这样的道理,一个函数返回另外一个函数,也就是分别把两个函数按顺序压入调用栈。咱们知道栈是先进后出,那全局的变量(也处于栈底),越是不能获得垃圾回收,存活的时间越长。但也许全局变量在某个时候开始就没有做用了,就不能被回收,形成了内存泄漏。因此又引出另外一个常见的注意事项:不要过分利用闭包。用得越多,栈越深,变量越不能被回收。

浏览器的全局对象为window,关闭浏览器天然一切结束。Node中全局对象为global,若是global中有属性已经没有用处了,必定要设置为null,由于只有等到程序中止运行,才会销毁。而咱们的服务器固然是长期不关机的,内存泄漏聚沙成塔,爆内存是迟早的事情。

Node中,当一个模块被引入,这个模块就会被缓存在内存中,提升下次被引用的速度(缓存代理)。通常状况下,整个Node程序中对同一个模块的引用,都是同一个实例(instance),这个实例一直存活在内存中。因此,若是任意模块中有变量已经再也不须要,最好手动设置为null,否则会白白占用内存

相关文章
相关标签/搜索