JavaScript闭包原理与用法学习笔记


Posted in Javascript onMay 29, 2020

本文实例讲述了JavaScript闭包原理与用法。分享给大家供大家参考,具体如下:

闭包(Closure)

闭包是一个函数和词法环境的组合,函数声明在这个词法环境中。

  • 词法作用域:

看下面的一个例子:

function init() {
    var name = 'GaoPian';
    // name是局部变量
    function displayName() {
      //displayName();是内部函数,一个闭包
      alert(name); // 使用外部函数声明的变量
    }
 
    displayName();
  }
  init();

init()创建了一个局部变量name和一个函数displayName()。

函数displayName()是一个已经定义在init()内部的函数,并且只能在函数init()里面才能访问得到。

函数displayName()没有自己的局部变量,但由于内部函数可以访问外部函数变量,displayName()可以访问到声明在外部函数init()的变量name,如果局部变量还存在的话,displayName()也可以访问他们。

  • 闭包

看下面一个例子

function makeFunc() {
    debugger
    var name = 'GaoPian';
 
    function displayName() {
      alert(name);
    }
 
    return displayName;
  }
  var myFunc = makeFunc();
  myFunc();

运行这段代码和之前init()的方法的效果是一样。

经过debugger一遍之后发现:

二者不同之处是,displayName()在执行之前,这个内部方法是从外部方法返回来的。   

首先,代码还是会正确运行,在一些编程语言当中,一个函数内的局部变量只存在于该函数的执行期间,随后会被销毁,一旦makeFunc()函数执行完毕的话,变量名就不能够被获取,但是,由于代码仍然正常执行,这显然在JS里是不会这样的。这是因为函数在JS里是以闭包的形式出现的。

闭包是一个函数和词法作环境的组合,词法环境是函数被声明的那个作用域,这个执行环境包括了创建闭包时同一创建的任意变量,即创建的这个函数和这些变量处于同一个作用域当中。在这个例子当中,myFunc()是displayName()的函数实例,makeFunc创建的时候,displayName随之也创建了。displayName的实例可以获得词法作用域的引用,在这个词法作用域当中,存在变量name,对于这一点,当myFunc调用的话,变量name,仍然可以被调用,因此,变量'GaoPian'传递给了alert函数。

这里还有一个例子

function makeAdder(x) {
    return function (y) {
      return x + y;
    }
  }
  var add5 = makeAdder(5);
  var add10 = makeAdder(10);
  console.log(add5(2)); // 7
  console.log(add10(2)); // 12

在这个例子当中,我们定义了一个函数makeAdder(x),传递一个参数x,并且返回一个函数,这个返回函数接收一个参数y,并返回x和y的和。   

实际上,makeAdder是一个工厂模式:它创建了一个函数,这个函数可以计算特定值的和。在上面这个例子当中,我们使用工厂模式来创建新的函数, 一个与5进行加法运算——add5,一个与10进行加法运算——add10。add5和add10都是闭包,他们共享相同的函数定义,但却存储着不同的词法环境,在add5的词法环境当中,x为5;在add10的词法环境当中,x变成了10。

  • 闭包的实践

闭包是很有用的,因为他让我们把一些数据(词法环境)和一些能够获取这些数据的函数联系起来,这有点和面向对象编程类似,在面向对象编程当中,对象让我们可以把一些数据(对象的属性)和一个或多个方法联系起来。

因此,你能够像对象的方法一样随时使用闭包。实际上,大多数的前端JS代码都是事件驱动性的:我们定义一些事件,当这个事件被用户所触发的时候(例如用户的点击事件和键盘事件),我们的事件通常会带上一个回调:即事件触发所执行的函数。举个栗子,假设我们希望在页面上添加一些按钮,这些按钮能够调整文字的大小,实现这个功能的方式是确定body的字体大小,然后再设置页面上其他元素(例如标题)的字体大小,我们使用em作为单位。

<style>
    body {
      font-family: Helvetica, Arial, sans-serif;
      font-size: 12px;
    }
 
    h1 {
      font-size: 1.5em;
    }
 
    h2 {
      font-size: 1.2em;
    }
  </style>

我们设置的调节字体大小的按钮能够改变body的font-size,并且这个调节能够通过相对字体单位,反应到其他元素上,

function makeSizer(size) {
    return function () {
      document.body.style.fontSize = size + 'px';
    };
  }
  var size12 = makeSizer(12);
  var size14 = makeSizer(14);
  var size16 = makeSizer(16);

size12,size14,size16是三个分别把字体大小调整为12,14,16的函数,我们可以把他们绑定在按钮上。

<button id="size-12">12</button>
<button id="size-14">14</button>
<button id="size-16">16</button>
document.getElementById('size-12').onclick = size12; 
document.getElementById('size-14').onclick = size14; 
document.getElementById('size-16').onclick = size16;

通过闭包来封装私有方法:类似JAVA语言能够声明私有方法,意味着只能够在相同的类里面被调用,JS无法做到这一点,但却可以通过闭包来封装私有方法。私有方法不限制代码:他们提供了管理命名空间的一种强有力方式。

下面代码阐述了怎样使用闭包来定义公有函数,公有函数能够访问私有方法和属性。

var counter = (function () {
    debugger;
    var privateCounter = 0;
 
    function changeBy(val) {
      privateCounter += val;
    }
 
    return {
      increment: function () {
        changeBy(1);
      },
      decrement: function () {
        changeBy(-1);
      },
      value: function () {
        return privateCounter;
      }
    };
  })();
  console.log(counter.value());// 0
  counter.increment();
  counter.increment();
  console.log(counter.value());// 2
  counter.decrement();
  console.log(counter.value()); // 1

在之前的例子当中,每个闭包具有他们自己的词法环境,而在这个例子中,我们创建了一个单独的词法环境,这个词法环境被3个函数所共享,这三个函数是counter.increment, counter.decrement和counter.value。

共享的词法环境是由匿名函数创建的,一定义就可以被执行,词法环境包含两项:变量privateCounter和函数changeBy,这些私有方法和属性不能够被外面访问到,然而,他们能够被返回的公共函数访问到。这三个公有函数就是闭包,共享相同的环境,JS的词法作用域的好处就是他们可以互相访问变量privateCounter和changeBy函数。

下面一个例子:

var makeCounter = function () {
    var privateCounter = 0;
 
    function changeBy(val) {
      privateCounter += val;
    }
 
    return {
      increment: function () {
        changeBy(1);
      }, decrement: function () {
        changeBy(-1);
      }, value: function () {
        return privateCounter;
      }
    }
  };
  var counter1 = makeCounter();
  var counter2 = makeCounter();
  alert(counter1.value());
  /* Alerts 0 */
  counter1.increment();
  counter1.increment();
  alert(counter1.value());
  /* Alerts 2 */
  counter1.decrement();
  alert(counter1.value());
  /* Alerts 1 */
  alert(counter2.value());
  /* Alerts 0 */

两个计数器counter1和counter2分别是互相独立的,每个闭包具有不同版本的privateCounter,每次计数器被调用,词法环境会改变变量的值,但是一个闭包里变量值的改变并不影响另一个闭包里的变量。

  • 循环中创建闭包:常见错误

看下面一个例子:

<p id="help">Helpful notes will appear here</p>
<p>E-mail:
  <input type="text" id="email" name="email">
</p>
<p>Name:
  <input type="text" id="name" name="name">
</p>
<p>Age:
  <input type="text" id="age" name="age">
</p>
function showHelp(help) {
    document.getElementById('help').innerHTML = help;
  }
  function setupHelp() {
    var helpText = [{'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}];
    for (var i = 0; i < helpText.length; i++) {
      var item = helpText[i];
      document.getElementById(item.id).onfocus = function () {
        showHelp(item.help);
      }
    }
  }
  setupHelp();

helpText 数组定义了三个有用的hint,每个分别与输入框的id相对应,每个方法与onfocus事件绑定起来。当你运行这段代码的时候,不会像预期的那样工作,不管你聚焦在哪个输入框,始终显示你的age信息。

原因在于,分配给onfocus事件的函数是闭包,他们由函数定义构成,从setupHelp函数的函数作用域获取。三个闭包由循环所创建,每个闭包具有同一个词法环境,环境中包含一个变量item.help,当onfocus的回调执行时,item.help的值也随之确定,循环已经执行完毕,item对象已经指向了helpText列表的最后一项。

解决这个问题的方法是使用更多的闭包,具体点就是提前使用一个封装好的函数:

function showHelp(help) {
    document.getElementById('help').innerHTML = help;
  }
  function makeHelpCallback(help) {
    return function () {
      showHelp(help);
    };
  }
  function setupHelp() {
    var helpText = [{'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}];
    for (var i = 0; i < helpText.length; i++) {
      var item = helpText[i];
      document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
    }
  }
  setupHelp();

上面代码运行正常,回调此时不共享一个词法环境,makeHelpCallback函数给每个回调创造了一个词法环境,词法环境中的help指helpText数组中对应的字符串,使用匿名闭包来重写的例子如下:

function showHelp(help) {
    document.getElementById('help').innerHTML = help;
  }
  function setupHelp() {
    var helpText = [{'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}];
    for (var i = 0; i < helpText.length; i++) {
      (function () {
        var item = helpText[i];
        document.getElementById(item.id).onfocus = function () {
          showHelp(item.help);
        }
      })();
      // Immediate event listener attachment with the current value of item (preserved until iteration).
    }
  }
  setupHelp();

如果不想使用闭包,也可以使用ES6的let关键字:

function showHelp(help) {
    document.getElementById('help').innerHTML = help;
  }
  function setupHelp() {
    var helpText = [{'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}];
    for (var i = 0; i < helpText.length; i++) {
      let item = helpText[i];
      document.getElementById(item.id).onfocus = function () {
        showHelp(item.help);
      }
    }
  }
  setupHelp();

这个例子使用let代替var,所以,每个闭包绑定了块级作用域,也就意味着不需要额外的闭包。

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行效果。

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
javascript中的作用域和上下文使用简要概述
Dec 05 Javascript
鼠标移到图片上变大显示而不是放大镜效果
Jun 15 Javascript
jQuery中Find选择器用法示例
Sep 21 Javascript
js学使用setTimeout实现轮循动画
Jul 17 Javascript
JQuery 获取多个select标签option的text内容(实例)
Sep 07 jQuery
JS实现关键词高亮显示正则匹配
Jun 22 Javascript
Vue CLI3搭建的项目中路径相关问题的解决
Sep 17 Javascript
webpack自动打包和热更新的实现方法
Jun 24 Javascript
Node.js API详解之 timer模块用法实例分析
May 07 Javascript
vue scroll滚动判断的实现(是否滚动到底部、滚动方向、滚动节流、获取滚动区域dom元素)
Jun 11 Javascript
vue element 关闭当前tab 跳转到上一路由操作
Jul 22 Javascript
vuex管理状态仓库使用详解
Jul 29 Javascript
Jquery+AJAX实现无刷新上传并重命名文件操作示例【PHP后台接收】
May 29 #jQuery
JS组件库AlloyTouch实现图片轮播过程解析
May 29 #Javascript
基于vue实现探探滑动组件功能
May 29 #Javascript
JS实现前端路由功能示例【原生路由】
May 29 #Javascript
JavaScript如何实现图片处理与合成
May 29 #Javascript
jQuery+css实现的点击图片放大缩小预览功能示例【图片预览 查看大图】
May 29 #jQuery
JavaScript基于用户照片姓名生成海报
May 29 #Javascript
You might like
比较discuz和ecshop的截取字符串函数php版
2012/09/03 PHP
分享十款最出色的PHP安全开发库中文详细介绍
2015/03/22 PHP
php解析字符串里所有URL地址的方法
2015/04/03 PHP
PHP多线程模拟实现秒杀抢单
2018/02/07 PHP
基于Jquery的简单图片切换效果
2011/01/06 Javascript
分享一个自定义的console类 让你不再纠结JS中的调试代码的兼容
2012/04/20 Javascript
jQuery实现类似标签风格的导航菜单效果代码
2015/08/25 Javascript
jquery京东商城双11焦点图多图广告特效代码分享
2015/09/06 Javascript
javascript实现添加附件功能的方法
2015/11/18 Javascript
基于javascript制作微信聊天面板
2020/08/09 Javascript
bootstrap与Jquery UI 按钮样式冲突的解决办法
2016/09/23 Javascript
深入理解Vue 的条件渲染和列表渲染
2017/09/01 Javascript
解决npm管理员身份install时出现权限的问题
2018/03/16 Javascript
js合并两个数组生成合并后的key:value数组
2018/05/09 Javascript
详解基于Vue,Nginx的前后端不分离部署教程
2018/12/04 Javascript
jquery 遍历hash操作示例【基于ajax交互】
2019/10/12 jQuery
node.js使用http模块创建服务器和客户端完整示例
2020/02/10 Javascript
ES6中new Function()语法及应用实例分析
2020/02/19 Javascript
[01:55]2014DOTA2国际邀请赛快报:国土生病 紧急去医院治疗
2014/07/10 DOTA
详谈python read readline readlines的区别
2017/09/22 Python
Python实现希尔排序算法的原理与用法实例分析
2017/11/23 Python
Python3.4 tkinter,PIL图片转换
2018/06/21 Python
Python3 SSH远程连接服务器的方法示例
2018/12/29 Python
python 列表中[ ]中冒号‘:’的作用
2019/04/30 Python
python 模拟创建seafile 目录操作示例
2019/09/26 Python
python根据文本生成词云图代码实例
2019/11/15 Python
Ubuntu下Python+Flask分分钟搭建自己的服务器教程
2019/11/19 Python
南非最受欢迎的时尚品牌:MRP
2016/09/18 全球购物
公务员转正鉴定材料
2014/02/11 职场文书
《他得的红圈圈最多》教学反思
2014/04/24 职场文书
关于青春的演讲稿
2014/05/05 职场文书
2014年财务工作总结范文
2014/11/11 职场文书
行政助理岗位职责范本
2015/04/11 职场文书
大学感恩节活动总结
2015/05/05 职场文书
《7的乘法口诀》教学反思
2016/02/18 职场文书
小学语文教师竞聘演讲稿范文
2019/08/09 职场文书