闭包你真的了解吗?

开始之前

这是回北京后被疫情困在家的第四周了,在弹尽粮绝和刚刚看完《十二道锋味》芬兰之旅,还沉浸在奇幻的极光和诱人的西餐中开始写这篇网络日记。关于闭包在之前面试的时候就想写了,但是因为事儿多,忙了给耽搁下了。而且写一篇技术类的网络日记真的还是挺费时间的。写这种技术类的日记不像小时候写的日记,记录一些流水,这个真的还是要备课。这片日记还是以阮一峰老师讲闭包为主。但是阮老师写的关于闭包单刀直入,似乎少了一些前情知识,所以在内容上面做了一些调整,然后加入了几个实战的案例来分析JavaScript这个重要的概念——闭包。

环境与作用域

闭包之前先了解一些 JavaScript 的环境和作用域有助于对闭包的理解,可以说是这部分内容是闭包的先行知识。

任何的编程语言其实都有这个概念,环境就是函数运行的环境,作用域就是函数作用的范围。举个类似的例子,一座城市。城市里有学校,医院,商场等基础设施是市民的生活环境,城市的存在需要市民的依赖,只有市民依赖城市生活这座城市才会存在,当某一天市民不再依赖这座城市生活了,城市基本上也就破败了,也就不复存在了。城市里面的一个个基础设施单位学校,超市等也是一样,没人用了就倒闭了回收了。城市里面一些基础设施会有自己的辐射范围,尤其像是学校,都会划学区,就规定了这个学校就服务自己所在片区的孩子。现在我们做对比的话我们可以把城市想象成JavaScript中的全局环境。城市中一个个的基础设置就相当于函数,函数中也会有自己的小环境。每个环境都有自己的作用范围,全局环景就对应全局作用域,函数环境就对应函数作用域。
javaScript全局环境有一个特点就是全局环境永远不会被回收。举个例子


    .....
  
    
  

上面的代码不完整啊,就是简单的写一个html页面,里面只有上面的一个script定义的内容,其他什么都没有。我们会发现,当我们用浏览器打开,看控制台打印出title了,当我们再次在控制台打印title,title还会会打印出来,可见这个脚本执行完,这个环境并没有被回收。


上面是全局环境,我们再看一下全局环境中的局部环境,先看一下下面这段代码:


上面的代码中我们再全局环境中创建了一个 foo 函数,它会形成自己的一个局部环境,里面有有数据 n 和 函数 show,调用show时,他会在父级foo的环境中创建自己的环境。环境是只有在函数调用的才会创建,对应计算机语言就是创建一块内存区域。

image

我们运行这个文件会看到控制台打印出总是会打印出 1 来,即使我们多次调用 foo(), 每次的结果都还是一样的,这说明 foo 这个换环境每次都是新的,也就是我们每调用一次就会开辟一块新的内存空间。
image

关于作用域,全局作用域只有一个,每个函数又都有作用域(环境)。

  • 编译器运行时会将变量定义在所在作用域
  • 使用变量时会从当前作用域开始向上查找变量
  • 作用域就像攀亲戚一样,晚辈总是可以向上辈要些东西


    image

    作用域链只向上查找,找到全局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中 letconst 可以变量的声明放在块级作用域中(放在新环境,而不是全局中)。

{
    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闭包


    
    


image

我们会看到这个按钮是一直抖动的,因为我们每次点击的时候都会开辟一块作用域,如下图:

如果我们要改进它,需要我们将 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()());

写在最后

这篇日记总共写了三天,这种基础性的知识说起来感觉比应用型的难好多。应用性的直接介绍一下属性,放几个实例,说一下就可以,放一下效果图就可以,这种语言知识说的时候还真的不好说起,当然了也是我水平不行,自己对这一块并没有理解透彻。所以如果大家看到了这篇日记,发现上面讲的不好不对,还请在下面留言。希望能和大家在前端学习的道路上一起进步。另外今天是女神节,祝愿所有女生都能成为女神,所有男生都能成为自己女神心中的“闭包”。

你可能感兴趣的:(闭包你真的了解吗?)