一、什么是闭包
function foo() { var a = 2; //函数bar( )的词法作用域能访问函数foo( )的内部作用域。将bar( )函数当做值传递。 //bar( )在foo( )内声明,它拥有涵盖foo( )内部作用域的闭包,使得该作用域能一直存活,供bar( ) 在之后任何时间引用。bar( )本身在使用foo( )的内部作用域,因此foo执行后不会被销毁。 function bar(){ console.log( a ); } return bar; } //bar( )可以正常运行,而且是在自己的词法作用域以外执行。 var baz = foo(); //foo( )执行后,其返回值bar()函数赋值给变量baz,并调用baz( ),实际上是调用了内部的函数bar( )。 baz();
bar( )依然持有对该作用域的引用,这个引用叫作闭包。
无论通过任何手段将内部函数传递到所在词法作用域以外的,它都会有对原始定义作用域的引用,无论在何处执行这个函数都会产生闭包。
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); // 闭包 }
var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz(); } function bar() { fn(); // 闭包 } foo(); bar();
将内部函数timer传递给setTImeout,timer涵盖wait作用域的闭包,因此还保有对message的引用。wait执行1000毫秒后,它的内部作用域不会消失,timer函数还保有wait作用域的闭包。
function wait(message) { for(var i = 0; i <= 5; i++){ setTimeout( function timer() { console.log(i); }, i*1000); } }
如果将(访问它们各自词法作用域的)函数当做第一级的值类型并到处传递,就能看到闭包在这些函数的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或任何其他的异步(同步)
任务中,只要使用了回调函数,就是在使用闭包。
function setup(name, selector) { $( selector ).click( function activator( ) { console.log( "Activating: " + name ); }); } setupBot( "Closure Bot 1", "#bot_1" ); setupBot( "Closure Bot 2", "#bot_2" );
函数IIFE并不是在本身的词法作用域以外执行,在它定义时所在的作用域执行。a是通过普通的词法作用域查找而非闭包被发现的。尽管IEFF本身不少观察闭包的恰当例子,但它的确创建了闭包,
并且也是最常用来创建可以被封闭起来的闭包的工具。
var a = 2; (function IIFE( ) { console.log( a ); }());
循环和闭包
for(var i = 0; i <= 5; i++){
setTimeout( function() {
console.log(i);
}, i*1000);
}
// 每秒一次输出5个6
延迟函数的回调会在循环结束后才执行。当定时器运行时即使每个迭代中执行的是setTimeout(..., 0),所有的回调函数依然在循环结束后执行。
根据作用域原理,尽管循环的五个函数在各个迭代分别定义,但它们都被在全局作用域,实际只有一个i。
需要更多的闭包作用域,特别是在循环的过程中每次迭代都需要闭包作用域。
//这个例子不能实现
for (var i = 0; i <= 5; i++){ (function( ) { setTimeout( function() { console.log(i); }, i*1000); }()) } //每个延迟函数都会在IIFE在每次迭代中创建的作用域封闭 //但这里的是空的作用域
它需要有自己的变量,用来在每个迭代中存储 i。
for(var i = 0; i <= 5; i++){ (function(){ var j = i; setTimeout( function() { console.log(j); }, j*1000); }()) }
稍加改进
for(var i = 0; i <= 5; i++){ (function(j){ setTimeout( function() { console.log(j); }, j*1000); }(i)) }
在迭代内使用IIFE会给每个迭代生成新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有i一个具有正确值的变量。
使用块作用域
for(var i = 0; i <= 5; i++){ let j = i; //闭包的块作用域 setTimeout( function timer() { console.log(j); }, j*1000); }
for循环的let声明有一个特殊行为,变量在循环过程不止声明一次,每次迭代都会声。随后的每次迭代都会用上一个迭代结束时的值来初始化这个变量。
for(let i = 0; i <= 5; i++){ setTimeout( function timer() { console.log( i ); }, i*1000); }
模块
function coolModule() { var something = "cool"; var another = [1, 2, 3]; function doSomething() { console.log( something ); } function doAnother () { console.log( another.join( " ! " ) ); } return { doSomething: doSomething, doAnother: doAnother }; } var foo = coolModule(); foo.doSomething(); //cool foo.doAnother(); // 1 ! 2 ! 3
这种模式被成为模块,最常见的实现模块方式被成为模块暴露,这里的是其变体。
首先,coolModule( )只是一个函数,必须通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
其次,coolModule( )返回一个用对象字面量语法 { key: value, ... }来表示的对象。这个对象包含对内部函数而不是内部变量的引用。外面保持内部变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上的是模块的公共API。
这个对象类型的返回值最终被赋值给外部的变量foo,然后就可以通过它来访问API的属性方法。
- 从模块返回实际的对象不少必须的,可以返回一个内部函数,如jQuery。
模块模式需要具备两个必要条件。
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态。
当只需一个实例时,可以实现单例模式:
var foo = (function coolModule() { var something = "cool"; var another = [1, 2, 3]; function doSomething() { console.log( something ); } function doAnother () { console.log( another.join( " ! " ) ); } return { doSomething: doSomething, doAnother: doAnother }; }()); foo.doSomething(); //cool foo.doAnother(); // 1 ! 2 ! 3
模块也是普通的函数,因此可以接受参数:
function coolModule(id) { function identify() { console.log( id ); } return { identify: identify }; } var foo1 = coolModule( "foo 1" ); foo1.identify(); // "foo 1"
模块模式另一个简单但强大的用法是命名将要作为公共API返回的对象:
var foo = (function coolModule(id){ function change(){ publicAPI.identify = identify2; } function identify1() { console.log( id ); } function identify2() { console.log( id.toUpperCase() ); } var publicAPI = { change: change, identify: identify1 } return publicAPI; }("foo module")); foo.identify(); // foo module foo.change(); foo.identify(); // FOO MODULE
现代的模块机制
var MyModules = (function Manager() { var modules = {}; function define(name, deps, impl) { for(var i = 0; i < deps.length; i++) { deps[i] = modules[deps[i]]; } modules[name] = impl.apply( impl, deps); } function get(name) { return modules[name]; } return { define: define, get: get } }());
这段代码的核心是 modules[name] = impl.apply( impl, deps)。为了模块的定义引入了包装函数,并将返回值(模块API)存储在一个根据名字来管理的模块列表中
//定义模块
MyModules.define( "bar", [], function() { function hello(who) { return "Let me introduce: " + who; } return { hello: hello }; }); MyModules.define( "foo", ["bar"], function(bar) { var hungry = "hippo"; function awesome() { console.log( bar.hello( hungry).toUpperCase() ); } return { awesome: awesome }; });
//使用 var bar = MyModules.get( "bar" ); var foo = MyModules.get( "foo" ); console.log( bar.hello( "hippo" )); foo.awesome();
foo和bar模块都是通过一个返回公共API的函数来定义的。“foo”甚至接受“bar”的实例作为依赖参数,并能相应地使用它。