JavaScript闭包原理与用法实例分析


Posted in Javascript onAugust 10, 2018

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

1、与闭包有关的两个概念:

1) 变量的作用域

不带有关键字var的变量会成为全局变量;

在函数中使用关键字var声明的变量是局部变量。

局部变量只有在函数内部才能访问到,在函数外面是访问不到的。但在函数内部可以通过作用域链一直向上搜索直到全局对象,也就是说,函数内部可以访问函数外部的变量。

2) 变量的生存周期

对于全局变量,其生存周期是永久的,除非主动销毁这个全局变量;

而对于在函数内用关键字var声明的局部变量,当退出函数时,这些局部变量会随着函数调用结束而被销毁。

var func = function() {
  var i = 1;
  alert(i); // 输出:1
};
alert(i); // 报错:i is not defind.

例外情况:闭包

var func = function() {
  var i = 1;
  return function() {
    alert(i);
    i++;
  }
};
var f1 = func();
f1(); // 输出:1
f1(); // 输出:2
var f2 = func();
f2(); // 输出:1
f2(); // 输出:2

2、从闭包的一个经典应用谈起

<div>0</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<script>
  var divs = document.getElementsByTagName("div");
  for (var i = 0; i < divs.length; i++) {
    divs[i].onclick = function() {
      alert(i);
    };
  }
</script>

问题:无论单击哪个div,都会弹出5。

原因:onclick事件是异步触发的,当事件被触发时,for循环早已结束,此时变量i的值早已经是5。

解决:在闭包的帮助下,把每次循环的i值都封闭起来。当事件函数顺着作用域链从内到外查找变量i时,会先找到被封闭在闭包环境的i,单击div时,会分别输出0,1,2,3,4。

<div>0</div>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<script>
var divs = document.getElementsByTagName("div");
for (var i = 0; i < divs.length; i++) {
  divs[i].onclick = (function(num) {
    return function() {
      alert(num);
    };
  })(i);
}
</script>

类似实例:闭包直接赋给数组

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++){
    result[i] = function(){
      return i;
    };
  }
  return result;
}
for (var i = 0; i < 10; i++)
  alert(createFunctions()[i]());

结果:result的每个元素都返回10。

说明:闭包的作用域链有明显的副作用——闭包总是获得外部函数变量的最终值。上面代码中,外部函数产生一个函数数组result并返回。函数数组中的每个元素都是一个函数,每个函数都返回 i变量。似乎每个函数应该返回每次循环的i值,即依次返回0到9,但事实是,每个函数的返回结果都是10。这是因为每个内部函数返回的是变量i,而不是i在某个时刻的特定值,而i的作用域是整个外部函数,当外部函数执行完成后,i的值是10。

解决:在每个内部函数的内部,再产生一个匿名函数并返回。

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = (function(num) {
      return function() {
        return num;
      };
    })(i);
  }
  return result;
}
for (var i = 0; i < 10; i++)
  alert(createFunctions()[i]());

结果:result依次返回0到9。

说明:(i)使得该层匿名函数立即执行。

3、闭包

有时候需要得到函数内的局部变量。如何从外部读取局部变量?那就是在函数的内部,再定义一个函数。

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域,将函数内部的变量和方法传递到外部。

① 闭包的原理

1) 后台执行环境中,闭包的作用域链包含着自己的作用域、函数的作用域和全局作用域。

2) 通常,函数的作用域和变量会在函数执行结束后销毁。

3) 但是,当函数返回一个闭包时,这个函数的作用域将会一直在内存中保存到闭包不存在为止。

② 闭包的特性

1) 函数内再嵌套函数。

2) 内部函数可以引用外层的参数和变量。

3) 参数和变量不会被垃圾回收机制回收。

③ 闭包的用途

1) 读取函数内部的变量。

function f1(){
  var n = 999;
  function f2(){
    alert(n);//999
  }
}

在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,就可以在f1外部读取它的内部变量了。

function f1(){
  var n = 999;
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1();
result();//999

闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

function f1(){
  var n = 999;
  nAdd = function(){n += 1}
  function f2(){
    alert(n);
  }
  return f2;
}
var result=f1();
result();//999
nAdd();
result();//1000

result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

2) 让函数内部的变量的值始终保持在内存中(延长局部变量的寿命)。

var print, add, set;
function closure() {
  var number = 8;
  print = function() {
    alert(number);
  }
  add = function() {
    number++;
  }
  set = function(x) {
    number = x;
  }
}
closure();//创建一个闭包
add();
print();//9
set(0);
print();//0
var oldClosure = print;
closure();//创建一个新的闭包
print();//8
oldClosure();//0

使用闭包的注意点:由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。也就是说,闭包会引用外部函数作用域,会占用更多的内存,过度使用闭包,会导致性能问题。所以,仅当必要时才使用闭包。对产生闭包的函数,使用后应该解除引用。

3)自执行函数+闭包减少全局变量污染(封装私有变量)

var person = (function() {
  var_name = "Alice";
  var _id = 16;
  return {
    getUserInfo: function() {
      return _name + ": " + _id;
    }
  }
})();

使用下划线来约定私有变量_name和_age,它们被封装在闭包产生的作用域中,外部是访问不到这两个变量的,这就避免了对全局的命令污染。

④ 闭包的缺点:

1) 需要维护额外的作用域。

2) 过渡使用闭包会占用大量内存。

4、this对象

在闭包内使用this对象将产生一些复杂的行为。this对象的值基于函数所在的执行环境在运行时决定:在全局函数中使用时,this等于window(非严格模式)或undefined(严格模式);而当作为对象的方法调用时,this等于这个对象。

var name = "The window";
var object = {
  name: "My object",
  getName: function() {
    return function() {
      return this.name;
    };
  }
};
alert(object.getName()());//输出:"The window"

每个函数一旦被调用,它将自动获得this和arguments两个变量。一个内部函数是不能直接从外部函数访问到这两个变量的。可以通过将this对象存储在另一个变量中来解决这个问题。把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

var name ="The window";
var object = {
  name: "My object",
  getName: function() {
    var that = this;
    return function() {
      return that.name;
    };
  }
};
alert(object.getName()());//输出:"My object"

要让闭包访问外部函数的this和arguments对象,可以通过将它们的引用存储在另一个变量中来完成。

5、内存泄漏

使用闭包的时候很容易造成循环引用,若闭包的作用域包含着一些DOM节点,这时候就有可能造成内存泄漏,但其实,这本身不是闭包的问题,而是由于:BOM和DOM中的对象是使用C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是引用计数策略,在基于引用计数策略的垃圾回收机制中,若两个对象之间形成了循环引用,则这两个对象都无法被回收。

function assignHandler() {
  var element = document.getElementById("id");
  element.onclick = function() {
    alert(element.id);
  }
}

匿名函数保存了一个对element的引用,只要匿名函数存在,element的引用数至少为1,它所占用的内存就永远不会被回收。

function assignHandler() {
  var element = document.getElementById("id");
  var id = element.id;
  element.onclick = function() {
    alert(id);
  }
  element = null;
}

通过把element.id的一个副本保存在变量中,并且在闭包中引用该变量消除了循环引用,但是仅仅做到这一步还不能解决内存泄漏的问题,闭包会引用包含函数的所有活动对象,包含element,即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用,因此有必要把element元素设置为null,这样就能解除对DOM对象的引用,确保正常回收其占用的内存。

6、模仿块级作用域

JavaScript中没有直接的块级作用域。

function output(count) {
  for (var i = 0; i < count; i++) {
    alert(i);
  }
  alert(i);//输出:10
}

使用闭包可以模仿块级作用域——创建并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。结果是内部函数的所有变量都会立即被销毁,除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

用作块级作用域的匿名函数:将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式,紧随其后的另一对圆括号会立即调用这个函数。

(function() {
  //这里是块级作用域;
}) ();

可以使用匿名函数表达式来模拟块级作用域,任何定义在匿名函数中的变量在匿名函数执行完之后都将被销毁,在匿名函数外访问这些变量将会产生错误。

function output(count) {
  (function() {
    for (var i = 0; i < count; i++) {
      alert(i);
    }
  }) ();
  alert(i);//出错
}

更多关于JavaScript相关内容可查看本站专题:《javascript面向对象入门教程》、《JavaScript常用函数技巧汇总》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

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

Javascript 相关文章推荐
学习ExtJS border布局
Oct 08 Javascript
JAVASCRIPT style 中visibility和display之间的区别
Jan 22 Javascript
提示$ is not defined错误分析及解决
Apr 09 Javascript
Jquery 复选框取值兼容FF和IE8(测试有效)
Oct 29 Javascript
js中继承的几种用法总结(apply,call,prototype)
Dec 26 Javascript
Javascript学习笔记之 对象篇(四) : for in 循环
Jun 24 Javascript
jquery+ajax验证不通过也提交表单问题处理
Dec 12 Javascript
javascript学习笔记之函数定义
Jun 25 Javascript
JavaScript返回上一页的三种方法及区别介绍
Jul 04 Javascript
JavaScript每天必学之事件
Sep 18 Javascript
SSM VUE Axios详解
Oct 05 Vue.js
vue打包时去掉所有的console.log
Apr 10 Vue.js
vue 动态绑定背景图片的方法
Aug 10 #Javascript
原生JS实现$.param() 函数的方法
Aug 10 #Javascript
取消Bootstrap的dropdown-menu点击默认关闭事件方法
Aug 10 #Javascript
JavaScript创建对象的常用方式总结
Aug 10 #Javascript
详解如何在nuxt中添加proxyTable代理
Aug 10 #Javascript
解决bootstrap中下拉菜单点击后不关闭的问题
Aug 10 #Javascript
快速解决bootstrap下拉菜单无法隐藏的问题
Aug 10 #Javascript
You might like
法兰绒滤网冲泡
2021/03/03 冲泡冲煮
用PHP和ACCESS写聊天室(一)
2006/10/09 PHP
php下的权限算法的实现
2007/04/28 PHP
按上下级层次关系输出内容的PHP代码
2010/07/17 PHP
PHP高级对象构建 多个构造函数的使用
2012/02/05 PHP
鸡肋的PHP单例模式应用详解
2013/06/03 PHP
解决FastCGI 进程超过了配置的活动超时时限的问题
2013/07/03 PHP
php图片缩放实现方法
2014/02/20 PHP
给WordPress的编辑后台添加提示框的代码实例分享
2015/12/25 PHP
Laravel 5.4重新登录实现跳转到登录前页面的原理和方法
2017/07/13 PHP
js批量设置样式的三种方法不推荐使用with
2013/02/25 Javascript
jquery+css+ul模拟列表菜单具体实现思路
2013/04/15 Javascript
jquery获取焦点和失去焦点事件代码
2013/04/21 Javascript
jQuery实现长文字部分显示代码
2013/05/13 Javascript
JavaScript自动设置IFrame高度的小例子
2013/06/08 Javascript
Jquery动态进行图片缩略的原理及实现
2013/08/13 Javascript
jquery实现table鼠标经过变色代码
2013/09/25 Javascript
JavaScript里四舍五入函数round用法实例
2015/04/06 Javascript
JavaScript解析JSON格式数据的方法示例
2017/01/24 Javascript
基于JavaScript实现简单的轮播图
2021/03/03 Javascript
Python文件夹与文件的相关操作(推荐)
2016/07/25 Python
详解python tkinter教程-事件绑定
2019/03/28 Python
python安装numpy和pandas的方法步骤
2019/05/27 Python
python导入pandas具体步骤方法
2019/06/23 Python
Django实现发送邮件找回密码功能
2019/08/12 Python
PyQt5事件处理之定时在控件上显示信息的代码
2020/03/25 Python
Python的collections模块真的很好用
2021/03/01 Python
德国消费电子产品购物网站:Guter Kauf
2020/09/15 全球购物
请用Python写一个获取用户输入数字,并根据数字大小输出不同信息的脚本
2014/05/20 面试题
化工专业应届生求职信
2013/11/08 职场文书
计算机专业毕业生的自我评价
2013/11/18 职场文书
建筑工地宣传标语
2014/06/18 职场文书
2014七年级班主任工作总结
2014/12/05 职场文书
酒店保洁员岗位职责
2015/02/26 职场文书
酒店工程部的岗位职责汇总大全
2019/10/23 职场文书
使用Python的开发框架Brownie部署以太坊智能合约
2021/05/28 Python