目录
闭包的定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的
闭包是什么?闭包是一个函数对自己所在的词法作用域的引用
闭包的作用? 闭包可以使得函数可以继续访问自己被定义时的词法作用域
- 1. 通过return返回内部函数,然后在作用域外引用
- 2. 将内部函数当作实参传递,然后在作用域外引用
- 3. 将内部函数赋值到另外一个变量当中,然后在作用域外引用
闭包的理解
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz();
bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
上述代码中,函数 bar() 的词法作用域能够访问 foo() 的内部作用域,因为函数bar引用了函数foo的变量a。然后我们将 bar() 函数本身当作 一个值类型进行传递,当做函数foo()的返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),执行函数baz()实际上是调用了foo()内部的函数 bar(),这就是定义中的“函数是在当前词法作用域之外执行”,即函数bar()在foo()内部定义,在foo()外部调用。
一般情况下,当一个函数执行完之后,函数内部的作用域会被销毁,js引擎的垃圾回收器会释放函数内的不再使用的内存空间。然而上述代码中,当函数foo()执行完之后,其内部的作用域未被销毁,因为函数bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。闭包使得函数可以继续访问定义时的词法作用域。
闭包的运用
回调函数中的闭包
function wait(message) {
setTimeout( function timer() {
console.log( message );
}, 1000 );
}
wait( "Hello, closure!" );
上述代码中,将一个内部函数(名为 timer)传递给 setTimeout(…)。此时timer 具有涵盖 wait(…) 作用域的闭包,因此还保有对wait()的变量 message 的引用。在wait(…) 执行 1000 毫秒后,它的内部作用域并不会被销毁,因为timer 函数依然保有 wait(…) 作用域的闭包。
在定时器、事件监听器、 Ajax 请求等任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
for循环与闭包
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
正常情况下,我们对上述代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。
首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是 6。因此,输出显示的是循环结束时 i 的最终值。
我们明明执行了五次函数,并且每次都是打印i,按理说i每次都是不同的,但最终结果是每次打印的i都是6,这是为什么呢?一个合理的解释是虽然这五个函数是在各个迭代中分别定义的, 但是它们其实都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
这段可以用事件循环去解释,i的赋值是同步事件,在执行栈中不断执行直到i=6,执行栈空并开始传递i=6给消息队列中的5个异步函数setTimeout(),每次i赋值都会在消息队列中添加一个setTimeout()
重返块作用域
每次迭代我们都需要一个块作用域。let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
for (var i=1; i<=5; i++) {
let j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
}
--------------------------
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
模块
如果要更简单的描述,模块模式需要具备两个必要条件。
- 1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
命名将作为公共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
未来的模块机制
这里所说的模块概念,我觉得核心是其对数据的封装特性,而要达到这种效果就只能利用闭包原理来实现数据私有化的效果,所以,要实现一个模块需要分2步,一是创建一个函数并保证执行一次,二是返回一个具有函数特性的对象。本质上就是创建闭包的过程
// 母模块
var MyModules = (function Manager() {
var modules = {};
// 可以用户定义自己的模块
// (name:要定义的模块名字,deps:模块所要的依赖,impl:接口实现过程)
function define(name, deps, impl) {
// deps他告诉MyModules所要依赖的子模块
// 遍历deps并且把模块转为字符串【bar】---【MyModules.bar】
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
//【MyModules.bar】注入到第三个参数impl:接口实现 function(bar){}
// this仍然指向自身,并把该函数赋值给modules
modules[name] = impl.apply(impl, deps);
}
// 获取已有模块
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
MyModules.define("bar", [], function () {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
});
// 我们需要一个参数告诉MyModules帮我们把bar注入到foo
MyModules.define("foo", ["bar"], function (bar) {
var hungry = "hippo";
// 需要调用bar方法的hello,需要把模块bar传入
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
MyModules
|_ _ bar
| |_ _ hello(who)
| _ _ foo
|_ _awesome()
以上代码运行后如结构图所示:MyModules是个母模块,下面有两个子模块,分别实现了自己的方法。
- 1. 然后解释一下母模块,MyModules提供了两个方法,define可以给用户定义自己的模块,get获取已有的模块。重点解释一下define。define有三个参数:name :要定义模块的名字,deps:模块所要依赖的,impl「接口」:实现的过程;
- 2. 然后直接看foo子模块的定义: MyModules.define("foo", ["bar"], function(bar) { … },这里比较模糊的是第二个参数 ["bar”] 有什么作用。在foo 的实现中,需要调用bar的方法hello,因此需要把模块bar传入,但是,bar是在哪里实现的?在MyModules实现的,我们需要提供一个参数来告诉MyModules帮我们把bar注入到foo中,这就是define函数第二个参数的用法。
- 3. 回到define的实现代码中:for (var i=0; i<deps.length; i++) { deps[i] = modules[deps[i]]; }, 还记得第二个参数的作用吗?['bar'],它是告知MyModules我所要依赖的子模块,但这个参数是个数组,并且里面的元素'bar'是字符串,所以这段代码的功能是:遍历deps,并把字符串换成模块,也就是['bar'] 变成了[MyModules.bar]
- 4. 回到最重要的一段代码:modules[name] = impl.apply(impl,deps),这个的用法是把[MyModules.bar] 注入到 第三个参数impl : function(bar) { … } 的实现中,然后this仍然指向自身,并把该函数赋值给modues。
总结:这种实现既保证了各个子模块的封闭性,又不缺乏可扩展性,很值得学习
es6模块机制
ES6中为模块增加了一级语法支持。在通过模块系统进行加载时,ES6会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的API成员,同样也可以导出自己的API成员
bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello;
foo.js
// 仅从"bar"模块导入hello()
import hello from "bar";
var hungry = "hippo";
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("rhino")
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
import和export编译成es5,其实就是一个个的自执行函数生成的单例模块
闭包的缺点
使用闭包可以帮我们解决很多问题,但闭包的缺点是导致内存泄露,因为闭包会一直保持着对某些本该释放的作用域的引用,因而作用域内的变量所占用的内存不会被释放。