JS学习20高级技巧

高级函数

函数本质上是很简单且进程化的,但是由于JS天生的动态的特性,从使用方式上可以很复杂。

安全的类型检测

虽然JS中是有类型检测的,但是由于阅读器实现等它们其实不完全可靠。比如typeof在Safari中对正则表达式也返回function。
instanceof在存在多个全局作用域时也会把同种却不同作用域中构造函数的实例辨认为不同的实例:

var isArray = value instanceof Array;

这个表达式要是想返回true,value必须是个数组,且必须与Array构造函数在同1个全局作用域中,如果value是另外一个全局作用域中定义的数组,那这个表达式返回false。
检测某个对象是原生的还是开发人员自定义的对象时也会有问题。由于阅读器开始原生支持JSON了,而有些开发人员还是在用第3方库来实现JSON,这个库里会有全局的JSON对象,这样想肯定JSON对象是否是原生的就麻烦了。
解决这些问题的办法就是使用Object的toString方法,这个方法会返回1个[object NativeConstructorName]格式的字符串。

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]"; }

不过要注意的是,对在IE中任何以COM情势实现的函数,isFunction()都会返回false。
对JSON是不是为原生的问题可以这样:

var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON) == "[object JSON]";

作用域安全的构造函数

之前我们说的构造函数是这么使用的:

function Person(name,age,job){ this.name = name; this.age = age; this.job = job; } var person = new Person("Nicholas",29,"Software Engineer");

在这里,由于使用了new操作符,this被绑定在了新创建的Person对象上,如果不用new操作符直接调用Person(),this就会被绑定到window上,这明显是不行的。

function Person(name,job){ if (this instanceof Person){ this.name = name; this.age = age; this.job = job; } else { return new Person(name,job); } } var person1 = Person("Nicholas","Software Engineer"); alert(window.name); //"" alert(person1.name); //"Nicholas" var person2 = new Person("Shelby",34,"Ergonomist"); alert(person2.name); //"Shelby"

不过在使用了这样作用域安全的构造函数后,如果使用基于构造函数盗取的继承,就会有问题:

function Polygon(sides){ if (this instanceof Polygon) { this.sides = sides; this.getArea = function(){ return 0; }; } else { return new Polygon(sides); } } function Rectangle(width,height){ //这里调用时,传进去的this是Rectangle类型的,没办法拓展side属性 Polygon.call(this,2); this.width = width; this.height = height; this.getArea = function(){ return this.width * this.height; }; } var rect = new Rectangle(5,10); alert(rect.sides); //undefined

解决方法就是使Rectangle也是Polygon的1个实例就好啦

Rectangle.prototype = new Polygon(); var rect = new Rectangle(5,10); alert(rect.sides); //2

惰性载入函数

由于阅读器差异,大量的判断阅读器能力的函数需要被使用(通常是大量的if),但是这些判断1般其实没必要每次都履行,在履行1次后,阅读器的能力就肯定了,以后就应当不用在判断了。比如:

function createXHR(){ if (typeof XMLHttpRequest != "undefined"){ return new XMLHttpRequest(); } else if (typeof ActiveXObject != "undefined"){ if (typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"],i,len; for (i=0,len=versions.length; i < len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){ } } } return new ActiveXObject(arguments.callee.activeXString); } else { throw new Error("No XHR object available."); } }

这里的创建XHR对象的函数,每次创建对象时都会判断1次阅读器能力,这是没必要要的。
惰性载入有两种方式,第1种就是在函数第1次被调用时,根据不同情况,用不同的新函数把这个函数覆盖掉,以后调用就不需要再判断而是直接履行该履行的操作。

function createXHR(){ if (typeof XMLHttpRequest != "undefined"){ createXHR = function(){ return new XMLHttpRequest(); }; } else if (typeof ActiveXObject != "undefined"){ createXHR = function(){ if (typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0",len; for (i=0,len=versions.length; i < len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){ //skip } } } return new ActiveXObject(arguments.callee.activeXString); }; } else { createXHR = function(){ throw new Error("No XHR object available."); }; } return createXHR(); } createXHR(); alert(createXHR);

第2种思路1样,只不过是在声明函数时就指定新函数,不在第1次调用时再指定。两种办法其实本质上是1样的,看你想怎样用了。

var createXHR = (function(){ if (typeof XMLHttpRequest != "undefined"){ return function(){ return new XMLHttpRequest(); }; } else if (typeof ActiveXObject != "undefined"){ return function(){ if (typeof arguments.callee.activeXString != "string"){ var versions = ["MSXML2.XMLHttp.6.0",len=versions.length; i < len; i++){ try { new ActiveXObject(versions[i]); arguments.callee.activeXString = versions[i]; break; } catch (ex){ //skip } } } return new ActiveXObject(arguments.callee.activeXString); }; } else { return function(){ throw new Error("No XHR object available."); }; } })(); alert(createXHR);

函数绑定

函数绑定解决的问题是调用函数时函数的this对象被改成我们不想要的对象的问题。这类情况常常出现在指定事件处理函数时。看个例子:

var handler = { message: "Event handled",handleClick: function(event){ alert(this); alert(this.message); } }; var btn = document.getElementById("myButton"); EventUtil.addHandler(btn,"click",handler.handleClick); //[object HTMLButtonElement] undefined handler.handleClick(); // [object Object] Event handled

这里的this被改成了按钮元素。
解决办法就是将真实的函数套在1个闭包中,以此来保存着个函数的环境

var handler = { message: "Event handled",handleClick: function(event){ alert(this); // [object Object] alert(this.message); // Event handled } }; var btn = document.getElementById("myButton"); EventUtil.addHandler(btn,function(event){ alert(this); //[object HTMLButtonElement] handler.handleClick(event); });

可以看到,闭包的this被改成了button,而由于这个闭包的保护,我们的处理函数的环境保存住了。
为了不每次都手动创建1个闭包,我们可以创建1个工具函数bind:

function bind(fn,context){ alert(arguments[0]); //fn本身 return function(){ alert(arguments[0]); //event return fn.apply(context,arguments); }; }

这里就是把1个函数使用apply在特定的环境下调用,注意1下arguments对象,最后应当使用的是return过去的匿名函数(闭包)的arguments才对,这样事件给事件处理函数传递的参数才能穿到我们的函数里。

var handler = { message: "Event handled",handleClick: function(event){ alert(this); // [object Object] alert(this.message); // Event handled } }; var btn = document.getElementById("myButton"); EventUtil.addHandler(btn,bind(handler.handleClick,handler));

ES5中为所有函数都定义了1个原生的bind()方法。直接使用就行。

EventUtil.addHandler(btn,handler.handleClick.bind(handler));

只要是将某个函数指针以值的情势进行传递,同时该函数必须在特定环境中履行,被绑定函数的效果就显现了

函数柯里化

这个是用来创建已设置好1个或多个参数的函数,用1个例子看看基本思想:

function add(num1,num2){ return num1 + num2; } function curriedAdd(num2){ return add(5,num2); } alert(add(2,3)); //5 alert(curriedAdd(3)); //8

创建柯里化函数的通用方式:

function curry(fn){ var args = Array.prototype.slice.call(arguments,1); return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(null,finalArgs); }; }

这个函数主要的工作就是将外部函数和内部函数的参数都获得到传递给了返回的函数中。

function add(a,b) { alert(a); alert(b); alert(a+b); } var curriedAdd = curry(add,5); curriedAdd(3); //8 curriedAdd = curry(add,3); curriedAdd(5); //8 curriedAdd = curry(add,5,3); curriedAdd(); //8

可以将其利用在bind函数中来给事件处理函数传入多个参数。

function bind(fn,context){ var args = Array.prototype.slice.call(arguments,2); return function(){ var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(context,finalArgs); }; } var handler = { message: "Event handled",handleClick: function(name,event){ alert(this.message + ":"+ name + ":"+ event.type); } }; var btn = document.getElementById("myButton"); EventUtil.addHandler(btn,handler,"myButton"));

这里要注意的是参数的顺序,比如这里name和event反了可就不对了呦。
ES5中的bind也实现了柯里化,直接使用就能够。

EventUtil.addHandler(btn,handler.handleClick.bind(handler,"myButton"));

防篡改对象

JS同享的本质使任意对象都可被随便修改。这样有时很不方便。ES5增加了几个方法来设置对象的行动。1旦将对象设置为防篡改就不能撤消了。

不可拓展对象

不可以添加新的属性和方法

var person = { name: "Nicholas" }; Object.preventExtensions(person); person.age = 29; alert(person.age); //undefined alert(Object.isExtensible(person)); //false person.name = "hahah"; alert(person.name); //hahah

密封的对象

不可以添加或删除属性,已有成员的[[Configurable]]被设置为false。

var person = { name: "Nicholas" }; Object.seal(person); person.age = 29; alert(person.age); //undefined delete person.name; alert(person.name); //"Nicholas" alert(Object.isExtensible(person)); //false alert(Object.isSealed(person)); //true

冻结的对象

不可拓展,密封,且对象数据属性的[[Writable]]将被设为false。如果定义了[[Set]],访问器属性仍然可写。

var person = { name: "Nicholas" }; Object.freeze(person); person.age = 29; alert(person.age); //undefined delete person.name; alert(person.name); //"Nicholas" person.name = "Greg"; alert(person.name); //"Nicholas" alert(Object.isExtensible(person));//false alert(Object.isSealed(person));//true alert(Object.isFrozen(person));//true

高级定时器

setTimeout()和setInterval()是很实用的功能,不过有些事情是要注意的。
JS是单线程的,这就意味着定时器实际上是很有可能被阻塞的。我们在这两个函数中所设置的定时,实际上是代表将代码加入到履行队列的事件,如果在加入时恰巧JS是空闲的,那末这段代码会立即被履行,也就是说这个定时被准时的履行了。相反,如果这时候JS其实不空闲或队列中还有别的优先级更高的代码,那就意味着你的定时器会被延时履行。

重复的定时器

使用setInterval创建定时器的目的是使代码规则的插入到队列中。这个方式的问题在于,存在这样1种可能,在上次代码还没履行完的时候代码再次被添加到队列。JS引擎会解决这个问题,在将代码添加到队列时会检查队列中有无代码实例,如果有就不添加,这确保了定时器代码被加入队列中的最小间隔是规定间隔。但是在某些特殊情况下还是会出现两个问题,某些间隔由于JS的处理被跳过,代码之间的间隔比预期的小。
所以尽可能使用setTimeout()摹拟间隔调用。

setTimeout(function(){ setTimeout(arguments.callee,interval); },interval);

Yielding Processes

如果你的页面中要进行大量的循环处理,每次循环会消耗大量的时间,那就会阻塞用户的操作。这时候分块处理数据就是个好办法。
这个例子每100ms取1个数组元素并添加到页面。

function chunk(array,process,context){ setTimeout(function(){ var item = array.shift(); process.call(context,item); if (array.length > 0){ setTimeout(arguments.callee,100); } },100); } var data = [12,123,1234,453,436,23,4123,45,346,5634,2234,345,342]; function printValue(item){ var div = document.getElementById("myDiv"); div.innerHTML += item + "<br>"; } chunk(data,printValue);

函数节流

这个是为了不某个操作连续不停的触发,比如触及到DOM操作,连续大量的DOM操作非常耗资源。
函数节流的基本思想是,每次调用实际上是设置1个真正调用的setTimeout操作,每次调用都会先清除当前的setTimeout再设置1个新的。如果短时间内大量调用,就回1直设置新的setTimeout而不履行setTimeout内的操作。只有停止调用足够长的时间,直到setTimeout时间到了,内部的真正操作才会履行1次。这个对onresize事件特别有用。

function throttle(method,context) { clearTimeout(method.tId); method.tId= setTimeout(function(){ method.call(context); },100); } function reDiv(){ var div = document.getElementById("myDiv"); div.innerHTML += "qqqqqq" + "<br>"; } window.onresize = function(){ throttle(reDiv); };

这样只有当你停下调剂窗口大小100ms后才会履行reDiv操作。

自定义事件

事件这样的交互其实就是视察者模式,这类模式由两类对象组成:主体和视察者。主体负责发布事件,视察者通过定阅这些事件来视察该主体。
创建自定义事件实际上就是创建1个管理事件的对象,并在里面存入各种事件类型的处理函数,触发事件时,只要你给失事件类型,这个对象就会找到相应的事件处理程序并履行。
下面是1个事件管理对象的大体情势:

function EventTarget(){ this.handlers = {}; } EventTarget.prototype = { constructor: EventTarget,addHandler: function(type,handler){ if (typeof this.handlers[type] == "undefined"){ this.handlers[type] = []; } this.handlers[type].push(handler); },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: 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); } } };

添加事件处理程序时,addHandler会依照事件的类型将处理函数存入handlers属性中对应的数组里(如果还没有则新建)。
触发事件时使用fire,传入1个最少有type属性的对象。
使用时就像这样:

function handleMessage(event){ alert("Message received: " + event.message); } var target = new EventTarget(); target.addHandler("message",handleMessage); target.fire({ type: "message",message: "Hello world!"}); target.removeHandler("message",message: "Hello world!"});

自定义事件常常用来解耦对象之间的交互,使用事件就不需要有对象与对象之间的援用,使事件处理和事件触发保持隔离。

拖放

使用原始的鼠标事件

创建1个单例,使用模块模式来创建1个拖动的插件,返回两个方法,分别用来添加和移除所有的事件处理程序。

var DragDrop = function(){ var dragging = null; var diffX = 0; var diffY = 0; function handleEvent(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",handleEvent); EventUtil.addHandler(document,"mousemove","mouseup",handleEvent); },disable: function(){ EventUtil.removeHandler(document,handleEvent); EventUtil.removeHandler(document,handleEvent); } } }(); DragDrop.enable();

这样看来拖动的功能是实现了,不过有个问题。比如说这是我写的1个插件,使用的人想在拖动开始的时候做1些事情,那末他就不能不在起的源码里做出修改。他需要把所有要履行的代码和函数加到case “mousedown”里。如果这个插件我加密了呢,那想在这个时间点做些事情就更麻烦了。这样的做法明显其实不科学。
这时候如果使用了自定义事件,就能够很好的解决这个问题。

添加自定义事件

我们在这里新定义1个dragdrop变量,它是EventTarget类型的对象,在它上面我们可以添加事件处理函数或触发事件。在拖动开始时,进程中,结束时,都触发了自定义事件,这样有人想在这几个节点做甚么就直接添加事件处理函数就能够了。

var DragDrop = function(){ //这里的dragdrop是之前的EventTarget类型,可以用来保存和触发事件 var dragdrop = new EventTarget(),dragging = null,diffX = 0,diffY = 0; function handleEvent(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; //触发自定义事件 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",y: event.clientY}); } break; case "mouseup": dragdrop.fire({type:"dragend",y: event.clientY}); dragging = null; break; } }; dragdrop.enable = function(){ EventUtil.addHandler(document,handleEvent); EventUtil.addHandler(document,handleEvent); }; dragdrop.disable = function(){ EventUtil.removeHandler(document,handleEvent); EventUtil.removeHandler(document,handleEvent); }; return dragdrop; }(); DragDrop.addHandler("dragstart",function(event){ var status = document.getElementById("myDiv"); status.innerHTML = "Started dragging " + event.target.id; }); DragDrop.addHandler("drag",function(event){ var status = document.getElementById("myDiv"); status.innerHTML += "<br/> Dragged " + event.target.id + " to (" + event.x + "," + event.y + ")"; }); DragDrop.addHandler("dragend",function(event){ var status = document.getElementById("myDiv"); status.innerHTML += "<br/> Dropped " + event.target.id + " at (" + event.x + "," + event.y + ")"; }); DragDrop.enable();

相关文章

HTML代码中要想改变字体颜色,常常需要使用CSS样式表。CSS是...
HTML代码如何让字体盖住图片呢?需要使用CSS的position属性及...
HTML代码字体设置 在HTML中,我们可以使用标签来设置网页中的...
在网页设计中,HTML代码的字体和字号选择是非常重要的一个环...
HTML(Hypertext Markup Language,超文本标记语言)是一种用...
外链是指在一个网页中添加一个指向其他网站的链接,用户可以...