开始之前
这是回北京后被疫情困在家的第四周了,在弹尽粮绝和刚刚看完《十二道锋味》芬兰之旅,还沉浸在奇幻的极光和诱人的西餐中开始写这篇网络日记。关于闭包在之前面试的时候就想写了,但是因为事儿多,忙了给耽搁下了。而且写一篇技术类的网络日记真的还是挺费时间的。写这种技术类的日记不像小时候写的日记,记录一些流水,这个真的还是要备课。这片日记还是以阮一峰老师讲闭包为主。但是阮老师写的关于闭包单刀直入,似乎少了一些前情知识,所以在内容上面做了一些调整,然后加入了几个实战的案例来分析JavaScript这个重要的概念——闭包。
环境与作用域
闭包之前先了解一些 JavaScript 的环境和作用域有助于对闭包的理解,可以说是这部分内容是闭包的先行知识。
任何的编程语言其实都有这个概念,环境就是函数运行的环境,作用域就是函数作用的范围。举个类似的例子,一座城市。城市里有学校,医院,商场等基础设施是市民的生活环境,城市的存在需要市民的依赖,只有市民依赖城市生活这座城市才会存在,当某一天市民不再依赖这座城市生活了,城市基本上也就破败了,也就不复存在了。城市里面的一个个基础设施单位学校,超市等也是一样,没人用了就倒闭了回收了。城市里面一些基础设施会有自己的辐射范围,尤其像是学校,都会划学区,就规定了这个学校就服务自己所在片区的孩子。现在我们做对比的话我们可以把城市想象成JavaScript中的全局环境。城市中一个个的基础设置就相当于函数,函数中也会有自己的小环境。每个环境都有自己的作用范围,全局环景就对应全局作用域,函数环境就对应函数作用域。
javaScript全局环境有一个特点就是全局环境永远不会被回收。举个例子
.....
上面的代码不完整啊,就是简单的写一个html页面,里面只有上面的一个script定义的内容,其他什么都没有。我们会发现,当我们用浏览器打开,看控制台打印出title了,当我们再次在控制台打印title,title还会会打印出来,可见这个脚本执行完,这个环境并没有被回收。
上面是全局环境,我们再看一下全局环境中的局部环境,先看一下下面这段代码:
上面的代码中我们再全局环境中创建了一个 foo
函数,它会形成自己的一个局部环境,里面有有数据 n 和 函数 show,调用show时,他会在父级foo
的环境中创建自己的环境。环境是只有在函数调用的才会创建,对应计算机语言就是创建一块内存区域。
我们运行这个文件会看到控制台打印出总是会打印出 1 来,即使我们多次调用 foo(), 每次的结果都还是一样的,这说明
foo
这个换环境每次都是新的,也就是我们每调用一次就会开辟一块新的内存空间。
关于作用域,全局作用域只有一个,每个函数又都有作用域(环境)。
- 编译器运行时会将变量定义在所在作用域
- 使用变量时会从当前作用域开始向上查找变量
-
作用域就像攀亲戚一样,晚辈总是可以向上辈要些东西
作用域链只向上查找,找到全局window即终止,所以我们会看到 show 函数是可以访问到 title 这个变量。但是我们尽量不要在全局作用域上定义变量
但是有的时候我们想保留一个函数环境或者说成是作用域。比如我们说上面函数中的 n
, 我们想实现的效果就是我们每次调用一次 show
函数 n
就要累加一次。也就说但我们执行完 foo
函数的时候他的那块内存区域不会被清空回收。
我们可以看到当我们把 show 函数的引用返回出去,foo
的环境就会被保留。那么也就是说子函数被外部使用时,它的父级环境就是会保留的。
其实我们经常使用的构造函数就是一个很好的环境例子,子函数被外部使用时,父级环境将会保留。
function User() {
let a = 1;
this.show = function() {
console.log(a++);
};
}
let a = new User();
a.show(); //1
a.show(); //2
let b = new User();
b.show(); //1
其实构造函数我们可以看作是下面代码的一种变形(构造函数实际上会复杂一些)
function User(){
let a = 1;
function show(){
console.log(a++)
}
return {
show:show
}
}
ES6中 let
和 const
可以变量的声明放在块级作用域中(放在新环境,而不是全局中)。
{
let a = 9;
}
console.log(a); //ReferenceError: a is not defined
if (true) {
var i = 1;
}
console.log(i);//1
对于这一点我们之前写 for 循环肯定最有感触,比如下面的例子:
let arr = [];
for (var i = 0; i < 10; i++) {
arr.push((() => i));
}
console.log(arr[3]()); //10
=================================
let arr = [];
for (let i = 0; i < 10; i++) {
arr.push((() => i));
}
console.log(arr[3]()); //3
下面这个图可以表示一下着两个区别:
for 循环的时候会开辟很多会块级作用于,当使用 var 的时候,i 是定义在了全局环境中,而使用 let 就会在新开辟的块级作用域声明一个 i 变量,这样在函数执行的时候在本块级作用域就找到了,就不用去全局找了。全局中如果没有其他地方定义 i,其实全局中 i 也是不存在的。
当然了如果我们既要保留现在的效果,又要保留全局中的 i ,我们使用我们的老方法
//自行构建闭包
var arr = [];
for (var i = 0; i < 10; i++) {
(function (a) {
arr.push(()=>a);
})(i);
}
console.log(arr[3]()); //3
console.log(i); //10
闭包
在 JavaScript 中,函数内部可以直接读取全局变量,在函数外部无法读取函数内的局部变量。但是出于种种原因,我们有时候需要得到函数内的局部变量,那就是在函数内部再定义一个函数,他可以读取函数内部的变量,然后我们把这个函数返回,就到了这个效果。上面 show
那个例子就可以看出,我们利用它成功读取了 foo
函数内部变量 n
。这个就是闭包。
那么到底什么是闭包,用阮老师的话是:闭包就是能够读取其他函数内部变量的函数。
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。
所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。他是函数作用域的一种延伸。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
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,可以在函数外部对函数内部的局部变量进行操作。
实战应用
假设我们现在有一个按钮,我们想要实现的效果是当我们点击按钮的时候,按钮会向右边移动。下面来实现一下这个效果:
Version 1
JavaScript闭包
我们会看到这个按钮是一直抖动的,因为我们每次点击的时候都会开辟一块作用域,如下图:
如果我们要改进它,需要我们将 left 这个值保留下来,也就是放到父级中去:
Version 2
我们将 left 放到父级作用域来,我们会看到这个按钮就不会再抖动了。当然了目前还是不完善的,因为当我们多点击按钮几次,这个按钮会越来越快,原因跟V1是相同的,left是成了父级了,但是setinterval还在,我们点击 n次,相当于interval形成了n个事件队列,执行的事件间隔就相当于 100 / n。所以我们要判定,如果事件绑定了我们就不要再次绑定了。
Version 3
使用闭包注意的点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
对于第一个举个例子解释一下:
对于下面点击事件我们要输出元素的desc信息,因为一个个点击事件一直驻留在内存当中的,所以其父级中的 item 就会一直存在,有几个存在几份。
JavaScript
Closure
如果有很多按钮,就有很多个 item,而我们只需要一个描述信息的文本而已,所以我们只要一个他的属性就可以了,不需要它一直在,免得太多了造成了内存泄漏,所以可以这样改进:
我们将其属性赋个一个变量,让他来代替 item 驻留内存总,在最后我们把item设置为 null,释放空间。一个对象的属性总是比这个完整的对象消耗的空间小,所以我们就到达了内存优化的目的。
阮老师的思考题
如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。
代码片段一:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()());
代码片段二:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
};
}
};
alert(object.getNameFunc()());
写在最后
这篇日记总共写了三天,这种基础性的知识说起来感觉比应用型的难好多。应用性的直接介绍一下属性,放几个实例,说一下就可以,放一下效果图就可以,这种语言知识说的时候还真的不好说起,当然了也是我水平不行,自己对这一块并没有理解透彻。所以如果大家看到了这篇日记,发现上面讲的不好不对,还请在下面留言。希望能和大家在前端学习的道路上一起进步。另外今天是女神节,祝愿所有女生都能成为女神,所有男生都能成为自己女神心中的“闭包”。