概念
之前在作用域那篇博客中说作用域链时提到一个叫闭包的概念,在MDN中解释是:闭包是函数和声明该函数的词法环境的组合,而Js高程里的解释是:有权访问另一个函数作用域中的变量的函数。其实两个解释都是同一个意思,后者可能更好理解。
闭包要解决什么问题?
1.局部变量无法长久保存(一般来说,当函数执行完毕后,局部变量会被销毁。闭包除外),而全局变量可能造成变量污染,所以闭包这种机制可以长久的保存变量,又不会造成全局污染。
2.局部变量无法共享,闭包可以间接访问。
然而也带来了一些问题,闭包引用的作用域不会被垃圾回收处理,因此不合理使用会消耗内存。
举例分析
首先举一个简单的例子,若我们想要设置或者获取一个函数内部的变量值,应该怎么做?
function foo () {
var name = "张三";
var age = "18";
return {
getName: function() {
return name;
},
setName: function(value) {
name = value;
},
getAge: function() {
return age;
},
setAge: function(value) {
age = value;
}
}
}
var person = foo();
console.log(person.getName()); // 张三
相当于foo通过返回一个key是函数名,value是函数的对象,来对外提供设置、获取foo局部变量的方法。而这一切的实现,依赖于foo中的四个闭包。大家是否发现,这个方法实现了修改、查询,跟jquery的 .data()方法有相似之处,data功能多了删除、增加,其实现要相对复杂。大家有兴趣可以参考JQuery数据缓存源码解析、jQuery 源码系列(八)data 缓存机制
下面再来看看稍微复杂一点的例子。
这是前段时间一个朋友参加招聘,给我发的一题校招题。这段代码执行会在界面显示0-9共计10个button,但是点击每个button后alert的都是”You are on Cloud -1”。
首先,我们可以看到最外层是一个立即执行函数(IIFE),里面的循环跑了10次后,我们点击button后,click触发的回调函数会在它的作用域中寻找i变量,而i变量并不在回调函数的变量对象中,所以它会沿着作用域链去搜索,回调函数的上级作用域就是IIFE的作用域,在该作用域中找到了i这个变量,此时i的值为-1(i是IIFE下变量,最后一个值将之前的0、1、2…9覆盖),所以点击每个button都会alert -1。
为何IIFE执行完毕后,其作用域中的i变量不销毁呢?
这是由于i被回调函数引用着,必须等到这些回调函数执行完毕后才会销毁。
此时,我们发现了问题所在,我们想要到达Cloud9,就必须让click触发的回调函数有自己的局部变量,那么闭包此时就能解决我们的问题。
具体做法:
(function(){
var i;
for (i = 9; i >= 0; i--) {
var button = document.createElement('button');
button.innerHTML = 'Take your career to Cloud' + i;
button.addEventListener('click', (function (j) {
return function(){
alert("You are on Cloud " + j);
}
})(i));
document.body.appendChild(button);
}
})();
便于大家理解,也可以这样写:
(function(){
var i;
for (i = 9; i >= 0; i--) {
var button = document.createElement('button');
button.innerHTML = 'Take your career to Cloud' + i;
var foo = function (j) {
return function(){
alert("You are on Cloud " + j);
}
}
button.addEventListener('click', foo(i));
document.body.appendChild(button);
}
})();
为何加入闭包(foo)以后就可以alert You are on Cloud 9了呢?
因为foo函数的作用域中存在j这个变量(函数的形参也会存放在变量对象里),所以在循环跑完的时候,点击button,click触发的对应foo函数可以在其对应的变量对象中找到对应的j的值。
既然已经有了ES6标准,我就来说说如何用ES6来解决这个问题。
(function(){
for (let i = 9; i >= 0; i--) {
var button = document.createElement('button');
button.innerHTML = 'Take your career to Cloud' + i;
button.addEventListener('click', function () {
alert("You are on Cloud " + i);
});
document.body.appendChild(button);
}
})();
let声明的变量只在所在代码块内有效,每个闭包都绑定了块作用域的变量,而且特殊的是for循环中循环的那部分是个父级作用域,所以for循环中每一个i的值都是一个新的变量,这在阮一峰老师ES6入门中有介绍。所以我们点击button触发对应click回调后去对应for的作用域(子作用域)搜索的时候,会得到与click回调绑定的对应i值。
题外话
ES6这套标准让JS慢慢向着规范化靠拢,我也在ES6中慢慢前行,我发现写博客是一个知识总结的过程,把工作中用到的、学习中了解到的,不熟悉、模糊的地方,能通过写博客慢慢总结、然后翻看各种资料,最后明朗化。这就是知识吸收的过程。在此建议没有写博客的同学马上上手搭建一个,并定期更新。关键搭建的教程,在我的第一篇博客中有详细介绍。-)-