浅谈JavaScript闭包

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

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

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

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

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

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

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

如上图:第三次执行 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闭包详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Javascript中return的使用与闭包详解

    前言 Javascript中闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.而return语句在js中起到举足轻重的作用,该关键字不仅具有返回函数值得功能,还具有一些特殊的用法,下面就来看看关于Javascript中return的使用与闭包的详细介绍吧. 一.return的使用 案例一: var a=1; for(var b=0; b<10; b++){ return b; }; sonsole.log(b)//返回为空 个人认为此处左右

  • 深入理解javascript函数参数与闭包

    最近在学习javascript的函数,函数是javascript的一等对象,想要学好javascript,就必须深刻理解函数.本人把学习的过程整理成文章,一是为了加深自己函数的理解,二是给读者提供学习的途径,避免走弯路.内容有些多,但都是笔者对于函数的总结. 1.函数参数 1.1:参数是什么 1.2:参数的省略 1.3:参数默认值 1.4:参数传递方式 1.5:同名参数 1.6:arguments对象 2.闭包 2.1:闭包定义 2.2:立即调用的函数表达式(IIFE, Immediately

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

  • JS继承与闭包及JS实现继承的三种方式

    前  言 在之前的两篇博客中,我们详细探讨了JavaScript OOP中的各种知识点(JS OOP基础与JS 中This指向详解 . 成员属性.静态属性.原型属性与JS原型链).今天我们来继续探讨剩余的内容吧. 我们都知道,面向对象的三大特征--封装.继承.多态. 封装无非就是属性和方法的私有化,所以我们JS中提供了私有属性和私有方法. 而JS中并没有多态,因此我们说JS是一门基于对象的语言,而非面向对象的语言. 那么,面向对象三大特征中,在JS中最重要的就是继承了. 一.继承的基本概念 使用

  • 通俗易懂地解释JS中的闭包

    1. "闭包就是跨作用域访问变量." [示例一] var name = 'wangxi' function user () { // var name = 'wangxi' function getName () { console.log(name) } getName() } user() // wangxi 在 getName 函数中获取 name,首先在 getName 函数的作用域中查找 name,未找到,进而在 user 函数的作用域中查找,同样未找到,继续向上回溯,发现在

  • js实现为a标签添加事件的方法(使用闭包循环)

    本文实例讲述了js实现为a标签添加事件的方法.分享给大家供大家参考,具体如下: 以示例说明: 实现效果:循环为带有ml-praise样式类的a标签添加事件,并且在点击a标签后,相应的数量增加1. Html结构如下所示: <ul> <li><a href="javascript:;" class="ml-praise">数量:<span class="ding-num">100</span>

  • 一分钟理解js闭包

    什么是闭包? 先看一段代码: function a(){ var n = 0; function inc() { n++; console.log(n); } inc(); inc(); } a(); //控制台输出1,再输出2 简单吧.再来看一段代码: function a(){ var n = 0; this.inc = function () { n++; console.log(n); }; } var c = new a(); c.inc(); //控制台输出1 c.inc(); //

  • js闭包用法实例详解

    本文实例讲述了js闭包用法.分享给大家供大家参考,具体如下: 引言 在公司中需要写一个js脚本来进行网站的统计,实现类似百度统计或者站长统计的功能,在实现的过程中自己感觉写的代码还是可以的,因为之前的js代码都是这些写,但是在组长代码走查的时候却非常的不满意,因为我们在js中写的方法都是全局的方法,因为我们写的东西需要嵌入到别人的界面中,所以这些全局的东西很可能会和别人的东西重名从而引发错误,所以说组长就给我留下一句话:用js闭包包起来. 变量作用域 我们都非常的熟悉变量的作用域就分为:全局变量

  • JavaScript中闭包的写法和作用详解

    1.什么是闭包 闭包是有权访问另一个函数作用域的变量的函数. 简单的说,Javascript允许使用内部函数---即函数定义和函数表达式位于另一个函数的函数体内.而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量.参数和声明的其他内部函数.当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包. 2.变量的作用域 要理解闭包,首先要理解变量的作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部可以直接读取全局变

  • 浅谈JavaScript for循环 闭包

    有个网友问了个问题,如下的html,为什么每次输出都是5,而不是点击每个p,就alert出对应的1,2,3,4,5. <html > <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>闭包演示</title> <script type="text/javascript&quo

随机推荐