认识闭包之前需要先了解作用域,如果你对作用域还没有足够了解,请移步JavaScript一看就懂(1)作用域

什么是闭包?

我们可以先简单认为:一个函数a定义在另一个函数b里面,这个函数a就是闭包

function b() {
...
function a() { //闭包
...
}
...
}

另外,函数a能够直接读取函数b的变量x

function b() {
var x = 1;
function a() {
alert(x);
}
a();
}
b(); //1

这其实不是什么新鲜事,这是由作用域决定的。

上面的代码只是为了简单地认识闭包的组成部分,但却不是真正意义上的闭包,下面才是正宗长沙臭豆腐,哦不,正宗闭包

function b() {
var x = 1;
function a() {
alert(x++);
}
return a;
}
var func = b();
func(); //1
func(); //2
func(); //3

b()这次没有直接调用a(),而是返回了函数a给变量func,而func()才是真正调用a()的地方,等于是跳出了原来的圈子在外围被调用了,而函数a竟然能神奇地记住变量x,这就是闭包的魔力。

所以什么是闭包?结合上面的代码来解释的话:

闭包就是无论函数a在哪里执行,它都能联系上变量x

纯字面上来理解的话:

一个函数没有在它定义时所在的作用域里执行,但它仍能访问那个作用域里的变量

说人话那就是:

函数记得它周围发生了什么

也就是书上说的:

函数以及它所连接的周围作用域中的变量即为闭包

闭包:使得函数可以维持其创建时所在的作用域。如果一个函数离开了它被创建时的作用域,它还是会与这个作用域以及其外部的作用域的变量相关联

闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时

闭包就是当一个函数即使是在它的词法作用域之外被调用时,也可以记住并访问它的词法作用域

看看哪个说法好理解就拿来对着代码多揣摩几遍,不信你不懂

闭包有什么用?

1.回调

我们其实经常用到闭包,可能自己都不知道,比如下面这个点击按钮获取验证码的例子:

function countDown(){
...
document.getElementById('getCode').onclick = function(){ //闭包
...
};
}

页面加载完成时,获取验证码的按钮被绑定了一个点击回调事件(没有执行),当用户点击时,才真正被执行

完整代码:

<!DOCTYPE html>
<html lang="zh-cn">
<head></head>
<body>
<button type="button" id="getCode">获取验证码</button> <script>
countDown(9); function countDown(time_wait){
time_wait = time_wait || 60; //等待时间
var time_left = time_wait; //剩余等待时间
document.getElementById('getCode').onclick = function(){ //闭包
var btn = this;
btn.disabled = true;
btn.innerHTML=time_left--;
var count = setInterval(function(){ //闭包
if(time_left<1){
btn.disabled = false;
btn.innerHTML="获取验证码";
time_left = time_wait;
clearInterval(count);
}else{
btn.innerHTML=time_left--;
}
},1000);
};
}
</script>
</body>
</html>

当用户点击按钮时,执行了这个回调函数并且它能够读取到外围的time_wait和time_left变量。

细心的朋友可能已经发现这里出现两个闭包了,一个是onclick,还有一个是setInterval定时函数。

2.面向对象

当然还有一个更重要的原因需要它,那就是OO,把上面发送验证码的代码改造一下变成面向对象风格

var c1 = CountDown(document.getElementById('getCode'), 5);
c1.run(); function CountDown(btn, time_wait) {
time_wait = time_wait || 60;
var time_left = time_wait; //剩余等待时间 function resetBtn(){
btn.disabled = false;
btn.innerHTML="获取验证码";
} function run(){
btn.onclick = function(){
btn.innerHTML=time_left--;
btn.disabled = true;
var count = setInterval(function(){ if(time_left<1){
resetBtn();
time_left = time_wait;
clearInterval(count);
}else{
btn.innerHTML=time_left--;
}
},1000);
};
} //相当于公共方法
return {
run: run
};
}

只有在return返回的对象里面的run方法才能对外服务,而resetBtn()相当于是私有方法

所以回过头来看问题,为什么要用闭包?

  • 回调(setTimeout、setInterval、onclick...)
  • 面向对象

闭包会带来什么问题?

1.循环里的闭包

<!DOCTYPE html>
<html lang="zh-cn">
<head></head>
<body>
<input type="button" id="btn1" value="1" />
<input type="button" id="btn2" value="2" />
<input type="button" id="btn3" value="3" /> <script>
window.addEventListener("load", function() {
for (var i = 1; i < 4; i++) {
var btn = document.getElementById("btn" + i); btn.onclick = function() {
alert(i);
};
}
});
</script>
</body>
</html>

乍一看感觉就是对应地弹出1,2,3,但现实是无论你点击哪个按钮,都会弹出4,因为onclick绑定的函数是个闭包,页面加载后for循环开始执行,当你点击按钮的时候,for循环已经执行完了,此时i值为4,而闭包拿到的i值当前的i值,而不是循环中的i值,所以弹出的都是4。

要解决这个问题,可以把打印的代码提炼到另一个函数里面,这样就会产生一个新的作用域,从而避开闭包带来的共享:

function show(i){
return function(){
alert(i);
}
} window.addEventListener("load", function() {
for (var i = 1; i < 4; i++) {
var btn = document.getElementById("btn" + i); btn.onclick = show(i);
}
});

或者用立即执行函数表达式(IIFE):

window.addEventListener("load", function() {
for (var i = 1; i < 4; i++) {
var btn = document.getElementById("btn" + i); btn.onclick = (function(i) {
return function(){
alert(i);
}
}(i));
}
});

当然还有一种更牛逼的方式:

window.addEventListener("load", function() {
for (let i = 1; i < 4; i++) {
var btn = document.getElementById("btn" + i); btn.onclick = function() {
alert(i);
};
}
});

2.构造器

方法应该定义到对象的原型,而不要定义到对象的构造器中。原因是这将导致每次对象实例化时,方法都会被重新定义一次。

function User(name) {
this.name = name; this.getName = function() {
return this.name;
};
}

改成如下,继承的原型可以为所有实例共享:

function User(name) {
this.name = name;
} User.prototype.getName = function() {
return this.name;
};

参考资料

  1. 阮一峰>学习Javascript闭包
  2. MDN>Web技术文档>JavaScript>闭包
  3. JavaScript Closures Demystified
  4. JavaScript for PHP Developers
  5. 深入理解JavaScript
  6. secrets of javascript closures
  7. You Don't Know JS

JavaScript一看就懂(2)闭包的更多相关文章

  1. JavaScript一看就懂(1)作用域

    函数级作用域 1.函数外声明的变量为全局变量,函数内可以直接访问全局变量: var global_var = 10; //全局变量 function a(){ alert(global_var); / ...

  2. JavaScript一看就懂(3)数组

    定义数组 var a = [1, 2, 3]; typeof a; //"object", 数组是对象 a.length; //数组长度 相关操作 a[0]; //下标访问 a.p ...

  3. 一篇文章看懂JS闭包,都要2020年了,你怎么能还不懂闭包?

     壹 ❀ 引 我觉得每一位JavaScript工作者都无法避免与闭包打交道,就算在实际开发中不使用但面试中被问及也是常态了.就我而言对于闭包的理解仅止步于一些概念,看到相关代码我知道这是个闭包,但闭包 ...

  4. 每个JavaScript工程师都应懂的33个概念

    摘要: 基础很重要啊! 原文:33 concepts every JavaScript developer should know 译文:每个 JavaScript 工程师都应懂的33个概念 作者:s ...

  5. 每个 JavaScript 工程师都应懂的33个概念

    简介 这个项目是为了帮助开发者掌握 JavaScript 概念而创立的.它不是必备,但在未来学习(JavaScript)中,可以作为一篇指南. 本篇文章是参照 @leonardomso 创立,英文版项 ...

  6. 一看就懂的ReactJs入门教程(精华版)

    一看就懂的ReactJs入门教程(精华版) 现在最热门的前端框架有AngularJS.React.Bootstrap等.自从接触了ReactJS,ReactJs的虚拟DOM(Virtual DOM)和 ...

  7. 读《你不知道的JavaScript(上卷)》后感-作用域闭包(二)

    github原文 一. 序言 最近我在读一本书:<你不知道的JavaScript>,这书分为上中卷,内容非常丰富,认真细读,能学到非常多JavaScript的知识点,希望广大的前端同胞们, ...

  8. 【译】学习JavaScript中提升、作用域、闭包的终极指南

    这似乎令人惊讶,但在我看来,理解JavaScript语言最重要和最基本的概念是理解执行上下文.通过正确学习它,你将很好地学习更多高级主题,如提升,作用域链和闭包.考虑到这一点,究竟什么是"执 ...

  9. Jquery和Javascript 实际项目中写法基础-闭包 (2)

    一.什么是闭包? 概念性的我就不去百度了,感兴趣的可以自己去搜下,我自己的理解,闭包就是一个封装的包,相当于类的概念,把乱七八糟的的东西封装到一起,然后统一使用一个对象来调用,实现代码部分对外开放,部 ...

随机推荐

  1. 【bzoj 4176】 Lucas的数论 莫比乌斯反演(杜教筛)

    Description 去年的Lucas非常喜欢数论题,但是一年以后的Lucas却不那么喜欢了. 在整理以前的试题时,发现了这样一道题目“求Sigma(f(i)),其中1<=i<=N”,其 ...

  2. 程序设计语言——实践之路 笔记:Beginning

    这本书已经看了不下3遍了,计划在6月写完1,3,6,7,8,9章的笔记. 为什么要写笔记呢,我觉得有这么几个必要: 1.一个概念的首次提出与补充会跨越几个章节,整理在一起有助记忆 2.所有书籍的安排都 ...

  3. Bagging之随机森林

    随机森林(Random Forest)是一种Bagging(Bootstrap Aggregating)集成算法,在样本随机(样本扰动)的基础上,进一步运用特征随机(属性扰动)的机制,得到比一般的Ba ...

  4. 直观理解神经网络最后一层全连接+Softmax

    目录 写在前面 全连接层与Softmax回顾 加权角度 模板匹配 几何角度 Softmax的作用 总结 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 这篇文章将从3 ...

  5. EIGRP 高级实验

    一.环境准备 1. 软件:GNS3 2. 路由:c7200 二.实验操作 实验要求: 1.掌握EIGRP  的不等价均衡的条件. 2.掌握EIGRP  的metric  值修改方法. 3.掌握 EIG ...

  6. Asp.NetCore轻松学-部署到 Linux 进行托管

    前言 上一篇文章介绍了如何将开发好的 Asp.Net Core 应用程序部署到 IIS,且学习了进程内托管和进程外托管的区别:接下来就要说说应用 Asp.Net Core 的特性(跨平台),将 .Ne ...

  7. Docker最全教程之Go实战,墙裂推荐(十八)

    前言 与其他语言相比,Go非常值得推荐和学习,真香!为什么?主要是可以直接编译成机器代码(性能优越,体积非常小,可达10来M,见实践教程图片)而且设计良好,上手门槛低.本篇主要侧重于讲解了Go语言的优 ...

  8. Django-restframework 之认证源码分析

    Django-restframework 源码分析之认证 前言 最近学习了 django 的一个 restframework 框架,对于里面的执行流程产生了兴趣,经过昨天一晚上初步搞清楚了执行流程(部 ...

  9. C# 接口的使用(工厂模式)

    接口(interface)与抽象类(abstract)的区别: 相同点: 1.都不能被直接实例化,都可以通过继承实现其抽象方法. 2.都是面向抽象编程的技术基础,实现诸多模式 不同点: 1.接口可以多 ...

  10. 关于.net导出数据到excel/word【占位符替换】

    1]excel的占位符替换 效果如图 关键代码: ///savedFilePath需要保存的路径 templateDocPath模板路径 替换的关键字和值 格式 [姓名]$%$小王 public st ...