JS中的闭包(closure)

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

一.什么是闭包

JS中,在函数内部可以读取函数外部的变量

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

但,在函数外部自然无法读取函数内的局部变量

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出来。

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

代码中的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();//
test();//

实现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();//
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()函数执行时的作用域链。

后台的每个执行环境都有一个表示变量的对象——变量对象。全局环境的变量对象始终存在,而像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的过程中产生的作用域链之间的关系。

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

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

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

JS中的闭包(closure)的更多相关文章

  1. 详解js中的闭包

    前言 在js中,闭包是一个很重要又相当不容易完全理解的要点,网上关于讲解闭包的文章非常多,但是并不是非常容易读懂,在这里以<javascript高级程序设计>里面的理论为基础.用拆分的方式 ...

  2. js中的闭包之我理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  3. 浅谈JS中的闭包

    浅谈JS中的闭包 在介绍闭包之前,我先介绍点JS的基础知识,下面的基础知识会充分的帮助你理解闭包.那么接下来先看下变量的作用域. 变量的作用域 变量共有两种,一种为全局变量,一种为局部变量.那么全局变 ...

  4. js中的“闭包”

    js中的“闭包” 姓名:闭包 官方概念:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. ( ⊙o⊙ )!!!这个也太尼玛官方了撒,作为菜鸟的 ...

  5. Js中的闭包原理

    要了解清楚js中的闭包制机,那么得先了解全局执行环境.块级执行环境.函数执行环境.变量对象.环境栈.作用域链.摧毁执行环境. 全局执行环境 全局执行环境指的是最外层的执行环境.在web中全局执行环境被 ...

  6. js中的闭包理解一

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  7. js中的闭包理解

    闭包是一个比较抽象的概念,尤其是对js新手来说.书上的解释实在是比较晦涩,对我来说也是一样. 但是他也是js能力提升中无法绕过的一环,几乎每次面试必问的问题,因为在回答的时候.你的答案的深度,对术语的 ...

  8. javascript中的闭包closure详解

    目录 简介 函数中的函数 Closure闭包 使用闭包实现private方法 闭包的Scope Chain 闭包常见的问题 闭包性能的问题 总结 简介 闭包closure是javascript中一个非 ...

  9. 初识js中的闭包

    今天看了关于js闭包方面的文章,还是有些云里雾里,对于一个菜鸟来说,学习闭包确实有一定的难度,不说别的,能够在网上找到一篇优秀的是那样的不易. 当然之所以闭包难理解,个人觉得是基础知识掌握的不牢,因为 ...

随机推荐

  1. 网络设备配置与管理(华为)基础系列 :VLAN故障排除和GVRP

    一.VLAN故障排除 故障排除的三步骤:故障定位 → 分析故障 → 排除故障 一般情况下,网络设备配置的故障有两种排错方式 A.静态排错:主要靠display查看配置信息的方式进行 在相关vlan下d ...

  2. Python:zip 函数的用法

    zip() 接受一系列可迭代的对象作为参数,将对象中对应的元素打包成一个个 tuple,然后返回由这些 tuple 组成的 list. 若传入参数的长度不等,则返回 list 的长度和参数中长度最短的 ...

  3. nodejs接收get参数和post参数

    get请求用query //http://localhost:3000?a=3&b=4&c=5 router.get('/', function (req, res, next) { ...

  4. C语言实现顺序栈的初始化&进栈&出栈&读取栈顶元素

    /*顺序表实现栈的一系列操作*/ #include<stdio.h> #include<stdlib.h> #define Stack_Size 50 //设栈中元素个数为50 ...

  5. PE知识复习之PE的重定位表

    PE知识复习之PE的重定位表 一丶何为重定位 重定位的意思就是修正偏移的意思.  如一个地址位 0x401234 ,Imagebase = 0x400000 . 那么RVA就是 1234.  如果Im ...

  6. Java读取Excel指定列的数据详细教程和注意事项

    本文使用jxl.jar工具类库实现读取Excel中指定列的数据. jxl.jar是通过java操作excel表格的工具类库,是由java语言开发而成的.这套API是纯Java的,并不依赖Windows ...

  7. C#实现以太仿DApp合约编译、部署

    在网上找了一些关于C#开发以太仿的资料,大概了解了以太仿常用名词,后续可能需要根据资料查看开源的源码进一步熟悉一下. 一.准备合约 这里准备了一个EzToken.sol合约,目前还不会solidity ...

  8. DSAPI多功能组件编程应用-图形图像篇(中)

    [DSAPI.DLL下载地址]   说到计算机上使用代码来处理各种图像特效,是一份太有挑战性的工作.以下涉及的所有图像效果均不是从网上复制的源码,而是本人试验数次并编写的,所以原理上会和网上的有所不同 ...

  9. C# 跨进程 设置窗口owner

    窗口间跨进程通信 1. 发送方 public const int WM_InsertChart_Completed = 0x00AA; //查找窗口 [DllImport("User32.d ...

  10. es简单打造站内搜索

    最近挺忙的,在外出差,又同时干两个项目.白天一个晚上一个,特别是白天做的项目,马上就要上线了,在客户这里 三天两头开会,问题很多真的很想好好静下来怼代码,半夜做梦都能fix bugs~ 和客户交流真的 ...