Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【重要】对闭包 作用域链 垃圾回收机制以及内存泄露的理解 #5

Open
Ray1993 opened this issue Aug 29, 2017 · 0 comments

Comments

@Ray1993
Copy link
Owner

Ray1993 commented Aug 29, 2017

什么是闭包

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

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

来个定义总结

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

创建一个简单的闭包

var sayName = function(){
    var name ="Ray" ;
    return function(){
        alert(name) ;  
    }
} 
var say = sayName() ;
say() ;

解读:

  • 创建一个匿名函数并将它赋值给变量sayName,匿名函数中返回的函数引用了外部函数的变量name,从而形成了闭包,由于垃圾回收机制,sayName函数执行完毕后,变量name由于被引用并没有被销毁,继续存在内存当中。

  • 当执行say()的时候 ,执行返回的内部函数,依然能访问变量name,输出 'Ray' .

闭包中的作用域链

讲到闭包就不能不提作用域链,理解作用域链对理解闭包很重要。

先来看个普通的作用域链:

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

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

image

这图在JS高级程序设计书上也有(P179)。

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

一般来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是,闭包的情况又有所不同 ;
再来看看看闭包的作用域链:

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

这个闭包实例比上一个例子多了一个匿名函数的作用域:
image
这图在JS高级程序设计书上也有(P180)。

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

这里说一些题外话:关于作用域链和原型链的区别

原型链:
在JavaScript中,一共有两种类型的值,原始值和对象值.每个对象都有一个内部属性[[prototype]],我们通常称之为原型.原型的值可以是一个对象,也可以是null.如果它的值是一个对象,则这个对象也一定有自己的原型.这样就形成了一条线性的链,我们称之为原型链;
作用域链:
一般来说,作用域链是针对变量的,js里面大的范围上来说,只有两种作用域,全局作用域和函数内部作用域,如果函数1里面又定义了函数2(一般都是匿名函数), 那么就有了这么一个作用域链全局作用域==>函数1作用域==>函数2作用域;特点是函数1里面可以直接使用全局作用域的变量,函数2里面可以直接使用全局作用域和函数1作用域的变量

闭包的运用

我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。

1. 匿名自执行函数

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

function outputNumbers(count){
  (function(){
      for(var i=0 ; i<count ; i++){
          alert( i ) ; 
      }
  })();  //这里是块级作用域
  alert( i ); //导致一个错误!
}

我们在for循环外部插入了一个私有的作用域,在匿名函数里定义的任何变量,都会在执行结束时被销毁。因此,变量 i 只能在循环中使用,使用后即被销毁。而在私有作用域中能够访问变量count,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的所有变量。

通过这种创建私有作用域的方法,可以防止我们在全局作用域添加过多的变量和函数,导致命名冲突的问题,污染全局对象。同时也减少了闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕就可以立即销毁其作用域链

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();    
console.log(person1.getName());  //default  
person1.setName("person1");    
console.log(person1.getName());  // person1  

var person2= Person();    
console.log(person2.getName());  //default
person2.setName("person2");    
console.log(person2.getName());  //person2

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

闭包造成的内存泄露及解决方案

说到内存管理,自然离不开JS中的垃圾回收机制,有两种策略来实现垃圾回收(GC):标记清除 和 引用计数。
但是严格意义上讲,闭包不是真正产生内存泄漏的原因!关于内存泄漏的详细内容

标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量的标记和被环境中的变量引用的变量的标记,此后,如果变量再被标记则表示此变量准备被删除。 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对象就会存在循环引用的问题!(主要是低版本IE的垃圾回收机制问题

举个例子:

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; // 将闭包引用的外部函数中活动对象清除
}

总结闭包的优缺点

优点:

  • 可以让一个变量常驻内存 (如果用的多了就成了缺点)
  • 避免全局变量的污染(自执行的匿名函数 IIFE)
  • 实现面向对象中的对象
  • 封装,私有化变量

缺点

  • 因为闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,当内存占用过多时,会导致系统奔溃
  • 当闭包存在**循环引用(闭包保存了外层函数中element的引用,而element本身又引用了闭包)**将导致内存泄露,主要是IE9之前的版本因为对JScript对象和COM对象使用不同的垃圾收集机制,因此才有机会出现内存泄露

最后的最后。。。我们再来理解两个闭包的经典栗子:

首先我们要知道,this对象是基于函数的执行环境绑定的;在全局函数中,this等于window,而当函数 被作为某个对象的方法调用时,this等于那个对象;匿名函数的执行环境具有全局性,因此其this通常指向window;得出结论就是:只要不是对象的方法直接调用,就指向window ;

var name ="The Window" ;
var object = {
      name : "My Object" ,
      getNameFunc : function(){
           return function(){
                return this.name
           }
      }
}
alert(object.getNameFunc( )( )) ; //"The Window"

解读:

  • 这里由于getNameFunc( )返回一个函数,因此调用object.getNameFunc( )( )立即返回它调用的函数,这个函数不是对象的方法,是一个匿名函数,所以指向了window,因此返回"The Window";
var name ="The Window" ;
var object = {
      name : "My Object" ,
      getNameFunc : function(){
           **var that = this ;**
           return function(){
                **return that.name**
           }
      }
}
alert(object.getNameFunc( )( )) ; //"My Object"

解读:

  • 首先每个函数在被调用的时候都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象(什么是活动对象可以通过上面的图片了解)为止,因此永远不可能直接访问外部函数中的这两个变量
  • 上面的例子,通过将getNameFunc作用域中的this对象保存了在一个闭包能够访问的变量里,就可以让闭包访问到该对象,而getNameFunc是对象的方法,所以this指向对象,从而that也指向对象,所以返回"My Object"
@Ray1993 Ray1993 changed the title 闭包 原型链以及垃圾回收机制的理解 【重要】对闭包 作用域链 垃圾回收机制以及内存泄露的理解 Aug 30, 2017
@Ray1993 Ray1993 closed this as completed Aug 31, 2017
@Ray1993 Ray1993 reopened this Aug 31, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant