最hardcore的键盘
闭包是学习JavaScript的一大难题。
所以觉得系列第20篇就来讨论闭包。
参考文章:
https://github.com/mqyqingfeng/Blog/issues/9
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
https://zhuanlan.zhihu.com/p/22486908
https://www.jianshu.com/p/21a16d44f150
首先是对闭包的定义:
在《JavaScript高级程序设计 第三版》中对闭包的定义是指有权访问另一个函数作用域中的变量的函数。
但是在MDN中,简单明了的定义:闭包是函数和声明该函数的词法环境的组合。
个人倾向于MDN的解释。
ECMAScript中,闭包指的是:
从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量
总的来说,广义上所有函数都是闭包。
闭包的作用,模拟私有变量和私有方法。
有点说不下去,看看代码吧
var fn = null;
function foo() {
var a = 2;
function innnerFoo() {
console.log(a);
}
fn = innnerFoo; // 将 innnerFoo的引用,赋值给全局变量中的fn
}
function bar() {
fn(); // 此处的保留的innerFoo的引用
}
foo();
bar(); // 2
在上面的例子中,foo()执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。
闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量
所以,闭包会造成内存泄露。
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。
add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。
模拟私有方法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
但我们不想直接暴露私有变量(return这个变量),可以用闭包提供接口调用私有变量和私有函数。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问
再如:
var foo = (function() {
var secret = 'secret';
return {
get_secret : function () {
// 通过定义的接口来访问secret
return secret;
},
new_secret: function (new_secret) {
// 通过定义的接口来修改secret
secret = new_secret;
}
};
})();
闭包用于模块化
function add(num1, num2) {
var num1 = !!num1 ? num1 : a;
var num2 = !!num2 ? num2 : b;
return num1 + num2;
}
window.add = add;
})();
add(10, 20);
典型题目
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
解答
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
或者使用let
典型题目:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
解答:
for (var i = 0; i < 5; i++) {
// 用立即执行函数包裹定时器只为了形成单独的作用域
(function(e) {
setTimeout(() => console.log(e), i * 1000);
})(i);
}
for (var i = 0; i < 5; i++) {
// setTimeout里面的匿名函数和里面函数形成闭包,只为了形成单独的作用域
// 只所以return函数,只是为了setTimeout第一个参数必须为函数
setTimeout(function(e) {
return function() {
console.log(e);
}
}(i), i*1000)
}
for (var i = 0; i < 5; i++) {
// 注意,setTimeout第一个函数要用到后面的参数,所以必须要把这个参数传进来
setTimeout((i) => console.log(i), i * 1000, i);
}
或者使用let