JS中的闭包(closure)

JS中的闭包(closure)

闭包(closure)是Javascript语言的一个难点,也是它的特点,不少高级应用都要依靠闭包实现。
下面就是个人学习笔记,对于Javascript初学者应该是颇有用的。javascript

一.什么是闭包

JS中,在函数内部能够读取函数外部的变量前端

function outer(){
     var localVal = 30;
     return localVal;
}
outer();//30

但,在函数外部天然没法读取函数内的局部变量java

function outer(){
     var localVal = 30;
}
alert(localVal);//error

这里有个须要注意的地方,函数内部声明变量的时候,必定要使用var命令。若是不用的话,其实是声明了一个全局变量。数组

function outer(){
     localVal = 30;
     return localVal;
}
outer();
alert(localVal);//30

以上的表述,是JS变量的做用域的知识,它包括全局变量和局部变量。缓存

Javascript语言的特殊之处,就在于函数内部能够直接读取全局变量。安全

function outer(){
     var localVal = 30;
function inner(){
          alert(localVal);
     }
 
 
     return inner; 
}
var func = outer();
func();//30

咱们看到在上面的代码中,outer函数内又定义一个函数inner,outer函数的返回值是inner函数,inner函数把localVal alert出来。闭包

咱们能够看出以上代码的特色:函数嵌套函数,内部函数能够引用外部函数的参数和变量,参数和变量不会被垃圾回收机制收回。dom

代码中的inner函数,就是闭包。简单的说,闭包(closure)就是可以读取其余函数内部变量的函数。模块化

因为在Javascript语言中,只有函数内部的子函数才能读取局部变量,所以能够把闭包简单理解成“定义在一个函数内部的函数”。因此,在本质上,闭包就是将函数内部和函数外部链接起来的一座桥梁。函数

在上面的代码中,函数inner被包含在函数outer内部,这时outer内部的全部局部变量,对inner都是可见的。可是inner内部的局部变量,对oute 是不可见的。这是Javascript语言特有的“链式做用域”结构(chain scope),子对象会一级一级地向上寻找全部父对象的变量。因此,父对象的全部变量,对子对象都是可见的,反之则不成立。

补充--JS中的函数定义

JS中定义一个函数,最经常使用的就是函数声明和函数表达式

Js中的函数声明是指下面的形式:

function functionName(){  
  
}

函数表达式则是相似表达式那样来声明一个函数:

var functionName = function(){  

}

咱们可使用函数表达式建立一个函数并立刻执行它,如:

(function() {
  var a, b    // local variables
  // ...      // and the code
})()

()();第一个括号里放一个无名的函数。

两者区别:js的解析器对函数声明与函数表达式并非一视同仁地对待的。对于函数声明,js解析器会优先读取,确保在全部代码执行以前声明已经被解析,而函数表达式,如同定义其它基本类型的变量同样,只在执行到某一句时也会对其进行解析,因此在实际中,它们仍是会有差别的,具体表如今,当使用函数声明的形式来定义函数时,可将调用语句写在函数声明以前,然后者,这样作的话会报错。

二.闭包的应用

使用闭包的好处:

-但愿一个变量长期驻扎在内存当中;

-避免全局变量的污染;

-私有成员的存在

1.模块化代码

使用自执行的匿名函数来模拟块级做用域

(function(){
        // 这里为块级做用域
 })();

该方法常常在全局做用域中被用在函数外部,从而限制向全局做用域中添加过多的变量和函数影响全局做用域。也能够减小如闭包这样的对内存的占用,因为匿名函数没有变量指向,执行完毕就能够当即销毁其做用域链。

示例:

var test = (function(){
    var a= 1;
    return function(){
        a++;
        alert(a);
    }
})();

test();//2
test();//3

实现a的自加,不污染全局。

2.循环闭包

循环给每一个li注册一个click事件,点击alert序号。代码以下:

var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
    for( var i =0,len = aLi.length ;i<len;i++ ){
            aLi[i].onclick = function(){
            alert( i );//all are aLi.length!
        }
    }
}

点击后会一直弹出同一个值 aLi.length 而不是123。当点击以前,循环已经结束,i值为aLi.length。

利用闭包,建一个匿名函数,将每一个i存在内存中,onclick函数用的时候提取出外部匿名函数的i值。代码以下:

var aLi = document.getElementByClassName("test");
function showAllNum( aLi ){
    for( var i =0,len = aLi.length ;i<len;i++ ){
        (function(i){
            aLi[i].onclick = function(){
            alert( i );
        }
        })(i);
    }
}

或者:

function showAllNum( aLi ){
    for( var i =0,len = aLi.length ;i<len;i++ ){
            aLi[i].onclick = (function(i){
                return function(){
                    alert( i );
                }
            })(i);
    }
}

实现解释:

1.做用域链

2.闭包函数的赋值与运行

实际上只是经过函数的赋值表式方式付给了标签点击事件,并无运行;当遍历完后,i变成标签组的长度,根据做用域的原理,向上找到for函数里的i,因此点击执行的时候都会弹出标签组的长度。闭包可使变量长期驻扎在内存当中,咱们在绑定事件的时候让它自执行一次,把每一次的变量存到内存中;点击执行的时候就会弹出对应本做用域i的序号。

3.封装

外部没法直接获取函数内的变量,可经过暴露的方法获取

var info = function(){
    var _userId = 23492;
    var _typeId = 'item';

    function getUserId(){
        alert(_userId);
    }

    function getTypeId(){
        alert(_typeId);
    }
};

info.getUserId();//23492
info.getTypeId();//item

info._userId//undefined
info._typeId//undefined

可是这种方式会使咱们在每一次建立新对象的时候都会建立一个这种方法。使用原型来建立一个这种方法,避免每一个实例都建立不一样的方法。在这里不作深究(通常构造函数加属性,原型加方法)。

4.关于 this 对象

this 对象是在运行时基于函数的执行环境绑定的(匿名函数中具备全局性)(this:当前发生事件的元素),有时候在一些闭包的状况下就有点不那么明显了。

代码1:

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

代码2:

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

javascript是动态(或者动态类型)语言,this关键字在执行的时候才能肯定是谁。因此this永远指向调用者,即对‘调用对象‘者的引用。第一部分经过代码:执行代码object.getNameFunc()以后,它返回了一个新的函数,注意这个函数对象跟object不是一个了,能够理解为全局函数;它不在是object的属性或者方法,此时调用者是window,所以输出是 The Window。

第二部分,当执行函数object.getNameFunc()后返回的是:

function( )
{
         return _this.name;
}

此时的_this=this。而this指向object,因此that指向object。他是对object的引用,因此输出My Object。

总结:关于js中的this,记住谁调用,this就指向谁;要访问闭包的this,要定义个变量缓存下来。通常喜欢var _this = this。

5.闭包在IE下内存泄露问题

IE9以前,JScript对象和COM对象使用不一样的垃圾收集例程,那么闭包会引发一些问题。

建立一个闭包,然后闭包有建立一个循环引用,那么该元素将没法销毁。常见的就是dom获取的元素或数组的属性(或方法)再去调用本身属性等。例如:

function handler(){
    var ele = document.getElementById("ele");
    ele.onclick = function(){
        alert(ele.id);
    }
}

闭包会引用包含函数的整个活动对象,便是闭包不直接引用ele,活动对象依然会对其保存一个引用,那么设置null就能够断开保存的引用,释放内存。代码以下:

function handler(){
    var ele = document.getElementById("ele");
    var id = ele.id;
    ele.onclick = function(){
        alert(id);
    }
    ele = null;
}

固然还有其余方法,推荐此法。

三.闭包的原理

当某个函数第一次被调用时,会建立一个执行环境(execution context)及相应的做用域链,并把做用域链赋值给一个特殊的内部属性(即[[Scope]])。而后,使用this、arguncmts 和其余命名参数的值来初始化函数的活动对象(activation object)。但在做用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至做为做用域链终点的全局执行环境。

在函数执行过程当中,为读取和写入变量的值,就须要在做用域链中查找变量。来看下面的例子:

function compare(valael, value2){ 
    if (valuel < value2){ 
       return -1;
    } else if (vaiuel > value2){ 
       return 1;
     } else {
       return 0;
    }
}

var result = compare(5, 10);

以上代码先定义了compare()函数,而后又在全局做用域中调用了它。当第一次调用compare()时,会建立一个包含this、arguments、valuel和value2的活动对象。全局执行环境的变量对象 (包含this、result和compare)在compare()执行环境的做用域链中则处于第二位。图展现了包含上述关系的compare()函数执行时的做用域链。

image

后台的每一个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程当中存在。在建立compare()函数时,会建立一个预先包含全局变童对象的做用域链,这个做用域链被保存在内部的[[Scope]]属性中。当调用compare()函数时,会为函数建立一个执行环境,而后经过复制函数的[[Scope]]属性中的对象构建起执行环境的做用域链。此后,又有一个活动对象(在此做为变量对象使用)被建立并被推入执行环境做用域链的前端。对于这个例子中compare()函数的执行环境而言,其做用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,做用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

不管何时在函数中访问一个变量时,就会从做用域链中搜索具备相应名字的变量。通常来说,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局做用域(全局执行环境的变量对象)。 可是,闭包的状况又有所不一样。

function createComparisonFunction(propertyName) {
  return function(object1, object2){
    var valuel = objectl[propertyName]; 
    var value2 = object2[propertyName]; 
    if (valuel < value2){ 
       return -1;
    } else if (valuel > value2){ 
       return 1;
    } else {
       return 0;
     }
  };
}

在另外一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的做用域链中。所以,在createComparisonFunction()涵数内部定义的匿名函数的做用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象。图展现了当下列代码执行时,包含函数与内部匿名函数的做用域链。

var compare = createComparisonFunction("name");

var result = compare({ name: "Nicholas" }, { naine: BGreg" });

在匿名函数从createComparisonFunction()中被返冋后,它的做用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样,匿名函数就能够访问在createComparisonFunction()中定义的全部变量。更重要的是,createCoir.parisonFunction() 函数在执行完毕后,其活动对象也不会被销毁,由于匿名函数的做用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction()函数返回后,其执行环境的做用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁,例如:

var compareNames = createComparisonFunction("name");

//调用函数
var result = compareNames({ name: "Nicholas" ), { name:"Greg" });

//解除对匿名函数的引用(以便释放内存)
compareNanies = null;

首先,建立的比较函数被保存在变量coinpareNames中。而经过将compareNames设置为等于null解除该函数的引用,就等于通知垃圾问收例程将其清除。随着匿名函数的做用域链被销毁,其余做用域 (除r全局做用域)也均可以安全地销毁了。图 展现了调用conpareNamesO的过程当中产生的做用域链之间的关系。

image

-------------------------------------------------------------------------------------------------------------------------------------

闭包无处不在,弄懂它很重要。

转载需注明转载字样,标注原做者和原博文地址。

相关文章
相关标签/搜索