1.高级函数
1.安全的类型检测
instanceof操作符在存在多个全局作用域(一个页面包含多个框架)的情况下,也会出现问题
如果要返回true,value必须是一个数组,必须与Array构造函数在同一个全局作用域(Array是window的属性),如果value定义在另一个框架中,就会返回falsevar isArray = value instanceof Array
由于JSON库中定义了一个全局JSON对象。很难确定页面中的JSON对象是不是原生的
解决:使用原生的toString()方法,返回[object NativeConstructorName] 格式的字符串。内部[[Class]]属性指定了字符串中的构造函数名。
由于原生数组的构造函数名与全局作用域无关,使用toString()能保证返回一致的值
function isArray(value){ return Object.prototype.toString.call(value) == "[object Array]"; } function isFunction(value){ return Object.prototype.toString.call(value) == "[object Function]"; } var isNativeJSON = window.JSON&&Object.prototype.toString.call(JSON)=="[object JSON]";
2.作用域安全的构造函数
构造函数内使用this的问题:
当我们使用new关键字去调用构造函数时,this会指向新创建的对象。当不使用new操作符来调用构造函数时,this对象在运行时绑定,直接调用Person()构造函数,this就会映射到全局对象window上。
解决:创建一个作用域安全的构造函数
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 person = Person("zhangsan",12,"stu"); console.log(window.name); console.log(person.name);
一旦实现这个模式后,就相当于锁定了可以调用构造函数的环境。如果使用构造函数窃取模式的继承,这个继承就会被破坏。
使用原型链或寄生组合的方式去实现继承function Person(name,age){ if(this instanceof Person){ this.name = name; this.age = age; }else{ return new Person(name,age); } } function Child(name,age,color){ Person.call(this,name,age); //会新创建一个对象,不会直接调用构造函数 this.color = color; this.sayHi = function(){ console.log("hi"); } } var child = new Child("zhangsan",13,"black"); console.log(child.name);//undefined
function Person(name,age){ if(this instanceof Person){ this.name = name; this.age = age; }else{ return new Person(name,age); } } function Child(name,age,color){ Person.call(this,name,age); //会新创建一个对象,不会直接调用构造函数 this.color = color; this.sayHi = function(){ console.log("hi"); } } Child.prototype = new Person();//原型指向Person,调用构造函数时,检查对象的类型就是Person var child = new Child("zhangsan",13,"black"); console.log(child instanceof Person);//true console.log(child.name);// zhangsan
3.惰性载入函数
函数执行的分支仅会发生一次。有两种实现惰性载入的方式:
1.在函数被调用时再处理函数,第一次调用时,函数被覆盖为另一个适合执行的函数
if每个语句都会给sayHi变量赋值,有效覆盖了原有的函数。下次执行时会直接调用被分配的函数。function sayHi(type){ if(type == 'teacher'){ sayHi = function(){ return "teacher"; } }else if(type == "child"){ sayHi = function(){ return "child"; } }else{ sayHi = function(){ return "nobody"; } } return sayHi(); } console.log(sayHi("teacher")); //teacher console.log(sayHi());//不会再判断执行分支 teacher
2.在声明函数时指定适当的函数(代码首次加载时会损失一点性能,第一次调用函数时就不会损失性能了)
var sayHi = (function(type){ if(type == 'teacher'){ return function(){ return "teacher"; } }else if(type == "child"){ return function(){ return "child"; } }else{ return function(){ return "nobody"; } } })("child"); console.log(sayHi()); //teacher console.log(sayHi());//不会再判断执行分支 teacher
惰性载入函数的优点是在执行分支代码时牺牲一点性能,从而避免执行不必要的代码。
4.函数绑定
函数绑定要创建一个函数,可以在特定的this环境中指定参数调用另一个函数。该技巧常常和回调函数与事件处理程序一起使用 ,以便在将函数作为变量传递的同时保留代码执行环境
问题:
没有保存handler.handlerClick()的环境,this对象最后指向了DOM按钮,而不是handlervar handler = { message:"event handler", handlerClick:function(){ console.log(this);//指向button按钮 console.log(this.message); } } var btn = document.getElementById("btn"); btn.onclick = handler.handlerClick; //undefined
解决:使用闭包
var handler = { message:"event handler", handlerClick:function(){ console.log(this);//指向button按钮 console.log(this.message); } } var btn = document.getElementById("btn"); btn.onclick = function(event){ handler.handlerClick(event); }; //event handler
bind():将函数绑定到指定环境的函数
自定义bind()函数,接收一个函数和一个环境
function bind(fn,context){ return function(){//接收event参数 return fn.apply(context,arguments);//arguments是传给内部函数的参数 } } var handler ={ message:"event handled", handleClick:function(event){ console.log(this.message); console.log(event.type); } } var btn = document.getElementById("btn"); btn.onclick = bind(handler.handleClick,handler);//click事件触发内部函数,给内部函数传递event参数
用bind()函数创建了一个保持了执行环境的函数。
ECMAScript5为所有函数定义了原生的bind()方法。
btn.onclick = handler.handleClick.bind(handler);
只要是将某个函数指针以值的形式进行传递,同时该函数必须在特定环境中执行,被绑定函数的效用就突显出来了。它们主要用于事件处理程序以及setTimeout()和setInterval()。然而,被绑定函数与普通函数相比有更多的开销,热门需要更多内存,同时也因为多重函数调用稍微慢一点,所以最好只在必要时使用。
5.函数柯里化
用于创建已经设置好了一个或多个参数的函数。创建方式:使用闭包返回一个函数。
function add(num1,num2){ return num1 + num2; } function curriedAdd(num2){ return add(3,num2); } console.log(curriedAdd(2)); //2+3=5
函数柯里化:调用另一个函数并为它传入要柯里化的函数和必要参数。
function curry(fn){ var args = Array.prototype.slice.call(arguments,1);//2 return function(){ var innerArgs = Array.prototype.slice.call(arguments);//3 var finalArgs = args.concat(innerArgs); //2,3 return fn.apply(null,finalArgs); } } function add(num1,num2){ return num1+num2; } var curriedAdd = curry(add,2); console.log(curriedAdd(3)); //5
函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更复杂的bind()函数
function bind(fn,context){ //获取柯里化的参数 var args = Array.prototype.slice.call(arguments,2);//"zhangsan",12 return function(){ //获取调用函数时传入的参数 var innerArgs = Array.prototype.slice.call(arguments);//event var finalArgs = innerArgs.concat(args);//event,"zhangsan",12 return fn.apply(context,finalArgs); } } handler = { message:"hehe", handleClick:function(event,name,age){ console.log(this.message); console.log(event.type); console.log(name+":"+age); } } var btn = document.getElementById("btn"); btn.onclick = bind(handler.handleClick,handler,"zhangsan",12);
ECMAScript5的bind()方法也实现了函数柯里化,只要在this的值后再传入另一个参数即可.
handler = { message:"hehe", handleClick:function(name,age,event){//event对象作为最后一个参数传入 console.log(this.message); console.log(event.type); console.log(name+":"+age); } } var btn = document.getElementById("btn"); btn.onclick = handler.handleClick.bind(handler,"zhangsan",12);
JS中的柯里化函数和绑定函数提供了强大的动态函数创建功能。使用bind()还是curry()要根据是否需要object对象响应来决定。它们都能用于创建复杂的算法和功能,当然两者都不应滥用,因为每个函数都会带来额外的开销。
2.防篡改对象
JavaScript共享的本质会导致很多问题,因为任何对象都可以被在同一环境中运行的代码修改。开发人员很可能以外修改别人的代码。
ECMAScript定义了防篡改对象(tamper-proof object)。 一旦将对象定义成防篡改,就无法撤销了。
1.不可扩展对象
默认情况下,所有对象都是可以扩展的,任何时候都可以向对象中添加属性和方法。Object.preventExtensions()方法可以禁止给对象添加属性和方法。Object.istExtensible():检测对象是否可扩展
var person ={ name:"张三", age:"12" } Object.preventExtensions(person); person.job = "child"; person.name = "lisi"; console.log(Object.isExtensible(person));//false console.log(person.job);//undefined console.log(person.name);// lisi
2.密封的对象
ECMAScript5为对象定义的第二个保护级别是密封对象(sealed object)。密封对象不可扩展,而且已有成员的[[Configurable]]特性为false。不能删除属性和方法,可以修改var person ={ name:"张三", age:"12" } Object.seal(person); person.job = "child"; person.name = "lisi";//可以修改 delete person.name;//无法删除 console.log(Object.isExtensible(person));//false console.log(Object.isSealed(person));//true console.log(person.job);//undefined console.log(person.name);// lisi
3.冻结的对象
最严格的防篡改级别是冻结对象(frozen object)。不可扩展,又是密封的,而且对象的属性的[[Writable]]特性会被设置为false。无法直接修改属性,但是如果定义[[Set]]函数,访问器属性任然可以修改。Object.freeze() : 冻结对象。Object.isFrozen():检测对象是否冻结。
var person ={ name:"张三", age:"12" } Object.freeze(person);//冻结对象 person.name = "lisi";//冻结后不能修改属性 console.log(Object.isExtensible(person));//false console.log(Object.isSealed(person));//true console.log(Object.isFrozen());//true console.log(person.name);// 张三
3.高级定时器
JavaScript是 运行在单线程环境中的,定时器仅仅只是 计划代码在未来某个时间执行,但并不能保证。因为在页面生命周期中,不同时间可能有其他代码在控制JavaScript进程。在页面下载完后的代码运行,事件处理程序,Ajax回调函数都必须使用同样的线程来执行。实际上,浏览器负责排序,指派某段代码在某个时间点运行的优先级。
可以把JavaScript想象成在时间线上运行的。当页面载入时,首先执行的是任何包含在<script>元素中的代码,通常是页面生命周期后面要用到的一些简单的函数和变量的声明,有时候也包含一切初始数据的处理。
除了主JavaScript执行进程外,还有一个需要在进程下一次空闲时执行的代码队列。随着页面在其生命周期中的推移,代码会按照执行顺序加入队列。如:当按下某个按钮时,时间处理程序代码会被添加到队列,并在下一个可能的时间里执行,当接收到某个Ajax响应时,回调函数的代码会被添加到队列。
定时器工作方式: 特定时间过去后将代码插入队列(并不意味着立刻执行,如果这个时间点上,队列中没有其他东西,这段代码就会得到执行)
执行完一套代码后,JavaScript进程会返回一段很短的时间,这样页面上的其他处理就可以进行了。由于JavaScript进程会阻塞其他页面处理,所以必须有这些小间隔来防止用户界面被锁定(代码长时间运行中还有可能出现)。这样设置一个定时器,可以确保在定时器代码执行前至少有一个进程间隔。
1.重复的定时器
setInterval()创建定时器,确保了定时器代码规则地插入队列中。问题:定时器代码可能在代码再次被添加到队列之前还没执行完。导致定时器代码连续运行好几次,而之间的时间没有任何停顿。
面对这种情况,JavaScript引擎做了处理。setInterval()时,仅当没有该定时器代码时,才向队列中添加定时器代码。导致(1)某些时间间隔被跳过。(2)多个定时器代码执行之间的时间间隔可能比预期小。
使用链式setTimeout()方式调用来避免上述问题。
setTimeout(function(){ var div = document.getElementById("editDiv"); left = parseInt(div.style.left)+5; div.style.left = left+"px"; console.log(div.style.left); if(left<300){ setTimeout(arguments.callee,500); } },100);
2.Yielding Processes
在浏览器中的JS都被分配了一个确定数量的资源。需要的内存大小和处理器时间都被严格限制了,防止恶意的Web程序员把用户机器资源耗尽。
脚本长时间运行的问题通常由两个原因之一造成:过长的,过深嵌套的函数调用或者是进行大量处理的循环。
对于后者,可以使用定时器分割这个循环。这是一种叫做数组分块(array chunking)的技术,小块小块地处理数组。
基本思路:为要处理的项目创建一个队列,然后使用定时器取出下一个要处理的项目进行处理,接着再设置另一个定时器。
setTimeout(function(){ //取出下一个条目进行该处理 var item = array.shift(); process(item); //如果还有条目,再设置另一个定时器 if(array.length>0){ setTimeout(arguments.callee,100);//使用arguments.callee调用同一个匿名函数 } },100);
实现数组分块:可将多个项目的处理在执行队列上分开,在每个项目处理后,给予其他的浏览器处理机会运行,可避免长时间运行脚本的错误。function chunk(array,process,context){ setTimeout(function(){ var item = array.shift();//待处理事项 process.call(context,item); if(array.length>0){//如果还有待办事项 setTimeout(arguments.callee,100); } },100); } function printValue(value){ console.log(value); } var data = [1,2,3,4,5,6,7,8]; chunk(data.concat(),printValue);//传递数组的克隆,保持原数组不变
一旦某个函数要花50ms以上的时间完成,最好将任务分割为一系列可以使用定时器的小任务。
3.函数节流
浏览器中某些计算和处理要比其他的昂贵很多。如,DOM操作比非DOM交互需要更多的内存和CPU时间。连续尝试进行过多的DOM相关操作可能会导致浏览器挂起,有时候甚至会崩溃。尤其在IE中使用onresize事件处理程序时。为了绕开这个问题,可以使用定时器对该函数进行节流。
基本思想:指某些代码不可以在没有间断的情况连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。如果前一个定时器未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。
基本形式:
由于setTimeout(0中用到的函数环境总是window,所以需要保存this的引用以便以后使用。var processor ={ timeoutId:null, //实际处理的程序 performProcessing:function(){ //实际执行的代码 } //初始处理调用的方法 process:function(){ //清除前一个定时器 clearTimeout(this.timeoutId); //保留this当前执行环境 var processContext = this; //设置新的处理程序定时器 this.timeoutId = setTimeout(function(){ processContext.performProcessing();//调用实际处理程序 },100); } } //开始执行 processor.process();
process()之后至少100ms后才会调用performProcessing()。如果100ms内调用了多次process(),performProcessing()只调用一次。
使用throttle()函数来简化,该函数可自动进行定时器的设置和清除。
只要代码是周期性执行的,都应该使用节流。function throttle(method,context){ clearTimeout(method.tId); method.tId = setTimeout(function(){ method.call(context);//如果没有给定执行环境,就会在全局作用域内执行 },1000); } function resizeDiv(){ var div = document.getElementById("editDiv"); div.style.height = div.offsetWidth + "px"; } window.onresize = function(){ //resizeDiv(); throttle(resizeDiv); }
4.自定义事件
事件是JavaScript与浏览器交互的主要途径。事件是一种叫做观察者的设计模式,是一种创建松散耦合代码的技术。
观察者模式由两类对象组成:主体和观察者。主体负责发布事件,同时观察者通过订阅这些事件来观察该主体。该模式的一个关键概念是主体不知道观察者的任何事情,也就是说它可以独立只存在并正常运作,即使观察者不存在。从另一方面来说,观察者知道主体并能注册事件的回调函数(事件处理程序)。
DOM元素便是主体,事件处理代码便是观察者。
事件是与DOM交互的最常见的方式,但它们也可以用于非DOM代码中--通过实现自定义事件。创建一个管理事件的对象,让其他对象监听那些事件。基本模式:使用自定义的事件:function EventTarget(){ this.handlers = {};//存储事件处理程序 } EventTarget.prototype = { constructor:EventTarget, //type事件类型,handler处理该事件的函数 addHandler:function(type,handler){//注册给定类型事件的事件处理程序 if(typeof this.handlers[type] == 'undefined'){ this.handlers[type] = []; } this.handlers[type].push(handler); }, //event,至少包含type属性的对象 fire:function(event){//触发一个事件 if(!event.target){//给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);//从数组中删除该处理程序 } } }
function handlerMessage(event){ console.log("消息处理:"+event.message); } //创建一个新的对象 var target = new EventTarget(); //添加事件处理程序,类型是message target.addHandler("message",handlerMessage); //触发message类型的事件,并自定义event对象的message属性 target.fire({type:"message",message:"消息事件处理"}); //删除事件处理程序 target.removeHandler("message",handlerMessage); //再次触发事件,已删除 target.fire({type:"message",message:"再次触发事件,已删除"});
因为这种功能是封装在一种自定义类型中的,其他对象可以继承EventTarget并获得这个行为。
function Person(name,age){ EventTarget.call(this); this.name = name; this.age = age; } Person.prototype = new EventTarget(); Person.prototype.constructor = Person; //自定义方法 Person.prototype.say = function(msg){ //触发自定义的message事件 person.fire({type:"message",message:msg}); } var person = new Person("zhangsan",23); //添加事件处理程序 person.addHandler("message",function(event){ console.log(event.message + event.target.name ); }); person.say("hi,i'm ");// hi,i'm zhangsan
当代码中存在多个部分在特定时刻相互交互的情况下,自定义事件就非常有用了。这时,如果每个对象都有对其他所有对象的引用,那么整个代码就会紧密耦合,同时维护也变得很困难,因为对某个对象的修改也会影响到其他对象。使用自定义事件有助于解耦相关对象,保持功能的隔绝。在很多情况中,触发事件的代码和监听事件的代码是完全分离的。