javascript高级技巧篇,作用域安全、防篡改、惰性载入、节流、自定义事件,拖放

安全的类型检测

在任何值上调用Object原生的toString()方法,都会返回一个[object NativeConstructorName]格式字符串。每个类在内部都有一个[[Class]]属性,这个属性中就指定了各个类型的构造函数名。

使用这种方法判断值属于什么类型最可靠;比如:

    function isArray(value){
            return Object.prototype.toString.call(value)=="[object Array]";
      }
      function isFunction(value){
            return Object.prototype.toString.call(value)=="[object Function]";
      }
      function isRegExp(value){
            return Object.prototype.toString.call(value)=="[object RegExp]";
      }

判断是不是原生的JSON对象

    function isNativeJSON(value){
            return window.JSON&&Object.prototype.toString.call(value)=="[object JSON]";
      }

在javascript中,自定义的构造函数上述方法检测都会返回[object object]。

作用域安全构造函数

    function Person(name,age,job){
            if(this instanceof Person){
                  this.name=name;
                  this.age=age;
                  this.job=job;
            }else{
                  return new Person(name,age,job);
            }
      }

继承作用域安全的构造函数

    function LittleBoy(classNum,name,age,job){
            Person.call(this,name,age,job);//Person的作用域是安全的,this并不指向Person实例,所以必须让LittleBoy的原型链指向Person的实例
            this.classNum=classNum;
            this.getClassNum=function(){
                  return this.classNum;
            }
      }
      LittleBoy.prototype=new Person();

惰性载入函数

在if判断后,将原函数重新定义,再次执行原函数时,就不用if判断了。也就是在一定环境下,只需第一次有if判断,后面就不需要if判断,从而提高性能。

防篡改对象

    var person={name:"ha"};
      Object.preventExtensions(person);//禁止再添加新属性和方法,但可以修改和删除现有的属性和方法

调用Object.isExtensible()方法可以判断对象是否可拓展

    console.log(Object.isExtensible(person));//false

密封对象

    Object.seal(person);//不能添加和删除属性和方法

判断是否是密封对象

    Object.isSealed(person);

冻结对象

    Object.freeze(person);//不能修改,不能添加,不能删除

判断是否被冻结

    console.log(Object.isFrozen(person));//判断是否被冻结

Yielding Processes(屈服/投降 进程)

运行在浏览器中的javascript代码都被分配了一个确定数量的资源。如果脚本运行时间过长,浏览器会弹出一个错误对话框。这对用户体验非常不好。

脚本运行过长的原因通常由两种:

1、过长的或过深的嵌套的函数的调用。

2、进行大量处理的循环。

这两者中后者是比较容易解决的。

例如:

    for(var i=0,len=data.length;i<len;i++){
            process(data[i]);
      }

假如一个process程序需要执行100ms才能完成,如果data数组有10个项目,那么循环处理完这个数组需要花费1s,这花费的时间就有些长了,会严重阻塞后续代码的执行,对用户体验非常不好。我们可以用setTimeout定时函数来对数组分块执行,因为setTimeout可以为代码创建一个队列,在合适的时候插入队列,就不会阻塞后续代码的执行了。不过使用这个数组分块的技术,你需要保证在两个条件下使用:

1、该处理是否应该同步完成。

2、数据是否需要按顺序完成。

如果上述两个问题你都回答否,那么你就可以安心的使用数组分块技术了,例子:

    function chunk(array,process,context){
            setTimeout(function(){
                  var item=array.shift();//截取出原数组的第一项,原数组会改变
                  process.call(context,item);//将运行环境抱定在context上,可选参数
                  if(array.length>0){
                        setTimeout(arguments.callee,100);
                  }
            },100);//100毫秒效果很好
            
      }

chunk()方法接收三个参数:处理的数组,用于处理项目的函数,以及可选的运行该函数的环境(不传就为null);

使用事例:

    var data=[12,123,1234,453,436,23,23,5,4123,45,346,5634,345,342];
      function printValue(item){
            var div=document.getElementById("myDiv");
            div.innerHTML+=item+"<br>"
      }
      chunk(data,printValue);

如果想保持原数组不变,可以将原数组拷贝一下再传入使用,用数组的concat()方法就可以拷贝数组:

    chunk(data.concat(),printValue);

函数节流

函数节流用于使高频触发的事件,变为指定事件段执行一次,比如说onresize,等等,我们可以将onresize里执行的代码变成低频触发,而不是让onresize变成低频触发,这个是改变不了的。

同样,是使用定时器函数来实现的。

    var processor={
            timeoutId:null,
            performProcessing:function(){
                  //实际执行的代码
                  console.log(11);//例子
            },
            process:function(){
                  clearTimeout(this.timeoutId);
                  var that=this;
                  this.timeoutId=setTimeout(function(){
                        that.performProcessing();
                  },100);
            }
      }
      //使用
      window.onresize=function(){
            processor.process();
      }

以上模式可以用throttle()函数来简化 throttle:节流阀

这个函数可以自动进行定时器的设置和清除:

    function throttle(method,context){
            clearTimeout(method.tId);//清除计时器
            method.tId=setTimeout(function(){
                  method.call(context);//将函数绑定在一定环境上执行
            },100)
      }

throttle()方法接收两个参数,要执行的函数和在哪个作用域中执行。

使用:

    //使用
      var resizeDiv=function(){
            var div=document.getElementById("myDiv");
            div.style.height=div.offsetWidth+"px";
      };
      window.onresize=function(){
            throttle(resizeDiv,window);//window可省略
      }

自定义事件

事件是javascript与浏览器交互的主要途径。事件是一种叫作观观察者的实际模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象声明周期中某个有趣的时刻到了。然后其他对象可以观察该对象,等待这些有趣的时刻到来并通过运行代码来响应。

观察者模式有两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说他可以独自存在并正常运作即观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及DOM上时,DOM元素便是主体,你的事件处理代码便是观察者。

事件是与DOM交互的最常见的方式,但他们也可以用于非DOM代码中----通过定义自定义事件。自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式如下定义:

      function EventTarget(){
            this.handlers={};//存放事件处理程序
      }

      EventTarget.prototype={
            constructor:EventTarget,
            //addHandler:用来注册给定类型的事件处理程序
            addHandler:function(type,handler){//接收两个参数,一个事件类型,一个该类型的事件处理程序
                  if(typeof this.handlers[type]=="undefined"){//如果handlers对象中没有这个类型的属性,则添加一个这个属性,为数组类型
                        this.handlers[type]=[];
                  }
                  this.handlers[type].push(handler);
            },
            // fire 用来触发某个事件
            fire:function(event){
                  if(!event.target){
                        event.target=this;
                  }
                  if(this.handlers[event.type] instanceof Array){
                        var handlers=this.handlers[event.type];
                        for(var i=0,len=handlers.length;i<len;i++){
                              handlers[i](event);
                        }
                  }
            },
            // removeHandler;用于注销某个事件类型的事件处理程序
            removeHandler:function(type,handler){
                  if(this.handlers[type] instanceof Array){
                        var handlers=this.handlers[type];
                        for(var i=0,len=handlers.length;i<len;i++){
                              if(handlers[i]===handler){
                                    break;//退出循环
                              }
                        }
                        handlers.splice(i,1);
                  }
            }
      }

使用

      // 声明一个事件处理程序
      function handleMessage(event){
            console.log("message receive:"+event.message);
      }
      // 创建一个新对象
      var target=new EventTarget();
      // 添加一个事件处理程序(传入事件类型和事件处理程序)
      target.addHandler("message",handleMessage);
      // 触发事件(传入自定义的event对象)
      target.fire({type:"message",message:"hello"});
      //删除事件处理程序
      target.removeHandler("message",handleMessage);
      // 再次,就没有处理程序
      target.fire({type:"message",message:"hello"});

因为这种功能封装在一种自定义类型中,其他对象可以继承EventTarget并获得这个行为:

      function inheritPrototype(subType,superType){//寄生组合继承方法
            var prototype=Object(superType.prototype);
            prototype.constructor=subType;
            subType.prototype=prototype;
      }

      function Person(name,age){//建一个类
            EventTarget.call(this);//改变EventTarget的执行环境
            this.name=name;
            this.age=age;
      }
      inheritPrototype(Person,EventTarget);//Person继承EventTarget
      Person.prototype.say=function(message){
            this.fire({type:"message",message:message});
      }

上面采用的是寄生组合继承

然后在Person类的实例上调用自定义方法:

      function handleMessage(event){
            console.log(event.target.name+"say"+event.message);
      }
      // 创建新person
      var person = new Person("Nicholas",29);
      // 添加一个事件处理程序
      person.addHandler("message",handleMessage);
      // 在该对象上调用一个方法,它触发消息事件
      person.say("Hi there");

拖放

简单的拖放需要为文档指定一个onmousemove事件处理程序,指定一个绝对定位的元素,然后让元素移动到鼠标指针的位置:

     document.onmousemove=function(event){
            var myDiv=document.getElementById("myDiv");
            myDiv.style.left=event.clientX+"px";
            myDiv.style.top=event.clientY+"px";
      }

下面实现一个简单的拖放功能,用的是跨浏览器添加事件的方法,所以需要将跨浏览器事件的对象先写在前面,然后再单独写拖放功能:

      var EventUtil={
            addHandler:function(element,type,handler){
                  if(element.addEventListener){
                  element.addEventListener(type,handler,false);
                  }else if(element.attachEvent){
                  element.attachEvent("on"+type,handler);
                  }else{
                  element["on"+type]=handler;
                  }
            },
            removeHandler:function(element,type,handler){
                  if(element.removeEventListener){
                  element.removeEventListener(type,handler,false);
                  }else if(element.detachEvent){
                  element.detachEvent("on"+type,handler);
                  }else{
                  element["on"+type]=null;
                  }
            },
            getEvent:function(event){//获取事件对象
                  return event?event:window.event;
            },
            getTarget:function(event){//获取事件目标对象
                  return event.target||event.srcElement;
            },
            preventDefault:function(event){//阻止事件默认行为
                  if(event.preventDefault){
                  event.preventDefault();
                  }else{
                  event.returnValue=false;//IE
                  }
            },
            stopPropagation:function(event){//阻止事件冒泡
                  if(event.stopPropagation){
                  event.stopPropagation();
                  }else{
                  event.cancelBubble=true;//IE
                  }
            }
      }

下面来用跨浏览器的方法实现一个拖动案例:

        var DragDrop=function(){
            var dragging=null,
                  diffX=0,//鼠标在元素上的水平位置
                  diffY=0;//鼠标在元素上的垂直位置
            function handlerEvent(event){
                  //获取事件和目标对象
                  event=EventUtil.getEvent(event);
                  var target=EventUtil.getTarget(event);
                  //确定事件类型
                  switch(event.type){
                        case "mousedown":
                              if(target.className.indexOf("draggable")>-1){
                                    dragging=target;
                                    diffX=event.clientX-target.offsetLeft;
                                    diffY=event.clientY-target.offsetTop;
                              }
                              break;
                        case "mousemove":
                              if(dragging!==null){
                                    //指定位置
                                    dragging.style.left=(event.clientX-diffX)+"px";
                                    dragging.style.top=(event.clientY-diffY)+"px";
                              }
                              break;
                        case "mouseup":
                              dragging=null;
                              break;
                  }
            };
            // 公共接口
            return {
                  enable:function(){
                        EventUtil.addHandler(document,"mousedown",handlerEvent);
                        EventUtil.addHandler(document,"mousemove",handlerEvent);
                        EventUtil.addHandler(document,"mouseup",handlerEvent);
                  },
                  disable:function(){
                        EventUtil.removeHandler(document,"mousedown",handlerEvent);
                        EventUtil.removeHandler(document,"mousemove",handlerEvent);
                        EventUtil.removeHandler(document,"mouseup",handlerEvent);
                  }
            }
                  
      }

使用

主要在页面上包含这些代码并调用enable()。拖放会自动针对所包含“draggable”类的元素启用:

    <div class="box draggable"></div>//元素必须是绝对定位
    DragDrop().enable();//调用

为拖放添加自定义事件

拖放功能还不能真正的应用起来,除非能知道什么时候拖动开始了。从这点上看,当前的方法没有提供任何的拖动开始、正在拖动、拖动结束。这时可以使用自定义事件来指定这几个事件的发生,让应用的其它部分与拖动功能进行交互。

由于DragDrop对象是一个使用了模块模式的单例,所以需要一些更改来使用EventTarget类型。首先,创建一个新的EventTarget对象,然后添加enable()和disable()方法,最后返回这个对象。修改如下:

        var DragDrop=function(){
            var   dragdrop=new EventTarget(),//创建一个自定义事件对象
                  dragging=null,
                  diffX=0,//鼠标在元素上的水平位置
                  diffY=0;//鼠标在元素上的垂直位置
            function handlerEvent(event){
                  //获取事件和目标对象
                  event=EventUtil.getEvent(event);
                  var target=EventUtil.getTarget(event);
                  //确定事件类型
                  switch(event.type){
                        case "mousedown":
                              if(target.className.indexOf("draggable")>-1){
                                    dragging=target;
                                    diffX=event.clientX-target.offsetLeft;
                                    diffY=event.clientY-target.offsetTop;
                                    //触发dragstart事件
                                    dragdrop.fire({type:"dragstart",target:dragging,
                                                x:event.clientX,y:event.clientY});
                              }
                              break;
                        case "mousemove":
                              if(dragging!==null){
                                    //指定位置
                                    dragging.style.left=(event.clientX-diffX)+"px";
                                    dragging.style.top=(event.clientY-diffY)+"px";
                                    // 触发拖动中事件
                                    dragdrop.fire({type:"drag",target:dragging,
                                                x:event.clientX,y:event.clientY});
                              }
                              break;
                        case "mouseup":
                              // 触发拖动结束事件
                              dragdrop.fire({type:"dragend",target:dragging,
                                          x:event.clientX,y:event.clientY});
                              dragging=null;
                              break;
                  }
            };
            // 公共接口
            dragdrop.enable=function(){
                  EventUtil.addHandler(document,"mousedown",handlerEvent);
                  EventUtil.addHandler(document,"mousemove",handlerEvent);
                  EventUtil.addHandler(document,"mouseup",handlerEvent);
            };
            dragdrop.disable=function(){
                  EventUtil.removeHandler(document,"mousedown",handlerEvent);
                  EventUtil.removeHandler(document,"mousemove",handlerEvent);
                  EventUtil.removeHandler(document,"mouseup",handlerEvent);
            };
            return dragdrop;
                  
      }();//自执行

这段代码新增了三个事件,dragstart、drag、dragend。他们都将被拖动的元素设置为了target,并给出了x和y属性来表示当前的位置。他们触发于dragdrop对象上,之后在返回对象前给对象增加了enable()和disable()方法。这些模块模式中的细小更改令DragDrop对象支持了事件,如:

      DragDrop.enable();//执行拖放效果
      DragDrop.addHandler("dragstart",function(event){
            var status=document.getElementById("status");
            status.innerHTML="Started dragging"+event.target.id;
      });
      DragDrop.addHandler("drag",function(event){
            var status=document.getElementById("status");
            status.innerHTML+="<br/> Dragged"+event.target.id+"to("+event.x+","+event.y+")";
      });
      DragDrop.addHandler("dragend",function(event){
            var status=document.getElementById("status");
            status.innerHTML+="<br/> Dragged"+event.target.id+"at("+event.x+","+event.y+")";
      });