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 相关文章推荐
点击隐藏页面左栏或右栏实现js代码
Apr 01 Javascript
js函数模拟显示桌面.scf程序示例
Apr 20 Javascript
js禁止页面刷新与后退的方法
Jun 08 Javascript
Javascript函数的参数
Jul 16 Javascript
基于jquery实现页面滚动到底自动加载数据的功能
Dec 19 Javascript
javascript字符串对象常用api函数小结(连接,替换,分割,转换等)
Sep 20 Javascript
jquery实现的table排序功能示例
Mar 10 Javascript
bootstrap multiselect下拉列表功能
Aug 22 Javascript
了解Javascript中函数作为对象的魅力
Jun 19 Javascript
详解Vuex下Store的模块化拆分实践
Jul 31 Javascript
vue-cli3配置与跨域处理方法
Aug 17 Javascript
详解Vue3中对VDOM的改进
Apr 23 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
ADODB类使用
2006/11/25 PHP
yii的CURD操作实例详解
2014/12/04 PHP
php把时间戳转换成多少时间之前函数的实例
2016/11/16 PHP
PHP二维数组实现去除重复项的方法【保留各个键值】
2017/12/21 PHP
PHP单文件上传原理及上传函数的封装操作示例
2019/09/02 PHP
jQuery提交表单ajax查询实例代码
2012/10/07 Javascript
javascript中比较字符串是否相等的方法
2013/07/23 Javascript
JavaScript实现点击文字切换登录窗口的方法
2015/05/11 Javascript
javascript字符串替换函数如何一次性全部替换掉
2015/10/30 Javascript
jQuery实现的指纹扫描效果实例(附演示与demo源码下载)
2016/01/26 Javascript
JS获取元素多层嵌套思路详解
2016/05/16 Javascript
Ionic3 UI组件之autocomplete详解
2017/06/08 Javascript
微信小程序wx:for和wx:for-item的用法详解
2018/04/01 Javascript
vue vue-Router默认hash模式修改为history需要做的修改详解
2018/09/13 Javascript
vue实现的下拉框功能示例
2019/01/29 Javascript
详解Vue中使用Axios拦截器
2019/04/22 Javascript
微信小程序可滑动周日历组件使用详解
2019/10/21 Javascript
使用vue-router切换页面时实现设置过渡动画
2019/10/31 Javascript
uni-app从安装到卸载的入门教程
2020/05/15 Javascript
JavaScript实现拖拽和缩放效果
2020/08/24 Javascript
[01:04:35]2018DOTA2亚洲邀请赛 4.3 突围赛 Secret vs VG 第一场
2018/04/04 DOTA
Python数据结构之图的应用示例
2018/05/11 Python
详解Python中pandas的安装操作说明(傻瓜版)
2019/04/08 Python
一篇文章搞定Python操作文件与目录
2019/08/13 Python
使用python脚本自动创建pip.ini配置文件代码实例
2019/09/20 Python
python3实现单目标粒子群算法
2019/11/14 Python
Python3爬虫里关于代理的设置总结
2020/07/30 Python
利用Python实现自动扫雷小脚本
2020/12/17 Python
Html5页面内使用JSON动画的实现
2019/01/29 HTML / CSS
新西兰演唱会和体育门票网站:Ticketmaster新西兰
2017/10/07 全球购物
美国现代家具购物网站:LexMod
2019/01/09 全球购物
澳大利亚拥有最好的家具和家居用品在线目的地:Nestz
2019/02/23 全球购物
豪华床上用品 :Jennifer Adams
2019/09/15 全球购物
请介绍一下WSDL的文档结构
2013/03/17 面试题
体育教师个人工作总结
2015/02/09 职场文书
解决Navicat for Mysql连接报错1251的问题(连接失败)
2021/05/27 MySQL