首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议185:减少对象成员访问

关灯直达底部

大多数JavaScript代码是以面向对象的形式编写的,无论是创建的自定义对象还是使用内置的对象,如文档对象模型(DOM)和浏览器对象模型(BOM)之中的对象,因此,存在很多对象成员访问。

对象成员包括属性和方法,在JavaScript中,二者差别甚微。对象成员可以包含任何数据类型。既然函数也是一种对象成员,那么对象成员除了包含传统数据类型外,也可以包含一个函数。当一个命名成员引用了一个函数时,该成员又被称做方法,而一个非函数类型的数据则被称做属性。

访问对象成员比访问直接量或局部变量速度慢,在某些浏览器上比访问数组项还要慢。要弄清其中的原因,首先要理解JavaScript中对象的性质。

JavaScript中的对象是基于原型的。原型是其他对象的基础,定义并实现了一个新对象所必须具有的成员。这一概念完全不同于传统面向对象编程中类的概念,它定义了创建新对象的过程。原型对象为所有给定类型的对象实例所共享,因此所有实例共享原型对象的成员。

一个对象通过一个内部属性绑定到它的原型。Firefox、Safari和Chrome浏览器向开发人员开放这一属性(__proto__),其他浏览器不允许脚本访问这一属性。在任何时候创建一个内置类型的实例,如Object或Array,它们自动拥有一个Object作为它们的原型。

因此,对象可以有两种类型的成员:实例成员和原型成员。实例成员直接存在于实例自身中,而原型成员则从对象原型继承。例如:


var book={

title:/"Javascript/",

publisher:/"机械工业出版社/"

};

alert(book.toString);///"[object Object]/"


在此代码中,book对象有两个实例成员:title和publisher。注意这里并没有定义toString接口,但这个接口却被调用了,而且并没有抛出错误。toString函数就是一个book对象继承的原型成员。

处理对象成员的过程与变量处理十分相似。当book.toString被调用时,对成员进行名为“toString”的搜索,首先从对象实例开始,如果book没有名为toString的成员,那么就转向搜索原型对象,在那里发现toString方法并执行它。通过这种方法,book可以访问它的原型所拥有的每个属性或方法。

可以利用hasOwnProperty函数确定一个对象是否具有特定名称的实例成员,它的参数就是成员名称。要确定对象是否具有某个名称的属性,可以使用操作符in,例如:


var book={

title:/"Javascript/",

publisher:/"机械工业出版社/"

};

alert(book.hasOwnProperty(/"title/"));//true

alert(book.hasOwnProperty(/"toString/"));//false

alert(/"title/"in book);//true

alert(/"toString/"in book);//true


在上面代码中,当hasOwnProperty传入title时,返回true,因为title是一个实例成员。当hasOwnProperty传入toString时,返回false,因为toString不在实例之中。如果使用in操作符检测这两个属性,那么返回都是true,因为它既搜索实例又搜索原型。

对象的原型决定了一个实例的类型。在默认情况下,所有对象都是Object的实例,并且继承了所有基本方法,如toString。可以用构造器创建另外一种类型的原型,例如:


function Book(title,publisher){

this.title=title;

this.publisher=publisher;

}

Book.prototype.sayTitle=function{

alert(this.title);

};

var book1=new Book(/"CSS/",/"电子/");

var book2=new Book(/"HTML/",/"清华/");

alert(book1 instanceof Book);//true

alert(book1 instanceof Object);//true

book1.sayTitle;///"CSS/"

alert(book1.toString);///"[object Object]/"


Book构造器用于创建一个新的Book实例。book1的原型(__proto__)是Book.prototype,Book.prototype的原型是Object。这就创建了一个原型链,book1和book2继承了它们的成员。

注意:两个Book实例共享同一个原型链,每个实例拥有自己的title和publisher属性,但其他成员均继承自原型。当book1.toString被调用时,搜索工作必须深入原型链才能找到对象成员toString。深入原型链越深,搜索的速度就会越慢。

虽然采用优化JavaScript引擎的新式浏览器在此任务中表现良好,但是对于旧版本的浏览器,特别是IE和Firefox 3.5,每深入原型链一层都会增加性能损失。记住,搜索实例成员的过程比访问直接量或局部变量负担更重,增加遍历原型链的开销正好放大了这种效果。

由于对象成员可能包含其他成员,因此对于不太常见的写法,例如window.location.href这种模式,每遇到一个点号,JavaScript引擎就要在对象成员上执行一次解析过程。

成员嵌套越深,访问速度越慢。location.href总是快于window.location.href,而hasOwn-Property也要比window.location.href.toString更快。如果这些属性不是对象的实例属性,那么成员解析还要在每个点上搜索原型链,这将需要更长时间。

由于所有这些性能问题与对象成员有关,因此如果可能避免使用它们。更确切地说,只在必要情况下使用对象成员。例如,没有理由在一个函数中多次读取同一个对象成员的值。


function hasEitherClass(element,className1,className2){

return element.className==className1||element.className==className2;

}


在上面代码中,element.className被访问了两次。很明显,在这个函数的执行过程中element.className的值是不会改变的,但仍然引起两次对象成员搜索过程。可以将element.className的值存入一个局部变量,消除一次搜索过程。


function hasEitherClass(element,className1,className2){

var currentClassName=element.className;

return currentClassName==className1||currentClassName==className2;

}


在重写后的代码中成员搜索只进行了一次。既然两次对象搜索都在读属性值,因此有理由只读一次并将值存入局部变量中。局部变量的访问速度要快得多。

一般来说,要在同一个函数中多次读取同一个对象属性,最好将它存入一个局部变量。以局部变量替代属性,避免多余的属性查找带来性能开销。在处理嵌套对象成员时这点特别重要,因为多次属性查找会对运行速度产生难以想象的影响。

JavaScript的命名空间,如YUI所使用的技术,是经常访问嵌套属性的来源之一,例如:


function toggle(element){

if(YAHOO.util.Dom.hasClass(element,/"selected/")){

YAHOO.util.Dom.removeClass(element,/"selected/");

return false;

}else{

YAHOO.util.Dom.addClass(element,/"selected/");

return true;

}

}


此代码重复YAHOO.util.Dom 3次以获得3种不同的方法。每个方法都产生3次成员搜索过程,总共3次,导致此代码相当低效。一个更好的方法是将YAHOO.util.Dom存储在局部变量中,然后访问局部变量。


function toggle(element){

var Dom=YAHOO.util.Dom;

if(Dom.hasClass(element,/"selected/")){

Dom.removeClass(element,/"selected/");

return false;

}else{

Dom.addClass(element,/"selected/");

return true;

}

}


总的成员搜索次数从9次减少到5次。在一个函数中,绝不应该对一个对象成员进行超过一次的搜索,除非该值可能改变。