我寫JavaScript程式碼已經很久了,我都記不起是什麼年代開始的了。對於JavaScript這種語言近幾年所取得的成就,我感到非常的興奮;我很幸運也是這些成就的受益者。我寫了不少的文章,章節,還有一本專門討論它的書,然而,我現在依然能發現一些關於這種語言的新知識。下面的描述的就是過去讓我不由得發出「啊!」的感嘆的程式技巧,這些技巧你應該現在就試試,而不是等著未來的某個時候偶然的發現它們。
簡潔寫法
JavaScript裡我最喜歡的一種東西就是生成物件和陣列的簡寫方法。在過去,如果你想創建一個對象,你需要這樣:
1 var car = new Object(); 2 car.colour = 'red'; 3 car.wheels = 4; 4 car.hubcaps = 'spinning'; 5 car.age = 4;
下面的寫法能夠達到同樣的效果:
1 var car = { 2 colour:'red', 3 wheels:4, 4 hubcaps:'spinning', 5 age:4 6 }
簡單多了,你不需要反複使用這個對象的名稱。這樣 car 就定義好了,也許你會遇到 invalidUserInSession 的問題,這只有你在使用IE時會碰到,只要記住一點,不要右大括號前面寫逗號,你就不會有麻煩。
另外一個十分方便的簡寫是針對數組的。傳統的定義數組的方法是這樣:
1 var moviesThatNeedBetterWriters = new Array( 2 'Transformers','Transformers2','Avatar','IndianaJones 4' 3 );
簡寫版的是這樣:
1 var moviesThatNeedBetterWriters = [ 2 'Transformers','Transformers2','Avatar','IndianaJones 4' 3 ];
對於數組,這裡有個問題,其實沒有什麼圖組功能。但你會常常發現有人這樣定義上面的 car ,就像這樣
1 var car = new Array(); 2 car['colour'] = 'red'; 3 car['wheels'] = 4; 4 car['hubcaps'] = 'spinning'; 5 car['age'] = 4;
數組不是萬能的;這樣寫不對,會讓人困惑。圖組其實是物件的功能,人們混淆了這兩個概念。
另一個非常酷的簡寫方法是使用與三元條件符號。你不必寫成下面的樣子…
var direction; if(x < 200){ direction = 1; } else { direction = -1; }
你可以使用三元條件符號簡化它:
var direction = x < 200 ? 1 : -1;
當條件為true 時取問號後面的值,否則取冒號後面的值。
用JSON 形式儲存資料
在我發現JSON之前,我使用各種瘋狂的方法把資料存貯在JavaScript固有的資料類型裡面,例如:數組,字串,中間夾雜著容易進行分割的標誌符號以及其它的令人討厭的東西。 Douglas Crockford 發明了JSON 之後,一切都變了。使用JSON,你可以使用JavaScript自有功能把資料存貯成複雜的格式,而且不需要再做其它的額外轉換,直接可以存取使用。 JSON 是 “JavaScript Object Notation” 的縮寫,它用到了上面提到的兩種簡寫方法。於是,如果你想描述一個樂隊,你可能會像這樣寫:
01 var band = { 02 "name":"The Red Hot Chili Peppers", 03 "members":[ 04 { 05 "name":"Anthony Kiedis", 06 "role":"lead vocals" 07 }, 08 { 09 "name":"Michael 'Flea' Balzary", 10 "role":"bass guitar, trumpet, backing vocals" 11 }, 12 { 13 "name":"Chad Smith", 14 "role":"drums,percussion" 15 }, 16 { 17 "name":"John Frusciante", 18 "role":"Lead Guitar" 19 } 20 ], 21 "year":"2009" 22 }
你可以在JavaScript裡直接使用JSON,可以把它封裝在函數裡,甚至作為一個API的返回值形式。我們把這稱為 JSON-P ,很多的API都使用這種形式。
你可以呼叫一個資料提供來源,在script程式碼裡直接回傳 JSON-P 資料:
01 <div id="delicious"></div><script> 02 function delicious(o){ 03 var out = '<ul>'; 04 for(var i=0;i<o.length;i++){ 05 out += '<li><a href="' + o[i].u + '">' + 06 o[i].d + '</a></li>'; 07 } 08 out += '</ul>'; 09 document.getElementById('delicious').innerHTML = out; 10 } 11 </script> 12 <script src="http://feeds.delicious.com/v2/json/codepo8/javascript?count=15&callback=delicious"></script>
這是呼叫 Delicious 網站提供的 Web service 功能,獲得JSON格式的最近的無序書籤清單。
基本上,JSON是最輕便的描述複雜資料結構的方法,而且它能在瀏覽器裡運作。你甚至可以在PHP裡用 json_decode() 函數來運行它。 JavaScript的自帶函數(Math, Array 和 String)讓我感到驚訝的一個事情是,當我研究了JavaScript裡的math和String函數後,發現它們能極大的簡化我的程式設計勞動。使用它們,你可以省去複雜的循環處理和條件判斷。例如,當我需要實現一個功能,找出數字數組裡最大的一個數字時,我過去是這樣寫出這個循環的,就像下面:
1 var numbers = [3,342,23,22,124]; 2 var max = 0; 3 for(var i=0;i<numbers.length;i++){ 4 if(numbers[i] > max){ 5 max = numbers[i]; 6 } 7 } 8 alert(max);
我們不用循環也能實現:
1 var numbers = [3,342,23,22,124]; 2 numbers.sort(function(a,b){return b - a}); 3 alert(numbers[0]);
需要注意的是,你不能對一個數字字元數組進行sort() ,因為這種情況下它只會按照字母順序進行排序。如果你想知道更多的用法,可以閱讀 這篇不錯的關於 sort() 的文章。
再有一個有趣的函數就是 Math.max()。這個函數傳回參數裡的數字裡最大的一個數字:
Math.max(12,123,3,2,433,4); // returns 433
因為這個函數能夠校驗數字,並傳回其中最大的一個,所以你可以用它來測試瀏覽器對某個特性的支援:
1 var scrollTop=Math.max( 2 doc.documentElement.scrollTop, 3 doc.body.scrollTop 4 );
這個是用來解決IE問題的。你可以得到目前頁面的scrollTop 值,但根據頁面上DOCTYPE的不同,上面這兩個屬性中只有一個會存放這個值,而另一個屬性會是undefined,所以你可以透過使用Math.max() 得到這個數。閱讀這篇文章你會得到更多的關於使用數學函數來簡化JavaScript的知識。
另外有一對非常有用的操作字串的函數是 split() 和 join()。我想最具代表性的例子應該是,寫一個功能,用來給頁面元素附加CSS樣式。
是这样的,当你给页面元素附加一个CSS class时,要么它是这个元素的第一个CSS class,或者是它已经有了一些class, 需要在已有的class后加上一个空格,然后追加上这个class。而当你要去掉这个class时,你也需要去掉这个class前面的空格(这个在过去非常重要,因为有些老的浏览器不认识后面跟着空格的class)。
于是,原始的写法会是这样:
1 function addclass(elm,newclass){ 2 var c = elm.className; 3 elm.className = (c === '') ? newclass : c+' '+newclass; 4 }
你可以使用 split() 和 join() 函数自动完成这个任务:
1 function addclass(elm,newclass){ 2 var classes = elm.className.split(' '); 3 classes.push(newclass); 4 elm.className = classes.join(' '); 5 }
这会确保所有的class都被空格分隔,而且你要追加的class正好放在最后。
事件委派
Web应用都是由事件驱动运转的。我喜欢事件处理,尤其喜欢自己定义事件。它能使你的产品可扩展,而不用改动核心代码。有一个很大的问题(也可以说是功能强大的表现),是关于页面上事件的移除问题。你可以对某个元素安装一个事件监听器,事件监听器就开始运转工作。但页面上没有任何指示说明这有个监听器。因为这种不可表现的问题 (这尤其让一些新手头疼) ,以及像IE6这样的”浏览器“在太多的使用事件监听时会出现各种的内存问题,你不得不承认尽量少使用事件编程是个明智的做法。
于是 事件委托 就出现了。
当页面上某个元素上的事件触发时,而在 DOM 继承关系上,这个元素的所有子元素也能接收到这个事件,这时你可以使用一个在父元素上的事件处理器来处理,而不是使用一堆的各个子元素上的事件监听器来处理。究竟是什么意思?这样说吧,页面上有很多超链接,你不想直接使用这些链接,想通过一个函数来调用这个链接,HTML代码是这样的:
1 <h2>Great Web resources</h2> 2 <ul id="resources"> 3 <li><a href="http://opera.com/wsc">Opera Web Standards Curriculum</a></li> 4 <li><a href="http://sitepoint.com">Sitepoint</a></li> 5 <li><a href="http://alistapart.com">A List Apart</a></li> 6 <li><a href="http://yuiblog.com">YUI Blog</a></li> 7 <li><a href="http://blameitonthevoices.com">Blame it on the voices</a></li> 8 <li><a href="http://oddlyspecific.com">Oddly specific</a></li> 9 </ul>
常见的做法是通过循环这些链接,将每个链接上附加一个事件处理器:
01 // 典型的事件处理例子 02 (function(){ 03 var resources = document.getElementById('resources'); 04 var links = resources.getElementsByTagName('a'); 05 var all = links.length; 06 for(var i=0;i<all;i++){ 07 // Attach a listener to each link 08 links[i].addEventListener('click',handler,false); 09 }; 10 function handler(e){ 11 var x = e.target; // Get the link that was clicked 12 alert(x); 13 e.preventDefault(); 14 }; 15 })();
我们用一个事件处理器也能完成这项任务:
01 (function(){ 02 var resources = document.getElementById('resources'); 03 resources.addEventListener('click',handler,false); 04 function handler(e){ 05 var x = e.target; // get the link tha 06 if(x.nodeName.toLowerCase() === 'a'){ 07 alert('Event delegation:' + x); 08 e.preventDefault(); 09 } 10 }; 11 })();
因为点击事件就发生在这些页面元素里,你要做的就是比较它们的 nodeName,找出应该回应这个事件的那个元素。
免责声明:上面说的这两个关于事件的例子,在所有浏览器里都能运行,除了IE6,在IE6上你需要使用一个事件模型,而不是简单的W3C的标准实现。这也就是我们推荐使用一些工具包的原因。
这种方法的好处并不是仅限于把多个事件处理器缩减为一个。你想想,举个例子,你需要动态的往这个链接表里追加更多的链接。使用事件委托后,你就不需要做其它修改了;否则的话,你需要重新循环这个链接表,重新给每个链接安装事件处理器。
匿名函数和模块化
在JavaScript里最令人懊恼的事情是变量没有使用范围。任何变量,函数,数组,对象,只要不在函数内部,都被认为是全局的,这就是说,这个页面上的其它脚本也可以访问它,而且可以覆盖重写它。
解决办法是,把你的变量放在一个匿名函数内部,定义完之后立即调用它。例如,下面的写法将会产生三个全局变量和两个全局函数:
1 var name = 'Chris'; 2 var age = '34'; 3 var status = 'single'; 4 function createMember(){ 5 // [...] 6 } 7 function getMemberDetails(){ 8 // [...] 9 }
如果这个页面上的其它脚本里也存在一个叫 status 的变量,麻烦就会出现。如果我们把它们封装在一个 myApplication 里,这个问题就迎刃而解了:
01 var myApplication = function(){ 02 var name = 'Chris'; 03 var age = '34'; 04 var status = 'single'; 05 function createMember(){ 06 // [...] 07 } 08 function getMemberDetails(){ 09 // [...] 10 } 11 }();
但是,这样一来,在函数外面就没有什么功能了。如果这是你需要的,那就可以了。你还可以省去函数的名称:
01 (function(){ 02 var name = 'Chris'; 03 var age = '34'; 04 var status = 'single'; 05 function createMember(){ 06 // [...] 07 } 08 function getMemberDetails(){ 09 // [...] 10 } 11 })();
如果你想在函数外面也能使用里面的东西,那就要做些修改。为了能访问 createMember() 或 getMemberDetails(),你需要把它们变成 myApplication的属性,从而把它们暴露于外部的世界:
01 var myApplication = function(){ 02 var name = 'Chris'; 03 var age = '34'; 04 var status = 'single'; 05 return{ 06 createMember:function(){ 07 // [...] 08 }, 09 getMemberDetails:function(){ 10 // [...] 11 } 12 } 13 }(); 14 //myApplication.createMember() 和 15 //myApplication.getMemberDetails() 就可以使用了。
这被称作 module 模式或 singleton。Douglas Crockford 多次谈到过这些,Yahoo User Interface Library YUI 里对此有大量的使用。但这样一来让我感到不便的是,我需要改变句式来使函数和变量能被外界访问。更甚者,调用时我还需要加上myApplication 这个前缀。所以,我不喜欢这样做,我更愿意简单的把需要能被外界访问的元素的指针导出来。这样做后,反倒简化了外界调用的写法:
01 var myApplication = function(){ 02 var name = 'Chris'; 03 var age = '34'; 04 var status = 'single'; 05 function createMember(){ 06 // [...] 07 } 08 function getMemberDetails(){ 09 // [...] 10 } 11 return{ 12 create:createMember, 13 get:getMemberDetails 14 } 15 }(); 16 //现在写成 myApplication.get()和 myApplication.create() 就行了。
我把这个称作 “revealing module pattern.”
可配置化
一旦我把所写的JavaScript代码发布到这个世界上,就有人想改动它,通常是人们想让它完成一些它本身完成不了的任务—但通常也是我写的程序不够灵活,没有提供用户可自定义的功能。解决办法是给你的脚本增加一个配置项对象。我曾经写过一篇深入介绍JavaScript配置项对象的文章,下面是其中的要点:
在你的腳本了加入一個叫做 configuration 的物件。
這個物件裡面,存放所有人們在使用這個腳本時經常要改動的東西:
CSS ID 和類別名稱;
按鈕的名稱,標籤字等;
諸如」每頁顯示圖片數」的值, “圖像的顯示的尺寸”的值;
地點,位置,以及語言設定。
將這個物件作為一個公用屬性傳回給用戶,這樣用戶可以修改覆蓋它。
通常情況下這是你編程過程中的最後一步要做的事情。我把這些集中表現在了一個例子裡: “Five things to do to a script before handing it over to the next developer.”
實際上,你也希望你的程式碼能讓人們很方面的使用,並且根據他們各自的需要進行一些改變。如果你實現了這個功能,你會少收到一些抱怨你的腳本的人發送給你的讓你困惑的郵件,這些信件會告訴你,有人修改了你的腳本,而且很好用。
與後台互動
在這麼多年的程式設計經驗中,我所領悟到的一個重要的事情就是,JavaScript是一個很優秀的開發介面互動的語言,但如果用來處理數位或存取資料來源,那就有點使不上勁了。
最初,我學習JavaScript,是用來替代Perl的,因為我很討厭非要把程式碼拷貝到 cgi-bin 資料夾下才能讓Perl運作。後來,我明白了應該使用一種後台工作的語言來處理主要的數據,而不能什麼事情都讓JavaScript去做。更重要的是我們要考慮安全性和語言特徵。
如果我訪問一個Web service, 我可以獲取到JSON-P 格式的數據,在客戶端瀏覽器裡我把它做各種各樣的數據轉換,但當我有了服務器時,我有了更多的方法來轉換數據,我可以在Server端產生JSON或HTML格式的數據回傳給客戶端,以及快取資料等操作。如果你事先了解了並準備了這些,你會長期收益,省去了很多頭痛的時間。編寫適用各種瀏覽器的程式是種浪費時間,使用工具包吧!
在我最初開始搞Web開發時,在訪問頁面時,究竟是使用 document.all 還是使用 document.layers 的問題上痛苦的掙扎了很久。我選擇了 document.layers,因為我喜歡任何層都是自己的document的思想 (而且我寫了太多的 document.write 來產生元素)。層模式最終失敗了,於是我開始使用 document.all。當Netscape 6 公佈只支援 W3C DOM 模型時,我很高興,但其實使用者並不關心這些。使用者只是看見這種瀏覽器不能顯示大多數瀏覽器都能正常顯示的東西—這是我們編碼的問題。我們寫了短視的程式碼,只能運行在目前的環境下,但不幸的是,我們的運行環境卻在不停的改變。
我已經浪費了太多的時間來處理對各種瀏覽器各種版本相容的問題。善於處理這類問題提供了我一個好的工作機會。但現在我們不必在忍受這種痛苦了。
一些工具包,例如 YUI, jQuery 以及Dojo 都能夠幫我們處理這類問題。它們透過抽象化各種介面實作來處理瀏覽器的各種問題,像是版本不相容,設計缺陷等,把我們從痛苦中解救出來。除非你要測試某個Beta版的瀏覽器,千萬不要在自己的程式裡加入修正瀏覽器的缺陷的程式碼,因為你很有可能當瀏覽器已經修改了這個問題時,你卻忘了刪除你的代碼。
另一方面,完全依賴工具包也是個短視的行為。工具包可以幫你快速的開發,但如果你不深入理解JavaScript,你也會做錯事。