[ JS 进阶 ] 闭包,做用域链,垃圾回收,内存泄露

1. 什么是闭包?

来看一些关于闭包的定义:javascript

  1. 闭包是指有权访问另外一个函数做用域中变量的函数 --《JS高级程序设计第三版》 p178
  2. 函数对象能够经过做用域链相关联起来,函数体内部的变量均可以保存在函数做用域内,这种特性称为 ‘闭包’ 。 --《JS权威指南》 p183
  3. 内部函数能够访问定义它们的外部函数的参数和变量(除了thisarguments)。 --《JS语言精粹》 p36

来个定义总结php

  1. 能够访问外部函数做用域中变量的函数
  2. 被内部函数访问的外部函数的变量能够保存在外部函数做用域内而不被回收---这是核心,后面咱们遇到闭包都要想到,咱们要重点关注被闭包引用的这个变量。

来建立个简单的闭包前端

var sayName = function(){ var name = 'jozo'; return function(){ alert(name); } }; var say = sayName(); say(); 

来解读后面两个语句:java

  • var say = sayName() :返回了一个匿名的内部函数保存在变量say中,而且引用了外部函数的变量name,因为垃圾回收机制,sayName函数执行完毕后,变量name并无被销毁。
  • say() :执行返回的内部函数,依然能访问变量name,输出 'jozo' .

2. 闭包中的做用域链

理解做用域链对理解闭包也颇有帮助。算法

变量在做用域中的查找方式应该都很熟悉了,其实这就是顺着做用域链往上查找的。chrome

当函数被调用时:闭包

  1. 先建立一个执行环境(execution context),及相应的做用域链;
  2. 将arguments和其余命名参数的值添加到函数的活动对象(activation object)

做用域链:当前函数的活动对象优先级最高,外部函数的活动对象次之,外部函数的外部函数的活动对象依次递减,直至做用域链的末端--全局做用域。优先级就是变量查找的前后顺序;模块化

先来看个普通的做用域链:函数

function sayName(name){ return name; } var say = sayName('jozo'); 

这段代码包含两个做用域:a.全局做用域;b.sayName函数的做用域,也就是只有两个变量对象,当执行到对应的执行环境时,该变量对象会成为活动对象,并被推入到执行环境做用域链的前端,也就是成为优先级最高的那个。 看图说话:字体

这图在JS高级程序设计书上也有,我从新绘了遍。

在建立sayName()函数时,会建立一个预先包含变量对象的做用域链,也就是图中索引为1的做用域链,而且被保存到内部的[[Scope]]属性中,当调用sayName()函数的时候,会建立一个执行环境,而后经过复制函数的[[Scope]]属性中的对象构建起做用域链,此后,又有一个活动对象(图中索引为0)被建立,并被推入执行环境做用域链的前端。

通常来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域。可是,闭包的状况又有所不一样 :

再来看看看闭包的做用域链:

function sayName(name){ return function(){ return name; } } var say = sayName('jozo'); 

这个闭包实例比上一个例子多了一个匿名函数的做用域:

在匿名函数从sayName()函数中被返回后,它的做用域链被初始化为包含sayName()函数的活动对象和全局变量对象。这样,匿名函数就能够访问在sayName()中定义的全部变量和参数,更为重要的是,sayName()函数在执行完毕后,其活动对象也不会被销毁,由于匿名函数的做用域链依然在引用这个活动对象,换句话说,sayName()函数执行完后,其执行环境的做用域链会被销毁,但他的活动对象会留在内存中,知道匿名函数会销毁。这个也是后面要讲到的内存泄露的问题。

做用域链问题不写那么多了,写书上的东西也很累 o(╯□╰)o

3. 闭包的实例

实例1:实现累加

// 方式1 var a = 0; var add = function(){ a++; console.log(a) } add(); add(); //方式2 :闭包 var add = (function(){ var a = 0; return function(){ a++; console.log(a); } })(); console.log(a); //undefined add(); add(); 相比之下方式2更加优雅,也减小全局变量,将变量私有化 

实例2 :给每一个li添加点击事件

var oli = document.getElementsByTagName('li'); var i; for(i = 0;i < 5;i++){ oli[i].onclick = function(){ alert(i); } } console.log(i); // 5 //执行匿名函数 (function(){ alert(i); //5 }()); 

上面是一个经典的例子,咱们都知道执行结果是都弹出5,也知道能够用闭包解决这个问题,可是我刚开始始终不能明白为何每次弹出都是5,为何闭包能够解决这问题。后来捋一捋仍是把它弄清晰了:

a. 先来分析没用闭包前的状况:for循环中,咱们给每一个li点击事件绑定了一个匿名函数,匿名函数中返回了变量i的值,当循环结束后,变量i的值变为5,此时咱们再去点击每一个li,也就是执行相应的匿名函数(看上面的代码),这是变量i已是5了,因此每一个点击弹出5. 由于这里返回的每一个匿名函数都是引用了同一个变量i,若是咱们新建一个变量保存循环执行时当前的i的值,而后再让匿名函数应用这个变量,最后再返回这个匿名函数,这样就能够达到咱们的目的了,这就是运用闭包来实现的!

b. 再来分析下运用闭包时的状况:

var oli = document.getElementsByTagName('li'); var i; for(i = 0;i < 5;i++){ oli[i].onclick = (function(num){ var a = num; // 为了说明问题 return function(){ alert(a); } })(i) } console.log(i); // 5 

这里for循环执行时,给点击事件绑定的匿名函数传递i后当即执行返回一个内部的匿名函数,由于参数是按值传递的,因此此时形参num保存的就是当前i的值,而后赋值给局部变量 a,而后这个内部的匿名函数一直保存着a的引用,也就是一直保存着当前i的值。 因此循环执行完毕后点击每一个li,返回的匿名函数执行弹出各自保存的 a 的引用的值。

4. 闭包的运用

咱们来看看闭包的用途。事实上,经过使用闭包,咱们能够作不少事情。好比模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提高代码的执行效率。

1. 匿名自执行函数

咱们在实际状况下常常遇到这样一种状况,即有的函数只须要执行一次,其内部变量无需维护,好比UI的初始化,那么咱们可使用闭包:

//将所有li字体变为红色 (function(){ var els = document.getElementsByTagName('li'); for(var i = 0,lng = els.length;i < lng;i++){ els[i].style.color = 'red'; } })(); 

咱们建立了一个匿名的函数,并当即执行它,因为外部没法引用它内部的变量,
所以els,i,lng这些局部变量在执行完后很快就会被释放,节省内存!
关键是这种机制不会污染全局对象。

2. 实现封装/模块化代码

var person= function(){ //变量做用域为函数内部,外部没法访问 var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }(); console.log(person.name);//直接访问,结果为undefined console.log(person.getName()); //default person.setName("jozo"); console.log(person.getName()); //jozo 

3. 实现面向对象中的对象
这样不一样的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,可是经过使用闭包,
咱们能够模拟出这样的机制。仍是以上边的例子来说:

function Person(){ var name = "default"; return { getName : function(){ return name; }, setName : function(newName){ name = newName; } } }; var person1= Person(); print(person1.getName()); john.setName("person1"); print(person1.getName()); // person1 var person2= Person(); print(person2.getName()); jack.setName("erson2"); print(erson2.getName()); //person2 

Person的两个实例person1 和 person2 互不干扰!由于这两个实例对name这个成员的访问是独立的 。

5. 内存泄露及解决方案

垃圾回收机制

说到内存管理,天然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收:标记清除 和 引用计数;

标记清除:垃圾收集器在运行的时候会给存储在内存中的全部变量都加上标记,而后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,若是变量再被标记则表示此变量准备被删除。 2008年为止,IE,Firefox,opera,chrome,Safari的javascript都用使用了该方式;

引用计数:跟踪记录每一个值被引用的次数,当声明一个变量并将一个引用类型的值赋给该变量时,这个值的引用次数就是1,若是这个值再被赋值给另外一个变量,则引用次数加1。相反,若是一个变量脱离了该值的引用,则该值引用次数减1,当次数为0时,就会等待垃圾收集器的回收。

这个方式存在一个比较大的问题就是循环引用,就是说A对象包含一个指向B的指针,对象B也包含一个指向A的引用。 这就可能形成大量内存得不到回收(内存泄露),由于它们的引用次数永远不多是 0 。早期的IE版本里(ie4-ie6)采用是计数的垃圾回收机制,闭包致使内存泄露的一个缘由就是这个算法的一个缺陷。

咱们知道,IE中有一部分对象并非原生额javascript对象,例如,BOM和DOM中的对象就是以COM对象的形式实现的,而COM对象的垃圾回收机制采用的就是引用计数。所以,虽然IE的javascript引擎采用的是标记清除策略,可是访问COM对象依然是基于引用计数的,所以只要在IE中设计COM对象就会存在循环引用的问题!

举个栗子:

window.onload = function(){ var el = document.getElementById("id"); el.onclick = function(){ alert(el.id); } } 

这段代码为何会形成内存泄露?

el.onclick= function () { alert(el.id); }; 

执行这段代码的时候,将匿名函数对象赋值给el的onclick属性;而后匿名函数内部又引用了el对象,存在循环引用,因此不能被回收;

解决方法:

window.onload = function(){ var el = document.getElementById("id"); var id = el.id; //解除循环引用 el.onclick = function(){ alert(id); } el = null; // 将闭包引用的外部函数中活动对象清除 } 

6. 总结闭包的优缺点

优势:

  • 可让一个变量常驻内存 (若是用的多了就成了缺点
  • 避免全局变量的污染
  • 私有化变量

缺点

  • 由于闭包会携带包含它的函数的做用域,所以会比其余函数占用更多的内存
  • 引发内存泄露
相关文章
相关标签/搜索