JS 闭包理解

来源:互联网 发布:03版倚天屠龙记 知乎 编辑:程序博客网 时间:2024/04/27 16:00

在很多情况下,我们经常可以看到类似于这样的js写法:

 (function(){ ...})(window)
(function(){})()

这其实就相当于是js的闭包。表明立刻执行程序


那对于这个方法到底怎么理解呢?在解释完闭包之后,在回来看这个函数写法。



1、什么是js闭包:

闭包:  指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。 

简单可以理解为通常是一个函数或者表达式,这个函数或者表达式定义在另外一个函数的内部,但是可以访问外部函数的局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。



Object对象都会有一个prototype的值


/* 创建 - MyObject1 - 类型对象的函数*/function MyObject1(formalParameter){/* 给创建的对象添加一个名为 - testNumber - 的属性并将传递给构造函数的第一个参数指定为该属性的值:*/this.testNumber = formalParameter;}/* 创建 - MyObject2 - 类型对象的函数*/function MyObject2(formalParameter){/* 给创建的对象添加一个名为 - testString - 的属性并将传递给构造函数的第一个参数指定为该属性的值:*/this.testString = formalParameter;}/* 接下来的操作用 MyObject1 类的实例替换了所有与 MyObject2 类的实例相关联的原型。而且,为 MyObject1 构造函数传递了参数 - 8 - ,因而其 - testNumber - 属性被赋予该值:*/MyObject2.prototype = new MyObject1( 8 );/* 最后,将一个字符串作为构造函数的第一个参数,创建一个 - MyObject2 - 的实例,并将指向该对象的引用赋给变量 - objectRef - :*/var objectRef = new MyObject2( “String_Value” );


运行结果:

MyObject1.prototypeMyObject1 {}MyObject2.prototypeMyObject1 {testNumber: 8}objectRefMyObject2 {testString: "String_Value", testNumber: 8}


闭包的特点:

1、作函数变量引用 - 当函数返回时其处于激活状态
2、闭包函数返回时没有释放资源栈区


实上面两点合成点,闭包函数返回时,该函数内部变量处于激活状态,函数所栈区依保留

function a(){ var i=0; function b(){ alert(++i); } return b;}var c = a();c();

调用c的时候,发现i的值是保留的。。

这样在执行完var c=a()后,变量c实际上是指向了函数b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
 当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个闭包。
那么我们来想象另一种情况,如果a返回的不是函数b,情况就完全不同了。因为a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引 用,因此函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。

闭包的应用场景  1、保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全性。  2、在内存中维持一个变量。依然如前例,由于闭包,函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1。



1闭包外层函数.2闭包内部都有函数.3闭包会return内部函数.4闭包返回函数内部能有return.(因结束了)5执行闭包,闭包内部变量会存,而闭包内部函数内部变量会存.闭包应用场景(呵呵,复制参考资料)1、保护函数内变量安全开始例子函数ai只有函数b才能访问而无法通过其途径访问因此保护了i安全性2、内存维持变量前例由于闭包函数ai直存于内存因此每次执行c()都会给i自加1


js中,闭包主要涉及到js的几个其他的特性:作用域链,垃圾(内存)回收机制,函数嵌套,等等.

作用域链的定义:

作用域链就是函数在定义的时候创建的,用于寻找使用到的变量的值的一个索引,而他内部的规则是,把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止.当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续.如果找到最后也没找到需要的变量,则解释器返回undefined.


在一个demo:

function f1(){    var n=999;    nAdd=function(){n+=1}    function f2(){      alert(n);    }    return f2;  }  var result=f1();  result(); // 999  nAdd();  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个

匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。


Js代码 function outerFun(){ //没有var  a =0; alert(a);  }var a=4;outerFun();alert(a);结果为 0,0 真是奇怪,为什么呢?


这个是作用域链的影响:

作用域链是描述一种路径的术语,沿着该路径可以确定变量的值 .当执行a=0时,因为没有使用var关键字,因此赋值操作会沿着作用域链到var a=4;  并改变其值.


3、下面这些情况,不适用闭包,更好一些,(注意构造出的意外的闭包):

var quantaty = 5; //全局变量function addGlobalQueryOnClick(linkRef){    /* 如果可以将参数 - linkRef - 通过类型转换为 ture      (说明它引用了一个对象):    */    if(linkRef){        /* 对一个函数表达式求值,并将对该函数对象的引用           指定给这个链接元素的 onclick 事件处理器:        */        linkRef.onclick = function(){            /* 这个内部函数表达式将查询字符串               添加到附加事件处理器的元素的 - href - 属性中:            */            this.href += ('?quantaty='+escape(quantaty));            return true;        };    }}

无论什么时候调用 addGlobalQueryOnClick 函数,都会创建一个新的内部函数(通过赋值构成了闭包)。从效率的角度上看,如果只是调用一两次 addGlobalQueryOnClick 函数并没有什么大的妨碍,但如果频繁使用该函数,就会导致创建许多截然不同的函数对象(每对内部函数表达式求一次值,就会产生一个新的函数对象)。 

这时候不使用闭包:

/* 定义一个全局变量,通过下面的函数将它的值   作为查询字符串的一部分添加到链接的 - href - 中:*/var quantaty = 5;/* 当把一个链接(作为函数中的参数 - linkRef -)传递给这个函数时,   会给这个链接添加一个 onclick 事件处理器,该事件处理器会   将全局变量  - quantaty - 的值作为查询字符串的一部分添加到   链接的 - href -  中,然后返回 true,以便单击链接时定位到由   作为 - href - 属性值的查询字符串所指定的资源:*/function addGlobalQueryOnClick(linkRef){    /* 如果 - linkRef - 参数能够通过类型转换为 true    (说明它引用了一个对象):    */    if(linkRef){        /* 将一个对全局函数的引用指定给这个链接           的事件处理属性,使函数成为链接元素的事件处理器:        */        linkRef.onclick = forAddQueryOnClick;    }}/* 声明一个全局函数,作为链接元素的事件处理器,   这个函数将一个全局变量的值作为要添加事件处理器的   链接元素的  - href - 值的一部分:*/function forAddQueryOnClick(){    this.href += ('?quantaty='+escape(quantaty));    return true;}

Ex2:

function ExampleConst(param){    /* 通过对函数表达式求值创建对象的方法,      并将求值所得的函数对象的引用赋给要创建对象的属性:    */    this.method1 = function(){        ... // 方法体。    };    this.method2 = function(){        ... // 方法体。    };    this.method3 = function(){        ... // 方法体。    };    /* 把构造函数的参数赋给对象的一个属性:*/    this.publicProp = param;}

每当通过 new ExampleConst(n) 使用这个构造函数创建一个对象时,都会创建一组新的、作为对象方法的函数对象。因此,创建的对象实例越多,相应的函数对象也就越多。 
function ExampleConst(param){    /* 将构造函数的参数赋给对象的一个属性:*/    this.publicProp = param;}/* 通过对函数表达式求值,并将结果函数对象的引用      指定给构造函数原型的相应属性来创建对象的方法:*/ExampleConst.prototype.method1 = function(){    ... // 方法体。};ExampleConst.prototype.method2 = function(){    ... // 方法体。};ExampleConst.prototype.method3 = function(){    ... // 方法体。};

这那种情况下,只创建一次函数对象,并把它们指定给构造函数 prototype 的相应属性显然更有效率。这样一来,它们就能被构造函数创建的所有对象共享了.


 

那么,现在我们再来分析开头说到的那个问题:

(function(){})


这个就相当于是个闭包,闭包是一个匿名函数,

所以要给 function 添加括弧是为了让它形成一个表达式 (expression), 有了表达式,并且确定它的类型是个函数 (Function 实例), 就可以直接调用它。


后面的一对括弧是可以工作的,它的意义是:我要调用 (call) 这个函数。既然是函数调用,那就可以像一般的函数那样,在调用时传入参数,传入 window 参数。


(function(win) {// ...})(window); 

你可以在闭包内任何地方使用 win, 它都会指向 window 对象。


不过,便利的同时也会带来陷阱。在 IE 上,window 总是指向当前窗口对象,这个没有问题,但是在某些场景下,使用闭包内的 win 变量会导致拒绝访问错误 (Access denied). 重现方式大致是这样的:当页面引用其他域名的脚本,并且该脚本调用了闭包内的 window.document, 而且这个闭包代码是来自另一个域名的脚本。在这种情况下,使用 win 会保持对 window 最早的引用,通过另一个域的脚本访问 win 会导致 IE 认为脚本产生了跨越冲突,从而拒绝了对 win.document 的访问。解决办法是不使用形参 win, 而是直接使用 window. 需要说明的是,给闭包传入 document 也会导致 IE 出现同样的问题。


这个,为什么要将window和undefined作为参数传给它?
因为 ecmascript 执行JS代码是从里到外,因此把全局变量window或jQuery对象传进来,就避免了到外层去寻找,提高效率。undefined在老一辈的浏览器是不被支持的,直接使用会报错,js框架要考虑到兼容性,因此增加一个形参undefined。

//方式一  (function(undefined ) {     window.property1 = ……;     window.property2 = ……;     ……  })();  //方式二  (function( window, undefined ) {      ... // code goes here    })(window);  //方式三  (function(undefined ) {     var tmp = window;     tmp.property1 = ……;     tmp.property2 = ……;     ……  })();  

方式一的效率明显最低,方式二和方式三应该差不多。将window作为参数传递进去就可以让代码里面的语句可以直接用参数中的window,而不用再去找最外层的对象。假如要在函数中为window再设置 100000个属性,用参数传递过去只需要找一次最外层对象。不用参数传递,用到window的语句都要去找一次最外层对象。

0 0
原创粉丝点击