再看闭包定义
闭包的概念看了几十回不止了,但是让我描述一遍,我应该还是说不出个什么东西。下面就再来看看闭包的概念:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
下面配合一个例子来更好的理解
function foo() {
var a = 2;
function bar() {
console.log(a) // 2
}
bar()
}
foo()
复制代码
看了这段代码,很容易有个疑惑,这就是闭包吗?这不应该是作用域的例子吗?对,这就是词法作用域的查找规则。但是,这些规则就是闭包的一部分。
通过这段代码,我们不能清晰的理解闭包的意义,却可以很容易的理解词法作用域。下面我们来稍加修改一下,来看看到底闭包应该什么样子,和词法作用域有什么联系?
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2
复制代码
再结合定义来看,就很容易理解了。
bar函数可以访问所在函数foo的词法作用域。这就产生了闭包。而bar函数本身作为返回值进行传递,并且赋值给了baz,在foo的词法作用域外,我们执行baz,依然可以执行。
下面我们来进一步的理解闭包。
依据JavaScript的垃圾回收机制,当var baz = foo()执行之后,理论上来说foo的内容不会再被使用,所以应该释放所在的内存空间,但是这时候却不会被回收,这就是闭包的神奇的地方。
bar()依然持有该作用域的引用。这个引用就是闭包。
所以当我们执行baz时,以然可以访问到a。
下面再来几个例子,加深我们对闭包的理解
function foo() {
var a = 2
function baz() {
console.log(a)
}
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()
复制代码
循环和闭包
我第一次接触到闭包的这个概念是在这样一次使用之后:
for(var i = 0; i < 5;i++){
setTimerout(function timer(){
console.log(i)
}, i * 1000)
}
复制代码
天真的我等待这每秒输出一个0-4的数字,等结果出现的时候,我直接蒙了,这是JavaScript出bug了啊。
现在看来但是的自己是多么的无知。。。
解决办法对现在的自己来说很简单了
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
})();
}
复制代码
或这样
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j * 1000 );
})( i );
}
复制代码
原因也是很简单的,
- 闭包
- 更多的词法作用域
块作用域
es6的出现让我们有了解决上面问题的更简单的方法。那就是利用块作用域。
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
复制代码
模块
在大规模使用es6的模块机制的现在,我们对模块已经有了也许详细的认识。但是,具体的模块是怎么实现的呢?我们来看一下。
function Foo() {
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 = Foo()
foo.doSomething()
foo.doAnother()
复制代码
上面的这个模式就是最基本的模块了,很显然,我们利用了闭包的知识。
Foo函数对外暴漏了一个对象,这个对象包含了doSomething和doAnother两个函数。
模块模式需要具备的两个必要条件:
- 必须有外部的封闭函数, 该函数必须至少被调用一次(每次调用都会创建一个新的模块 实例)。
- 封闭函数必须返回至少一个内部函数, 这样内部函数才能在私有作用域中形成闭包, 并 且可以访问或者修改私有的状态。
上面的模块我们可以调用很多次,每次都会创建一个新的模块实例。如果我们只需要使用一次时,我们可以这么做
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
复制代码
看,闭包是不是无处不在。
当然,模块还可以接受参数,根据参数来设置一些模块内的内容,这都是很简单的应用,很容易理解。
es6的使用,使我们对模块的使用更加的方便。es6会将文件作为独立的模块来处理,每一个暴露的函数都可以用关键词export来导出。使用模块暴露出来的函数时,我们用import来引入。
结
通过这一次学习,对闭包有个更加深入的认识,我么再来看一下闭包的定义:
当函数可以记住并访问所在的词法作用域, 即使函数是在当前词法作用域之外执行, 这时 就产生了闭包
另一个重要的概念就是模块:
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回值必须至少包括一个对内部函数的引用, 这样就会创建涵盖整个包装函数内部作用域闭包
闭包无处不在,以后的工作学习中能熟练的运用闭包,一眼认出来是闭包就是真正的掌握了