浅谈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 相关文章推荐
javascript 支持ie和firefox杰奇翻页函数
Jul 22 Javascript
js简单实现点击左右运动的方法
Apr 10 Javascript
js表单处理中单选、多选、选择框值的获取及表单的序列化
Mar 08 Javascript
Bootstrap组件学习之导航、标签、面包屑导航(精品)
May 17 Javascript
javascript 小数乘法结果错误的处理方法
Jul 28 Javascript
Angular路由简单学习
Dec 26 Javascript
基于JavaScript定位当前的地理位置
Apr 11 Javascript
微信小程序开发之选项卡(窗口底部TabBar)页面切换
Apr 12 Javascript
jquery仿京东商品放大浏览页面
Jun 06 jQuery
微信小程序实现弹出层效果
May 26 Javascript
vue实现滑动到底部加载更多效果
Oct 27 Javascript
openlayers4.6.5实现距离量测和面积量测
Sep 25 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中curl和file_get_content的区别
2014/05/10 PHP
PHP生成随机密码类分享
2014/06/25 PHP
php搜索文件程序分享
2015/10/30 PHP
PHPMailer使用QQ邮箱实现邮件发送功能
2017/08/18 PHP
PHP+Apache环境中如何隐藏Apache版本
2017/11/24 PHP
Laravel 手动开关 Eloquent 修改器的操作方法
2019/12/30 PHP
jquery 仿QQ校友的DIV模拟窗口效果源码
2010/03/24 Javascript
原生Js与jquery的多组处理, 仅展开一个区块的折叠效果
2011/01/09 Javascript
什么是cookie?js手动创建和存储cookie
2014/05/27 Javascript
javascript初学者常用技巧
2014/09/02 Javascript
node.js中的fs.statSync方法使用说明
2014/12/16 Javascript
jQuery选择器源码解读(六):Sizzle选择器匹配逻辑分析
2015/03/31 Javascript
node.js回调函数之阻塞调用与非阻塞调用
2015/11/13 Javascript
url中的特殊符号有什么含义(推荐)
2016/06/17 Javascript
Vue.js中用webpack合并打包多个组件并实现按需加载
2017/02/17 Javascript
JavaScript中双符号的运算详解
2017/03/12 Javascript
利用Angular.js编写公共提示模块的方法教程
2017/05/28 Javascript
解决JQuery全选/反选第二次失效的问题
2017/10/11 jQuery
在vscode中统一vue编码风格的方法
2018/02/22 Javascript
JS编写兼容IE6,7,8浏览器无缝自动轮播
2018/10/12 Javascript
vue 集成 vis-network 实现网络拓扑图的方法
2019/08/07 Javascript
过滤器vue.filters的使用方法实现
2019/09/18 Javascript
JavaScript交换变量常用4种方法解析
2020/09/02 Javascript
python使用rsa加密算法模块模拟新浪微博登录
2014/01/22 Python
Django小白教程之Django用户注册与登录
2016/04/22 Python
Django模板变量如何传递给外部js调用的方法小结
2017/07/24 Python
Python实现购物系统(示例讲解)
2017/09/13 Python
Python使用pymysql从MySQL数据库中读出数据的方法
2018/07/25 Python
解决Shell执行python文件,传参空格引起的问题
2018/10/30 Python
基于Python解密仿射密码
2019/10/21 Python
PYTHON发送邮件YAGMAIL的简单实现解析
2019/10/28 Python
8款使用 CSS3 实现超炫的 Loading(加载)的动画效果
2015/03/17 HTML / CSS
Canvas与图片压缩的示例代码
2017/11/28 HTML / CSS
优秀少先队员主要事迹材料
2014/05/28 职场文书
2015社区个人工作总结范文
2015/05/13 职场文书
JS封装cavans多种滤镜组件
2022/02/15 Javascript