JavaScript闭包实例详解


Posted in Javascript onJune 03, 2016

一、充电

1、一切(引用类型)都是对象,对象是属性的集合。

2、函数是一种对象,但是函数却不像数组一样——你可以说数组是对象的一种,因为数组就像是对象的一个子集一样。但是函数与对象之间,却不仅仅是一种包含和被包含的关系,函数和对象之间的关系比较复杂,甚至有一点鸡生蛋蛋生鸡的逻辑。

function Fn() {this.name = '王福朋';this.year = 1988;}
var fn1 = new Fn();
var obj = { a: 10, b: 20 };等价于var obj = new Object();
obj.a = 10;obj.b = 20;
var arr = [5, 'x', true];等价于var arr = new Array();
arr[0] = 5;arr[1] = 'x';arr[2] = true;

对象是函数创建的,而函数却又是一种对象。

3、每个函数都有一个属性叫做prototype。这个prototype的属性值是一个对象(属性的集合,再次强调!),默认的只有一个叫做constructor的属性,指向这个函数本身。

4、每个对象都有一个__proto__属性,指向创建该对象的函数的prototype。这个__proto__是一个隐藏的属性,JavaScript不希望开发者用到这个属性值,有的低版本浏览器甚至不支持这个属性值。obj.__proto__=== Object.prototype

二、闭包

1、闭包的基本概念

有权访问另一个函数作用域中的变量的函数。简单理解为“定义在一个函数内部的函数”例如:

function createComparisonFunction(propertyName){ 
return function(object1,object2){//匿名函数 
value1=object1[propertyName]; 
value2=object2[propertyName]; 
if(value1<value2){ 
return -1; 
}else if(value1>value2){ 
return 1; 
}else{ 
return 0; 
} 
} 
} 
//创建函数 
var compareNames=createComparisonFunction("name"); 
//调用函数 
var result=compareNames({name:"Nicolas"},{name:"Greg"}); 
//解除对匿名函数的引用(以便释放内存) 
compareNames=null; 
alert(result);//1

(1).好处:保护函数内的变量安全,加强了封装性;在内存中维持一个变量(缓存);匿名自执行函数;模拟面向对象编程。

(2).应用场景:使用闭包代替全局变量;函数外或在其他函数中访问某一函数内部的参数;包装相关功能;为节点循环绑定click事件,在事件函数中使用当次循环的值或节点,而不是最后一次循环的值或节点;

(3).缺点:常驻内存,会增大内存使用量,使用不当很容易造成内存泄露,更重要的是,对闭包的使用不当会造成无效内存的产生。

只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数。而且JavaScript运行时需要跟踪引用这个内部函数的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间。父函数定义的变量在子函数的作用域链中,子函数没有被销毁,其作用域链中所有变量和函数就会被维护,不会被销毁。

2、闭包的用途

闭包有两个用途,一是方便实现嵌套的回调函数,二是隐藏对象的细节。

对于前者,NodeJS的编程风格已经可以说明问题,后者对于函数内部的局部变量外部是不可见的,但可以提供访问函数来访问和修改相应的局部变量,从而实现OO封装的意图。

(1)、简单的例子

首先从一个经典错误谈起,页面上有若干个div, 我们想给它们绑定一个onclick方法,于是有了下面的代码

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"> 
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
<script language="javascript" src="jquery.js"></script> 
<script> 
$(document).ready(function() { 
var spans = $("#divTest span"); 
for (var i = 0; i < spans.length; i++) { 
spans[i].onclick = function() { 
alert(i); 
} 
} 
}); 
</script> 
<style> 
#divTest{ 
margin-top:30px; 
margin-bottom:40px; 
} 
span{ 
border-color:#3C0; 
border-style:solid; 
margin-bottom:5px; 
padding:10px 30px 10px 30px; 
border-radius:10px 
} 
</style> 
<title>无标题文档</title> 
</head> 
<body> 
<div id="divTest"> 
<span>0</span> <span>1</span> <span>2</span> <span>3</span> 
</div> 
</body> 
</html>

很简单的功能可是却偏偏出错了,每次alert出的值都是4,简单的修改就好使了

$(document).ready(function() { 
var spans = $("#divTest span"); 
for (var i = 0; i < spans.length; i++) { 
(function(num){//匿名函数表达式 
spans[i].onclick = function() { 
alert(num); 
} 
})(i);//立即执行,并且把i的值传给num 
} 
});

上面代码在页面加载后就会执行,当i的值为4的时候,判断条件不成立,for循环执行完毕,但是因为每个span的onclick方法这时候为内部函数,所以i被闭包引用,内存不能被销毁,i的值会一直保持4,直到程序改变它或者所有的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。这样每次我们点击span的时候,onclick函数会查找i的值(作用域链是引用方式),一查等于4,然后就alert给我们了。而第二种方式是使用了一个立即执行的函数又创建了一层闭包,函数声明放在括号内就变成了表达式,后面再加上括号括号就是调用了,这时候把i当参数传入,函数立即执行,num保存每次i的值。

(2)、内部函数

让我们从一些基础的知识谈起,首先了解一下内部函数。内部函数就是定义在另一个函数中的函数。例如:

function outerFn () { 
function innerFn () {} 
}

innerFn就是一个被包在outerFn作用域中的内部函数。这意味着,在outerFn内部调用innerFn是有效的,而在outerFn外部调用innerFn则是无效的。下面代码会导致一个JavaScript错误:

function outerFn() { 
document.write("Outer function<br/>"); 
function innerFn() { 
document.write("Inner function<br/>"); 
} 
} 
innerFn();

不过在outerFn内部调用innerFn,则可以成功运行:

function outerFn() { 
document.write("Outer function<br/>"); 
function innerFn() { 
document.write("Inner function<br/>"); 
} 
innerFn(); 
} 
outerFn();

(3)、伟大的逃脱

JavaScript允许开发人员像传递任何类型的数据一样传递函数,也就是说,JavaScript中的内部函数能够逃脱定义他们的外部函数。

逃脱的方式有很多种,例如可以将内部函数指定给一个全局变量:

var globalVar; 
function outerFn() { 
document.write("Outer function<br/>"); 
function innerFn() { 
document.write("Inner function<br/>"); 
} 
globalVar = innerFn; 
} 
outerFn(); 
globalVar();

调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn一样。这时在outerFn外部直接调用innerFn仍然会导致错误,这是因为内部函数虽然通过把引用保存在全局变量中实现了逃脱,但这个函数的名字依然只存在于outerFn的作用域中。
也可以通过在父函数的返回值来获得内部函数引用

function outerFn() { 
document.write("Outer function<br/>"); 
function innerFn() { 
document.write("Inner function<br/>"); 
} 
return innerFn; 
} 
var fnRef = outerFn(); 
fnRef();

这里并没有在outerFn内部修改全局变量,而是从outerFn中返回了一个对innerFn的引用。通过调用outerFn能够获得这个引用,而且这个引用可以可以保存在变量中。

这种即使离开函数作用域的情况下仍然能够通过引用调用内部函数的事实,意味着只要存在调用内部函数的可能,JavaScript就需要保留被引用的函数。而且JavaScript运行时需要跟踪引用这个内部函数的所有变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间。

说了半天总算和闭包有关系了,闭包是指有权限访问另一个函数作用域的变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数,就是我们上面说的内部函数,所以刚才说的不是废话,也是闭包相关的。

(4)、变量的作用域

内部函数也可以有自己的变量,这些变量都被限制在内部函数的作用域中:

function outerFn() { 
document.write("Outer function<br/>"); 
function innerFn() { 
var innerVar = 0; 
innerVar++; 
document.write("Inner function\t"); 
document.write("innerVar = "+innerVar+"<br/>"); 
} 
return innerFn; 
} 
var fnRef = outerFn(); 
fnRef(); 
fnRef(); 
var fnRef2 = outerFn(); 
fnRef2(); 
fnRef2();

每当通过引用或其它方式调用这个内部函数时,就会创建一个新的innerVar变量,然后加1,最后显示

Outer function
Inner function innerVar = 1
Inner function innerVar = 1
Outer function
Inner function innerVar = 1
Inner function innerVar = 1

内部函数也可以像其他函数一样引用全局变量:

var globalVar = 0; 
function outerFn() { 
document.write("Outer function<br/>"); 
function innerFn() { 
globalVar++; 
document.write("Inner function\t"); 
document.write("globalVar = " + globalVar + "<br/>"); 
} 
return innerFn; 
} 
var fnRef = outerFn(); 
fnRef(); 
fnRef(); 
var fnRef2 = outerFn(); 
fnRef2(); 
fnRef2();

现在每次调用内部函数都会持续地递增这个全局变量的值:

Outer function
Inner function globalVar = 1
Inner function globalVar = 2
Outer function
Inner function globalVar = 3
Inner function globalVar = 4

但是如果这个变量是父函数的局部变量又会怎样呢?因为内部函数会引用到父函数的作用域(有兴趣可以了解一下作用域链和活动对象的知识),内部函数也可以引用到这些变量

function outerFn() { 
var outerVar = 0; 
document.write("Outer function<br/>"); 
function innerFn() { 
outerVar++; 
document.write("Inner function\t"); 
document.write("outerVar = " + outerVar + "<br/>"); 
} 
return innerFn; 
} 
var fnRef = outerFn(); 
fnRef(); 
fnRef(); 
var fnRef2 = outerFn(); 
fnRef2(); 
fnRef2();

这一次结果非常有意思,也许或出乎我们的意料

Outer function
Inner function outerVar = 1
Inner function outerVar = 2
Outer function
Inner function outerVar = 1
Inner function outerVar = 2

我们看到的是前面两种情况合成的效果,通过每个引用调用innerFn都会独立的递增outerVar。也就是说第二次调用outerFn没有继续沿用outerVar的值,而是在第二次函数调用的作用域创建并绑定了一个一个新的outerVar实例,两个计数器完全无关。

当内部函数在定义它的作用域的外部被引用时,就创建了该内部函数的一个闭包。这种情况下我们称既不是内部函数局部变量,也不是其参数的变量为自由变量,称外部函数的调用环境为封闭闭包的环境。从本质上讲,如果内部函数引用了位于外部函数中的变量,相当于授权该变量能够被延迟使用。因此,当外部函数调用完成后,这些变量的内存不会被释放(最后的值会保存),闭包仍然需要使用它们。

(5)、闭包之间的交互

当存在多个内部函数时,很可能出现意料之外的闭包。我们定义一个递增函数,这个函数的增量为2

function outerFn() { 
var outerVar = 0; 
document.write("Outer function<br/>"); 
function innerFn1() { 
outerVar++; 
document.write("Inner function 1\t"); 
document.write("outerVar = " + outerVar + "<br/>"); 
} 
function innerFn2() { 
outerVar += 2; 
document.write("Inner function 2\t"); 
document.write("outerVar = " + outerVar + "<br/>"); 
} 
return { "fn1": innerFn1, "fn2": innerFn2 }; 
} 
var fnRef = outerFn(); 
fnRef.fn1(); 
fnRef.fn2(); 
fnRef.fn1(); 
var fnRef2 = outerFn(); 
fnRef2.fn1(); 
fnRef2.fn2(); 
fnRef2.fn1();

我们映射返回两个内部函数的引用,可以通过返回的引用调用任一个内部函数,结果:

Outer function
Inner function 1 outerVar = 1
Inner function 2 outerVar = 3
Inner function 1 outerVar = 4
Outer function
Inner function 1 outerVar = 1
Inner function 2 outerVar = 3
Inner function 1 outerVar = 4

innerFn1和innerFn2引用了同一个局部变量,因此他们共享一个封闭环境。当innerFn1为outerVar递增一时,久违innerFn2设置了outerVar的新的起点值,反之亦然。我们也看到对outerFn的后续调用还会创建这些闭包的新实例,同时也会创建新的封闭环境,本质上是创建了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,而且这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。

3、模仿块级作用域

JavaScript是函数作用域(function scope),没有块级作用域。无论函数体内的变量在什么地方声明,对整个函数都是可见的,即JavaScript函数里声明的所有变量都被提前到函数体的顶部,只是提前变量声明,变量的赋值还是保留在原位置。

匿名函数可以用来模仿块级作用域避免变量提前声明为全局变量,用作块级作用域(通常称为私有作用域)的匿名函数的语法如下:

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

例如:

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

在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在循环中使用,使用后即被销毁。

4、私有变量

我们把有权访问私有变量和私有函数的公有方法称为特权方法。

(1)、构造函数中定义特权方法

function Person(name){ 
//name为私有变量 
this.getName=function(){//特权方法 
return name; 
}; 
this.setName=function(value){//特权方法 
name=value 
}; 
} 
var person=new Person("zoumiao"); 
alert(person.getName()); 
person.setName("zouzou"); 
alert(person.getName()); 
var person1=new Person("zou"); 
alert(person1.getName());//"zou" 
alert(person.getName());//"zouzou"

在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的,构造函数模式的缺点就是针对每一个实例都会创建同样一组新方法,使用静态私有变量来实现特权方法就可以避免这个问题。

(2)、静态私有变量

通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下:

(function(){ 
var name='';//静态的、由所有实例共享的属性 
Person=function(value){//全局构造函数 
name=value; 
} 
Person.prototype.getName=function(){//公有/特权方法 
return name; 
} 
Person.prototype.setName=function(value){//公有/特权方法 
name=value; 
} 
})(); 
var person=new Person("zoumiao"); 
alert(person.getName()); 
person.setName("zouzou"); 
alert(person.getName()); 
var person1=new Person("zou"); 
alert(person1.getName());//"zou" 
alert(person.getName());//"zou"

这个模式与构造函数中定义特权方法的区别,就在于私有变量和函数是由实例共享的。

Javascript 相关文章推荐
ext监听事件方法[初级篇]
Apr 27 Javascript
js关于命名空间的函数实例
Feb 05 Javascript
node+experss实现爬取电影天堂爬虫
Nov 20 Javascript
完美解决IE不支持Data.parse()的问题
Nov 24 Javascript
移动适配的几种方案(三种方案)
Nov 25 Javascript
jQuery实现遍历复选框的方法示例
Mar 06 Javascript
ES6字符串模板,剩余参数,默认参数功能与用法示例
Apr 06 Javascript
Bootstrap datepicker日期选择器插件使用详解
Jul 26 Javascript
Angular 容器部署的方法
Apr 17 Javascript
详解vue中axios的封装
Jul 18 Javascript
微信小程序实现form表单本地储存数据
Jun 27 Javascript
解决ele ui 表格表头太长问题的实现
Nov 13 Javascript
js判断某个字符出现的次数的简单实例
Jun 03 #Javascript
JS中判断字符串中出现次数最多的字符及出现的次数的简单实例
Jun 03 #Javascript
jQuery针对input的class属性写了多个值情况下的选择方法
Jun 03 #Javascript
javascript闭包概念简单解析(推荐)
Jun 03 #Javascript
让你一句话理解闭包(简单易懂)
Jun 03 #Javascript
Js类的静态方法与实例方法区分及jQuery拓展的两种方法
Jun 03 #Javascript
JS两个数组比较,删除重复值的巧妙方法(推荐)
Jun 03 #Javascript
You might like
php preg_match_all结合str_replace替换内容中所有img
2008/10/11 PHP
Ext.data.PagingMemoryProxy分页一次性读取数据的实现代码
2010/04/07 PHP
Win2003+apache+PHP+SqlServer2008 配置生产环境
2014/07/29 PHP
PHP 微信支付类 demo
2015/11/30 PHP
PHP用正则匹配form表单中所有元素的类型和属性值实例代码
2017/02/28 PHP
PHP memcache在微信公众平台的应用方法示例
2017/09/13 PHP
PHP设计模式之抽象工厂模式实例分析
2019/03/25 PHP
Js 获取当前日期时间及其它操作实现代码
2021/03/04 Javascript
IE无法设置短域名下Cookie
2010/09/23 Javascript
jQuery技巧总结
2011/01/01 Javascript
基于JQuery制作可编辑的表格特效
2014/12/23 Javascript
html的DOM中Event对象onblur事件用法实例
2015/01/21 Javascript
jQuery使用getJSON方法获取json数据完整示例
2016/09/13 Javascript
微信小程序 rpx 尺寸单位详细介绍
2016/10/13 Javascript
seajs学习教程之基础篇
2016/10/20 Javascript
JavaScript实现图片瀑布流和底部刷新
2017/01/02 Javascript
Javascript下拉刷新的简单实现
2017/02/14 Javascript
JavaScript中的return布尔值的用法和原理解析
2017/08/14 Javascript
Js面试算法详解
2018/04/08 Javascript
详解使用element-ui table组件的筛选功能的一个小坑
2018/11/02 Javascript
基于Fixed定位的框选功能的实现代码
2019/05/13 Javascript
微信小程序判断页面是否从其他页面返回的实例代码
2019/07/03 Javascript
[02:11]完美世界DOTA2联赛10月28日赛事精彩集锦:来吧展示实力强劲
2020/10/29 DOTA
python实现倒计时的示例
2014/02/14 Python
python3新特性函数注释Function Annotations用法分析
2016/07/28 Python
python爬虫之urllib3的使用示例
2018/07/09 Python
python opencv判断图像是否为空的实例
2019/01/26 Python
python config文件的读写操作示例
2019/09/27 Python
python 画函数曲线示例
2019/12/04 Python
Django实现whoosh搜索引擎使用jieba分词
2020/04/08 Python
Sixt美国租车:高端豪华车型自驾体验
2017/09/02 全球购物
领导班子四风问题个人对照检查材料
2014/10/04 职场文书
人事任命通知书
2015/04/21 职场文书
孔繁森观后感
2015/06/10 职场文书
MySQL pt-slave-restart工具的使用简介
2021/04/07 MySQL
Oracle11g R2 安装教程完整版
2021/06/04 Oracle