• 关于Javascript的内存泄漏问题的整理稿


    写了好长时间javascript小功能模块,从来没有关注过内存泄漏问题。记得以前写C++程序的时候,内存泄漏是个严重的问题,我想是时候关注一下了。网上找了篇文章,Mark一下。原文地址:http://www.blogjava.net/tim-wu/archive/2006/05/29/48729.html 

    常规循环引用内存泄漏和Closure内存泄漏 

    要了解javascript的内存泄漏问题,首先要了解的就是javascript的GC原理。 

    我记得原来在犀牛书《JavaScript: The Definitive Guide》中看到过,IE使用的GC算法是计数器,因此只碰到循环 引用就会造成memory leakage。后来一直觉得和观察到的现象很不一致,直到看到Eric的文章,才明白犀牛书的说法没有说得很明确,估计该书成文后IE升级过算法吧。 在IE 6中,对于javascript object内部,jscript使用的是mark-and-sweep算法,而对于javascript object与外部object(包括native object和vbscript object等等)的引用时,IE 6使用的才是计数器的算法。 

    Eric Lippert在http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx一文中提到IE 6中JScript的GC算法使用的是nongeneration mark-and-sweep。对于javascript对算法的实现缺陷,文章如是说: 
    "The benefits of this approach are numerous, but the principle benefit is that circular references are not leaked unless the circular reference involves an object not owned by JScript. " 
    也就是说,IE 6对于纯粹的Script Objects间的Circular References是可以正确处理的,可惜它处理不了的是JScript与Native Object(例如Dom、ActiveX Object)之间的Circular References。 
    所以,当我们出现Native对象(例如Dom、ActiveX Object)与Javascript对象间的循环引用时,内存泄露的问题就出现了。当然,这个bug在IE 7中已经被修复了[http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html]。 

    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp 中有个示意图和简单的例子体现了这个问题: 

    Html代码  收藏代码
    1. html >   
    2.      head >   
    3.          script language = " JScript " >   
    4.   
    5.          var  myGlobalObject;  
    6.   
    7.          function  SetupLeak()  // 产生循环引用,因此会造成内存泄露   
    8.         {  
    9.              //  First set up the script scope to element reference   
    10.             myGlobalObject  =   
    11.                 document.getElementById( " LeakedDiv " );  
    12.   
    13.              //  Next set up the element to script scope reference   
    14.             document.getElementById( " LeakedDiv " ).expandoProperty  =   
    15.                 myGlobalObject;  
    16.         }  
    17.   
    18.   
    19.          function  BreakLeak()  // 解开循环引用,解决内存泄露问题   
    20.         {  
    21.             document.getElementById( " LeakedDiv " ).expandoProperty  =   
    22.                  null ;  
    23.         }  
    24.          </ script >   
    25.      </ head >   
    26.   
    27.      body onload = " SetupLeak() "  onunload = " BreakLeak() " >   
    28.          div id = " LeakedDiv " ></ div >   
    29.      </ body >   
    30. </ html >  


       上面这个例子,看似很简单就能够解决内存泄露的问题。可惜的是,当我们的代码中的结构复杂了以后,造成循环引用的原因开始变得多样,我们就没法那么容易观察到了,这时候,我们必须对代码进行仔细的检查。 
    尤其是当碰到Closure,当我们往Native对象(例如Dom对象、ActiveX Object)上绑定事件响应代码时,一个不小心,我们就会制造出Closure Memory Leak。其关键原因,其实和前者是一样的,也是一个跨javascript object和native object的循环引用。只是代码更为隐蔽,这个隐蔽性,是由于javascript的语言特性造成的。但在使用类似内嵌函数的时候,内嵌的函数有拥有一 个reference指向外部函数的scope,包括外部函数的参数,因此也就很容易造成一个很隐蔽的循环引用,例如: 
    DOM_Node.onevent ->function_object.[ [ scope ] ] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。

    [http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp]有个例子极深刻地显示了该隐蔽性: 

    Html代码  收藏代码
    1. html >   
    2.      head >   
    3.          script language = " JScript " >   
    4.   
    5.          function  AttachEvents(element)  
    6.         {  
    7.              //  This structure causes element to ref ClickEventHandler  //element有个引用指向函数ClickEventHandler()   
    8.             element.attachEvent( " onclick " , ClickEventHandler);  
    9.   
    10.              function  ClickEventHandler()  
    11.             {  
    12.                  //  This closure refs element  //该函数有个引用指向AttachEvents(element)调用Scope,也就是执行了参数element。   
    13.                   
    14.             }  
    15.         }  
    16.   
    17.          function  SetupLeak()  
    18.         {  
    19.              //  The leak happens all at once   
    20.             AttachEvents(document.getElementById( " LeakedDiv " ));  
    21.         }  
    22.   
    23.          </ script >   
    24.      </ head >   
    25.   
    26.      body onload = " SetupLeak() "  onunload = " BreakLeak() " >   
    27.          div id = " LeakedDiv " ></ div >   
    28.      </ body >   
    29. </ html >  


    还有这个例子在IE 6中同样原因会引起泄露 


    Html代码  收藏代码
    1. function  leakmaybe() {  
    2. var  elm  =  document.createElement( " DIV " );  
    3.   elm.onclick  =   function () {  
    4. return   2   +   2 ;  
    5.   }  
    6. }  
    7.   
    8. for  ( var  i  =   0 ; i   10000 ; i ++ ) {  
    9.   leakmaybe();  
    10. }  


    btw: 
    关于Closure的知识,大家可以看看http://jibbering.com/faq/faq_notes/closures.html这篇文章,习惯中文也可以看看zkjbeyond的blog,他对Closure这篇文章进行了简要的翻译:http://www.blogjava.net/zkjbeyond/archive/2006/05/19/47025.html。 之所以会有这一系列的问题,关键就在于javascript是种函数式脚本解析语言,因此javascript中“函数中的变量的作用域是定义作用域,而 不是动态作用域”,这点在犀牛书《JavaScript: The Definitive Guide》中的“Funtion”一章中有所讨论。 
    http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555中也对这个问题举了很详细的例子。 


    一些 简单的解决方案 


    目前大多数ajax前端的javascript framework都利用对事件的管理,解决了该问题。 

    如果你需要自己解决这个问题,可以参考以下的一些方法: 

    http://outofhanwell.com/ieleak/index.php?title=Main_Page:有个不错的检测工具 
    http://youngpup.net/2005/0221010713 中提到:可以利用递归Dom树,解除event绑定,从而解除循环引用: 

    Html代码  收藏代码
    1.                                   
    2. if (window.attachEvent) {  
    3.     var clearElementProps = [  
    4.         'data',  
    5.         'onmouseover',  
    6.         'onmouseout',  
    7.         'onmousedown',  
    8.         'onmouseup',  
    9.         'ondblclick',  
    10.         'onclick',  
    11.         'onselectstart',  
    12.         'oncontextmenu'  
    13.     ];  
    14.   
    15.     window.attachEvent("onunload", function() {  
    16.         var el;  
    17.         for(var d = document.all.length;d--;){  
    18.             el = document.all[d];  
    19.             for(var c = clearElementProps.length;c--;){  
    20.                 el[clearElementProps[c]] = null;  
    21.             }  
    22.         }  
    23.     });  
    24. }                                                 


    而http://novemberborn.net/javascript/event-cache一文中则通过增加EventCache,从而给出一个相对结构化的解决方案 

    Html代码  收藏代码
    1. /*     EventCache Version 1.0  
    2.     Copyright 2005 Mark Wubben  
    3.   
    4.     Provides a way for automagically removing events from nodes and thus preventing memory leakage.  
    5.     See <http://novemberborn.net/javascript/event-cache> for more information.  
    6.       
    7.     This software is licensed under the CC-GNU LGPL <http://creativecommons.org/licenses/LGPL/2.1/>  
    8. */   
    9.   
    10. /*     Implement array.push for browsers which don't support it natively.  
    11.     Please remove this if it's already in other code  */   
    12. if (Array.prototype.push  ==   null ){  
    13.     Array.prototype.push  =   function (){  
    14.          for ( var  i  =   0 ; i  <  arguments.length; i ++ ){  
    15.              this [ this .length]  =  arguments[i];  
    16.         };  
    17.          return   this .length;  
    18.     };  
    19. };  
    20.   
    21. /*     Event Cache uses an anonymous function to create a hidden scope chain.  
    22.     This is to prevent scoping issues.  */   
    23. var  EventCache  =   function (){  
    24.      var  listEvents  =  [];  
    25.       
    26.      return  {  
    27.         listEvents : listEvents,  
    28.       
    29.         add :  function (node, sEventName, fHandler, bCapture){  
    30.             listEvents.push(arguments);  
    31.         },  
    32.       
    33.         flush :  function (){  
    34.              var  i, item;  
    35.              for (i  =  listEvents.length  -   1 ; i  >=   0 ; ii  =  i  -   1 ){  
    36.                 item  =  listEvents[i];  
    37.                   
    38.                  if (item[ 0 ].removeEventListener){  
    39.                     item[ 0 ].removeEventListener(item[ 1 ], item[ 2 ], item[ 3 ]);  
    40.                 };  
    41.                   
    42.                  /*  From this point on we need the event names to be prefixed with 'on"  */   
    43.                  if (item[ 1 ].substring( 0 ,  2 )  !=   " on " ){  
    44.                     item[ 1 ]  =   " on "   +  item[ 1 ];  
    45.                 };  
    46.                   
    47.                  if (item[ 0 ].detachEvent){  
    48.                     item[ 0 ].detachEvent(item[ 1 ], item[ 2 ]);  
    49.                 };  
    50.                   
    51.                 item[ 0 ][item[ 1 ]]  =   null ;  
    52.             };  
    53.         }  
    54.     };  
    55. }();  


    使用方法也很简单: 

    Html代码  收藏代码
    1.                                                   
    2. <script type="text/javascript">  
    3. function addEvent(oEventTarget, sEventType, fDest){  
    4.         if(oEventTarget.attachEvent){  
    5.         oEventTarget.attachEvent("on" + sEventType, fDest);  
    6.     } elseif(oEventTarget.addEventListener){  
    7.         oEventTarget.addEventListener(sEventType, fDest, true);   
    8.     } elseif(typeof oEventTarget[sEventType] == "function"){  
    9.                 var fOld = oEventTarget[sEventType];  
    10.         oEventTarget[sEventType] = function(e){ fOld(e); fDest(e); };  
    11.     } else {  
    12.         oEventTarget[sEventType] = fDest;  
    13.     };  
    14.   
    15.     /* Implementing EventCache for all event systems */  
    16.     EventCache.add(oEventTarget, sEventType, fDest, true);  
    17. };  
    18.   
    19.   
    20. function createLeak(){  
    21.          var body = document.body;  
    22.   
    23.     function someHandler(){  
    24.                return body;  
    25.     };  
    26.   
    27.     addEvent(body, "click", someHandler);  
    28. };  
    29.   
    30. window.onload = function(){  
    31.        var i = 500;  
    32.        while(i > 0){  
    33.         createLeak();  
    34.         ii = i - 1;  
    35.     }  
    36. };  
    37.   
    38. window.onunload = EventCache.flush;  
    39. </script>                                                   


    http://talideon.com/weblog/2005/03/js-memory-leaks.cfm 一文中的方法类似: 

    Html代码  收藏代码
    1. /*   
    2.  * EventManager.js  
    3.  * by Keith Gaughan  
    4.  *  
    5.  * This allows event handlers to be registered unobtrusively, and cleans  
    6.  * them up on unload to prevent memory leaks.  
    7.  *  
    8.  * Copyright (c) Keith Gaughan, 2005.  
    9.  *  
    10.  * All rights reserved. This program and the accompanying materials  
    11.  * are made available under the terms of the Common Public License v1.0  
    12.  * (CPL) which accompanies this distribution, and is available at  
    13.  * http://www.opensource.org/licenses/cpl.php  
    14.  *  
    15.  * This software is covered by a modified version of the Common Public License  
    16.  * (CPL), where Keith Gaughan is the Agreement Steward, and the licensing  
    17.  * agreement is covered by the laws of the Republic of Ireland.  
    18.   */   
    19.   
    20. //  For implementations that don't include the push() methods for arrays.   
    21. if  ( ! Array.prototype.push) {  
    22.     Array.prototype.push  =   function (elem) {  
    23.          this [ this .length]  =  elem;  
    24.     }  
    25. }  
    26.   
    27. var  EventManager  =  {  
    28.     _registry:  null ,  
    29.   
    30.     Initialise:  function () {  
    31.          if  ( this ._registry  ==   null ) {  
    32.              this ._registry  =  [];  
    33.   
    34.              //  Register the cleanup handler on page unload.   
    35.             EventManager.Add(window,  " unload " ,  this .CleanUp);  
    36.         }  
    37.     },  
    38.   
    39.      /* *  
    40.      * Registers an event and handler with the manager.  
    41.      *  
    42.      * @param  obj         Object handler will be attached to.  
    43.      * @param  type        Name of event handler responds to.  
    44.      * @param  fn          Handler function.  
    45.      * @param  useCapture  Use event capture. False by default.  
    46.      *                     If you don't understand this, ignore it.  
    47.      *  
    48.      * @return True if handler registered, else false.  
    49.       */   
    50.     Add:  function (obj, type, fn, useCapture) {  
    51.          this .Initialise();  
    52.   
    53.          //  If a string was passed in, it's an id.   
    54.          if  ( typeof  obj  ==   " string " ) {  
    55.             obj  =  document.getElementById(obj);  
    56.         }  
    57.          if  (obj  ==   null   ||  fn  ==   null ) {  
    58.              return   false ;  
    59.         }  
    60.   
    61.          //  Mozilla/W3C listeners?   
    62.          if  (obj.addEventListener) {  
    63.             obj.addEventListener(type, fn, useCapture);  
    64.              this ._registry.push({obj: obj, type: type, fn: fn, useCapture: useCapture});  
    65.              return   true ;  
    66.         }  
    67.   
    68.          //  IE-style listeners?   
    69.          if  (obj.attachEvent  &&  obj.attachEvent( " on "   +  type, fn)) {  
    70.              this ._registry.push({obj: obj, type: type, fn: fn, useCapture:  false });  
    71.              return   true ;  
    72.         }  
    73.   
    74.          return   false ;  
    75.     },  
    76.   
    77.      /* *  
    78.      * Cleans up all the registered event handlers.  
    79.       */   
    80.     CleanUp:  function () {  
    81.          for  ( var  i  =   0 ; i  <  EventManager._registry.length; i ++ ) {  
    82.              with  (EventManager._registry[i]) {  
    83.                  //  Mozilla/W3C listeners?   
    84.                  if  (obj.removeEventListener) {  
    85.                     obj.removeEventListener(type, fn, useCapture);  
    86.                 }  
    87.                  //  IE-style listeners?   
    88.                  else   if  (obj.detachEvent) {  
    89.                     obj.detachEvent( " on "   +  type, fn);  
    90.                 }  
    91.             }  
    92.         }  
    93.   
    94.          //  Kill off the registry itself to get rid of the last remaining   
    95.          //  references.   
    96.         EventManager._registry  =   null ;  
    97.     }  
    98. };  


    使用起来也很简单 


    Html代码  收藏代码
    1. <html>  
    2. <head>  
    3. <script type=text/javascript src=EventManager.js></script>  
    4. <script type=text/javascript>  
    5.     function onLoad() {  
    6.   
    7.     EventManager.Add(document.getElementById(testCase),click,hit );  
    8. returntrue;  
    9.     }  
    10.   
    11.     function hit(evt) {  
    12.         alert(click);  
    13.     }  
    14. </script>  
    15. </head>  
    16.   
    17. <body onload='javascript: onLoad();'>  
    18.   
    19. <div id='testCase' style='100%; height: 100%; yellow;'>  
    20.   <h1>Click me!</h1>  
    21. </div>  
    22.   
    23. </body>  
    24. </html>  



    google map api同样提供了一个类似的函数用在页面的unload事件中,解决Closure带来的内存泄露问题。 
    当然,如果你不嫌麻烦,你也可以为每个和native object有关的就阿vascript object编写一个destoryMemory函数,用来手动调用,从而手动解除Dom对象的事件绑定。 

    还有一种就是不要那么OO,抛弃Dom的一些特性,用innerHTML代替appendChild,避开循环引用。详细见http://birdshome.cnblogs.com/archive/2005/02/16/104967.html中的讨论贴。 

    Cross-Page Leaks 

        Cross-Page Leaks和下一节提到的Pseudo-Leaks在我看来,就是IE的bug, 虽然MS死皮赖脸不承认:) 

         大家可以看看这段例子代码: 

    Html代码  收藏代码
    1. html >   
    2.      head >   
    3.          script language = " JScript " >   
    4.   
    5.          function  LeakMemory()  // 这个函数会引发Cross-Page Leaks   
    6.         {  
    7.              var  hostElement  =  document.getElementById( " hostElement " );  
    8.   
    9.              //  Do it a lot, look at Task Manager for memory response   
    10.   
    11.              for (i  =   0 ; i  <   5000 ; i ++ )  
    12.             {  
    13.                  var  parentDiv  =   
    14.                     document.createElement( " <div onClick='foo()'> " );  
    15.                  var  childDiv  =   
    16.                     document.createElement( " <div onClick='foo()'> " );  
    17.   
    18.                  //  This will leak a temporary object   
    19.                 parentDiv.appendChild(childDiv);  
    20.                 hostElement.appendChild(parentDiv);  
    21.                 hostElement.removeChild(parentDiv);  
    22.                 parentDiv.removeChild(childDiv);  
    23.                 parentDiv  =   null ;  
    24.                 childDiv  =   null ;  
    25.             }  
    26.             hostElement  =   null ;  
    27.         }  
    28.   
    29.   
    30.          function  CleanMemory()  // 而这个函数不会引发Cross-Page Leaks   
    31.         {  
    32.              var  hostElement  =  document.getElementById( " hostElement " );  
    33.   
    34.              //  Do it a lot, look at Task Manager for memory response   
    35.   
    36.              for (i  =   0 ; i  <   5000 ; i ++ )  
    37.             {  
    38.                  var  parentDiv  =   document.createElement( " <div onClick='foo()'> " );  
    39.                  var  childDiv  =   document.createElement( " <div onClick='foo()'> " );  
    40.   
    41.                  //  Changing the order is important, this won't leak   
    42.                 hostElement.appendChild(parentDiv);  
    43.                 parentDiv.appendChild(childDiv);  
    44.                 hostElement.removeChild(parentDiv);  
    45.                 parentDiv.removeChild(childDiv);  
    46.                 parentDiv  =   null ;  
    47.                 childDiv  =   null ;  
    48.             }  
    49.             hostElement  =   null ;  
    50.         }  
    51.          </ script >   
    52.      </ head >   
    53.   
    54.      body >   
    55.          button onclick = " LeakMemory() " > Memory Leaking Insert </ button >   
    56.          button onclick = " CleanMemory() " > Clean Insert </ button >   
    57.          div id = " hostElement " ></ div >   
    58.      </ body >   
    59. </ html >  


    LeakMemory和CleanMemory这两段函数的唯一区别就在于他们的代码的循序,从代码上看,两段代码的逻辑都没有错。 

    但LeakMemory却会造成泄露。原因是LeakMemory()会先建立起parentDiv和childDiv之间的连接,这时候,为了让 childDiv能够获知parentDiv的信息,因此IE需要先建立一个临时的scope对象。而后parentDiv建立了和 hostElement对象的联系,parentDiv和childDiv直接使用页面document的scope。可惜的是,IE不会释放刚才那个临 时的scope对象的内存空间,直到我们跳转页面,这块空间才能被释放。而CleanMemory函数不同,他先把parentDiv和 hostElement建立联系,而后再把childDiv和parentDiv建立联系,这个过程不需要单独建立临时的scope,只要直接使用页面 document的scope就可以了, 所以也就不会造成内存泄露了 

    详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。 

    btw: 
    IE 6中垃圾回收算法,就是从那些直接"in scope"的对象开始进行mark清除的: 
    Every variable which is "in scope" is called a "scavenger". A scavenger may refer to a number, an object, a string, whatever. We maintain a list of scavengers – variables are moved on to the scav list when they come into scope and off the scav list when they go out of scope. 

    Pseudo-Leaks 

    这个被称为“秀逗泄露”真是恰当啊:) 
    看看这个例子: 

    Html代码  收藏代码
    1. html >   
    2.      head >   
    3.          script language = " JScript " >   
    4.   
    5.          function  LeakMemory()  
    6.         {  
    7.              //  Do it a lot, look at Task Manager for memory response   
    8.   
    9.              for (i  =   0 ; i  <   5000 ; i ++ )  
    10.             {  
    11.                 hostElement.text  =   " function foo() { } " ;//看内存会不断增加  
    12.             }  
    13.         }  
    14.          </ script >   
    15.      </ head >   
    16.   
    17.      body >   
    18.          button onclick = " LeakMemory() " > Memory Leaking Insert </ button >   
    19.          script id = " hostElement " > function  foo() { } </ script >   
    20.      </ body >   
    21. </ html >  


    MS是这么解释的,这不是内存泄漏。如果您创建了许多无法获得也无法释放的对象,那才是内存泄漏。在这里,您将创建许多元素,Internet Explorer 需要保存它们以正确呈现页面。Internet Explorer 并不知道您以后不会运行操纵您刚刚创建的所有这些对象的脚本。当页面消失时(当您浏览完,离开浏览器时)会释放内存。它不会泄漏。当销毁页面时,会中断循 环引用。 

    唉~~~ 

    详细原因,大家可以看看http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp这篇文章。 

    其它一些琐碎的注意点 

    变量定义一定要用var,否则隐式声明出来的变量都是全局变量,不是局部变量; 
    全局变量没用时记得要置null; 
    注意正确使用delete,删除没用的一些函数属性; 
    注意正确使用try...cache,确保去处无效引用的代码能被正确执行; 
    open出来的窗口即使close了,它的window对象还是存在的,要记得删除引用; 
    frame和iframe的情况和窗口的情况类似。 

    参考资料 

    http://jibbering.com/faq/faq_notes/closures.html 
    http://javascript.weblogsinc.com/2005/03/07/javascript-memory-leaks/ 
    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp 
    http://72.14.203.104/search?q=cache:V9Bt4_HBzQ8J:jgwebber.blogspot.com/2005/01/dhtml-leaks-like-sieve.html+DHTML+Leaks+Like+a+Sieve+&hl=zh-CN&ct=clnk&cd=9 (这是DHTML Leaks Like a Sieve)一文在google上的cache,原文已经连不上了) 
    http://spaces.msn.com/siteexperts/Blog/cns!1pNcL8JwTfkkjv4gg6LkVCpw!338.entry 
    http://support.microsoft.com/default.aspx?scid=KB;EN-US;830555 
    http://www.ajaxtopics.com/leakpatterns.html 
    http://blogs.msdn.com/ericlippert/archive/2003/09/17/53028.aspx 
    http://www.quirksmode.org/blog/archives/2005/02/javascript_memo.html 
    http://youngpup.net/2005/0221010713 
    http://blogs.msdn.com/ericlippert/archive/2003/09/17/53038.aspx = 
    http://support.microsoft.com/kb/266071/EN-US ==>IE 5.0至5.5一些版本中的GC bug 
    http://www.quirksmode.org/blog/archives/2006/04/ie_7_and_javasc.html ==>ie 7的改进 
    http://erik.eae.net/archives/2006/04/26/23.23.02/ ==>ie 7的改进 
    http://www.feedbackarchive.com/spamvampire/today.html ==> Try this script for memory leaks - it leaked 50 megabytes in 15 minutes with firefox on linux: 
    http://birdshome.cnblogs.com/archive/2005/02/15/104599.html 
    http://www.quirksmode.org/dom/innerhtml.html 
    http://www.crockford.com/javascript/memory/leak.html 
    《JavaScript: The Definitive Guide》4th Edition 
    http://outofhanwell.com/ieleak/index.php?title=Main_Page

  • 相关阅读:
    定时器应用-最终版
    定时器应用-点击按钮,div向右移动
    通过js读取元素的样式
    延时调用
    定时器应用-切换图片的练习
    BOM对象属性定时器的调用
    BOM浏览器对象模型
    键盘移动
    Python-字符串方法
    Python实现注册和登录
  • 原文地址:https://www.cnblogs.com/ranzige/p/3832860.html
Copyright © 2020-2023  润新知