定义:闭包是有权访问另一函数作用域的变量的函数。
常见创建闭包的方式:在一个函数内部创建另一个函数。
理解执行环境和作用域链
执行环境(execution context)
1.执行环境:定义了变量或函数有权访问的其他数据。
2.变量对象:每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象里。
3.某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
4.全局执行环境:全局执行环境是最外围的一个执行环境,根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象。全局执行环境直到应用程序退出才会被销毁。
5.每个函数都有自己的执行环境:当执行流进入到一个函数时,函数的环境就会被推入一个环境栈中,在函数执行之后,栈将环境弹出,把控制权返回给之前的执行环境。
作用域链(scope chain)
概念
当代码在一个环境中执行时,会创建变量对象的一个作用域链。
作用域链保证对执行环境有权访问的所有变量和函数的有序访问。
内容:作用域链的前端,始终是当前执行代码所在环境的变量对象。(如果环境是函数,则其活动对象为变量对象,活动对象最开始只包含一个变量,即arguments对象)。下一个变量对象来自外部环境,下一个变量对象来自于下一个包含环境。全局执行环境的变量对象始终都是作用域链中最后一个对象。
作用域链的创建
当创建函数时: [[scope]]属性保存包含全局变量的作用域链;
当调用函数时: 创建函数执行环境(execution context);复制[[scope]]中对象构建执行环境的作用域链;活动对象(activation object)被创建并被推入执行环境作用域链前端;
例:
当创建sayHi()函数时: [[scope]]属性保存包含全局变量对象(包含sayHi()函数)的作用域链。
当调用sayHi()函数时: 创建sayHi()函数的执行环境;复制[[scope]]中对象构建执行环境的作用域链;包含arguments,str和hello的活动对象被创建并被推入执行环境作用域链前端;
作用域链的销毁
一般情况下:当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。
有闭包的情况:在外部函数,如sayHi()函数,执行完毕后,其包含arguments,str和hello的活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。使用将sayHi设置为null的方法解除对该函数的引用,即通知垃圾回收例程将其清除,匿名函数作用域被销毁。
作用域链本质
作用域链的本质是指向变量对象的指针列表,只引用,但不实际包含变量对象。
分析调用sayHi()函数过程中的作用域链
闭包的作用域链包含它自己的作用域,包含函数的作用域和全局作用域。
关于闭包值得注意的问题
1.闭包只能取得包含函数中任何变量的最后一个值
2.当闭包为匿名函数,this对象通常指向window,除非通过apply()或call()改变函数执行环境。
原因:每个函数在被调用时都会自动取得两个特殊的变量:this和arguments,因此当内部函数在搜索这两个变量时,只会搜索到其活动对象为止。而匿名函数的this指向window。
访问外部作用域中this对象的方法:把外部作用域中的this对象保存在闭包能够访问的变量里。
修改前:
修改后:
访问外部作用域中arguments对象的方法:同样需要把该对象保存在闭包能够访问的变量里。
另外,关于this对象:
上例中先执行赋值语句,在调用赋值后的结果,因为此赋值表达式的值是函数本身,所以this的值不能维持。
内存
1.闭包会比其他函数占用更多内存。
2.当函数返回了一个闭包,这个函数的作用域将会在内存中保存至闭包不存在为止。
解决:将外部函数设置为null。
3.如果闭包的作用域链中保存着HTML元素,意味着该元素将无法被销毁。
解决:将HTML元素保存在外部作用域中,并在不需要时将保存HTML元素的变量设置为null。
闭包的应用
闭包模仿块级作用域
方法
创建并立即调用一个函数;
修改前:
这里首先创建了一个outputNumbers()函数,在函数内有一个for循环,由于JavaScript没有块级作用域,所以即使在循环外也可以访问循环内的变量i。
修改后:
|
|
现在在for循环外创建了一个私有作用域,并产生了一个闭包,由于在匿名函数中定义的任何变量都会在执行结束后销毁,因此变量i只能在循环中使用。
应用
经常在全局作用域中被用在函数外部,限制向全局作用域中添加过多变量和函数。
作用
既可以执行其中的代码,又不会在内存中留下对该函数的引用;
函数内部的所有变量都会被立即销毁(除非将某些变量赋值给了外部作用域);
通过创建私有作用域,每个开发人员既可以使用自己的变量,又不并担心搞乱全局作用域;
减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁作用域链了;
闭包创建私有变量
任何在函数中定义的变量都可以认为是私有变量。
私有变量包括函数的参数、局部变量和在函数内部定义的其它函数。
访问私有变量和私有函数的公有方法被称为特权方法(privileged method)。
方法一:在构造函数中定义特权方法
在构造函数内部定义了所有私有变量和函数,然后创建能访问这些私有成员的特权方法。
利用私有和特权成员可以隐藏那些不应该被直接修改的数据。
例:
|
|
构造函数模式的缺点:每个实例都会创建同样一组新方法。静态私有变量来实现特权方法可以避免这个问题。
方法二:静态私有变量
这个模式创建一个私有作用域,并在其中封装一个构造函数及相应的方法。
例:
|
|
缺点:每个实例都没有自己的私有变量。
方法三:模块模式(module pattern)
为单例创建私有变量和特权方法。
模块模式使用一个返回对象的匿名函数,这个匿名函数内部首先定义了私有变量和函数,然后将一个对象字面量作为函数的值返回。
例:
方法四:增强的模块模式
适合单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。
创建一个匿名函数,定义私有变量和方法,创建一个对象实例,为实例添加特权方法,最后返回这个实例。
例: