所谓的javascript高级技巧

Js学的也差很少了,该是来总结一下Js中一些比较高级的智慧结晶了。基于Js的动态性、对象都是易变的、函数是第一对象等等其余语言所不包含的特性,能够在使用Js的时候创造出更高效、组织性更好的代码。下面提到的一些概念,是否是很熟悉:html

分支、惰性实例化、惰性载入函数、单例的两种模式、享元类、函数绑定(纠正函数一个执行上下文)、函数curry化、高级定时器、保护上下文的构造函数、函数节流、自定义事件……前端

js中的继承、原型、构造函数这些都是老生常谈的了。可是对于构造函数式继承和原型式继承的优缺点,仍是有必要了解一下的。原型式继承的主要优势就是共享方法和属性,使得从原型对象中继承出来的子对象均可以在内存中共享所有的方法和属性,这若是是在大型的继承链中将会大大的改善性能和减小内存的使用量。node

 

接下来看看上面所罗列的每个所谓的“高级”技巧的具体细节咧:web

  1. 惰性实例化
    惰性实例化所要解决的问题是这样的:避免了在页面中js初始化执行的时候就实例化了类,若是在页面中没有使用到这个实例化的对象,那么这就形成了必定的内存的浪费和性能的消耗,那么若是将一些类的实例化推迟到须要使用它的时候才开始去实例化,那么这就避免了刚才说的问题,作到了“按需供应”,简单代码示例以下:后端

    var myNamespace = function(){
      var Configure = function(){
        var privateName = "someone's name";
        var privateReturnName = function(){
          return privateName;
        }
        var privateSetName = function(name){
          privateName = name;
        }
        //返回单例对象
        return {
          setName:function(name){
            privateSetName(name);
          },
          getName:function(){
            return privateReturnName();
          }
        }
      }
      //储存configure的实例
      var instance;
      return {
        getInstance:function(){
          if(!instance){
            instance = Configure();
          }
          return instance;
        }
      }
    }();
    //使用方法上就须要getInstance这个函数做为中间量了:
    myNamespace.getInstance().getName();浏览器

    上面的就是简单的惰性实例化的示例,可是有一点缺点就是须要使用中间量来调用内部的Configure函数所返回的对象的方法(固然也可使用变量来储存myNamespace.getInstance()返回的实例对象)。将上面的代码稍微修改一下,就能够只用比较得体的方法来使用内部的方法和属性:缓存

    //惰性实例化的变体
    var myNamespace2 = function(){
      var Configure = function(){
        var privateName = "someone's name";
        var privateReturnName = function(){
          return privateName;
        }
        var privateSetName = function(name){
          privateName = name;
        }
        //返回单例对象
        return {
          setName:function(name){
            privateSetName(name);
          },
          getName:function(){
            return privateReturnName();
          }
        }
      }
      //储存configure的实例
      var instance;
      return {
        init:function(){
          //若是不存在实例,就建立单例实例
          if(!instance){
            instance = Configure();
          }
          //将Configure建立的单例
          for(var key in instance){
            if(instance.hasOwnProperty(key)){
              this[key]=instance[key];
            }
          }
          this.init = null;
          return this;
        }
      }
    }();
    //使用方式:
    myNamespace2.init();
    myNamespace2.getName();安全

    上面修改了自执行函数返回的对象的代码,在获取Configure函数返回的对象的时候,将该对象的方法赋给myNamespace2,这样,调用方式就发生了一点改变了。性能优化

  2. 分支
    分支技术解决的一个问题是处理浏览器之间兼容性的重复判断的问题。普通解决浏览器之间的兼容性的方式是使用if逻辑来进行特性检测或者能力检测,来实现根据浏览器不一样的实现来实现功能上的兼容,但问题是,没执行一次代码,可能都须要进行一次浏览器兼容性方面的检测,这个是没有必要的,可否在代码初始化执行的时候就检测浏览器的兼容性,在以后的代码执行过程当中,就无需再进行检测了呢?答案是有的,分支技术就能够解决这个问题(一样,惰性载入函数也能够实现Lazy Definied,这个在后面将会讲到),下面以声明一个XMLHttpRequest实例对象为例子:服务器

    //分支
    var XHR= function(){
      var standard = {
        createXHR : function(){
          return new XMLHttpRequest();
        }
      }
      var newActionXObject = {
        createXHR : function(){
          return new ActionXObject("Msxml2.XMLHTTP");
        }
      }
      var oldActionXObject = {
        createXHR : function(){
          return new ActionXObject("Microsoft.XMLHTTP");
        }
      }
      if(standard.createXHR()){
        return standard;
      }else{
        try{
          newActionXObject.createXHR();
          return newActionXObject;
        }catch(o){
          oldActionXObject.createXHR();
          return oldActionXObject;
        }
      }
    }();

    从上面的例子能够看出,分支的原理就是:声明几个不一样名称的对象,可是给这些对象都声明一个名称相同的方法(这个就是关键),并给这些来自于不一样的对象可是拥有相同的方法进行浏览器之间各自的实现,接着就开始进行一次浏览器检测,并通过浏览器检测的结果来决定返回哪个对象,这样不论返回的是哪个对象,最后名称相同的方法都做为了对外一致的接口。

    这个是在Javascript运行期期间进行动态检测,并将检测的结果返回赋值给其余的对象,并提供相同的接口,这样储存的对象就可使用名称相同的接口了。其实,惰性载入函数跟分支在原理是很是相近的,只是在代码实现方面有差别而已。

  3. 惰性载入函数
    惰性载入函数就是英文中传说的“Lazy Defined”,它的主要解决的问题也是为了处理兼容性。原理跟分支相似,下面是简单的代码示例:

    var addEvent = function(el,type,handle){
      addEvent = el.addEventListener ? function(el,type,handle){
        el.addEventListener(type,handle,false);
      }:function(el,type,handle){
        el.attachEvent("on"+type,handle);
      };
      //在第一次执行addEvent函数时,修改了addEvent函数以后,必须执行一次。
      addEvent(el,type,handle);
    }

    从代码上看,惰性载入函数也是在函数内部改变自身的一种方式,这样以后,当重复执行的时候,就不会再进行兼容性方面的检测了。

  4. 单例的两种模式
    单例模式是家喻户晓的了,也是当前最流行的一种编写方式,注明的模块模式的编写方式也是从这个思想中衍生出来的。单例模式有两种方式:一种是所谓的“门户大开型”,另一种就是使用闭包来建立私有属性和私有方法的方式。第一种方式跟构造函数的“门户大开型”是一个样的,声明的方法和属性对外都是开放的,能够经过实例来调用。可是使用闭包来实现的单例模式,能够在一个“封闭”的做用域内声明一些不为外部所调用的私有属性和私有方法,并且还有一个很主要的功能,就是私有属性能够做为一个“数据存储器”,在闭包内声明的方法均可以访问这些私有属性,可是外部不可访问。那么重复添加、修改、删除这些私有属性所储存的数据是安全的,不受外部其余程序的影响。

  5. 享元类
    顾名思义,“享”、“元”。就是共享通用的方法和属性,将差别比较大的类中相同功能的方法集中到一个类中声明,这样须要这些方法的类就能够直接从享元类中进行扩展,这样使得这样通用的方法只须要声明一遍就好了。这个从代码的大小、质量来讲仍是有必定的效益的。

  6. 函数绑定
    函数绑定就是为了纠正函数的执行上下文,特别是函数中带有this关键字的时候,这点尤为显得重要,稍微不当心,使得函数的执行上下文发生了跟预期的不一样的改变,致使了代码执行上的错误(有时候也不会出现错误,这样调试起来,会很变态)。对于这个问题,bind函数是再熟悉不过的了,bind函数的功能就是提供一个可选的执行上下文传递给函数,而且在bind函数内部返回一个函数,来纠正在函数调用上出现的执行上下文发生的变化。最容易出现的错误就是回调函数和事件处理程序一块儿使用了,下面是摘自《Javascript高级程序设计第二版》的一个示例:

    var handler = {
      message:"Event handler",
      handlerClick:function(e){
        alert(this.message);
      }
    }
    var btn = document.getElementById("my-btn");
    //这句就形成了回调函数执行上下文的改变了
    EventUtil.addHandler(btn,"click",handler.handlerClick);

    解决的办法之一,就是纠正一下handler.handlerClick执行的上下文环境,改成:

    //这样运行的很好
    EventUtil.addHandler(btn,"click",function(e){
      handler.handlerClick(e);
    });

    上面就很好的纠正了回调函数的执行上下文了。并且,也可使用传说中的bind函数来解决:

    var bind = function(fn,context){
      return function(){
        return fn.apply(context || this,arguments);
      }
    }
    EventUtil.addHandler(btn,"click",bind(handler.handlerClick));// So Good!

  7. 函数curry化
    函数curry化的主要功能就是提供了强大的动态函数建立的功能。经过调用另外一个函数并为它传入要curry的函数和必要的参数。说白点就是利用已有的函数,再建立一个动态的函数,该动态的函数内部仍是经过该已有的函数来发生做用,只是传入更多的参数来简化函数的参数方面的调用。具体示例:

    //curry function
    function curry(fn){
      var args = [].slice.call(arguments,1); //这个就至关于一个存储器了。
      return function(){
        return fn.apply(null,args.concat([].slice.call(arguments,0)));
      }
    }
    //Usage:
    function add(num1,num2){
      return num1+num2;
    }
    var newAdd = curry(add,5);
    alert(newAdd(6));

    在curry函数的内部,私有变量args就至关于一个存储器,来暂时的存储在curry函数调用的时候所传递的参数值,这样跟后面的动态建立的函数调用的时候的参数合并,并执行,就获得了同样的效果了。

  8. 高级定时器
    提到定时器,无非就是利用setTimeout/setInterval了。但问题是定时器并非至关于新开一个线程来执行js程序,也不会说是在指定的时间间隔内就会必定执行。指定的时间间隔表示什么时候将定时器的代码添加到浏览器的执行队列,而不是合适实际执行代码。对此,就有这样的一个问题了:若是代码执行时间超过了定时器指定的时间间隔,那么在指定的时间里代码仍是加入的执行队列,可是并无执行,这样就会形成了无心义的代码执行,这也是使用setInterval的弊端。为此,使用setTimeout才能更好的避免这个问题,在代码自己中执行完毕了,再经过setTimeout来从新设定定时器,把代码加入到执行队列。好比:

    setTimeout(function(){
      //many code here...
      setTimeout(arguments.callee,100); //Key
    },100);

    固然了,定时器还有不少其余的技巧和实际做用,看需求而定,更详细的解释能够查看《Javascript高级程序设计第二版》(第467页)。

  9. 保护上下文的构造函数
    这个主要是避免构造函数在没有使用new来实例化的时候,内部的this指向错误问题。一般没有使用new的话,this通常执行window去了,所以形成了执行错误,给代码带来了灾难。使用下面的方式就能够避免这个问题:

    function myClass(name,size){
      if(this instanceof myClass){ //Key,使用instanceof来检测当前实例是不是myClass的实例化对象
        this.name = name;
        this.size = size;
      }else{
        return new myClass(name,size);
      }
    }

    可是上面经过instanceof的方式,给继承形成了必定的困扰,由于子类并非myClass的实例对象,因此会出现属性和方法没法被继承的方式。在说解决办法以前,先来了解一下instanceof操做符的原理:它首先会检测对象当前的原型是否指向右边的构造函数,若是找不到,就会往上一级的原型去查找,直到找到为止,并返回true,不然就返回false。

    基于上面的instanceof的原理,在继承的时候,就能够给子类的prototype原型赋于一个父类的实例化对象就好了,这样就能够在子类继承的时候绕过instanceof的检测。

  10. 函数节流

    函数节流函数节流解决的问题是一些代码(特别是事件)在无间断的执行,这严重的影响了浏览器的性能,再没有给它设定间断来执行的话,可能形成浏览器反应速度变慢或者直接就崩溃了。好比:resize事件、mousemove、mouseover、mouseout等等事件。

    这个时候,就能够加入定时器的功能了,将事件进行“节流”,便是:在事件触发的时候,设定一个定时器来执行事件处理程序,这样能够很大的程度上缓解浏览器的负担,又缓冲的余地去更新页面。具体的实例能够查看支付宝中部“导购场景”的导航:http://life.alipay.com/?src=life_alipay_index_big,以及当当网首页左边的导航栏:http://www.dangdang.com/等等,这些都是为了解决mouseover和mouseout移动过快的时候加大浏览器处理的负担,特别是在涉及到有Ajax调用,并且Ajax调用是么有缓存的状况下,给服务器也形成了很大的负担。为此,函数节流就派上用场了。好比简单的示例以下(出自本人写的:http://www.ilovejs.net/lab/tween/tweener_tab_modify.html此连接貌似无论用)):

    oTrigger.onmouseover=function(e){
      //若是上一个定时器尚未执行,则先清除掉定时器
      oContainer.autoTimeoutId && clearTimeout(oContainer.autoTimeoutId);
      e = e || window.event;
      var target = e.target || e.srcElement;
      if((/li$/i).test(target.nodeName)){
        oContainer.timeoutId = setTimeout(function(){
          addTweenForContainer(oContainer,oTrigger,target);
        },300);
      }
    }

  11. 自定义事件

    首先要说的是,这里并非说自定义事件能够真的自定义跟mouseout、click等同样性质的“事件”。这里的自定义事件在执行的时候仍是须要依赖已有的键盘、鼠标、HTML等事件来执行,或者又其余函数“触发”执行,这里的“触发”是指直接调用自定义事件中声明的某个接口方法,来轮询的执行所有相关的添加到自定义事件中的函数。

    自定义事件内部有一个“事件”存储器,根据添加的事件的类型的不一样,来储存各种的事件执行函数,这样再出发这类事件的时候,就轮询执行添加到该类型下的函数。“自定义事件背后的概念是建立一个管理事件的对象,让其余对象监听那些事件”来自《Javascript高级程序设计第二版》的解释。基于自定义事件的原理,能够想象自定义事件不少时候是用于“订阅—发布—接收”性质的功能。

文章写的有点多了,可是上面介绍的Javascript高级技巧还远不止这些,特别是在Ajax方面的一些模式和技巧都尚未介绍,况且是客户端和服务端结合的一些技巧(好比压缩、Minify、服务器“推技术”等等)。更多的有待之后了解了介绍一二。

在前端基本技术方面,Javascript、HTML、CSS等你们都是已经掌握的差很少了,可是利用这些已有的基本技术,可否创造出不通常的应用和模式呢?这个才是在掌握了基本的能力以后接下来须要掌握的,好比下面是本人在天天晚上睡觉以前所总结的几点:

  1. 学会重构代码的技术,有计划性的重构下本身以前所编写过的一些代码,增强本身掌控代码的能力。

  2. Code review。这里说的review,并非指我的,而是对团队来讲的,一我的编写的代码的想象空间有限,若是在本身编写代码完成以后,邀请其余团队内的伙伴来查看你的代码,及时发现问题以及提出更好的解决方案,这也不失为一种即时重构的方式,提升代码的质量。

  3. 编写具体的功能代码以前,首先设计代码、规划代码、组织代码的模式。

  4. 在代码的质量、性能、大小之间能做出合理的权衡。

  5. 编写阅读性良好、一目了然、扩展性、可维护性良好、重复利用的代码,也是一门艺术。

  6. 关注web前端的性能优化,包括Javascript、HTML、CSS、客户端、服务端、前端、后端等总体性的优化。

  7. 最后一点或许也是最重要的:善于总结。这点比上面的任何一点都来的重要,由于上面的每一点都是出自这点的积累。

上面我总结的几点,也是后期本身要着重提升的能力,固然了,在实际的编码方面,还有不少的东西还须要去挖掘和了解。继续革命吧,将互联网革命进行到底……

相关文章
相关标签/搜索