【前端拾遗】JS核心知识点——关于闭包的一切(下)

之所以要分上下两个部分,是因为我实在是担心你没有耐心一口气读完全文. 但闭包之于 JS,就如同麻酱之于火锅. 那是灵魂
1.闭包的概念
闭包就是函数能够记住它的词法作用域,及时它在其他地方执行时.
负责任的说:如果你细细度了本文上篇的内容,这句话就能让你豁然开朗.我们举个例子
function foo() {
var a = 2;
function bar() {
console.log(a); // 2
}
bar(); //注意这句!!!
}
foo();
从定义上来讲,因为 bar()在 foo()中调用了,且 bar 访问了 foo()中的变量,我们认为bar()闭住了 foo()的作用域,它形成了一个闭包.
但是!
这不是我们要讨论的闭包.上面的代码虽然形成了闭包,但是 bar()并没有供外部调用.
我们来看一段真正的闭包:
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇噢,看到闭包了
我们来解释一下:
- 函数 bar()对于 foo()的函数作用域拥有访问权.
- 我们将 bar()这个函数像值一样传递(return) 即 return bar;
- 执行 bar = foo()时我们就获得了返回值 bar();
- 当我们调用 baz 的时候,我们本质上就调用了 bar();
这下聪明的你隐约间有了一种说不清道不明的感. 之所以说不清楚,因为你不知道这么做有什么用.
先说结论:
闭包可以避免垃圾回收机制
根据 JS 垃圾回收机制,一般来说 foo()执行后,其内部作用域都将消失,被垃圾回收机制释放掉. 但是当闭包出现后,垃圾回收机制就被阻止了!
在闭包出现后,foo()内部作用域仍然存在,因为函数 bar()在使用它. 通过闭包,我们依旧可以继续访问在程序编写时定义的词法作用域.
所以说,我们回头再看看闭包的定义.
闭包就是函数能够记住它的词法作用域,及时它在其他地方执行时.
形成闭包只需要 在函数 A 内部嵌套一个函数 B,只要函数 B 能够访问函数 A 的内容且被执行,就形成了闭包.
2.闭包的不同形式
除了通过值传递,闭包在其他位置调用也可以形成闭包.
function foo() {
var a = 2;
function baz() {
console.log(a); // 2
}
bar(baz);
}
function bar(fn) {
fn(); // 看妈妈,我看到闭包了!
}
foo(); //2
内部函数 bar()被传递给了 bar,而 bar 是定义在全局作用域中的函数. 这样就形成了一个闭包,且在外部 bar()作用域中被调用了.
这样的函数传递也可以是间接的.
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 将`baz`赋值给一个全局变量
}
function bar() {
fn(); // 看妈妈,我看到闭包了!
}
foo();
bar(); // 2
无论我们使用什么方法,只要将内部函数传送到其词法作用域外,函数都将维护一个最开始被声明时候的作用域的引用. 无论我们什么时候执行它,闭包都会运行.且运行的变量是最开始声明时候的作用域
3.无处不在的闭包
其实闭包,已经被应用在你的项目中且无处不在了.
function wait(message) {
setTimeout(function timer() {
console.log(message);
}, 1000);
}
wait("Hello, closure!");
虽然通常我们不这样写,但这段代码能够很好的说明闭包的运行规则,
- 首先 setTimeout 是一个 JS 自有的全局函数.
- 我们将 timer()传递给 setTimeout(..), timer()包含着对于 wait 词法作用域的引用
- 当我们执行 wait()时,虽然 1000ms 后才执行 timer(),但是它仍然记忆着 message 的内容
这就是闭包 就是这么简单
我们再举一个循环的例子,循环被认为是解释闭包原理最好的例子.
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
答案是: 6 (循环五次) 我们本来的期望是 1,2,3,4,5 但实际情况却事与愿违.
就定时器而言,定时器都是在循环执行结束后才执行的,此时 timer()所执行的值是当前全局作用域中的 i
如何解决这样的问题?
或许我们可以通过立即执行函数在每次一生成 setTimeout 时给其一个单独的 i
for (var i = 1; i <= 5; i++) {
(function () {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})();
}
但这样不行,因为虽然我们在立即函数执行过程中新建了许多空的作用域,**但这些作用域中并没有内容,它仍然会到全局作用域中查找变量 i . **
我们可以在被闭包的作用域加入内容
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);
}
但当我们采用块级作用域,代码会变得更加 NB
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
在用于 for 循环头部的 let 声明被定义了一种特殊行为。这种行为说,这个变量将不是只为循环声明一次,而是为每次迭代声明一次。并且,它将在每次后续的迭代中被上一次迭代末尾的值初始化。
简而言之,采用块级作用域为每一次循环附上单独的值.
4.闭包的用途(转自阮一峰 学习 Javascript 闭包(Closure))
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
function f1() {
var n = 999;
nAdd = function () {
n += 1;
};
function f2() {
alert(n);
}
return f2;
}
var result = f1();
result(); // 999
nAdd();
result(); // 1000
在这段代码中,result 实际上就是闭包 f2 函数。它一共运行了两次,第一次的值是 999,第二次的值是 1000。这证明了,函数 f1 中的局部变量 n 一直保存在内存中,并没有在 f1 调用后被自动清除。
为什么会这样呢?原因就在于 f1 是 f2 的父函数,而 f2 被赋给了一个全局变量,这导致 f2 始终在内存中,而 f2 的存在依赖于 f1,因此 f1 也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在 nAdd 前面没有使用 var 关键字,因此 nAdd 是一个全局变量,而不是局部变量。其次,nAdd 的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以 nAdd 相当于是一个 setter,可以在函数外部对函数内部的局部变量进行操作。
5.使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。