前言
闭包,听着很吊吊的样子,其实原理就是前面说的作用域变量查询(LHS、RHS)过程,不过这个说法实在是有点抽象。换句话说,想一下那个函数作用域气泡。闭包就是一个函数(a)内部声明了内部函数(b),那么a气泡包含b气泡,这个a气泡(作用域引用)就是闭包。
不过这里面也是有几点差异的,以下详说
实质问题
当函数可以记住并且访问所在的词法作用域,就产生了闭包。与函数在哪执行无关。
来个经典例子
这里执行到bar的时候对不在bar词法作用域之内的变量a进行了RHS查询引用,那么就产生了一个闭包(涵盖foo词法作用域)
本例这个函数是在foo内被调用,它在外面调用也一样。
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
闭包一个特点是阻止垃圾回收器对闭包内的内存空间回收。就像下栗的foo执行了之后a应该被回收,不过因为bar的原因,这个作用域一直存活。
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2
几点误区(教科书与Chrome理解差异)
以下为chrome表现
不是在函数内部创建了内部函数就会形成闭包,只有内部函数引用了其它函数作用域的标识符才会形成闭包
// chrome:这里baz函数未曾访问其它函数作用域,所以未曾出现闭包
// 教科书之上却是认为这里有闭包
function foo() {
var a = 'foo';
return function baz(a) {
console.log(a);
}
}
foo()('window');
闭包在运行时形成,且执行到形成闭包的内部函数时才形成闭包
// 执行到baz时,形成闭包foo
function foo() {
var a = 'foo';
return function baz() {
console.log(a);
}
}
foo()('window');
// 执行到baz时形成闭包foo(虽然此时fn未执行,没有调用至引用了a变量那行语句,但是baz作用域确实引用了foo作用域的a变量)
// 执行到fn时形成闭包foo、baz
function foo() {
var a = 'foo';
return function baz() {
var b = 'baz';
return function fn() {
console.log(a, b);
}
}
}
foo()()();
引用了哪个函数词法作用域,闭包就闭在哪
// 运行baz形成的闭包foo(fn词法作用域属于baz词法作用域)
// 运行fn时形成闭包baz、foo
function foo() {
var a = 'foo';
return function baz() {
var b = 'baz';
return function fn() {
console.log(a, b);
}
}
}
foo()()();
这个是最重要的一点,那就是我们能执行内部函数通常是通过return、在window或者其他对象挂载,若是在内返回或者挂载的函数被包了层(先挂载在别的对象上,然后将这个对象打包出来),那么这个是否引用其它函数作用域标识符就不是这个打包对象内挂载的某个方法函数,而是整个打包对象内的所有方法函数
// 这里没有形成闭包,因为obj之内没有哪个方法函数有引用别的函数作用域的标识符
function foo() {
var a = 'foo';
var obj = {
a: a,
fn: function() {
console.log('fn');
}
};
return obj;
}
foo().fn();
// 运行至fn时形成了闭包foo,虽然fn没有引用foo的a变量,但是它的打包对象obj内的a方法函数有引用
// 这里闭包形成原因其实是因为引用了obj对象
function foo() {
var a = 'foo';
var obj = {
a: function() {
console.log(a);
},
fn: function() {
console.log('fn');
}
};
return obj;
}
foo().fn();
最后一点很重要
循环和闭包
来个经典题
// 这个输出来的是五次6,每秒一次
// 这个得先了解event loop
// 原因在于setTimeout被压入macro-task,在for循环结束之后才开始执行setTimeout,那时候i已经是6了
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
// 改变方法很简单,只要生成5个封闭的副本就好
for (var i = 1; i <= 5; i++) {
(function(i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i);
}
// let也可以,因为它劫持了块作用域
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
模块
闭包因为其特性被用在很多地方,其中一个很重要的点就是模块
这里说的模块就是类于Java之类高级语言的类,JS里称为构造方法。其内包含一些数据属性和方法函数,并不是ES6的Module,是ES5的API封装一种实现
// 嗯,很简单,有点不想写
function Foo() {
var a = 'foo';
function fn() {
console.log('fn');
}
var obj = {
a: a,
fn: fn
};
return obj;
}
现代的模块机制
其实就是Requirejs、Seajs,详见
网友评论