早該知道的7個JavaScript技巧

edithfang發表於2014-09-12

我寫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;
陣列不是萬能的;這樣寫不對,會讓人困惑。圖組實際上是物件的功能,人們混淆了這兩個概念。

另外一個非常酷的簡寫方法是使用與三元條件符號。你不必寫成下面的樣子…
1         var direction;
2         if(x < 200){
3                 direction = 1;
4         } else {
5                 direction = -1;
6         }

你可以使用三元條件符號簡化它:
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()。這個函式返回引數裡的數字裡最大的一個數字:
1         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,你也會做錯事。
評論(2)

相關文章