讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議185:減少對像成員訪問 >

建議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次。在一個函數中,絕不應該對一個對像成員進行超過一次的搜索,除非該值可能改變。