从2004年下半年开始学习Web编程至今3年有余。从HTML,asp开始到现在的VS2008一路学过来,其中学的最多的还是服务器端编程,对客户端编程的学习还是不成系统。虽然在很多个系统里面应用过脚本,有些还起到了比较重要的作用。但一直是只知其然不知其所以然,用的是小心翼翼。现在脚本编程从以前的"雕虫小技"变成了一个Web开发不可或缺的元素,其地位大大提高了,特别是Ajax兴起之后它更是"炙手可热"了。鉴于此种情况及自己对脚本编程的热爱,于是就系统地学习一下脚本。 学习是理解和记忆的过程。在理解和记忆的过程中必不可少地就需要一些辅助的记录,于是我就将自己的学习记录写成随笔。一来是帮助自己理解和记忆,二来也给其它热爱脚本的同志一些参考。 1.JavaScript对象 ECMA-262将对象(object)定义为"属性的无序集合,每个属性存放一个原始值、对象或函数"(unordered collection of properties each of which contains a primitive value, object, or function)。这意味着对象是无特定顺序的值的数组。在ECMAScript中,对象由特性(Attribute)构成,特性可以是原始值,也可以是引用值。如果特性存放的是函数,它将被看作对象的方法(Method),否则该特性被看作属性(Property)。 2.对象的废除 ECMAScript有无用存储单元收集程序(就像C#的垃圾收集器),意味着不必专门销毁对象来释放内存。当再没有对对象的引用时,该对象就被废除了。运行无用存储单元收集程序时,所有废除的对象都会被销毁。每当函数执行完它的代码,无用存储单元收集程序都会运行,释放所有的局部变量,还有在一些其它不可预知的情况下,无用存储单元收集程序也会运行。 把对象的所有引用都设置为null,可以强制性的废除对象。例如: Var oObject=new Object(); // 程序逻辑 oObject=null; 当变量oObject设置为null后,对第一个创建的对象的引用就不存在了。这意味着下次运行无用存储单元收集程序时,该对象将被销毁。 每用完一个对象后,就将其废除,来释放内存,这是个好习惯。这样还确保不再使用已经不能访问的对象,从而防止程序设计错误的出现。此外,旧的浏览器(如IE)没有完全的无用存储单元收集程序,所以卸载页面时,对象可能不能被正确地销毁。以前IE6就有这样的诟病,当页面被关闭后对象还是没有被释放,所以总是会导致内存溢出的错误。废除对象和它所有的特性是确保内存正确使用的最好方法。 3.对象的类型 JavaScript中对象分为:本地对象(native object)、内置对象(built-in object)、宿主对象(host object)。其中本地对象和宿主对象大家一般用的比较多,比较熟。这里我就重点说明一下内置对象。 ECMA-262把内置对象定义为"由ECMAScript实现提供的、独立于宿主环境的所有对象,在ECMAScript程序开始执行时出现"(any object supplied by an ECMAScript implementation, independent of the host environment, which is present at the start of the execution of an ECMAScript program.)。这意味着开发者不必明确实例化内置对象,它已经被实例化了。但ECMAScript只定义了两个内置对象: 3.1 Math对象 Math对象就是解决数学问题的所有公式。这个在各种编程语言中都有类似的实现,就不做介绍了。 3.2 Global对象 园子里很多搞ASP.net的,相信大家对其中的Global.asax非常熟悉了。但这个对象在ECMAScript中却比较特殊。因为它实际上根本就不存在。如果尝试编写下面的代码去实例化它,将得到错误: Var _global=new Global(); 错误消息显示Global不是对象,但上文却说Global是一个内置对象,这不就自相矛盾了吗?不矛盾。这里需要理解的主要概念是,在ECMAScript中,不存在独立的函数,所有的函数都必须是某个对象的方法。ECMAScript中常用的函数例如isNaN()、isFinite()等,看起来都像独立的函数。实际上,它们都是Global对象的方法。而且Global对象的方法还不止这些。 4.定义类或对象 虽然ECMAScript越来越正规化了,但创建对象的方法却被置之不理。在高级的编程语言(如C#)中,创建对象的方法被明确的定义了。所以不会有太大的分歧。但在脚本语言中创建对象的方法就是多种多样了。 4.1 工厂方式 由于对象的属性可在对象创建后动态定义,所以许多开发者都在初次引入JavaScript时编写类似下面的代码: Var oCar=new Object(); oCar.color="red"; oCar.doors=4; oCar.mpg=23; oCar.showColor=function(){alert(this.color);}; 在这段代码中,创建对象car。然后给它设置几个属性:它的颜色是红色,有四个门,每加仑油23英里。最后一个属性是指向函数的指针,意味着该属性其实是个方法。执行这段代码后,就可以使用对象car了。可是要创建多个car实例就麻烦了。 要解决此问题,开发者可以创建并返回特定类型的对象的工厂函数。例如,函数CreateCar()可用于封装前面列出的创建car对象的操作: Function createCar() { Var oTempCar=new Object(); oTempCar.color="red"; oTempCar.doors=4; oTempCar.mpg=23; oTempCar.showColor=function(){alert(this.color)}; return oTempCar; } Var oCar1=createCar(); Var oCar2=createCar(); 这里,前面的所有代码都包含在createCar()函数中,此外还有一行额外的代码,返回Car对象作为函数值。调用此函数时,将创建新对象,并赋予它所有必要的属性,复制出一个前面说明的car对象。使用该方法,可以容易地创建car对象的两个版本,他们的属性完全一样。当然,还可以修改creatCar()函数,给它传递各个属性的默认值,而不是赋予属性默认值: Function createCar(sColor,iDoors,iMpg) { Var oTempCar=new Object(); oTempCar.color= sColor; oTempCar.doors= iDoors; oTempCar.mpg= iMpg; oTempCar.showColor=function(){alert(this.color)}; return oTempCar; } Var oCar1=createCar("red",4,23); Var oCar2=createCar("blue",2,26); oCar1.showColor(); // 输出"red" oCar2.showColor(); // 输出"blue" 给createCar()函数加上参数,即可为要创建的car对象的color、doors和mpg属性赋值。使这两个对象具有相同的属性,却有不同的属性值。但这里有个问题:每次调用函数createCar(),都要创建新函数showColor(),意味着每个对象都有自己的showColor()版本。事实上,每个对象用的都是同一段代码。这样的定义方法还有一个如下的变形: Function Car(sColor,iDoors,iMpg) { this.color= sColor; this.doors= iDoors; this.mpg= iMpg; this.showColor=function(){alert(this.color)}; } Var oCar1=new Car("red",4,23); Var oCar2=new Car("blue",2,26); oCar1.showColor(); // 输出"red" oCar2.showColor(); // 输出"blue" 这个方法和上一个方法有个一样的缺陷:重复的创建了showColor()函数。为了解决这个缺陷我们可以用下面的方法。 4.2 原型方式 该方法利用了对象的Prototype属性。用空构造函数来设置类名,然后所有的属性和方法都被直接赋予prototype属性: Function Car() {} Car.prototype.color="red"; Car.prototype.doors=4; Car.prototype.mpg=23; Car.prototype.showColor=function(){alert(this.color)}; Var oCar1=new Car(); Var oCar2=new Car(); 使用这个方法可以解决重复创建showColor()函数,但又会有新的问题,考虑下面的例子: Function Car() {} Car.prototype.color="red"; Car.prototype.doors=4; Car.prototype.mpg=23; Car.prototype.drivers=new Array("Mike","Sue"); Car.prototype.showColor=function(){alert(this.color)}; Var oCar1=new Car(); Var oCar2=new Car(); oCar1.drivers.push("Matt"); alert(oCar1.drivers); // 输出"Mike,Sue,Matt" alert(oCar2.drivers); // 输出"Mike,Sue,Matt" 这里,属性drivers是指向Array对象的指针。改变指针指向的内容,所有的实例都会改变。看来这种方法也不可取 4.3 混合方式 这种方式就是用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果所有的函数只创建一次,而每个对象都具有自己的对象属性实例。 Function Car(sColor,iDoors,iMpg) { this.color= sColor; this.doors= iDoors; this.mpg= iMpg; Car.drivers=new Array("Mike","Sue"); } Car.prototype.showColor=function(){alert(this.color)}; Var oCar1=new Car("red",4,23); Var oCar2=new Car("blue",3,25); oCar1.drivers.push("Matt"); alert(oCar1.drivers); // 输出"Mike,Sue,Matt" alert(oCar2.drivers); // 输出"Mike,Sue" 这种方式是ECMAScript主要采用的方式,它具有其他方式的特性,却没有它们的缺陷。在实际编程中应用的也是最多了。 另外还有JSON创建方式。其创建的方式如下: var Car = { color: "red", doors: 4, mpg: 23, drivers: [{name: "Mike", age: 20, Married: false}, {name: "Sue", age: 30, Marred: true}], showColor: function() {alert(this.color)} }; 这种创建对象的方式也比较优雅。可作为Ajax返回的文本,然后用eval()函数将其还原成一个对象。著名的脚本框架JQuery就有专门接收返回文本为JSON对象的异步方法。
前些天写了一篇关于JavaScript基础的帖子"JavaScript基础之对象"。在那篇帖子中说马上就会写一些关于继承方面的东西。可是上周杂事太多,都没有时间动笔,就一直拖到今天才终于下笔写了。上一篇帖子中很多人认为我自己的东西太少了,所以这篇帖子中在说完基础知识之后我会给出一个完整的例子。 大家都知道面向对象语言的三大特性为:封装、继承、多态。面向对象的语言之所以强大就全依赖于这三大特性。脚本作为一种准面向对象的语言虽然有先天的不足,但还是能在某种程度上去实现这些特性。实现最多、技术最成熟的就数继承了,不过脚本也不能像C#一样简单地用一个":"就能实现继承,而是要自己去实现继承的细节。 1 继承的几种实现方法 要实现继承首先就必须要有子类、父类(超类、基类等叫法)。子类去继承父类的方法和属性。在脚本里所有开发人员定义的类都可以作为父类,不过本地类(native object)和宿主类(host object)不能作为父类。由于脚本里面没有访问修饰符,父类所有的属性和方法都可以被子类直接访问。子类也可以扩展父类的属性和方法。 1.1 call()方法 Call方法是脚本的一个内置(build in)方法。官方说明为:调用一个对象的方法,以另一个对象替换当前对象。语法为:call([thisObj[,arg1[, arg2[, [,.argN]]]]])。用这个方法可以很容易地模拟继承的实现: function funCreateElement(sClassName) { this.sAClassName=sClassName; this.createALink=function() { var oA=document.createElement("a"); oA.className=this.sAClassName; return oA; } } 类(funCreateElement)有一个属性(sAClassName)和一个方法(createALink)。它的作用是创建一个DOM元素。这个类使用this关键字给属性赋值,这样就确保了所有的实例指向的都是自己的值。 function funCurrNavi() { // 继承元素创建类 funCreateElement.call(this,"tblNavigatorA"); } 这样就实现了一个最简单的继承了。在funCurrNavi的实例里就可以直接使用funCreateElement类里面的方法了。 var o=new funCurrNavi(); o.createALink(); 1.2 apply()方法 apply方法也是脚本的一个内置(build in)方法。其用法和call对象类似。官方说明为:应用某一对象的一个方法,用另一个对象替换当前对象。语法为:apply([thisObj[,argArray]])。实际上这两个的作用几乎是相同的,要注意的地方是call(thisObj[,arg1[, arg2[,)中的arg参数可以是变量,而apply([thisObj[,argArray]])中的参数为数组集合。由于和call方法就只有参数的不同而已所以就不举例子了。 1.3 原型链 Prototype对象是所有类的源头。如果修改了prototype类的方法则这个修改会反映到所有类的实例里面。比如:在string.prototype里面加一个方法trim(),String.prototype.trim=function(){}。那么所有的字符串都会具有trim()方法了("aa".trim())。由于prototype的这个特性,我们就可以实现继承方法了。如果用原型链的方式重定义前面的例子,它们将变为下面的形式: function funCreateElement() { } funCreateElement.prototype.sAClassName=sClassName; // 样式类 funCreateElement.prototype.createALink=function() { var oA=document.createElement("a"); oA.className=this.sAClassName; return oA; } function funCurrNavi() { } funCurrNavi.prototype=new funCreateElement(); funCurrNavi.prototype.showMeInDiv=function() { } 这种方式采用funCurrNavi.prototype=new funCreateElement()方法,将funCreateElement类里面所有的属性和方法都复制到funCurrNavi类里面。非常的快捷。这种方式虽然非常的快捷,但是也有其缺点。采用这种方式时子类所有的属性和方法都必须在funCurrNavi.prototype=new funCreateElement()语句之后。因为该语句会覆盖funCurrNavi类原有的prototype类,添加了新的属性和方法的原有prototype类将被销毁。而且父类和子类都没有参数,只能通过设置属性的方法传入参数。 1.4 混合方式 混合方式就是指将前两种方式结合起来使用,取长避短。在call方式里面定义类的方式不理想,相同的方法在实例里面被重复的实例化了。原型链的方法则因为没有参数而不理想,使用参数前得额外的去设置其属性,不如构造函数的参数来的方便。根据上一篇帖子中定义类的混合方式,我们重新定义funCreateElement类: function funCreateElement(sClassName) { this.sAClassName=sClassName; } funCreateElement.prototype.createALink=function() { var oA=document.createElement("a"); oA.className=this.sAClassName; return oA; } 然后在funCurrNavi类里面采用call方式继承funCreateElement类的属性,采用原型链的方式继承其方法。如下: function funCurrNavi(sLinkText) { funCreateElement.call(this,"tblNavigatorA"); this.LinkText=sLinkText; } funCurrNavi.prototype=new funCreateElement(); // 继承超链接类 funCurrNavi.prototype.showMeInDiv=function() { var oLink=this.createALink(); oLink.text=this.LinkText; } 采用funCreateElement.call(this,"tblNavigatorA")方式继承其参数,funCurrNavi.prototype=new funCreateElement()方式继承其方法。从而达到方法不会被实例化多次的目的。 2 实例 前面讲述了几种继承的实现方式,这一小节就利用前面讲述的方法去练习一个小例子。这个例子是根据一个字符串去呈现一个树状的列表,这种需求常见于页面的导航中。由于只是为了辅助讲述继承,例子有些地方不免有些牵强附会和小题大做的地方。这些就希望各位网友包涵了,请各位珍惜自己的砖头了。我在这里戴上钢盔等候大家的批评了。:) 下面是其运行成功的样图: 左面的那个是用table作为容器,右边的那个是用div作为容器。 <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Create Admin Pages’ Navigator</title> <style type="text/css"> /**//*导航table的样式*/.tblNavigator { border: 2px solid #FFD7AC; width: 200px; margin: 3px; background: #fff; margin-top: 5px; margin-bottom: 5px; border-collapse: collapse; } /**//*每个根节点的样式*/.tblNavigatorRoot { height: 30px; line-height: 30px; text-align: left; vertical-align: middle; } /**//*根节点的子节点的样式*/.tblNavigatorTD { border-collapse: collapse; border: 1px solid #FFE6CB; height: 22px; text-indent: 15px; background: #fff; width: 100%; } /**//*导航中的超链接*/.tblNavigatorA { font-weight: 700; text-decoration: none; color: Black; width: 100px; } </style> </head> <body> <div id="divMain"> </div> <div id="divMain2" class="tblNavigator"> </div> <script type="text/javascript"> var sNavigatorMain = { sNavigators:[ { root:[{name:’root1’}], sons:[{name:’root1Son1’,url:’bcc’},{name:’root1Son2’,url:’bcc’}] }, { root:[{name:’root2’}], sons:[{name:’root2Son1’,url:’bcc’}] }, { root:[{name:’root3’}], sons:[{name:’root3Son1’,url:’bcc’},{name:’root3Son2’,url:’bcc’},{name:’root3Son3’,url:’bcc’}] }, { root:[{name:’root4’}], sons:[{name:’root4Son1’,url:’bcc’},{name:’root4Son3’,url:’bcc’}] } , { root:[{name:’root4’}], sons:[{name:’root4Son1’,url:’bcc’},{name:’root4Son3’,url:’bcc’}] } ] }; /**//* * 创建一个元素 * sClassName:元素的样式控制类 */ function funCreateElement(sClassName) { // 样式类 this.sAClassName=sClassName; } /**//* * 创建超链接元素 * sText:超链接显示的文本 * sUrl:超链接的href * sTarget:打开链接的目标 * 返回:超链接元素 */ funCreateElement.prototype.createALink=function(sText,sUrl,sTarget) { var oA=document.createElement("a"); oA.href=sUrl; // 链接文本 oA.className=this.sAClassName; // 样式类 oA.target=sTarget; // 打开链接的目标 oA.appendChild(document.createTextNode(sText)); // 显示文本 return oA; } /**//* * 创建Div元素 * sText:显示的文本 * 返回:Div元素 */ funCreateElement.prototype.createDivBlank=function() { var oDiv=document.createElement("div"); oDiv.className=this.sAClassName; return oDiv; } /**//* * 创建Div元素 * sText:显示的文本 * 返回:Div元素 */ funCreateElement.prototype.createDivNoLink=function(sText) { var oDiv=document.createElement("div"); oDiv.className=this.sAClassName; oDiv.appendChild(document.createTextNode(sText)); return oDiv; } /**//* * 创建一个模块的节点 * oNaviJSON:一个模块节点的文本 * oParentObject:父节点 * sClassNameRoot:根节点的样式类 * sClassNameNode:子节点的样式类 */ function funCurrNavi(oNaviJSON,oParentObject,sClassNameRoot,sClassNameNode) { // 继承元素创建类 funCreateElement.call(this,"tblNavigatorA"); // 当前导航的菜单 this.oNavi=oNaviJSON; // 当前导航的父节点 this.oParent=oParentObject; // 根节点的样式类 this.oNClassNameRoot=sClassNameRoot; // 子节点的样式类 this.oNClassNameNode=sClassNameNode; } // 继承超链接类 funCurrNavi.prototype=new funCreateElement(); // 显示自己(在Table容器里) funCurrNavi.prototype.showMeInTable=function() { // 在父容器中添加一行,并将父节点写入 this.oParent.insertRow(0); this.oParent.rows[0].onclick=function(){alert(this.innerText)}; this.oParent.rows[0].insertCell(0); this.oParent.rows[0].cells[0].className=this.oNClassName; this.oParent.rows[0].cells[0].appendChild(document.createTextNode(this.oNavi.root[0].name)); var j=this.oNavi.sons.length; for(var i=0;i<j;i++) { this.oParent.insertRow(i+1); this.oParent.rows[i+1].insertCell(0); this.oParent.rows[i+1].cells[0].className=this.oNClassNameNode; this.oParent.rows[i+1].cells[0].appendChild(this.createALink(this.oNavi.sons[i].name,this.oNavi.sons[i].url,"_blank")); } } // 显示自己(在Div容器里) 返回:div funCurrNavi.prototype.showMeInDiv=function() { // 创建一个暂时的容器 var oDivContainer=document.createElement("div"); // 在父容器中将父节点写入 var oDivRoot=this.createDivNoLink(this.oNavi.root[0].name); oDivRoot.className=this.oNClassNameRoot; oDivRoot.onclick=function(){alert(this.innerText);}; oDivContainer.appendChild(oDivRoot); // 链接节点 var oDivLinkNode; var j=this.oNavi.sons.length; for(var i=0;i<j;i++) { oDivLinkNode=this.createDivBlank(); oDivLinkNode.appendChild(this.createALink(this.oNavi.sons[i].name,this.oNavi.sons[i].url,"_blank")); oDivLinkNode.className=this.oNClassNameNode; oDivContainer.appendChild(oDivLinkNode); } return oDivContainer; } /**//* * 创建所有的节点 * oNavisJSON:所有节点的文本 */ function funCurrNavis(oNavisJSON,sClassName) { // 当前导航的菜单 this.oNavis=oNavisJSON; // 样式类 this.oNsClassName=sClassName; } // 创建所有的节点(以Table为容器) funCurrNavis.prototype.createNavigatorsInTable=function() { // 创建父Table var oTable=document.createElement("table"); oTable.className=this.oNsClassName; // 创建tbody var oTBody=document.createElement("tbody"); oTable.appendChild(oTBody); var j=this.oNavis.sNavigators.length; var CNavi; for(var i=j-1;i>=0;i--) { CNavi=new funCurrNavi(this.oNavis.sNavigators[i],oTBody,"tblNavigatorRoot","tblNavigatorTD"); CNavi.showMeInTable(); } return oTable; } // 创建所有的节点(以Div为容器) funCurrNavis.prototype.createNavigatorsInDiv=function() { // 创建容器 var oContainer=document.createDocumentFragment(); var j=this.oNavis.sNavigators.length; var CNavi; for(var i=0;i<j;i++) { CNavi=new funCurrNavi(this.oNavis.sNavigators[i],{},"tblNavigatorRoot","tblNavigatorTD"); oContainer.appendChild(CNavi.showMeInDiv()); } return oContainer; } /**//* * 供调用的函数 * sNavigatorMain:所有节点的文本 * sParentID:导航父容器的ID */ function showNavigator(sNavigatorMain,sParentID,sFlag) { var oTemp=eval("ddkk="+sNavigatorMain); var main=new funCurrNavis(oTemp,"tblNavigator"); if(sFlag=="div") { document.getElementById(sParentID).appendChild(main.createNavigatorsInDiv()); } else { document.getElementById(sParentID).appendChild(main.createNavigatorsInTable()); } } </script> <script type="text/javascript"> // showNavigator("{sNavigators:[{root:[{name:’用户管理’}],sons:[{name:’个人信息管理’,url:’~/UserMng/frmMyInfoMng.aspx’},{name:’本单位用户管理’,url:’~/UserMng/frmUserMng.aspx’},{name:’本单位信息管理’,url:’~/SysMaintenance/frmDeptInfo.aspx’}]},{root:[{name:’申报管理’}],sons:[{name:’项目管理’,url:’~/Declaration/frmProjAppMng.aspx’}]},{root:[{name:’留言板管理’}],sons:[{name:’给管理员留言’,url:’~/Information/frmQuestion.aspx’}]}]}","divMain","table"); //showNavigator("{sNavigators:[{root:[{name:’用户管理’}],sons:[{name:’个人信息管理’,url:’~/UserMng/frmMyInfoMng.aspx’},{name:’本单位用户管理’,url:’~/UserMng/frmUserMng.aspx’},{name:’本单位信息管理’,url:’~/SysMaintenance/frmDeptInfo.aspx’}]},{root:[{name:’申报管理’}],sons:[{name:’项目管理’,url:’~/Declaration/frmProjAppMng.aspx’}]},{root:[{name:’留言板管理’}],sons:[{name:’给管理员留言’,url:’~/Information/frmQuestion.aspx’}]}]}","divMain2","div"); </script> </body> </html> 在这个实例里面采用了JSON字符串作为数据源"{sNavigators:[{root:[{name:’用户管理’}],sons:[{name:’个人信息管理’,url:’~/UserMng/frmMyInfoMng.aspx’},{name:’本单位用户管理’,url:’~/UserMng/frmUserMng.aspx’},{name:’本单位信息管理’,url:’~/SysMaintenance/frmDeptInfo.aspx’}]},{root:[{name:’申报管理’}],sons:[{name:’项目管理’,url:’~/Declaration/frmProjAppMng.aspx’}]},{root:[{name:’留言板管理’}],sons:[{name:’给管理员留言’,url:’~/Information/frmQuestion.aspx’}]}]}"。其格式好了的形式类似于上面例子中的 var sNavigatorMain = { sNavigators:[ { root:[{name:’root1’}], sons:[{name:’root1Son1’,url:’bcc’},{name:’root1Son2’,url:’bcc’}] }, { root:[{name:’root2’}], sons:[{name:’root2Son1’,url:’bcc’}] }, { root:[{name:’root3’}], sons:[{name:’root3Son1’,url:’bcc’},{name:’root3Son2’,url:’bcc’},{name:’root3Son3’,url:’bcc’}] }, { root:[{name:’root4’}], sons:[{name:’root4Son1’,url:’bcc’},{name:’root4Son3’,url:’bcc’}] } , { root:[{name:’root4’}], sons:[{name:’root4Son1’,url:’bcc’},{name:’root4Son3’,url:’bcc’}] } ] }; 用JSON字符串作为数据源非常的方便,他能直接转化成类。上面的例子非常简单就不详细地说明了,通过注释就能看明白了。
如果您知道内存泄漏的起因,那么在 JavaScript 中进行相应的防范就应该相当容易。在这篇文章中,作者 Kiran Sundar 和 Abhijeet Bhattacharya 将带您亲历 JavaScript 中的循环引用的全部基本知识,向您介绍为何它们会在某些浏览器中产生问题,尤其是在结合了闭包的情况下。在了解了您应该引起注意的常见内存泄漏模式之后,您还将学到应对这些泄漏的诸多方法。 JavaScript 是用来向 Web 页面添加动态内容的一种功能强大的脚本语言。它尤其特别有助于一些日常任务,比如验证密码和创建动态菜单组件。JavaScript 易学易用,但却很容易在某些浏览器中引起内存的泄漏。在这个介绍性的文章中,我们解释了 JavaScript 中的泄漏由何引起,展示了常见的内存泄漏模式,并介绍了如何应对它们。 注意本文假设您已经非常熟悉使用 JavaScript 和 DOM 元素来开发 Web 应用程序。本文尤其适合使用 JavaScript 进行 Web 应用程序开发的开发人员,也可供有兴趣创建 Web 应用程序的客户提供浏览器支持以及负责浏览器故障排除的人员参考。 我的浏览器存在泄漏么? Internet Explorer 和 Mozilla Firefox 是两个与 JavaScript 中的内存泄漏联系最为紧密的浏览器。两个浏览器中造成这种问题的“罪魁祸首”是用来管理 DOM 对象的组件对象模型。本机 Windows COM 和 Mozilla's XPCOM 都使用引用计数的垃圾收集来进行内存分配和检索。引用计数与用于 JavaScript 的标记-清除式的垃圾收集并不总是能相互兼容。本文侧重介绍的是如何应对 JavaScript 代码中的内存泄漏。有关如何处理 Firefox 和 IE 中 COM 层内存泄漏的更多信息,请参看 参考资料。 JavaScript 中的内存泄漏 JavaScript 是一种垃圾收集式语言,这就是说,内存是根据对象的创建分配给该对象的,并会在没有对该对象的引用时由浏览器收回。JavaScript 的垃圾收集机制本身并没有问题,但浏览器在为 DOM 对象分配和恢复内存的方式上却有些出入。 Internet Explorer 和 Mozilla Firefox 均使用引用计数来为 DOM 对象处理内存。在引用计数系统,每个所引用的对象都会保留一个计数,以获悉有多少对象正在引用它。如果计数为零,该对象就会被销毁,其占用的内存也会返回给堆。虽然这种解决方案总的来说还算有效,但在循环引用方面却存在一些盲点。 循环引用的问题何在? 当两个对象互相引用时,就构成了循环引用,其中每个对象的引用计数值都被赋 1。在纯垃圾收集系统中,循环引用问题不大:若涉及到的两个对象中的一个对象被任何其他对象引用,那么这两个对象都将被垃圾收集。而在引用计数系统,这两个对象都不能被销毁,原因是引用计数永远不能为零。在同时使用了垃圾收集和引用计数的混合系统中,将会发生泄漏,因为系统不能正确识别循环引用。在这种情况下,DOM 对象和 JavaScript 对象均不能被销毁。清单 1 显示了在 JavaScript 对象和 DOM 对象间存在的一个循环引用。 清单 1. 循环引用导致了内存泄漏 <html> <body> <script type="text/javascript"> document.write("circular references between JavaScript and DOM!"); var obj; window.onload = function(){ obj=document.getElementById("DivElement"); document.getElementById("DivElement").expandoProperty=obj; obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX")); }; </script> <div id="DivElement">Div Element</div> </body> </html> 如上述清单中所示,JavaScript 对象 obj 拥有到 DOM 对象的引用,表示为 DivElement。而 DOM 对象则有到此 JavaScript 对象的引用,由 expandoProperty 表示。可见,JavaScript 对象和 DOM 对象间就产生了一个循环引用。由于 DOM 对象是通过引用计数管理的,所以两个对象将都不能销毁。 另一种内存泄漏模式 在清单 2 中,通过调用外部函数 myFunction 创建循环引用。同样,JavaScript 对象和 DOM 对象间的循环引用也会导致内存泄漏。 清单 2. 由外部函数调用引起的内存泄漏 <html> <head> <script type="text/javascript"> document.write(" object s between JavaScript and DOM!"); function myFunction(element) { this.elementReference = element; // This code forms a circular reference here //by DOM-->JS-->DOM element.expandoProperty = this; } function Leak() { //This code will leak new myFunction(document.getElementById("myDiv")); } </script> </head> <body onload="Leak()"> <div id="myDiv"></div> </body> </html> 正如这两个代码示例所示,循环引用很容易创建。在 JavaScript 最为方便的编程结构之一:闭包中,循环引用尤其突出。 回页首 JavaScript 中的闭包 JavaScript 的过人之处在于它允许函数嵌套。一个嵌套的内部函数可以继承外部函数的参数和变量,并由该外部函数私有。清单 3 显示了内部函数的一个示例。 清单 3. 一个内部函数 function parentFunction(paramA) { var a = paramA; function childFunction() { return a + 2; } return childFunction(); } JavaScript 开发人员使用内部函数来在其他函数中集成小型的实用函数。如清单 3 所示,此内部函数 childFunction 可以访问外部函数 parentFunction 的变量。当内部函数获得和使用其外部函数的变量时,就称其为一个闭包。 了解闭包 考虑如清单 4 所示的代码片段。 清单 4. 一个简单的闭包 <html> <body> <script type="text/javascript"> document.write("Closure Demo!!"); window.onload= function closureDemoParentFunction(paramA) { var a = paramA; return function closureDemoInnerFunction (paramB) { alert( a +" "+ paramB); }; }; var x = closureDemoParentFunction("outer x"); x("inner x"); </script> </body> </html> 在上述清单中,closureDemoInnerFunction 是在父函数 closureDemoParentFunction 中定义的内部函数。当用外部的 x 对 closureDemoParentFunction 进行调用时,外部函数变量 a 就会被赋值为外部的 x。函数会返回指向内部函数 closureDemoInnerFunction 的指针,该指针包括在变量 x 内。 外部函数 closureDemoParentFunction 的本地变量 a 即使在外部函数返回时仍会存在。这一点不同于 C/C++ 这样的编程语言,在 C/C++ 中,一旦函数返回,本地变量也将不复存在。在 JavaScript 中,在调用 closureDemoParentFunction 的时候,带有属性 a 的范围对象将会被创建。该属性包括值 paramA,又称为“外部 x”。同样地,当 closureDemoParentFunction 返回时,它将会返回内部函数 closureDemoInnerFunction,该函数包括在变量 x 中。 由于内部函数持有到外部函数的变量的引用,所以这个带属性 a 的范围对象将不会被垃圾收集。当对具有参数值 inner x 的 x 进行调用时,即 x("inner x"),将会弹出警告消息,表明 “outer x innerx”。 清单 4 简要解释了 JavaScript 闭包。闭包功能非常强大,原因是它们使内部函数在外部函数返回时也仍然可以保留对此外部函数的变量的访问。不幸的是,闭包非常易于隐藏 JavaScript 对象 和 DOM 对象间的循环引用。 回页首 闭包和循环引用 在清单 5 中,可以看到一个闭包,在此闭包内,JavaScript 对象(obj)包含到 DOM 对象的引用(通过 id "element" 被引用)。而 DOM 元素则拥有到 JavaScript obj 的引用。这样建立起来的 JavaScript 对象和 DOM 对象间的循环引用将会导致内存泄漏。 清单 5. 由事件处理引起的内存泄漏模式 <html> <body> <script type="text/javascript"> document.write("Program to illustrate memory leak via closure"); window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction(){ alert("Hi! I will leak"); }; obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX")); // This is used to make the leak significant }; </script> <button id="element">Click Me</button> </body> </html> 回页首 避免内存泄漏 幸好,JavaScript 中的内存泄漏是可以避免的。当确定了可导致循环引用的模式之后,正如我们在上述章节中所做的那样,您就可以开始着手应对这些模式了。这里,我们将以上述的 由事件处理引起的内存泄漏模式 为例来展示三种应对已知内存泄漏的方式。 一种应对 清单 5 中的内存泄漏的解决方案是让此 JavaScript 对象 obj 为空,这会显式地打破此循环引用,如清单 6 所示。 清单 6. 打破循环引用 <html> <body> <script type="text/javascript"> document.write("Avoiding memory leak via closure by breaking the circular reference"); window.onload=function outerFunction(){ var obj = document.getElementById("element"); obj.onclick=function innerFunction() { alert("Hi! I have avoided the leak"); // Some logic here }; obj.bigString=new Array(1000).join(new Array(2000).join("XXXXX")); obj = null; //This breaks the circular reference }; </script> <button id="element">"Click Here"</button> </body> </html> 清单 7 是通过添加另一个闭包来避免 JavaScript 对象和 DOM 对象间的循环引用。 清单 7. 添加另一个闭包 <html> <body> <script type="text/javascript"> document.write("Avoiding a memory leak by adding another closure"); window.onload=function outerFunction(){ var anotherObj = function innerFunction() { // Some logic here alert("Hi! I have avoided the leak"); }; (function anotherInnerFunction(){ var obj = document.getElementById("element"); obj.onclick=anotherObj })(); }; </script> <button id="element">"Click Here"</button> </body> </html> 清单 8 则通过添加另一个函数来避免闭包本身,进而阻止了泄漏。 清单 8. 避免闭包自身 <html> <head> <script type="text/javascript"> document.write("Avoid leaks by avoiding closures!"); window.onload=function() { var obj = document.getElementById("element"); obj.onclick = doesNotLeak; } function doesNotLeak() { //Your Logic here alert("Hi! I have avoided the leak"); } </script> </head> <body> <button id="element">"Click Here"</button> </body> </html>