目录
关键词:词法作用域外部引用、外围函数、内部函数、单例模式
5.1 启示
闭包是基于词法作用域书写代码时所产生的自然结果,它在JavaScript中无处不在
5.2 实质问题
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用
域之外执行
看代码:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
调用 baz() 时,bar() 被执行了,它是在它被声明的词法作用域外部被执行的。
问:在执行 var baz = foo() 后,我们会期望 foo() 的整个内部作用域消失(垃圾回收)。但并没有,bar() 执行时依旧在使用内部的作用域。为何?
答:这得益于 bar() 声明的位置,bar() 拥有一个词法作用域闭包覆盖着 foo() 的内部作用域,闭包为了能使 bar() 在以后任意的时刻可以引用这个作用域而保持它的存在。
bar() 依然拥有对那个作用域的引用,而这个引用称为闭包。
再来点闭包:
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // 将 baz 分配给全局变量
}
function bar() {
fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2
无论通过何种手段将内部函数
传递
到所在的词法作用域以外,它都会持有对原始定义作用
域的引用,无论在何处执行这个函数都会使用闭包。
5.3 现在我懂了
常见的工具函数 setTimeout(..)是闭包:
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000)
}
wait( "Hello,closuer" ); // 1000毫秒后,timer还拥有wait的内部作用域
或者很熟悉的jQuery写法:
function setupBot(name,selector) {
$( selector ).click( function activator() {
console.log( "Activating:" +name );
})
}
setupBot( "Closure Bot 1","#bot_1"); // click时还持有setupBot作用域
setupBot( "Closure Bot 2","#bot_2");
只要使用回调函数,就是在使用闭包
5.4 循环和闭包
for (var i=1;i<=5;i++){
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
思考下这段代码的答应结果,是不是我们想象的分别输出1、2、3、4、5、6?若不是该如何改进?
解题思路: IIFE
最优解: IIFE 结合 LET 块作用域
答案见文尾
5.5 模块
JS模块也是用了闭包的威力:
function CoolModule() {
var something = "cool";
var anoher = [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
来创建一个模块实例,同时创建内部作用域和闭包
从模块中返回一个实际的对象并不是必须的,也可以直接 返回一个内部函数。 jQuery 就是一个很好的例子。 jQuery 和 $ 标识符就是 jQuery 模块的公共 API ,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)。var myModule1 = (function($, global){ ...// }(jQuery, window))
模块模式的两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用创建一个模块实例)
- 封闭函数必须至少返回一个内部函数,这样内部函数才能在私有作用域中形成闭包,并可以访问、修改私有的状态
经常会看到一个对象封装并返回一些函数。因为没有闭包,所以此种写法不是模块
单例模式模块
通过 IIFE 创建单例模式:
var foo = (function CoolModule(id) {
var something = "cool";
var another = [1,2,3];
function doSomething() {
console.log( something + id);
}
function doAnother() {
console.log( another.join(" ! ") );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();
// 也可以传参
foo.doSomething("123"); // cool123
foo.doAnother(); // 1!2!3
公共API
通过在模块实例的内部保留对公共
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
5.5.1 现代的模块机制
模块依赖加载器 / 管理器
大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 API
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++){
//将依赖的模块转换为modules列表内的一个元素
deps[i] = modules[deps[i]];
}
//定义模块并传入依赖模块参数
modules[name] = impl.apply( impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
在模块依赖中,有个声明模块 define ,有三个参数:
- name 声明的模块名称
- deps 该模块所依赖的模块名称集合
- impl 模块主体函数,返回该模块数据内容
modules[name] = impl.apply( impl, deps ) : 通过 apply 把 deps 中的依赖注入到主函数 impl
通过 get函数 获取到模块
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" )
); // Let me introduce: hippo
foo.awesome(); // LET ME INIRODUCE: HIPPO