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 相关文章推荐
小议Function.apply() 之一------(函数的劫持与对象的复制)
Nov 30 Javascript
处理文本部分内容的TextRange对象应用实例
Jul 29 Javascript
jQuery实现的纵向下拉菜单实例详解【附demo源码下载】
Jul 09 Javascript
js 获取范围内的随机数实例代码
Aug 02 Javascript
新入门node.js必须要知道的概念(必看篇)
Aug 10 Javascript
设置jquery UI 控件的大小方法
Dec 12 Javascript
基于js中的原型(全面讲解)
Sep 19 Javascript
node thread.sleep实现示例
Jun 20 Javascript
layui 实现加载动画以及非真实加载进度的方法
Sep 23 Javascript
JavaScript中的类型检查
Feb 03 Javascript
在Vue中使用Echarts可视化库的完整步骤记录
Nov 18 Vue.js
AngularJS实现多级下拉框
Mar 25 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.MVC的模板标签系统(二)
2006/09/05 PHP
PHP实现将textarea的值根据回车换行拆分至数组
2015/06/10 PHP
php计算给定日期所在周的开始日期和结束日期示例
2017/02/06 PHP
laravel手动创建数组分页的实现代码
2018/06/07 PHP
Laravel框架搜索分页功能示例
2019/02/01 PHP
php封装的pdo数据库操作工具类与用法示例
2019/05/08 PHP
laravel 修改记住我功能的cookie保存时间的方法
2019/10/14 PHP
在laravel框架中实现封装公共方法全局调用
2019/10/14 PHP
动态加载js文件 document.createElement
2006/10/14 Javascript
IE6 弹出Iframe层中的文本框“经常”无法获得输入焦点
2009/12/27 Javascript
基于JQuery的浮动DIV显示提示信息并自动隐藏
2011/02/11 Javascript
javascript面向对象之对象的深入理解
2015/01/13 Javascript
jQuery实现自动调整字体大小的方法
2015/06/15 Javascript
JS实现弹性漂浮效果的广告代码
2015/09/02 Javascript
原生js的数组除重复简单实例
2016/05/24 Javascript
BootStrap智能表单实战系列(四)表单布局介绍
2016/06/13 Javascript
使用SVG基本操作API的实例讲解
2017/09/14 Javascript
nodejs 图片预览和上传的示例代码
2017/09/30 NodeJs
微信小程序实现鼠标拖动效果示例
2017/12/01 Javascript
webpack实现一个行内样式px转vw的loader示例
2018/09/13 Javascript
新年快乐! javascript实现超级炫酷的3D烟花特效
2019/01/30 Javascript
layer提示框添加多个按钮选择的实例
2019/09/12 Javascript
JavaScript中作用域链的概念及用途讲解
2020/08/06 Javascript
vue+elementUI实现简单日历功能
2020/09/24 Javascript
零基础写python爬虫之抓取百度贴吧并存储到本地txt文件改进版
2014/11/06 Python
Python基于scrapy采集数据时使用代理服务器的方法
2015/04/16 Python
python利用requests库进行接口测试的方法详解
2018/07/06 Python
Flask之请求钩子的实现
2018/12/23 Python
python识别文字(基于tesseract)代码实例
2019/08/24 Python
Python3将ipa包中的文件按大小排序
2020/04/17 Python
python同时遍历两个list用法说明
2020/05/02 Python
浅谈html5之sse服务器发送事件EventSource介绍
2017/08/28 HTML / CSS
您熟悉ORM(Object-Relation Mapping)吗?请谈谈您所理解的ORM
2016/02/08 面试题
你们项目是如何进行变更控制的
2015/08/26 面试题
机关门卫岗位职责
2013/12/30 职场文书
《富饶的西沙群岛》教学反思
2016/02/16 职场文书