前端性能优化手册作用域链和闭包优化
上一篇:字符串专题

作用域链和闭包优化

作用域链和闭包优化

  • 作用域。

    • 作用域(scope)是JAVASCRIPT编程中一个重要的运行机制,在JAVASCRIPT同步和异步编程以及JAVASCRIPT内存管理中起着至关重要的作用。
    • JAVASCRIPT中,能形成作用域的有如下几点。

      • 函数的调用
      • with语句
        • with会创建自已的作用域,因此会增加其中执行代码的作用域的长度。
      • 全局作用域。

      以下代码为例:

      var foo = function() {
        var local = {};
      };
      foo();
      console.log(local); //=> undefined
      var bar = function() {
        local = {};
      };
      bar();
      console.log(local); //=> {}
      /**这里我们定义了foo()函数和bar()函数,他们的意图都是为了定义一个名为local的变量。在foo()函数中,我们使用var语句来声明定义了一个local变量,而因为函数体内部会形成一个作用域,所以这个变量便被定义到该作用域中。而且foo()函数体内并没有做任何作用域延伸的处理,所以在该函数执行完毕后,这个local变量也随之被销毁。而在外层作用域中则无法访问到该变量。而在bar()函数内,local变量并没有使用var语句进行声明,取而代之的是直接把local作为全局变量来定义。故外层作用域可以访问到这个变量。**/
      local = {};
      // 这里的定义等效于
      global.local = {};
  • 作用域链

    • JAVASCRIPT编程中,会遇到多层函数嵌套的场景,这就是典型的作用域链的表示。

      function foo() {
        var val = 'hello';
        function bar() {
          function baz() {
            global.val = 'world;'
          };
          baz();
          console.log(val); //=> hello
        };
        bar();
      };
      foo();
      /**在`JAVASCRIPT`中,变量标识符的查找是从当前作用域开始向外查找,直到全局作用域为止。所以`JAVASCRIPT`代码中对变量的访问只能向外进行,而不能逆而行之。baz()函数的执行在全局作用域中定义了一个全局变量val。而在bar()函数中,对val这一标识符进行访问时,按照从内到外的查找原则:在bar函数的作用域中没有找到,便到上一层,即foo()函数的作用域中查找。然而,使大家产生疑惑的关键就在这里:本次标识符访问在foo()函数的作用域中找到了符合的变量,便不会继续向外查找,故在baz()函数中定义的全局变量val并没有在本次变量访问中产生影响。**/
  • 减少作用域链上的查找次数

    • JAVASCRIPT代码在执行的时候,如果需要访问一个变量或者一个函数的时候,它需要遍历当前执行环境的作用域链,而遍历是从这个作用域链的前端一级一级的向后遍历,直到全局执行环境。

      /**效率低**/
      for(var i = 0; i < 10000; i++){
          var but1 = document.getElementById("but1");
      }
      /**效率高**/
      /**避免全局查找**/
      var doc = document;
      for(var i = 0; i < 10000; i++){
          var but1 = doc.getElementById("but1");
      }
      /**上面代码中,第二种情况是先把全局对象的变量放到函数里面先保存下来,然后直接访问这个变量,而第一种情况是每次都遍历作用域链,直到全局环境,我们看到第二种情况实际上只遍历了一次,而第一种情况却是每次都遍历了,而且这种差别在多级作用域链和多个全局变量的情况下还会表现的非常明显。在作用域链查找的次数是`O(n)`。通过创建一个指向`document`的局部变量,就可以通过限制一次全局查找来改进这个函数的性能。**/
  • 闭包

    • JAVASCRIPT中的标识符查找遵循从内到外的原则。

      function foo() {
        var local = 'Hello';
        return function() {
          return local;
        };
      }
      var bar = foo();
      console.log(bar()); //=> Hello
      /**这里所展示的让外层作用域访问内层作用域的技术便是闭包(Closure)。得益于高阶函数的应用,使foo()函数的作用域得到`延伸`。foo()函数返回了一个匿名函数,该函数存在于foo()函数的作用域内,所以可以访问到foo()函数作用域内的local变量,并保存其引用。而因这个函数直接返回了local变量,所以在外层作用域中便可直接执行bar()函数以获得local变量。**/
    • 闭包是JAVASCRIPT的高级特性,因为把带有​​内部变量引用的函数带出了函数外部,所以该作用域内的变量在函数执行完毕后的并不一定会被销毁,直到内部变量的引用被全部解除。所以闭包的应用很容易造成内存无法释放的情况。
    • 良好的闭包管理。

      • 循环事件绑定、私有属性、含参回调等一定要使用闭包时,并谨慎对待其中的细节。

        • 循环绑定事件,我们假设一个场景:有六个按钮,分别对应六种事件,当用户点击按钮时,在指定的地方输出相应的事件。

          var btns = document.querySelectorAll('.btn'); // 6 elements
          var output = document.querySelector('#output');
          var events = [1, 2, 3, 4, 5, 6];
          // Case 1
          for (var i = 0; i < btns.length; i++) {
            btns[i].onclick = function(evt) {
              output.innerText += 'Clicked ' + events[i];
            };
          }
          /**这里第一个解决方案显然是典型的循环绑定事件错误,这里不细说,详细可以参照我给一个网友的回答;而第二和第三个方案的区别就在于闭包传入的参数。**/
          // Case 2
          for (var i = 0; i < btns.length; i++) {
            btns[i].onclick = (function(index) {
              return function(evt) {
                output.innerText += 'Clicked ' + events[index];
              };
            })(i);
          }
          /**第二个方案传入的参数是当前循环下标,而后者是直接传入相应的事件对象。事实上,后者更适合在大量数据应用的时候,因为在JavaScript的函数式编程中,函数调用时传入的参数是基本类型对象,那么在函数体内得到的形参会是一个复制值,这样这个值就被当作一个局部变量定义在函数体的作用域内,在完成事件绑定之后就可以对events变量进行手工解除引用,以减轻外层作用域中的内存占用了。而且当某个元素被删除时,相应的事件监听函数、事件对象、闭包函数也随之被销毁回收。**/
          // Case 3
          for (var i = 0; i < btns.length; i++) {
            btns[i].onclick = (function(event) {
              return function(evt) {
                output.innerText += 'Clicked ' + event;
              };
            })(events[i]);
          }
  • 避开闭包陷阱
    • 闭包是个强大的工具,但同时也是性能问题的主要诱因之一。不合理的使用闭包会导致内存泄漏。
    • 闭包的性能不如使用内部方法,更不如重用外部方法。
      • 由于IE 9浏览器的DOM节点作为COM对象来实现,COM内存管理是通过引用计数的方式,引用计数有个难题就是循环引用,一旦DOM引用了闭包(例如event handler),闭包的上层元素又引用了这个DOM,就会造成循环引用从而导致内存泄漏。
  • 善用函数

    • 使用一个匿名函数在代码的最外层进行包裹。

      ;(function() { // 主业务代码 })();

    有的甚至更高级一点:

    ;(function(win, doc, $, undefined) {
      // 主业务代码
    })(window, document, jQuery);

    甚至连如RequireJS, SeaJS, OzJS 等前端模块化加载解决方案,都是采用类似的形式:

    /**RequireJS**/
    define(['jquery'], function($) {
      // 主业务代码
    });
    /**SeaJS**/
    define('m​​odule', ['dep', 'underscore'], function($, _) {
      // 主业务代码
    });

    被定义在全局作用域的对象,可能是会一直存活到进程退出的,如果是一个很大的对象,那就麻烦了。比如有的人喜欢在JavaScript中做模版渲染:

    
      
      你是猴子请来的逗比么?
      

      在从数据库中获取到的数据的量是非常大的话,前端完成模板渲染以后,data变量便被闲置在一边。可因为这个变量是被定义在全局作用域中的,所以JAVASCRIPT引擎不会将其回收销毁。如此该变量就会一直存在于老生代堆内存中,直到页面被关闭。可是如果我们作出一些很简单的修改,在逻辑代码外包装一层函数,这样效果就大不同了。当UI渲染完成之后,代码对data的引用也就随之解除,而在最外层函数执行完毕时,JAVASCRIPT引擎就开始对其中的对象进行检查,data也就可以随之被回收。

    上一篇:字符串专题