闭包

前言

闭包对每个前端来说都是一个绕不开的话题。学习之初也因为搞清闭包的概念耗费了不少精力,今天写一篇博客来记录本人对闭包的理解,笔者水平有限,若有疏漏及错误,愿不吝赐教。

什么是闭包?

你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的的表达式(通常是函数,因为ES6有了块级作用域的概念)。
可以先点击上面链接查看MDN对闭包的讲述。

闭包在JavaScript高级程序设计(第3版)中是这样描述:闭包是指有权访问另一个函数作用域中的变量的函数。

这样闭包的作用就很明显了

  • 可以在函数的外部访问到函数内部的局部变量。
  • 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。

我们来写一个简单的例子:

console.log('closure');
function add() {var count = 0;return function() {count += 1;return count;};
}
var a = add();
function fun() {alert(a());
}
fun();
fun();

我们来解释一下这段代码:
定义了add() 函数,函数内定义了count,返回值也是一个函数。子匿名函数引用外部变量count,因此形成了闭包。
当下面我们定义并赋值a = add()时,add()函数执行且仅执行一次。执行后a被赋值(匿名函数)了
上面代码中function() { count += 1; return count; };部分就形成了一个闭包。引用了函数外部变量count。因此count在函数执行结束也不会销毁,一直存在于内存中。然后每次调用a()函数时,返回的都是内层匿名函数(闭包)的执行结果,因为内层匿名函数在外层函数的内部,所以即使外层函数执行完了,第二个函数依然能访问count(JS设计的作用域链,当前作用域能访问上级的作用域)。

闭包经典案例

该案例是牛客网题库的一道题:

   <ul><li>i</li><li>i</li><li>i</li><li>i</li></ul>

js实现:要求点击每个li标签,弹出对应的索引。(0,1,2,3)
实现1:

var elements = document.getElementsByTagName('li');
for (var i = 0; i < elements.length; i++) {elements[i].onclick = function () {console.log(i);alert(i);};
}

获取dom元素,进行遍历不就可以了吗?但是当我们自信的点击标签时候发现弹出都是4……。整个人都觉得不好了!

ps:有人疑惑不应该都是3吗?为什么是4? 这是因为for循环的执行顺序都是以i++结束的,最后一次不进入循环。但是var i 是在全局中的,因此i会被赋值为4.具体for循环知识可以自行搜索这里不做赘述。

下面是这个题目的解答:
每个li标签的onclick事件执行时,本身onclick绑定的function的作用域中没有变量i,i为undefined,则解析引擎会寻找父级作用域,发现父级作用域中有i,且for循环绑定事件结束后,i已经赋值为4,所以每个li标签的onclick事件执行时,alert的都是父作用域中的i,也就是4。这是作用域的问题。这里onclick事件绑定的函数形成闭包,但引用的变量i是全局的。
闭包真正的含义是,如果一个函数访问了此函数的父级及父级以上的作用域变量,就可以称这个函数是一个闭包。

这里其实是一个作用域问题,闭包中引用的变量不会消失,所以我们把每次的i都保存到变量中,就可以实现想要的结果。

for (let i = 0; i < elements.length; i++) {elements[i].onclick = function () {return (function (n) {console.log(n);alert(n);})(i);};
}


这样我们就使用闭包将i作为参数传递到匿名函数(闭包)中,每个点击事件都有了一个局部的num变量,保存的即相应的i值。

拓展:
我们可以直接将for循环的var改为let就可以实现想要的效果。let的到来,让在循环体内创建闭包具有了更好的可读性。可搜索学习let和const的相关知识。或亲自动手,将let通过babel编译查看相关结果进行研读。
有关let的知识点会在以后的博文中进行整理。

闭包造成的内存泄漏问题

闭包怎么引起内存泄漏?谁会存在内存泄漏?
我们借助垃圾回收机制的标记清除法来看一下:
示例:

function fn () {let count = 1function fun () {count ++console.log(`函数内部调用${count}次`)}return fun
}
const result = fn();
result() //2
result() //3

我们来分析一下:

  1. result是一个全局变量,代码执行完毕不会立即销毁
  2. result可以找到fn()函数,fn()函数return fun(),因此 fun()也不会销毁。
  3. fun()引用到了父级作用域的count,因此,在引用的count便不会被垃圾回收机制回收。
    此时,闭包就引起了内存泄漏问题
    正常情况下,一般的局部变量函数执行完就会销毁,但是我们这里的result没有销毁,所以它引用到的count也会留存在内存当中。

闭包的应用

闭包的实际应用主要是函数工厂,以及模拟私有方法。

闭包实现函数工厂

举例:

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

这是MDN官网的一个makeAdder(x)函数,接受一个参数x,并返回新的函数,返回的函数接受一个参数y,并返回x+y的值。

从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。

应用案例:假如我们想在页面添加几个按钮用来调整页面字号。我可以给body元素设置font-size,然后通过em单位设置页面中其他元素的字号:

body {font-family: Helvetica, Arial, sans-serif;font-size: 12px;
}
h1 {font-size: 1.5em;
}
h2 {font-size: 1.2em;
}

然后可以对body的font-size进行调整:

function makeSizer(size) {return function() {document.body.style.fontSize = size + 'px';};
}var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

这样,size12,size14,size16就可以分别将body字体调整为12,14,16px,再将它们添加到点击事件上。

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

这样就实现了想要的效果。这就是一个简单的闭包实现函数工厂的案例。

闭包模拟私有方法

var Counter = (function () {var privteCounter = 0;function changeBy (val) {privteCounter += val;}return {increment: function () {changeBy(1)},decrement: function () {changeBy(-1)},value: function () {return privteCounter}}
})()console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:Counter.incrementCounter.decrementCounter.value

该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为 privateCounter 的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。

这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问 privateCounter 变量和 changeBy 函数。

备注: 你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。

var makeCounter = function () {var privteCounter = 0;function changeBy(val) {privteCounter += val;}return {increment: function () {changeBy(1);},decrement: function () {changeBy(-1);},value: function () {return privteCounter;},};
};var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。

备注: 以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。

闭包应用部分是引用MDN的案例,大家可以移步阅读(一定要读)。

今天的总结分享就到这里,继续努力搬砖!只作个人总结所用,有不足与疏漏请评论区补充。

闭包是什么?五分钟带你了解闭包相关推荐

  1. c++ map用法_Python专题——五分钟带你了解map、reduce和filter

    点击上方蓝字,和我一起学技术.今天是Python专题第6篇文章,给大家介绍的是Python当中三个非常神奇的方法:map.reduce和filter.不知道大家看到map和reduce的时候有没有什么 ...

  2. 五分钟学会python函数_五分钟带你搞懂python 迭代器与生成器

    前言 大家周末好,今天给大家带来的是Python当中生成器和迭代器的使用. 我当初第一次学到迭代器和生成器的时候,并没有太在意,只是觉得这是一种新的获取数据的方法.对于获取数据的方法而言,我们会一种就 ...

  3. 干货:五分钟带你看懂NestedScrolling嵌套滑动机制

    Android NestedScrolling嵌套滑动机制 Android在发布5.0之后加入了嵌套滑动机制NestedScrolling,为嵌套滑动提供了更方便的处理方案.在此对嵌套滑动机制进行详细 ...

  4. python内置函数与闭包_Python进阶(五)----内置函数Ⅱ 和 闭包

    Python进阶(五)----内置函数Ⅱ 和 闭包 一丶内置函数Ⅱ ####内置函数#### 特别重要,反复练习 ###print() 打印输入 #sep 设定分隔符 # end 默认是换行可以打印到 ...

  5. 五分钟带你理解python闭包(闭环)

    闭环是一种现象 要满足闭环需要达到三个条件: 1,外函数中定义了一个内函数 2,内函数中运用了外函数的局部变量 3,外函数的返回值是内函数的引用 话不多说,我接下来用一段代码让大家明白 def tex ...

  6. 二道波哥-五分钟带你了解好省

    什么是好省? 1.好省是一款导购APP,依托于淘宝天猫平台,为广大用户提供海量优惠券,致力于帮助用户买到最具性价比的网购商品.与此同时,好省还是一个创业平台,在这里,任何人都可以0门槛.0投资创业,只 ...

  7. 五分钟带你玩转Zabbix6.0容器化HA集群

    图片 蔡斯 | Zabbix开源社区签约专家 Zabbix资深玩家,SRE高级运维,架构师.精通服务组件监控.模板制作及告警治理. 擅长领域:Zabbix API定制化开发,对接企业蓝鲸,JMS保垒机 ...

  8. 五分钟带你了解Django框架设计思想!

    Python编程语言的持续火爆,在最新Tiobe编程语言排行榜中位列第五,热度持续增加,尤其是在 AI 和 大数据时代,Python是 AI 和大数据时代的第一开发语言.这已经是一个不争的事实了,唯一 ...

  9. 五分钟带你了解前端开发,以及前端开发的发展机会

    前端开发 前端开发是创建Web页面或app等前端界面呈现给用户的过程,通过HTML,CSS及JavaScript以及衍生出来的各种技术.框架.解决方案,来实现互联网产品的用户界面交互  .它从网页制作 ...

最新文章

  1. Java 学习(20)--异常 /  IO 流
  2. 多元二次方程 python_Python 二次方程
  3. asp批量生成html静态页面方法
  4. Angular应用i18n - internationalization翻译的实现单步调试
  5. SAP CRM pricing read的实现逻辑
  6. winform Outlookbar
  7. java vector list,Java基础之:List——ArrayList Vector
  8. python中如何调用类_python中如何调用类的方法
  9. java旅客接口_GitHub - s1mplecc/checkin-scheduler: 使用 Java 实现的旅客人数分布预估与值机排班调度算法...
  10. 【技术】Java打印菱形
  11. 浅谈软件开发的四大要素
  12. 华为OJ-奥运会排行榜C++
  13. 如何把Eclipse改成中文版
  14. 富士通Fujitsu DPK750 Pro 打印机驱动
  15. 论文-公式对齐和右侧自动编号
  16. 中国工程咨询行业十四五投资机会与运营风险评估报告2022-2028年
  17. 微信小程序商城项目实战(第四篇:商品详情页)
  18. 卷死我了,终于毕业了!!
  19. 2017 php 免费空间,免费空间免费php空间
  20. 子网划分及NAT技术总结

热门文章

  1. VS2008 使用小技巧-------快捷键
  2. WarShall算法求传递闭包(可达矩阵)
  3. 解决Vivado implementation拥塞的策略方法(一)
  4. ConnectionString
  5. 【Android】【自动填充】自定义AutofillService(二):编写AutofillService代码
  6. 经典PID控制算法原理以及优化思路
  7. mysqlbinlog如何恢复误删的数据
  8. AtCoder题解 —— AtCoder Beginner Contest 182 —— D - Wandering
  9. Hash(散列)冲突解决 线性探测再散列和二次探测再散列
  10. 可以安心过中秋了!黎曼猜想亡不了区块链!