这几天看到闭包一章,从工具书到各路大神博客,都各自有着不一样的理解,如下我将选择性的抄(咳咳,固然仍是会附上本身理解的)一些大神们对闭包的原理及其使用文章,看成是本身初步理解这一功能函数的过程吧。javascript
首先先上连接:html
还有一些也很不错,但主要是以应用为主,原理解释没有上面几篇深刻,不过做为闭包的拓展应用其实也能够看一看;前端
闭包是指有权访问另外一个函数做用域中的变量的函数。建立闭包的常见方式,就是在一个函数内部建立另外一个函数。
从这句话咱们知道:闭包是一个函数git
function createComparisonFunction(propertyName) { return function(obj1,obj2) { var value1 = obj1[propertyName]; var value2 = obj2[propertyName]; if (value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } }; }
这段代码,咱们能直接看出,共存在三个做用域,Global、createComparisonFunction、匿名函数funciton,因其JS的做用域链特性,后者能访问自身及前者的做用域。而返回的匿名函数即便在其余地方被调用了,但它仍能够访问变量propertyName。之因此还可以访问这个变量,是由于内部函数的做用域链中包含createComparisonFunction的做用域。咱们来深刻了解一下,函数执行时具体发生了什么?github
当第一个函数被调用时,会建立一个执行环境(Execution Context,也叫执行上下文)及相应的做用域链,并把做用域链赋值给一个特殊的内部属性[[Scope]]。而后,使用this、arguments和其余命名参数的值来初始化函数的活动对象(Activation Object)。但在做用域链中,外部函数的活动对象处于第二位,外部函数的外部函数处于第三位,最后是全局执行环境(Global Context)。面试
换一个栗子:chrome
function createFunctions() { var result = new Array(); for (var i=0;i<10;i++) { result[i] = function() { return i; }; } return result; } var arr = createFunctions(); alert(arr[0]()); // 10 alert(arr[1]()); // 10
/这个函数返回一个函数数组。表面上看,彷佛每一个函数都应该返回本身的索引值,位置0的函数返回0,位置1的函数返回1,以此类推。但但实际上,每一个函数都返回10,为何?
数组对象内的匿名函数里的i是引用createFunctions做用域内的,当调用数组内函数的时候,createFunctions函数早已执行完毕。编程
这图不传也罢了,画得忒丑了。
数组内的闭包函数指向的i,存放在createFunctions函数的做用域内,确切的说,是在函数的变量对象里,for循环每次更新的i值,就是从它那儿来的。因此当调用数组函数时,循环已经完成,i也为循环后的值,都为10;数组
有人会问,那result[i]为何没有变为10呢?
要知道,做用域的断定是看是否在函数内的,result[i] = function.......
是在匿名函数外,那它就仍是属于createFunctions
的做用域内,那result[i]
里的i就依然会更新
那么如何使结果变为咱们想要的呢?也是经过闭包。
function createFunctions() { var result = []; for (var i=0;i<10;i++) { !function(i) { result[i] = function() {console.log(i)}; }(i); } return result; } var arr = createFunctions(); arr[0](); arr[1](); arr[2]();
function createFunctions() { var result = []; function fn(i) { result[i] = function() {console.log(i)} }; for (var i=0;i<10;i++) { fn(i); } return result; } var arr = createFunctions(); arr[0](); arr[1](); arr[2]();
var arr = []; function fn(i) { arr[i] = function() {console.log(i)} } function createFunctions() { for (var i=0;i<10;i++) { fn(i); } } fn(createFunctions()); arr[0](); arr[1](); arr[2]();
以第一种为例,经过一个当即调用函数,将外函数当前循环的i
值做为实参传入,并存放在当即调用函数的变量对象内,此时,这个函数当即调用函数和数组内的匿名函数就至关于一个闭包,数组的匿名函数引用了当即调用函数变量对象内的i。当createFuncions
执行完毕,里面的i值已是10了。可是因为闭包的特性,每一个函数都有各自的i值对应着。对数组函数而言,至关于产生了10个闭包。
因此能看出,闭包也十分的占用内存,只要闭包不执行,那么变量对象就没法被回收,因此不是特别须要,尽可能不使用闭包。
this
对象在闭包中使用this对象也会致使一些问题。咱们知道,this对象是在运行时基于函数的执行环境绑定的;在全局对象中,this等于window,而当函数被做为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具备全局性,所以其this对象一般指向window。但有时候因为编写闭包的方式不一样,这一点可能不会那么明显。(固然能够用call和apply)
var name = "The Window"; var obj = { name:"My Object", getName:function () { var bibao = function () { return this.name; }; return bibao; } }; alert(obj.getName()()); // The Window
先建立一个全局变量name,又建立一个包含name属性的对象。这个对象包含一个方法——getName(),它返回一个匿名函数,而匿名函数又返回this.name。因为getName()返回一个函数,所以调用obj.getName()();就会当即调用它返回的函数,结果就是返回一个字符串。然而,这个例子返回的字符串是"The Window",即全局name变量的值。为何匿名函数没有取得其波包含做用域(或外部做用域)的this对象呢?
每一个函数调用时其活动对象都会自动取得两个特殊变量:this
和arguments
。
内部函数在搜索这两个变量时,只会搜索到其活动对象为止,所以永远不可能直接访问外部函数中的这两个变量。不过,把外部做用域中的this对象保存在一个闭包可以访问到的变量里,就可让闭包访问该对象了。
var name = "The Window"; var obj = { name:"My Object", getName:function () { var that = this; return function () { return that.name; }; } }; alert(obj.getName()());
this
和arguments
也存在一样的问题,若是想访问做用域中arguments对象,必须将该对象的引用保存到另外一个闭包可以访问的变量中。var name = "The Window"; var obj = { name:"My Object", getName:function (arg1,arg2) { var arg = []; arg[0] = arg1; arg[1] = arg2; function bibao() { return arg[0]+arg[1]; } return bibao; } }; alert(obj.getName(1,2)())obj.getName方法保存了其接收到的实参在它的变量对象上,并在执行函数结束后没有被回收,由于返回的闭包函数引用着obj.Name方法里的arg数组对象。使得外部变量成功访问到了函数内部做用域及其局部变量。
在几种特殊状况下,this引用的值可能会意外的改变。
var name = "The Window"; var obj = { name:"My Object", getName:function () { return this.name; } };
这里的getName()只简单的返回this.name的值。
var name = "The Window"; var obj = { name:"My Object", getName:function () { console.log(this.name); } }; obj.getName(); // "My Object" (obj.getName)(); // "My Object" (obj.getName = obj.getName)(); // "The Window"
第一个obj.getName
函数做为obj
对象的方法调用,则天然其this
引用指向obj
对象。
第二个,加括号将函数定义以后,做为函数表达式执行调用,this
引用指向不变。
第三个,括号内先执行了一条赋值语句,而后在调用赋值后的结果。至关于从新定义了函数,this
引用的值不能维持,因而返回"The Window"。
setTimeout()
用setTimeout
结合循环考察闭包是一个很老的面试题了
// 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5 for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
setTimeout
的执行与咱们日常的JS代码执行不同,这里须要提到一个队列数据结构执行的概念。我的理解:因为setTimeout
函数的特殊性,须等其余非队列结构代码执行完毕后,这个setTimeout
函数才会进入队列执行栈。
setTimeout(function() { console.log(a); }, 0); var a = 10; console.log(b); console.log(fn); var b = 20; function fn() { setTimeout(function() { console.log('setTImeout 10ms.'); }, 10); } fn.toString = function() { return 30; } console.log(fn); setTimeout(function() { console.log('setTimeout 20ms.'); }, 20); fn();
答案:
设置断点如图所示,今天刚学Chrome的开发者工具,有哪些使用上的错误还请指出。
我分别给变量a、b、fn函数都设置了观察,变量的值变化将会实时地在右上角中显示,能够看到,在JS解释器运行第一行代码前,变量a、b就已经存在了,而fn函数已经完成了声明。接下来咱们继续执行。要注意:蓝色部分说明这些代码将在下一次操做中执行,而不是已经执行完毕。
把第一个setTimeout函数执行完毕后也没有反应。我给三个setTimeout内的匿名函数也加上观察选项,却显示不可以使用。
因此,下一次执行会发生什么?对console出b的值,可是b没赋值,右上角也看到了,因此显示undefined。
而console.log(fn)就是将fn函数函数体从控制台弹出,要注意,console会隐式调用toString方法,这个会在后面讲到。
如今第26行以前(不包括26行)的代码都已略过,a,b变量也已获得赋值,继续执行。
重写了toString
方法前:
重写后:
toString方法是Object全部,全部由它构造的实例都能调用,如今这个方法被改写并做为fn对象的属性(方法)保留下来。
console会隐式调用toString方法,因此30行的console会弹出30;
继续执行,定义setTimeout函数也是什么没有发生,知道调用fn前。
调用fn,是否是就会执行setTimeout函数呢?其实没有,咱们能够看到call stack一栏已是fn的执行栈了,可是依旧没发生什么。
可是:
当call stack里的环境都已退出,执行栈里没有任何上下文时,三个setTimeout函数就执行了,那这三个时间戳函数那个先执行,那个后执行呢?由设定的延迟时间决定,这个延迟时间是相对于其余代码执行完毕的那一刻。
不信咱们能够经过改变延迟时间从新试一次就知道了。
咱们在看回原来的闭包代码:
// 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5 for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log(i); }, i*1000 ); }
先确认一个问题,setTimeout
函数里的匿名函数的i指向哪儿?对,是全局变量里的i。setTimeout
里的匿名函数执行前,外部循环已经结束,i值已经更新为6,这时setTimeout
调用匿名函数,里面的i固然都是6了。
咱们须要建立一个可以保存当前i值的"盒子"给匿名函数,使得匿名函数可以引用新建立的父函数。
// 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5 for (var i=1; i<=5; i++) { !function (i) { setTimeout( function timer() { console.log(i); }, i*1000 ); }(i); }
自调用函数就是那个"盒子"
考虑这个函数:
function f(arg) { var n = 123 + Number(arg); function g() {console.log("n is "+n);console.log("g is called");} n++; function gg() {console.log("n is "+n);console.log("g is called");} return [g,gg]; }
调用数组内函数的console结果是什么?
var arr = f(1); arr[0](); // 对闭包g的调用 // "n is 125" "g is called" arr[1](); // 对闭包gg的调用 // "n is 125" "gg is called"
函数g与函数gg保持了各自含有局部变量n的执行环境。因为声明函数g时与声明函数gg时的n值是不一样的,所以闭包g与闭包gg貌似将会表示各自不一样的n值。实际上二者都将表示相同的值。由于它们引用了同一个对象。
即都是引用了,f
函数执行环境内变量对象内的n
值。当执行f(1)
的时候,n值就已经更新为最后计算的值。
在JavaScript中,最外层代码(函数以外)所写的名称(变量名与函数名)具备全局做用域,即所谓的全局变量与全局函数。JavaScript的程序代码即便在分割为多个源文件后,也能相互访问其全局名称。在JavaScript的规范中不存在所谓的模块的语言功能。
所以,对于客户端JavaScript,若是在一个HTML文件中对多个JavaScript文件进行读取,则他们相互的全局名称会发生冲突。也就是说,在某个文件中使用的名称没法同时在另外一个文件中使用。
即便在独立开发中这也很不方便,在使用他们开发的库之类时就更加麻烦了。
此外,全局变量还下降了代码的可维护性。不过也不能就简单下定论说问题只是由全局变量形成的。这就如同在Java这种语言规范并不支持全局变量的语言中,一样能够很容易建立出和全局变量功能相似的变量。
也就是说,不该该只是一昧地减小全局变量的使用,而应该造成一种尽量避免使用较广的做用域的意识。对于较广的做用域,其问题在于修改了某处代码以后,会难以肯定该修改的影响范围,所以代码的可维护性会变差。
从形式上看,在JavaScript中减小全局变量的数量的方法时很简单的。首先咱们按照下面的代码这样预设一下全局函数与全局变量。
// 全局函数 function sum(a,b) { return Number(a)+Number(b); } // 全局变量 var position = {x:2,y:3}; // 借助经过对象字面量生成对象的属性,将名称封入对象的内部。因而从形式上看,全局变量减小了 var MyModule = { sum:function (a,b) { return Number(a)+Number(b); }, position:{x:2,y:3} }; alert(MyModule.sum(3,3)); // 6 alert(MyModule.position.x); // 2
上面的例子使用对象字面量,不过也能够像下面这样不使用对象字面量。
var MyModule = {}; // 也能够经过new表达式生成 MyModule.sum = function (a,b) {return Number(a)+Number(b);}; MyModule.position = {x:2,y:3};
这个例子中,咱们将MyModule称为模块名。若是彻底采用这种方式,对于1个文件来讲,只须要一个模块名就能消减全局变量的数量。固然,模块名之间仍然可能产生冲突,不过这一问题在其余程序设计语言中也是一个没法被避免的问题。
经过这种将名称封入对象之中的方法,能够避免名称冲突的问题。可是这并无解决全局名称的另外一个问题,也就是做用域过广的问题。经过MyModule.position.x这样一个较长的名称,就能够从代码的任意一处访问该变量。
// 在此调用匿名函数 // 因为匿名函数的返回值是一个函数,因此变量sum是一个函数 var sum = (function () { // 没法从函数外部访问该名称 // 实际上,这变成了一个私有变量 // 通常来讲,在函数被调用以后该名称就没法再被访问 // 不过因为是在被返回的匿名函数中,因此仍能够继续被使用 var p = {x:2,y:3}; // 一样是一个从函数外没法被访问的私有变量 // 将其命名为sum也能够。不过为了不混淆,这里采用其余名称 function sum_internal(a,b) { return Number(a)+Number(b); } // 只不过是为了使用上面的两个名称而随意设计的返回值 return function (a,b) { alert("x = "+p.x); return sum_internal(a,b); } })(); console.log(sum(3,4)); // "x = 2" // "y"
上面的代码能够抽象为下面这种形式的代码。在利用函数做用域封装名称,以及闭包可使名称在函数调用结束后依然存在这两个特性。这样信息隐藏得以实现。
(function(){函数体})();
像上面这样,当场调用函数的代码看起来或许有些奇怪。通常的作法是先在某处声明函数,以后在须要时调用。不过这种作法是JavaScript的一种习惯用法,加以掌握。
匿名函数的返回值是一个函数,不过即便返回值不是函数,也一样能采用这一方法。好比返回一个对象字面量以实现信息隐藏的功能。
var obj = (function() { // 从函数外部没法访问该名称 // 实际上,这是一个私有变量 var p = {x:2,y:3}; // 这一样是一个没法从函数外部访问的私有函数 function sum_internal(a,b) { return Number(a+b); } // 只不过为了使用上面的两个名称而随意设计的返回值 return { sum:function (a,b) { return sum_internal(a,b); }, x:p.x }; })(); alert(obj.sum(3,4)); // 7 alert(obj.x); // 2
利用函数做用域与闭包,能够实现访问在控制,上一节中,模块的函数在被声明以后当即就对其调用,而是用了闭包的类则可以在生成实例时调用。即使如此,着厚重那个作法在形式上仍然只是单纯的函数生命。下面是一个经过闭包来对类进行定义的例子
// 用于生成实例的函数 function myclass(x,y) { return {show:function () {alert(x+" | "+y)}}; } var obj = myclass(3,2); obj.show(); // 3 | 2
这里再举一个具体的例子,一个实现了计数器功能的类。
这里重申一下:JavaScript的语言特性没有"类"的概念。但这里的类指的是,实际上将会调用构造函数的Function对象。此外在强调对象是经过调用构造函数生成的时候,会将这些被生成的对象称做对象实例以示区别。
JavaScript有一种自带的加强功能,称为支持函数型程序设计的表达式闭包(Expression closure
)。
从语法结构上看,表达式闭包是函数声明表达式的一种省略形式。能够像下面这样省略只有return
的函数声明表达式中的return
与{}
。
var sum = function (a,b) {return Number(a+b)}; // 能够省略为 var sum = function (a,b) Number(a+b);