《JavaScript高级程序设计》笔记:高级技巧
高级函数
安全的类型检测
在任何值上调用object原生的tostring()方法,都会返回一个[object nativeconstructorname]格式的字符串。每个类在内部都有一个[[class]]属性,这个属性就指定了上述字符串中的构造函数名。
var arr = []; function fn(){ } var reg = /^\d/; var json = { "name":"jack", "age":20 ,} console.log(object.prototype.tostring.call(arr) == "[object array]"); //true console.log(object.prototype.tostring.call(fn) == "[object function]"); //true console.log(object.prototype.tostring.call(reg) == "[object regexp]"); //true console.log(window.json && object.prototype.tostring.call(json) == "[object object]"); //true
作用域安全的构造函数
作用域安全的构造函数在进行任何更改前,首先确认this对象是正确类型的实例。如果不是,会创建新的实例并返回,如下例子:
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); } } var person1 = person("jack",29,"it"); console.log(window.name); //"" console.log(person1.name);//jack var person2 = person("tom",20,"teacher"); console.log(person2.name);//tom
如下例子,rectangle实例中没有添加sides属性:
function polygon(sides){ if(this instanceof polygon){ this.sides = sides; this.getarea = function(){ return 0; } }else{ return new polygon(sides); } } function rectangle(width,height){ polygon.call(this,2); this.width = width; this.height = height; this.getarea = function(){ return this.width * this.height; } } var rect = new rectangle(5,10); console.log(rect.sides); //undefined
修改后,rectangle实例中添加了sides属性:
function polygon(sides){ if(this instanceof polygon){ this.sides = sides; this.getarea = function(){ return 0; } }else{ return new polygon(sides); } } function rectangle(width,height){ polygon.call(this,2); this.width = width; this.height = height; this.getarea = function(){ return this.width * this.height; } } rectangle.prototype = new polygon(); var rect = new rectangle(5,10); console.log(rect.sides); //2
惰性载入函数
function createxhr(){ if(typeof xmlhttprequest != "undefined"){ return new xmlhttprequest(); }else if(typeof activexobject != "undefined"){ if(typeof arguments.callee.activexstring != 'string'){ var verisions = ["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 = version[i]; break; }catch(ex){ //跳过 } } } return new activexobject(arguments.callee.activexstring); }else{ throw new error("no xhr object available."); } }
每次调用createxhr()时,他都要对浏览器所支持的能力仔细检查。如果if语句不必每次执行,那么代码可以运行的更快一些。解决方案称之为惰性载入的技巧。
惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式,第一种就是函数在被调用时再处理函数。在第一次调用的过程中,该函数会覆盖为另外一个按合适方式执行的函数,这样对原函数的调用都不用在经过执行的分支了。例如上面例子重写为:
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 verisions = ["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 = version[i]; break; }catch(ex){ //跳过 } } } return new activexobject(arguments.callee.activexstring); } }else{ createxhr = function(){ throw new error("no xhr object available."); } } return createxhr(); }
第二种实现惰性载入的方式是在声明函数时就指定适当的函数,如下代码:
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 verisions = ["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 = version[i]; break; }catch(ex){ //跳过 } } } return new activexobject(arguments.callee.activexstring); } }else{ return function(){ throw new error("no xhr object available."); } } })();
函数绑定
var handler = { message:"event handler", handleclick:function(event){ console.log(this.meesage); //undefined } } var btn = document.getelementsbyclassname("my-btn")[0]; btn.addeventlistener("click",handler.handleclick,false);
上面的结果貌似会显示“event handler”的结果,但是结果是undefined,是因为没有保存handler.handleclick()的环境。所以this对象最后是指向了dom按钮,而非handler对象(在ie8中,this指向window)。可以使用一个闭包来修正这个问题,如下代码:
var handler = { message:"event handler", handleclick:function(event){ console.log(this.message); //event handler } } var btn = document.getelementsbyclassname("my-btn")[0]; btn.addeventlistener("click",function(event){ handler.handleclick(event); },false);
很多javascript库实现了一个可以将函数绑定到指定环境的函数。这个函数一般都叫做bind()。
一个简单的bind()函数接收一个函数和一个环境。并返回一个在给定函数中调用给定函数的函数,并且将所有参数原封不动的传递过去。语法如下:
function bind(fn,context){ return function(){ return fn.apply(context,arguments); } }
那么我们就可以用上面的bind()方法来实现绑定,如下代码:
var handler = { message:"event handler", handleclick:function(event){ console.log(this.message); //event handler } } var btn = document.getelementsbyclassname("my-btn")[0]; btn.addeventlistener("click",bind(handler.handleclick,handler),false);
ecmascript5为所有函数定义了一个原生的bind()方法,那么上面代码可以如下:
var handler = { message:"event handler", handleclick:function(event){ console.log(this.message); //event handler } } var btn = document.getelementsbyclassname("my-btn")[0]; btn.addeventlistener("click",handler.handleclick.bind(handler),false);
原生的bind()方法和上面自定义的bind()方法很相似,都要传入作为this值的对象。支持原生bind()方法的浏览器有ie9+、firefox4+和chrome。
函数柯里化
与函数绑定紧密相关的是主题是函数柯里化(function curring),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数,如下例子:
function add(num1,num2){ return num1 + num2; } function curriedadd(num2){ return add(5,num2); } console.log(add(2,3)); //5 console.log(curriedadd(3)); //8
尽管从技术上来说curriedadd()并非柯里化的函数,但它很好的展示了其概念。
柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传入要柯里化的函数和必要参数。下面是创建柯里化函数的通用方式:
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(num1,num2){ return num1 + num2; } var curriedadd = curry(add,5); curriedadd(3); //8
也可像下面方式调用:
function add(num1,num2){ return num1 + num2; } var 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){ console.log(this.message + ":" + name + ":" + event.type); } } var btn = document.getelementbyid('my-btn'); btn.addeventlistener('click',bind(handler.handleclick,handler,"my-btn"),false); //event handled:my-btn:click
ecmascript5的bind()方法也实现了函数的柯里化,如下代码:
var handler = { message:"event handled", handleclick:function(name,event){ console.log(this.message + ":" + name + ":" + event.type); } } var btn = document.getelementbyid('my-btn'); btn.addeventlistener('click',handler.handleclick.bind(handler,"my_btn"),false); //event handled:my-btn:click
防篡改对象
注意:一旦把对象定义为防篡改,就无法撤销。
不可扩展对象object.preventextensions()
使用object.preventextensions()就不能给原对象添加新的属性和方法了,如下例子:
var person = { name:"jack" }; object.preventextensions(person); person.age = 20; console.log(person.age); //undefined 严格模式下抛出异常
使用object.isextensible()确定对象是否可以扩展,如下:
var person = { name:"jack" }; console.log(object.isextensible(person)); //true object.preventextensions(person); console.log(object.isextensible(person)); //false
密封的对象object.seal()
密封对象不可扩展,而且已有成员的[[configurable]]特性将被设置为false。这就意味着不能删除属性和方法,因为不能使用object.defineproperty()把数据属性修改为访问器属性,或者相反。属性值是可以修改的。
var person = { name:"jack" }; object.seal(person); person.age = 20; console.log(person.age); //非严格模式下:undefined 严格模式下:抛出异常 delete person.name; console.log(person.name); //非严格模式下:jack 严格模式下:抛出异常
使用object.issealed()方法可以确定对象是否被密封了。因为被密封的对象不可扩展,所以调用object.isextensible()检测密封的对象也会返回false。
var person = { name:"jack" }; console.log(object.isextensible(person)); //true console.log(object.issealed(person)); //false object.seal(person); console.log(object.isextensible(person));//false console.log(object.issealed(person));//true
冻结的对象object.freeze()
最严格的防篡改级别是冻结对象。冻结的对象既不可扩展,又是密封的,而且对象数据属性[[writable]]特性会被设置为false。如果定义[[set]]函数,访问器属性仍然是可写的。
ecmascript5定义的object.freeze()方法可以用来冻结对象,如下代码:
var person = { name:"jack" }; object.freeze(person); person.age = 20; console.log(person.age); //非严格模式:undefined 严格模式下:抛出异常 delete person.name; console.log(person.name);//非严格模式:jack 严格模式下:抛出异常 person.name = "tom"; console.log(person.name);//非严格模式:jack 严格模式下:抛出异常
使用object.isfrozen()方法检测冻结对象。因为冻结对象既是密封的又是不可扩展的,所以调用object.isextensible()和object.issealed()方法分别返回false和true。
var person = { name:"jack" }; console.log(object.isextensible(person)); //true console.log(object.issealed(person)); //false console.log(object.isfrozen(person)) //false object.freeze(person); console.log(object.isextensible(person));//false console.log(object.issealed(person));//true console.log(object.isfrozen(person)) //true
高级定时器
函数节流
dom操作比起非dom交互需要更多的内存和cpu。连续尝试进行过多的dom相关操作可能会导致浏览器挂起,有时候甚至会崩溃。比如在ie浏览器中使用onresize事件处理程序的时候容易发生,当调整浏览器窗口的时候,该事件会连续发生。在onresize事件处理程序内部如果尝试进行dom操作,其高频率的更改可能会让浏览器崩溃。为了解决这个问题,可以使用定时器对该函数进行节流。
基本形式代码结构如下:
var processor = { timeoutid:null, //实际进行处理的方法 performprocessing:function(){ //实际执行的代码 }, //初始处理调用的方法 process:function(){ cleartimeout(this.timeoutid); var that = this; this.timeoutid = settimeout(function(){ that.performprocessing(); },100); } } //尝试开始 执行 processor.process();
时间间隔设置为了100ms,这表示最后一次调用process()之后,至少100ms后才会调用performprocessing()。所以如果100ms之内调用了20次process(),也只会调用performprocessing()一次。
这个模式可以使用throttle函数来简化,这个函数可以自动进行定时器的设置和清除,如下代码:
function throttle(method,context){ cleartimeout(method.tid); method.tid = settimeout(function(){ method.call(context); },100); }
throttle()函数接受两个参数:要执行的函数以及在哪个作用域中执行。
来看个例子,假如有一个div元素需要保持它的高度始终等于宽度。那么实现这个js代码如下:
window.onresize = function(){ var div = document.getelementbyid("mydiv"); div.style.height = div.offsetwidth + "px"; }
上面代码有两个问题可能造成浏览器运行缓慢,一个是计算offsetwidth属性,如果该元素或者页面上的其它元素有非常复杂的css样式,那么这个过程将会很复杂。 另一个设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的css的话,这有需要很多计算。这就可以用到throttle()函数,如下代码:
function resizediv(){ var div = document.getelementbyid("mydiv"); div.style.height = div.offsetwidth + "px"; } window.onresize = function(){ throttle(resizediv); }
只要代码是周期性执行的,都应该使用节流。
自定义事件
事件是一种叫做观察者的设计模式,这是一种创建松散耦合代码的技术。对象可以发布事件,用来表示在该对象生命周期中某个有趣的时刻到了。然后其它对象可以观察该对象,等等这些有趣的时刻到来并通过运行代码来响应。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体并不知道观察者的任何事情,也就是说它可以独自存在并正常运作即便观察者不存在。从另一个方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。涉及dom上时,dom元素便是主体,你的事件处理代码便是观察者。
事件是与dom交互的最常见的方式,但它们也可以用于非dom代码中--通过实现自定义事件。
自定义事件背后的概念是创建一个管理事件的对象,让其他对象监听那些事件。实现此功能的基本模式可以如下定义:
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); } } };
eventtarget类型有一个单独的属性handlers,用于存储事件处理程序。
定义的三个方法如下:
- 1.addhandler:用于注册给定类型事件的事件处理程序;该方法接受两个参数,事件类型和用于处理该事件的函数。
- 2.fire:触发一个事件;该方法接受一个单独的参数,是一个至少包含type属性的对象。fire()方法先给event对象设置一个target属性,如果它尚未被指定的话。然后它就查找对应该事件类型的一组处理程序,调用各个函数,并给出event对象。因为这些都是自定义事件,所以event对象上还需要的额外信息由你自己决定。
- 3.removehandler:注销某个事件类型的事件处理程序;它接受的参数跟addhandler是一样的。
使用eventtarget类型的自定义事件可以如下使用:
function handlermessage(event){ console.log("message received:" + event.message); } //创建一个新对象 var target = new eventtarget(); //添加一个事件处理程序 target.addhandler("message",handlermessage); //触发事件 target.fire({type:"message",message:"hello world!"}); //移除事件处理程序 target.removehandler("message",handlermessage); //再次,应没有处理程序 target.fire({type:"message",message:"hello world!"});
如下实例:
function object(o){ function f(){}; f.prototype = o; return new f(); } function inheritprototype(subtype,supertype){ var prototype = object(supertype.prototype); prototype.constructor = subtype; subtype.prototype = prototype; } function person(name,age){ eventtarget.call(this); this.name = name; this.age = age; } inheritprototype(person,eventtarget); person.prototype.say = function(message){ this.fire({type:"message",message:message}); }
person类型使用了寄生组合继承方法来继承eventtarget。怎样使用:
function handlermessage(event){ console.log(event.target.name + " says:" + event.message); } //创建新person var person = new person("jack",29); //添加一个事件处理程序 person.addhandler("message",handlermessage); //在该对象上调用一个方法,它触发消息事件 person.say('hi there');
拖放
拖放功能
下例代码自己添加了一部分控制在屏幕区域的代码,如下:
var dragdrop = function(){ var dragging = null, differx = 0, differy = 0, targetwidth = 0, targetheight = 0, windowwidth = 0, windowheight = 0, _ismove = false; //是否移动 function handleevent(event){ //获取事件和目标 event = event || window.event; var target = event.target || event.srcelement; //确认事件类型 switch(event.type){ case "mousedown": if(target.classname.indexof("drggable") != -1){ dragging = target; _ismove = true; differx = event.clientx - target.offsetleft; differy = event.clienty - target.offsettop; targetwidth = target.offsetwidth; targetheight = target.offsetheight; windowwidth = document.documentelement.clientwidth; windowheight = document.documentelement.clientheight; } break; case "mousemove": if(dragging !== null && _ismove){ var left = event.clientx - differx, top = event.clienty - differy; if(left < 0){ left = 0; } else if(left > windowwidth - targetwidth){ left = windowwidth - targetwidth; } if(top < 0){ top = 0; } else if(top > windowheight - targetheight){ top = windowheight - targetheight; } dragging.style.left = left + "px"; dragging.style.top = top + "px"; } break; case "mouseup": dragging = null; _ismove =false; break; } } //公共接口 return { enable:function(){ document.addeventlistener("mousedown",handleevent,false); document.addeventlistener("mousemove",handleevent,false); document.addeventlistener("mouseup",handleevent,false); }, disable:function(){ document.removeeventlistener("mousedown",handleevent,false); document.removeeventlistener("mousemove",handleevent,false); document.removeeventlistener("mouseup",handleevent,false); } } }(); dragdrop.enable();
添加自定义事件
上面写的拖放功能还不能真正应用起来,除非能知道什么时候拖动开始了。从这点来看,前面的代码没有提供任何方法表示拖动开始、正在拖动或者拖动结束。这时,可以使用自定义事件来指示这几个事件的发生,让应用的其它部分和拖动功能进行交互。如下代码:
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); } } }; var dragdrop = function(){ var dragdrop = new eventtarget(), dragging = null, differx = 0, differy = 0; function handleevent(event){ //获取事件和目标 event = event || window.event; var target = event.target || event.srcelement; //确认事件类型 switch(event.type){ case "mousedown": if(target.classname.indexof("drggable") != -1){ dragging = target; _ismove = true; differx = event.clientx - target.offsetleft; differy = 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 - differx) + "px"; dragging.style.top = (event.clienty - differy) + "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(){ document.addeventlistener("mousedown",handleevent,false); document.addeventlistener("mousemove",handleevent,false); document.addeventlistener("mouseup",handleevent,false); }; dragdrop.disable = function(){ document.removeeventlistener("mousedown",handleevent,false); document.removeeventlistener("mousemove",handleevent,false); document.removeeventlistener("mouseup",handleevent,false); }; return dragdrop; }();
这段代码定义了三个自定义事件:dragstart、drag、dragend,它们都将被拖动的元素设置为了target,并给出了x和y属性来表示当前的位置。调用如下:
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/> dropped" + event.target.id + " at(" + event.x + "," + event.y + ")"; });
贴出html代码:
<div id="status"></div> <div id="mydiv" class="drggable"></div>
css代码:
*{margin:0;padding:0;} #mydiv{width:200px;height:200px;background: blue;position: absolute;top:50px;left:400px;}
上一篇: 任正非女儿为什么叫孟晚舟(孟晚舟近期照及现状如何)
下一篇: 函数的定义和调用