浅谈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 相关文章推荐
js change,propertychange,input事件小议
Dec 20 Javascript
快速解决jQuery与其他库冲突的方法介绍
Jan 02 Javascript
JS实现左右拖动改变内容显示区域大小的方法
Oct 13 Javascript
详解JavaScript中this关键字的用法
May 26 Javascript
基于JavaScript实现轮播图代码
Jul 14 Javascript
详解AngularJS 路由 resolve用法
Apr 24 Javascript
BootStrap自定义popover,点击区域隐藏功能的实现
Jan 23 Javascript
Vue中android4.4不兼容问题的解决方法
Sep 04 Javascript
11个教程中不常被提及的JavaScript小技巧(推荐)
Apr 17 Javascript
D3.js的基础部分之数组的处理数组的排序和求值(v3版本)
May 09 Javascript
在layui中layer弹出层点击事件无效的解决方法
Sep 05 Javascript
JavaScript arguments.callee作用及替换方案详解
Sep 02 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初学者头疼问题总结
2006/10/09 PHP
详解WordPress中调用评论模板和循环输出评论的PHP函数
2016/01/05 PHP
使用jquery给input和textarea设定ie中的focus
2008/05/29 Javascript
javascript preload&lazy load
2010/05/13 Javascript
JS中判断JSON数据是否存在某字段的方法
2014/03/07 Javascript
onclick和onblur冲突问题的快速解决方法
2016/04/28 Javascript
jQuery 实现评论等级好评差评特效
2016/05/06 Javascript
微信小程序 wxapp内容组件 progress详细介绍
2016/10/31 Javascript
js实现兼容PC端和移动端滑块拖动选择数字效果
2017/02/16 Javascript
javascript中的replace函数(带注释demo)
2018/01/07 Javascript
Babel 入门教程学习笔记
2018/06/13 Javascript
Vuex 使用 v-model 配合 state的方法
2018/11/13 Javascript
ES6中new Function()语法及应用实例分析
2020/02/19 Javascript
[37:21]完美世界DOTA2联赛PWL S2 Inki vs Magma 第二场 11.22
2020/11/24 DOTA
python基于socket实现网络广播的方法
2015/04/29 Python
Python的gevent框架的入门教程
2015/04/29 Python
python读写二进制文件的方法
2015/05/09 Python
Python中exit、return、sys.exit()等使用实例和区别
2015/05/28 Python
Python实现统计代码行的方法分析
2017/07/12 Python
Python Requests库基本用法示例
2018/08/20 Python
Pycharm之快速定位到某行快捷键的方法
2019/01/20 Python
Python for循环与range函数的使用详解
2019/03/23 Python
PyQt5实现简易电子词典
2019/06/25 Python
django使用xadmin的全局配置详解
2019/11/15 Python
Java Spring项目国际化(i18n)详细方法与实例
2020/03/20 Python
python和JavaScript哪个容易上手
2020/06/23 Python
Pycharm制作搞怪弹窗的实现代码
2021/02/19 Python
CSS3 选择器 伪类选择器介绍
2012/01/21 HTML / CSS
智能电子秤、手表和健康监测仪:Withings(之前为诺基亚健康)
2018/10/30 全球购物
八年级物理教学反思
2014/01/19 职场文书
责任心演讲稿
2014/05/14 职场文书
地球一小时宣传标语
2014/06/24 职场文书
2014党员批评和自我批评思想汇报
2014/09/21 职场文书
Vue过滤器(filter)实现及应用场景详解
2021/06/15 Vue.js
Android开发实现极为简单的QQ登录页面
2022/04/24 Java/Android
深入理解pytorch库的dockerfile
2022/06/10 Python