javascript之闭包深入理解(一)
曾经在开始学习javascript的时候,很是不理解闭包的概念。今天想对它详细的剖析。
在说清楚闭包之前,必须先清楚作用域链。
- 作用域链
我们知道,执行环境是js中最为重要的一个概念。执行环境定义了变量或函数有权访问的的其它数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,执行环境定义的所有变量和函数都保存在这个对象中,虽然我们编写的代码无法访问这个对象,但是解释器在处理数据时会在后台使用它。
全局执行环境是最外围的一个执行环境。所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码被执行完毕后,该环境就会被销毁,保存在其中的所有变量和函数定义也会被销毁。全局执行环境直到应用程序退出,web浏览器的全局执行环境被销毁的时机是浏览器的关闭时刻。
每一个函数都有自己的执行环境。当执行流进入到一个函数时,函数的环境就会被推入一个环境栈中。当函数执行完毕后,栈将其环境弹出,把控制权返回给前一个执行环境。ECMAScript程序中的执行流正式由这个方便的机制控制着。
当代码在一个环境中执行时,会创建变量对象的一个作用域链。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始的时候只包含一个变量,即arguments对象。作用域链中的下一个变量对象来自包含外部环境,而下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境。请看如下代码,
// window
var x=;
function fn1(args1){
// do sth1
var y=1;
function fn2(args2)
{
// do sth2
for(var i=;i<;i++)
{
// do sth3
}
}
}
那么它的作用域链参照下图:
标识符的解析是沿着作用域链一级一级的向上搜索标识符的过程,搜索过程始终是从作用域链的前端开始,然后逐级的向后回溯,直到找到标识符为止,如果找不到标识符,通常会发生错误 ,也就是我们经常看到的undefined之类的错误。任何环境都不可以向下搜索作用域链而进入下一级的搜索环境。既然作用域链只可以向上进行搜索,那么有没有办法来延长作用域链呢?
2.延长作用域链
有些语句可以在作用域的前端临时增加一个变量对象,该变量会在代码被执行后移除。在两种情况下会发生这种现象。
- with语句
- try{}catch{}语句
这两个语句都会在作用域链的前端添加一个变量对象。对with 语句来说,会将指定的对象添加到作用域链中。对catch 语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明。
3.垃圾回收机制
Javascript具有自动垃圾回收机制,也就是说,执行环境会负责管理代码执行过程中使用的内存。在编写javascript时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全是自动管理的。这种垃圾回收机制其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾回收器会按照固定的时间周期或在代码执行中预订的时间来执行垃圾回收。
局部变量只在函数的执行过程中存在,而在这个过程中,会为局部变量在栈或堆上内存上分配相应的空间,以便存储它们的值。在函数中会使用这些值,但是在函数执行完毕后,这些值就没有必要存在了,因此可以释放它们的内存。在这种情况下,很容易判断变量是否还有存在的必要,但是并非所有情况都这么容易得出结论。垃圾回收器会跟踪哪个变量有用,哪个变量没有用,对于不再用的变量打上标记,以备将来回收并占用其内存。用于标识无用变量的策略可能会因为实现而有所异,其中就有闭包这种特殊情况。
标记清除
javascript最常用的标记清除机制。当变量进入环境中,就将这个变量进行标记为“进入环境”。从逻辑上讲,永远不能释放进入环境中的变量所占用的内存。当变量离开环境时,就将其标记为“离开环境”。
可以使用任何方式来对变量进行标记,例如可以通过翻转某个特殊的位来记录变量是否进入了环境,或者变量列表来进行标记。
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。到2008 年为止,IE、Firefox、Opera、Chrome 和Safari 的JavaScript 实现使用的都是标记清除式的
垃圾收集策略(或类似的策略),只不过垃圾收集的时间间隔互有不同。
引用计数
另一种不太常见的垃圾收集策略叫做引用计数(reference counting)。引用计数的含义是跟踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减1。当这个值的引用次数变成0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。
Netscape Navigator 3.0 是最早使用引用计数策略的浏览器,但很快它就遇到了一个严重的问题:循环引用。循环引用指的是对象A 中包含一个指向对象B 的指针,而对象B 中也包含一个指向对象A 的引用。
function problem(){
var objectA = new Object();
var objectB = new Object();
objectA.someOtherObject = objectB;
objectB.anotherObject = objectA;
}
IE 中有一部分对象并不是原生JavaScript 对象。例如,其BOM 和DOM 中的对象就是使用C++以COM(Component Object Model,组件对象模型)对象的形式实现的,而COM 对象的垃圾收集机制采用的就是引用计数策略。因此,即使IE 的JavaScript 引擎是使用标记清除策略来实现的,但JavaScript 访问的COM 对象依然是基于引用计数策略的。换句话说,只要在IE 中涉及COM 对象,就会存在循环引用的问题。下面这个简单的例子,展示了使用COM 对象导致的循环引用问题:
var element = document.getElementById("some_element");
var myObject = new Object();
myObject.element = element;
element.someObject = myObject;
这个例子在一个DOM 元素(element)与一个原生JavaScript 对象(myObject)之间创建了循环引用。其中,变量myObject 有一个名为element 的属性指向element 对象;而变量element 也有一个属性名叫someObject 回指myObject。由于存在这个循环引用,即使将例子中的DOM 从页面中移除,它也永远不会被回收。
为了避免类似这样的循环引用问题,最好是在不使用它们的时候手工断开原生JavaScript 对象与DOM 元素之间的连接。
例如,可以使用下面的代码消除前面例子创建的循环引用:
myObject.element = null;
element.someObject = null;
将变量设置为null 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。为了解决上述问题,IE9 把BOM 和DOM 对象都转换成了真正的JavaScript 对象。这样,就避免了两种垃圾收集算法并存导致的问题,也消除了常见的内存泄漏现象。
3.性能问题
垃圾收集器是周期性运行的,而且如果为变量分配的内存数量很可观,那么回收工作量也是相当大的。在这种情况下,确定垃圾收集的时间间隔是一个非常重要的问题。说到垃圾收集器多长时间运行一次,不禁让人联想到IE 因此而声名狼藉的性能问题。IE 的垃圾收集器是根据内存分配量运行的,具体一点说就是256 个变量、4096 个对象(或数组)字面量和数组元素(slot)或者64KB 的字符串。达到上述任何一个临界值,垃圾收集器就会运行。这种实现方式的问题在于,如果一个脚本中包含那么多变量,那么该脚本很可能会在其生命周期中一直保有那么多的变量。而这样一来,垃圾收集器就不得不频繁地运行。结果,由此引发的严重性能问题促使IE7 重写了其垃圾收集例程。
随着IE7 的发布,其JavaScript 引擎的垃圾收集例程改变了工作方式:触发垃圾收集的变量分配、字面量和(或)数组元素的临界值被调整为动态修正。IE7 中的各项临界值在初始时与IE6 相等。如果垃圾收集例程回收的内存分配量低于15%,则变量、字面量和(或)数组元素的临界值就会加倍。如果例程回收了85%的内存分配量,则将各种临界值重置回默认值。这一看似简单的调整,极大地提升了IE在运行包含大量JavaScript 的页面时的性能。
事实上,在有的浏览器中可以触发垃圾收集过程,但不建议这样做。在IE 中,调用window.CollectGarbage()方法会立即执行垃圾收集。在Opera 7 及更高版本中,调用window.opera.collect()也会启动垃圾收集例程。
4.内存管理
使用具备垃圾收集机制的语言编写程序,开发人员一般不必操心内存管理的问题。但是,JavaScript在进行内存管理及垃圾收集时面临的问题还是有点与众不同。其中最主要的一个问题,就是分配给Web浏览器的可用内存数量通常要比分配给桌面应用程序的少。这样做的目的主要是出于安全方面的考虑,目的是防止运行JavaScript 的网页耗尽全部系统内存而导致系统崩溃。内存限制问题不仅会影响给变量分配内存,同时还会影响调用栈以及在一个线程中能够同时执行的语句数量。因此,确保占用最少的内存可以让页面获得更好的性能。而优化内存占用的最佳方式,就是为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设置为null 来释放其引用——这个做法叫做解除引用(dereferencing)。这一做法适用于大多数全局变量和全局对象的属性。局部变量会在它们离开执行环境时自动被解除引用,如下面这个例子所示:
function createPerson(name){
var localPerson = new Object();
localPerson.name = name;
return localPerson;
}
var globalPerson = createPerson("Nicholas");
// 手工解除globalPerson 的引用
globalPerson = null;
在这个例子中,变量globalPerson 取得了createPerson()函数返回的值。在createPerson()函数内部,我们创建了一个对象并将其赋给局部变量localPerson,然后又为该对象添加了一个名为name 的属性。最后,当调用这个函数时,localPerson 以函数值的形式返回并赋给全局变量globalPerson。由于localPerson 在createPerson()函数执行完毕后就离开了其执行环境,因此无需我们显式地去为它解除引用。但是对于全局变量globalPerson 而言,则需要我们在不使用它的时候手工为它解除引用,这也正是上面例子中最后一行代码的目的。不过,解除一个值的引用并不意味着自动回收该值所占用的内存。解除引用的真正作用是让值脱离执行环境,以便垃圾收集器下次运行时将其回收。
javascript之闭包深入理解(一)的更多相关文章
- 对JavaScript中闭包的理解
在前端开发中闭包是一个很重要的知识点,是面试中一定会被问到的内容.之前我对闭包的理解主要是"通过闭包可以在函数外部能访问到函数内部的变量",对闭包运用的也很少,甚至自己写过闭包自己 ...
- 转:对JavaScript中闭包的理解
关于 const let var 总结: 建议使用 let ,而不使用var,如果要声明常量,则用const. ES6(ES2015)出现之前,JavaScript中声明变量只有 ...
- 关于javascript中闭包的理解
闭包就是能够读取其他函数内部变量的函数. 在javascript中,只有函数内部的子函数可以读取局部变量,因此,我理解闭包就是定义在一个函数内部的函数. 例子: var f1 = function() ...
- javascript之闭包深入理解(二)
在上一节中,详细理解了作用域链和垃圾回收机制,似乎这两点跟闭包关系不大,但是仔细想一想就会发现,其实不然.这一节将通过上一部分的说明详细理解闭包.请看代码: function createCompar ...
- 第二话:javascript中闭包的理解
闭包是什么? 通过闭包,子函数得以访问父函数的上下文环境,即使父函数已经结束执行. OK,我来简单叙述下,先上图. 都知道函数是javascript整个世界,对象是函数,方法是函数,并且js中实质性的 ...
- JavaScript中闭包的理解
1.什么是闭包 我个人理解闭包就是函数中嵌套函数,但是嵌套的那个函数必须是返回值,才构成闭包: <!DOCTYPE html> <html> <head> < ...
- 深入理解JavaScript的闭包特性如何给循环中的对象添加事件
初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...
- 深入理解javascript的闭包
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域 ...
- 【转】理解JavaScript之闭包
闭包(closure)是掌握Javascript从人门到深入一个非常重要的门槛,它是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现.下面写下我的学习笔记~ 闭包-无处不 ...
随机推荐
- 如何在一台机器上安装两个MYSQL数据库
1.正常安装第一个mysql(安装步骤省略) 2.在控制面板里停止第一个mysql服务 3.将C:\Program Files\MySQL目录下的所有目录和文件copy到另外一个路径,我这里是copy ...
- 最小费用最大流MCMF zkw费用流
稀疏图慢死了...但是稠密图效果还是很好的 struct MCMF{ struct tedge{int x,y,cap,w,next;}adj[maxm];int ms,fch[maxn]; int ...
- BZOJ1711: [Usaco2007 Open]Dingin吃饭
1711: [Usaco2007 Open]Dingin吃饭 Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 508 Solved: 259[Submit ...
- -_-#【Canvas】回弹
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title> ...
- Android之路-------浅淡Android历史、系统架构与开发特色
前言 离上一篇发表的博客差不多有两个星期了吧,相信有些博友差点就对LP失望了,因为上一篇博文中说了,这次不管怎样,LP都会坚持写博客的. 由于工作关系LP才隔了这么久才再次发表博文,这篇博文主要是总结 ...
- [ 转]国内有时抽风,无法更新adt的解决方案
http://www.xidige.com/other/354 最近无意中发现mirrors.neusoft.edu.cn有android的目录,进去还能看到xml文件,所以网络搜索了一下,发现还有另 ...
- C++读写文件并排序
比如一条记录是 1987 9 2 1988 8 26 代表公司员工生日 然后需要读入到系统 现在需要放入容器,并且排序 最后输出到新的文件中,按照年龄由大到小. #include "stda ...
- lvs keepalived 安装配置详解【转】
lvs keepalived 安装配置详解 张映 发表于 2012-06-20 分类目录: 服务器相关 前段时间看了一篇文章,lvs做负载均衡根F5差不多,说实话不怎么相信,因为F5没玩过,也无法比较 ...
- Example of how to use both JDK 7 and JDK 8 in one build.--reference
JDK 8 Released Most of us won’t be able to use/deploy JDK 8 in production for a looong time. But tha ...
- 二叉排序树BST代码(JAVA)
publicclassTest{ publicstaticvoid main(String[] args){ int[] r =newint[]{5,1,3,4,6,7 ...