理解闭包的概念对于学习JavaScript至关重要,很多新手(包括我)开始学习闭包时,都会感觉似懂非懂,之前看了一些资料,整理了闭包的一篇博客,若有疏忽与错误,希望大家多多给意见。
理解闭包的概念前,建议大家先回想一下JS作用域的相关知识,如果有疑问的同学,可以参考:JavaScript基础–作用域。闭包的定义如下:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
意译出来就是:当函数在其词法作用域外执行时,依然可以访问其词法作用域里的变量。这里的“词法作用域”,就是我们通常理解的作用域。
我们先来看个例子
eg1
function foo() {
var a = 2;
function bar() {
console.log( a );
}
bar();
}
foo(); //--> 2
上述例子中,在调用函数bar()时,变量a的值是取自函数foo的作用域,也就是函数bar()的上层作用域,从闭包的概念来说,这个例子基本属于一个闭包。为什么说是“基本”,因为实际上a也是属于函数bar()的作用域链上的变量,我们更多称之为嵌套作用域。我们再看一个例子:
eg2
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var test = foo();
test(); // --> 2
eg2的例子也许更能体现闭包的概念:我们在定义函数foo()时,返回的是一个函数;var test = foo();将函数的引用赋值给test,然后在执行test();语句时,我们发现a的值依然能够取到,我们称bar()为一个闭包。
闭包的原理:编译器在执行var test = foo();时,会标识其为一个闭包,垃圾回收器在回收内存时,就会保留闭包的作用域链。所以运行test()时,就可以访问到闭包所定义的词法作用域了。
其实我们自己在写JS代码时,经常用到闭包,只是我们没有意识到,比如setTimeout():
eg3
function wait(message) {
setTimeout( function timer(){
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
相信同学们或多或少的用到过setTimeout(),在eg3中我们细心注意,就可以发现我们定义在wait()中的匿名函数是延迟运行的,但它依然可以访问到变量message。对照闭包的概念,是不是就明白了。同样我们在定义很多异步的函数时,都用到了闭包。是不是发现闭包其实我们时时刻刻都在用。
大家还是先来看一个例子
eg4
for (var i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
大家觉得eg4中会输入什么?是1,2,3,4,5吗?如果你把代码赋值到浏览器console面板中,也许会让你失望,代码输出结果为6,6,6,6,6;很多同学觉得每一个i不是单独运行的吗?输出怎么都是6。
分析这个例子前,我们脑子中要有一个概念:JS应用的是函数作用域,而不是块级作用域。反映到eg4中,就是循环中利用的i是公有的。所以在执行timer()时,i已经变为了6。
如果应用JS的IIFE(立即执行函数),输出结果还是5个6吗?比如:
eg5
for (var i=1; i<=5; i++) {
(function(){
setTimeout( function timer(){
console.log( i );
}, i*1000 );
})();
}
我们可以测试一下,结果还是6,6,6,6,6。或者有人认为如果把延迟时间缩小的足够短,结果是不是就可以正常了?实际结果也许会让你失望。我们就算把延迟时间设置为0,结果还是一样的,这是因为for执行效率天生就比setTimeout()高,setTimeout()再怎么缩短延迟时间,也赶不上for。
为了达到我们的期望的结果,解决的办法就是将每次循环的i引入到time()的作用域中,如:
eg6
for (var i=1; i<=5; i++) {
(function(){
var j = i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})();
}
或者是:
eg7
for (var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer(){
console.log( j );
}, j*1000 );
})( i );
}
扩展:在ES6中引入了let关键字,而引入的目的就是为了在JS中实现块级作用域,所以eg5中代码还可以修改为:
eg8
for (var i=1; i<=5; i++) {
let j = i;
setTimeout( function timer(){
console.log( j );
}, j*1000 );
}
或者
eg9
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
模块是应用闭包的典型例子,我们先来看一个例子:
eg10
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
在eg10中CoolModule是一个函数,返回值为一个对象;那么foo在调用doSomething和doAnother时,就产生了闭包。这是module中最简单的利用闭包的例子,接下来我们来看一个怎么解决module依赖的例子。
eg11
定义依赖模块的实现
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
};
})();
我们分析一下以上的代码。首先定义了一个空对象module,其次定义了函数define,其中三个参数:name,定义模块的名称;deps,定义模块的依赖项;impl,定义模块的实现方法。
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" )
); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
当然ES6中也引入了module,使得调用更加方便,直接看例子吧
eg12
bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello;
foo.js
// import only `hello()` from the "bar" module
import hello from "bar";
var hungry = "hippo";
function awesome() {
console.log(
hello( hungry ).toUpperCase()
);
}
export awesome;
// import the entire "foo" and "bar" modules
module foo from "foo";
module bar from "bar";
console.log(
bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
注意在eg12中调用module有两种方式,分别是import和module,前者调用的是接口,而后者调用的是模块,用法也有些许不同,前者是直接接口本身hello(hungry),而后者则是调用模块中的方法bar.hello("rhino")。
原文:http://www.cnblogs.com/fengzheqi/p/5132134.html