前言:
对于那些有一点Js使用经验但却完全不理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生,但是需要付出非常多的努力和牺牲才能理解这个概念。
1.什么是闭包?
函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。
2.闭包用途
1、在自身词法作用域之外的地方读取函数内部的变量
2、让这些变量的值始终保持在内存中。不会在调用后被自动清除。
3、方便调用上下文的局部变量。利于代码封装。
3.理解闭包概念中的作用域
function foo(a){
var b=2*a;
function bar(c){
console.log(a,b,c);
}
bar(b*3);
}
foo(2);//2,4,12
- foo()最外层是全局作用域,其中只有一个标识符:foo
- foo()函数内部所创建的作用域,其中有三个标识符:a、bar、c.
- bar()函数内部所创建的作用域,其中只有一个标识符:c
4.1 展示闭包1
function foo(){
var a=2;
function bar(){
console.log(a);
}
return bar;
}
var baz=foo();
baz();//2---这就是闭包的效果
解释:有人可能认为执行baz()时,在打印控制台上打印的结果是undefined,有这样想法的人可能认为:在foo()执行之后,通常会期待foo()的整个内部作用域被销毁,因为JS有垃圾回收用来释放不再使用的内存空间。由于看上去foo()不会再被使用,所以自然地会考虑对其进行回收。而闭包的特点就在于阻止垃圾回收事件发生。事实上,foo()内部作用域依然存在,因为bar()本身还要使用这作用域。bar()声明的位置,它拥有涵盖foo()内部作用域的闭包,使得该作用域一直存活,以使得bar()在之后任何时间可以引用。
这个函数在定义时的词法作用域以外的地方调用。闭包使得函数可以继续访问定义时的词法作用域。
4.2展示闭包2
function foo(){
var a=2;
function baz(){
console.log(a);//2
}
bar(baz);
}
function bar(func){
func();//在词法作用域之外调用baz(),这就是闭包
}
4.3 展示闭包3
var func;
function foo(){
var a=2;
function baz(){
console.log(a);
}
func=baz;//将baz分配给全局变量
}
function bar(){
func();//这就是闭包
}
foo();
bar();//2
由3个示例可以看出,无论通过何种手段将内部函数传递到所在词法作用域外,他都会持有原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
4.4上面这些例子都是为了解释如何使用闭包而人为的在结构上进行修饰,下面看看你已经使用过的闭包
function wait(message){
setTimeout(function timer(){
console.log(message);
},1000);
}
wait("hello 闭包");//一秒后在这执行timer(),不在定义时的作用域。
将timer()传递给setTimeout()。timer()具有涵盖wait()作用域的闭包,因此还保有对变量message的引用。
小结: 当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。如果没能认出闭包,也不了解它的工作原理,在使用它的过程中就很容易犯错,比如在循环中。但同时闭包也是一个非常强大的工具,可以用多种形式来实现模块等模式。
5.1模块与闭包
模块模式需要的两个条件:
1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态。
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
看上述代码是否满足模块条件?
- coolModule()就是封闭函数,且该函数使用“立即调用”(IIFE)方式调用了函数,因此满足模块的第一个条件。
- coolModule()使用一个对象{doSomething:doSomething,doAnother:doAnother};将内部的两个函数都返回,因此也满足模块第二个条件。
注意: 在这里的模块函数是立即执行函数。
5.2 模块也可以是普通函数,因此可以传递参数
function coolModule(id){
function identify(){
console.log(id);
}
return { identify:identify};
}
var foo1=coolModule("foo 1");
var foo2=coolModule("foo 2");
foo1.identify();//"foo 1"
foo2.identify();//"foo 2"
5.3模块模式一个简单但强大的用法将要作为“公共”API返回的对象:
var foo=(function coolModule(id){
function change(){//修改公共API的指向
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
这里的“公共”指的是内部函数identify1()与identify2()共用对外暴露的identify的API接口。
5.4 现代的模块机制
看下面代码:
var myModule=(function manager(){
var modules={};
function define(name,depend,imply){
for(var i=0;i<depend.length;i++){
depend[i]=modules[depend[i]];
}
modules[name]=imply.apply(imply,depend);
}
function get(name){//根据名字来获取相应的模块
return modules[name];
}
return {
define:define,
get:get
};
})();
代码中for()循环的分析:depend[i]=modules[depend[i]];
define()中第二个参数depend存储的是创建模板所需的依赖“名称”,在for循环中,根据“依赖名称”得到
modules对象中存储的相应依赖(即模板:modules[depend[i]],depend[i]是依赖名称),然后修改
depend[i]的值,由依赖名称变为真正的依赖。最后作为apply()的第二个参数。
这段代码的核心是modules[name]=imply.apply(imply,depend)。为了模块的定义引入了包装函数(可以传入任何依赖),并返回值(也就是模块的API),存储在一个根据名字来管理的模块列表中。
define()函数第一个参数是要定义的模块名;第二个参数是一个数组,用来存储创建模板时所需注入的依赖名称;
第三个是模板函数。
下面展示了如何使用上面模块来定义模块:
myModule.define("bar",[],function(){
function hello(who){
return "let me introduce:"+who;
}
return {hello:hello};
});
myModule.define("foo",["bar"],function(bar){
var hungry="yes";
function awesome(){
console.log(bar.hello(hungry).toUpperCase());
}
return {awesome:awesome};
})
var bar=myModule.get("bar");
var foo=myModule.get("foo");
console.log(bar.hello("xi"))//let me introduce xi
foo.awesome();//LET ME INTRODUCE: YES
"foo"和“bar"模块都是通过一个返回公共API的函数来定义。“foo”甚至接受"bar"的实例作为依赖参数,并能相应的使用它。
5.5 未来的模块机制
ES6的模块没有“行内”格式,必须被定义在独立的文件中(一个模块一个文件)。
//bar.js文件
function hello(who){
return "let me introduce:"+who;
}
export hello
//foo.js文件
import hello from "bar";
var hungry="xi";
function awesome(){
console.log(hello(hungry).toUpperCase());
}
export awesome;
//baz.js文件
//导入完整的"foo"和"bar"模块
module foo from "foo";
module bar from "bar";
console.log(
bar.hello("xi")
)//let me introduce xi
foo.awesome();//LET ME INTRODUCE XI
小结: 模块的两个主要特征
- 为创建内部作用域而调用了一个包装函数;
- 包装函数的返回值必须至少包含一个对外部函数的引用,这样就会创建包含整个包装函数内部作用域的闭包。