浅谈JavaScript闭包


Posted in Javascript onApril 09, 2019

最近朋友面试被问到了 JS 闭包的问题,本人一时语塞,想起了袁华的一句话:“这道题太难了,我不会做,不会做啊!”。

JS 闭包属于面向对象的一个重要知识点,特此本人又开始了一段说走就走的旅程。

闭包就是外层函数的作用域(AO)对象被内层函数所引用,无法被释放。

上面那句话听起来可能不是很理解,本人在之前写过一篇Python 闭包小记》的关于 Python 闭包的一些知识的文章,里面写了百度百科对于闭包的理解,虽然由于才疏学浅大部分都是引用的他人的知识架构,但语言这种东西都是相通的,我们不需要去记那些晦涩的名词,对于闭包,作为初学者我们只需知道:

函数作为返回值,函数作为参数传递。就可以将其理解为闭包。

话不多说,先上个代码缓和一下尴尬的气氛:

function outer() {
  var max = 10;
  function inner(num) {
    if (num > max) {
      console.log(num)
    }
  }
  return inner;
}
var foo = outer();
foo(20); // 20

上面代码满足函数作为返回值的条件,所以是一个闭包函数。

根据 JS 函数的执行机制,先执行第 10 行的 foo 代码,在函数执行完之后会被 JS 的垃圾回收机制将 outer 函数回收,但是在执行到第 3 行的时候我们发现 outer 函数内部又出现了一个 inner 函数,且 inner 函数里引用着 outer 函数的 max = 10; 的变量,这就无法被回收并且留在了内存里,当执行到第 11 行时由于 outer 函数内的 max = 10; 被留在内存中,所以会被 inner 函数调用,并满足 if 条件判断,所以输出 20;

以上我们实现了一个简单的闭包函数,但是却产生了一个问题,那就是无法被释放的对象留在了内存当中,造成了不必要的内存开销。

再看如下代码:

var max = 10,
  foo = function (num) {
    if (num > max) {
      console.log(num);
    }
  };
(function (bar) {
  var max = 100;
  bar(20)
})(foo);  // 20

上面代码满足函数作为参数传递的条件,所以是一个闭包函数。

函数 foo 作为一个参数被传入函数中,赋值个 bar 参数,当执行到 bar() 函数时,函数内部的 max 并不是 100,而是 10,这似乎匪夷所思。我们暂且将 7 — 10 行的函数叫 “父作用域”,其余叫“全局作用域”,当执行到 bar(20) 时,函数去执行第 2 行的代码,此时 foo 函数内部的 max 要去取值,而 max = 10; 正好在他所在的 “全局作用域” 内,所以会取 max = 10; 的值而不是 max = 100; 的值。由此可见,取值时要去创造这个函数的作用域内取值,而不是所谓的 “父作用域” 或者离函数近的地方取值。

我们再来看一段代码:

var num = 20;
function outer() {
  var max = 10;
  function inner() {
    if (num > max) {
      console.log(num);
    }
  }
  return inner;
}
var foo = outer(),
  num = 30;
foo(); //30

上面的代码在看完上面的解释后可以得知它是一个闭包函数,且定义了一个全局变量 num,最初定义为 num = 20,当代码执行到第 11 行时去调用执行第 2 行,待第 11 行执行完毕后执行第 12 行,此时将全局的 num = 20; 变为了 num = 30; 再执行第 13 行,此时执行时调用 inner 函数时,从输出结果我们可以看出调用的 num 为之后赋值的 30,

由此可见全局的 num 变量被污染了。

我们再来看下一段代码:

function outer() {
  var max = 10;
  function inner(num) {
    if (num > max) {
      console.log(num);
    }
  }
  return inner;
}
var foo = outer(),
  max = 100;
foo(20);  //20

上面的代码中当函数执行时,先执行第 10 行,然后调用执行第 1 行的函数,此时将 max 赋值为 10,但需要注意的是此时的 max = 10;并不是在全局作用域内,而是在 outer() 函数的作用域内,执行完第 10 行再执行第 11 行,此时将 max 赋值为 100,但需要注意的是此时的 max = 100;是在全局作用域内。所以在执行到第 12 行代码的时候调用执行 inner() 函数并将参数 20 传入,输出结果为 20,由此可见outer() 函数作用域内的对象 num 并没有被全局的对象 num 所污染。

由以上四段代码我们初步了解了一些闭包的基本特征,但是由于才疏学浅,怕总结的不够全面,这时突然想到了东东大神的笔记,于是上网搜到了一些,下面就将其再归纳总结一下。

闭包:既重用一个变量,又保护变量不被污染的一种机制。

为什么使用闭包:

全局变量和局部变量都具有不可兼得的优缺点。

全局变量:

  1. 优: 可重用,
  2. 缺: 易被污染。

局部变量:

  1. 优: 仅函数内可用,不会被污染
  2. 缺: 不可重用!

何时使用:

只要既重用一个变量,又保护变量不被污染时。

如何使用:

  1. 1. 用外层函数包裹要保护的变量和内层函数。
  2. 2. 外层函数将内层函数返回到外部。
  3. 3. 调用外层函数,获得内层函数的对象,保存在外部的变量中——形成了闭包。

闭包形成的原因:

外层函数调用后,外层函数的函数作用域(AO)对象无法释放,被内层函数引用着。

闭包的缺点:

  1. 比普通函数占用更多的内存。
  2. 解决:闭包不在使用时,要及时释放。
  3. 将引用内层函数对象的变量赋值为null。

结合上面举的四段代码栗子和东东的笔记,我们已经对闭包有了一个形象的认识,但是要到达全面理解的程度,只能说革命尚未成功,同志仍需努力。

令人可喜的是在网上又查到了东东对于闭包更形象的图形讲解,看完之后相信大家对闭包会有更加深刻的理解。

先来一段代码缓和一下字多的尴尬:

//1. 用外层函数包裹要保护的变量和内层函数
function outer() {
  var i = 1;
  //2. 外层函数返回内层函数对象到外部
  return function () {
    console.log(i++);
    
  }
}
//3. 调用外层函数获得内层函数对象
var getNum = outer(); //getNum:function(){ console.log(i++); }
getNum();//1
getNum();//2
i = 1;
getNum();//3
getNum();//4

上面的代码是定义了一个 outer() 外层函数,外层函数的作用域内定义了 i = 1;的变量,内部返回了一个函数,这就形成了闭包。当代码执行到第 10 行,其实就返回了一个 outer() 函数的内部函数,执行一次 getNum(),由于打印的是 i++ ,所以输出结果为 1,(注:如果打印的是 ++i,输出结果为 2 )。再执行一次 getNum(),由于之前 i 已经执行过一次 i++,所以此次执行结果为 2,再在全局设置 i = 1,再次执行 getNum() 两次,执行结果分别为 3 和 4,说明全局设置的 i = 1,并没有覆盖 outer() 函数作用域内的 i 值,outer() 函数内的 i 值被很好的保护起来并得到了重用。

我们来看看东东对上面代码的图形化分析:

浅谈JavaScript闭包

如上图:在 JavaScript 中有一个执行环境栈(ECS)概念,注:ECS = 局部EC + 全局EC,所有的函数都要通过进栈、出栈来执行,执行环境栈中有一个自带的 main() 函数的全局EC 指向全局的 window 作用域,它会指向全局的 window 对象,代码运行到红线部分的时候,执行环境栈中仅有一个全局执行环境 window,此时 window 中有两个全局变量(标识符):outer 、getNum,其中 outer() 函数开辟了一块内存用于存储所执行的方法,并且通过 scope 记住它的父级。

浅谈JavaScript闭包

如上图:当执行 outer() 函数时,outer() 相当于局部EC 进入执行环境栈,此时 outer() 会开辟一块属于自己的作用域(AO),里面定义了 i = 1,的环境变量。 由于 window 中引用着 i 对象,所以 outer 的 AO 会指向 window,同时 getNum 会调用 outer() 函数并返回一个方法,所以会开辟一块内存用于存储所执行的方法,该方法中又有 i 变量指向 outer 的 AO,绿色线三方互相牵连。

浅谈JavaScript闭包

如上图:当执行环境栈中的 outer() 函数执行完出栈时,理论上 outer 的 AO,即蓝色框应该被垃圾回收机制所回收,但是由于闭包作用,这块就被留了下来,闭包至此形成。

浅谈JavaScript闭包

如上图:当 outer() 函数出栈,getNum() 函数进栈,getNum 开辟属于自己的作用域(AO),且执行了一次 i++ 。此时输出结果为 1。

浅谈JavaScript闭包

如上图:当 getNum() 函数出栈时,自己多开辟的作用域被回收,但是 outer 的作用域由于闭包作用依然留在内存中,且变为了 i = 2。

浅谈JavaScript闭包

如上图:再次执行 getNum() 函数,相当于 getNum() 函数再次入栈出栈,原来由于闭包作用保留的 i = 2 再次做 ++ 运算。

浅谈JavaScript闭包

如上图:再往下执行 i = 1,即在全局 window 当中添加了 i 对象。此时 outer 作用域内的 i 由于上一次的 ++ 变为了 3。

浅谈JavaScript闭包

如上图:第三次执行 getNum() 函数,此时大家应该懂得该怎么执行了吧,getNum() 并不会去全局的 window 中去取 i = 1 使用,而是去所创造它的作用域去值,即 i = 3 做 ++ 运算。

至此闭包的运行流程就全部介绍完了,大家是不是对于闭包有了一个比较清晰的了解了。

别急,还差那么一点点,那就是主动释放闭包所产生的内存。如下

//1. 用外层函数包裹要保护的变量和内层函数
function outer() {
  var i = 1;
  //2. 外层函数返回内层函数对象到外部
  return function () {
    console.log(i++);
    i = null;
  }
}
//3. 调用外层函数获得内层函数对象
var getNum = outer(); //getNum:function(){ console.log(i++); }
getNum(); //1
getNum(); //0
i = 1;
getNum(); //0
getNum(); //0

在执行完第一次 getNum() 函数时我们就将 i 变量设为 null,再次执行 getNum() 函数时发现所得结果已经变为 0 了,说明 outer() 函数内的 i 变量内存已经被释放了!!!

至此 JavaScript 闭包的全部内容就讲解完毕了,以上内容如有纰漏请各位大神批评指正。

好记性不如烂笔头,特此记录,与君共勉!

以上所述是小编给大家介绍的JavaScript闭包详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
让IE6支持min-width和max-width的方法
Jun 25 Javascript
简单的jquery拖拽排序效果实现代码
Sep 20 Javascript
Extjs改变树节点的勾选状态点击按钮将复选框去掉
Nov 14 Javascript
Jquery中CSS选择器用法分析
Feb 10 Javascript
JavaScript为事件句柄绑定监听函数实例详解
Dec 15 Javascript
JS基于面向对象实现的拖拽功能示例
Dec 20 Javascript
JS条形码(一维码)插件JsBarcode用法详解【编码类型、参数、属性】
Apr 19 Javascript
JavaScript实现简单的四则运算计算器完整实例
Apr 28 Javascript
如何写好一个vue组件,老夫的一年经验全在这了(推荐)
May 18 Javascript
Vue+Koa2 打包后进行线上部署的教程详解
Jul 31 Javascript
如何通过javaScript去除字符串两端的空白字符
Feb 06 Javascript
JavaScript中使用Spread运算符的八种方法总结
Jun 18 Javascript
使用Three.js实现太阳系八大行星的自转公转示例代码
Apr 09 #Javascript
webpack4实现不同的导出类型
Apr 09 #Javascript
Vue中使用create-keyframe-animation与动画钩子完成复杂动画
Apr 09 #Javascript
基于three.js实现的3D粒子动效实例代码
Apr 09 #Javascript
Koa 中的错误处理解析
Apr 09 #Javascript
简单说说如何使用vue-router插件的方法
Apr 08 #Javascript
利用Bootstrap Multiselect实现下拉框多选功能
Apr 08 #Javascript
You might like
php_xmlhttp 乱码问题解决方法
2009/08/07 PHP
PHP HTML JavaScript MySQL代码如何互相传值的方法分享
2012/09/30 PHP
PHP实现创建一个RPC服务操作示例
2020/02/23 PHP
JavaScript面向对象之静态与非静态类
2010/02/03 Javascript
解决Extjs上传图片无法预览的解决方法
2012/03/22 Javascript
js中复制行和删除行的操作实例
2013/06/25 Javascript
JavaScript简单实现网页回到顶部功能
2013/11/12 Javascript
引入autocomplete组件时JS报未结束字符串常量错误
2014/03/19 Javascript
Javascript验证用户输入URL地址是否为空及格式是否正确
2014/10/09 Javascript
JavaScript常用的返回,自动跳转,刷新,关闭语句汇总
2015/01/13 Javascript
jQuery实现contains方法不区分大小写的方法
2015/02/13 Javascript
js实现缓冲运动效果的方法
2015/04/10 Javascript
javascript实现淘宝幻灯片广告展示效果
2015/04/27 Javascript
javascript如何操作HTML下拉列表标签
2015/08/20 Javascript
整理Javascript数组学习笔记
2015/11/29 Javascript
jQuery点击按钮弹出遮罩层且内容居中特效
2015/12/14 Javascript
javascript jquery对form元素的常见操作详解
2016/06/12 Javascript
Js 获取、判断浏览器版本信息的简单方法
2016/08/08 Javascript
JavaScript 程序错误Cannot use 'in' operator to search的解决方法
2017/07/10 Javascript
webpack4+Vue搭建自己的Vue-cli项目过程分享
2018/08/29 Javascript
利用JavaScript缓存远程窃取Wi-Fi密码的思路详解
2018/11/05 Javascript
Vue源码解析之Template转化为AST的实现方法
2018/12/14 Javascript
vue各种事件监听实例(小结)
2020/06/24 Javascript
python中的列表推导浅析
2014/04/26 Python
python 定时修改数据库的示例代码
2018/04/08 Python
使用CSS3来绘制一个月食图案
2015/07/18 HTML / CSS
移动端Web页面的CSS3 flex布局快速上手指南
2016/05/31 HTML / CSS
传统HTML页面实现模块化加载的方法
2018/10/15 HTML / CSS
XD健身器材:Kevlar球、Crossfit健身球
2019/03/26 全球购物
2014医学院领导班子对照检查材料思想汇报
2014/09/19 职场文书
天坛导游词
2015/02/02 职场文书
工程技术负责人岗位职责
2015/04/13 职场文书
党员干部廉洁自律承诺书
2015/04/28 职场文书
横空出世观后感
2015/06/09 职场文书
护士年终工作总结不会写?各科护士模板总结
2020/01/02 职场文书
ROS系统将python包编译为可执行文件的简单步骤
2021/07/25 Python