[1]引入 [2]代碼佈局 [3]註釋說明 [4]變數 [5]命名空間 [6]JS模板 ...
前面的話
編碼標準是有爭議的。幾乎每個人都有自己的標準,但對標準應該是什麼樣的,則似乎很少能達成共識。但編碼標準意味著,通過共同語言和一致的結構,把開發人員從無意義的工作中解放出來。允許開發人員把創新精神放在重要的邏輯上面。一個好的標準能提供清晰明瞭的意圖,是有效工作所必需的。本文將詳細介紹Javascript編碼標準
引入
給像JavaScript這種鬆散類型(loosely typed)的動態語言定義明確的標準,幾乎可以肯定,要比給較為嚴格的語言定義標準來得更加重要。JavaScript的高度靈活性,可能會使它成為編碼語法和實踐的潘多拉魔盒。較為嚴格的語言,本身就具備結構性和一致性,而JavaScript需要準則和應用標準才能達到相同的效果
維護代碼要比編寫代碼花費更多的時間。要編寫越是容易理解的代碼,在最開始就越是需要深思熟慮和良好的結構
好的Javascript編碼應該符合以下標準
1、編碼錯誤的可能性降至最低
2、代碼適合大規模的項目和團隊(一致的、可讀的、可擴展的和可維護的)
3、鼓勵編碼的效率、效果和重用
4、鼓勵使用JavaScript的優點,避免使用它的缺點
5、開發團隊的每個成員都使用
代碼佈局
通常,我們的代碼被閱讀的次數比編寫它的次數要多得多。對代碼規定格式和應用約定,以便我們的開發同事(包括幾個星期之後的我們自己),能夠很容易地理解代碼的內容
【使用一致的縮進和行長】
報紙上的文本列都在50~80個字元的長度之間。對人類的眼睛來說,超過80個字元的行,看起來會逐漸變得吃力。一般地,閱讀理解的最佳行長(linelength)在45~75個字元之間,66個字元的行長被認為是最舒適的。最好使用短製表符(2個空格)和稍短的行長(78個字元),每一行更窄一些,重要的內容也更易讀一些。使用短製表符也是意識到,像JavaScript這種事件驅動的語言比純過程語言,縮進要小一些,因為JavaScript有大量的回調函數和閉包
1、每級代碼縮進兩個空格
2、每行限製為78個字元
【按段落組織代碼】
在編排代碼的時候,要以清晰明白為目標,而不是減少代碼的位元組數。一旦代碼發佈到生產環境,在傳輸給用戶之前,JavaScript代碼會合併(concatenated)、壓縮(minified)。結果,那些用來幫助理解的工具(空白、註釋和更具描述性的變數名)對性能毫無影響。通過合理的使用空白符(white space),使代碼更易讀
1、按邏輯段落組織代碼,段落之間要空行
2、每一行最多只包含一條語句或賦值語句,但是允許每行同時聲明多個變數
3、運算符和變數之間要有空格,這樣就能更容易地識別變數
4、每個逗號之後要有空格
5、在段落內,相似的運算符要對齊
6、縮進註釋,縮進量和所解釋的代碼相同
// 把一個或多個聲明放在一行上,但每行只有一條賦值語句 var x, y, r, print_msg, get_rangdom, coef = 0.5, rot_delta = 1, x_delta = 1, y_delta = 1, first_name = 'sally' ; // 在下一行段落的前面添加空行 // function to write text to message container print_msg = function ( msg_text ){ //縮進註釋,和它所描述的段落層級一致 // .text() prevents xss injection $('#sl').text( msg_text ); }; // function to return a random number get_rangdom = function ( num_arg ){ return Math.random() * numn_arg; }; // initialize coorainates x = get_random( 10 ); y = get_random( 20 ); r = get_random( 360 ); // 添加空白,對齊相似的元素,相似的語句更容易閱讀 // adjust to offsets x += x_delta * coef; y += y_delta * coef; r += rot_delta * coef;
【換行要一致】
1、在運算符的前面換行,因為人們檢查左列的所有運算符是很容易的
2、把後續的語句縮進一個層次,比如使用兩個空格
3、在逗號分隔符的後面換行
4、方括弧或者括弧單獨占一行。清楚地表明這是語句的結尾,不會迫使讀者橫向掃尋分號
// 將運算符放在左邊,排成一列 long_quote = 'Four score and seven years ago our ' + 'fathers brought forth on this continent, a new , + 'nation, conceived in Liberty, ' + 'and dedicated to the proposition that ' + 'all men are created equal. '; // 方括弧單獨占一行,下一條語句就容易識別了 // 使用尾部逗號,更容易維護 cat_breed_list = [ 'Abyssinian', 'American Bobtail', 'American Curl', 'American Shorthair', 'American Whiterhair', 'Balinses', 'Balinese-Javanaese', 'Birman', 'Bombay' ];
【使用K&R風格的括弧】
K&R風格的括弧可以平衡垂直空間的使用,增加可讀性。當格式化對象和映射、數組、複合語句或者調用的時候,應該使用K&R風格的括弧
1、如果可能,就使用單行。比如,當一個很短的數組聲明能寫在一行上的時候,就沒必要把它拆分成三行。
2、把左括弧、左花括弧或者左方括弧放在開始行的末尾。
3、在分隔符(括弧、花括弧或者方括弧)的裡面把代碼縮進一個層級,比如,兩個空格。
4、右括弧、右花括弧或者右方括弧單獨占一行,縮進和開始行相同
var run_count, full_name, top_fruit_list, full_fruit_list, print_string; run_count = 2; full_name = 'Fred Burns'; top_fruit_list = ['Apple', 'Banana', 'Orange' ]; // 使用垂直對齊,增加可讀性 full_fruit_list = [ 'Apple', 'Apricot', 'Banana', 'Blackberry', 'Blueberry', 'Current', 'Cherry', 'Date', 'Grape', 'Grapefruit', 'Guava', 'Kiwi', 'Kumquat', 'Lemon', 'Lime', 'Lychee', 'Mango', 'Melon', 'Nectarine', 'Orange', 'Peach', 'Pear', 'Pineapple', 'Raspberry', 'Strawberry', 'Tangerine', 'Ugli' ] // 使用K&R風格的括弧 print_string = function ( text_arg ){ var text_arg, char_list, i; char_list = input_text.split(''); for( i = 0, i < char_list.length; i++ ){ document.write( char_list[i] ); } return true; } print_string( 'We have counted' + + String( run_count ) + ' invocations to date; );
【使用空格來區別函數和關鍵字】
很多語言有冠詞的概念,像an、a或者the這種單詞。冠詞的目的之一是提醒讀者或者聽者,下一個單詞將是名詞或者名詞短語。和函數以及關鍵字一起使用的空格,可以達到類似的效果
1、函數名後面沒有空格。在函數名和左括弧“(”之間沒有空格
2、關鍵字後面空一格,然後是左括弧“(”
3、當格式化for語句的時候,在每個分號的後面空一格
mystery_text = get_mystery( 'Hello JavaScript Denizens' ); for ( x = 1; x < 10; x++ ) { console.log( x ); }
【引號要一致】
使用單引號作為字元串的定義符號,而不是雙引號,因為HTML中標準屬性的定義符是雙引號
html_snip = '<input name="alley_cat" type="text" value="bone">';
註釋說明
註釋可能要比它們所解釋的代碼更加重要,因為它們能傳達在其他方面不明顯的關鍵細節。這在事件驅動編程中尤其明顯,因為大量的回調函數,導致跟蹤代碼的執行要耗費掉大量時間。這並不意味著添加更多的註釋總是更好的。擺放有策略、信息量大和精心維護的註釋,價值是很高的,而雜亂無章文不對題的註釋,還不如沒有的好
【解釋代碼】
好的註釋的標準是將註釋的數量最小化,將註釋的價值最大化。通過約定來減少註釋,儘可能地讓代碼進行自我說明。通過將註釋和它們所描述的段落對齊,並確保它們的內容對讀者是有價值的,從而使註釋的價值最大化
使用一致的、有意義的變數名,能提供更多的信息,需要的註釋很少
var welcome_html = '<h1>Welcom tho Color house</h1>', house_color_list = ['yellow', 'green', 'little pink'], spec_map, get_spec_map, run_init; // Begin /get_spec_map/ // Get a specification map based on colors get_spec_map = function ( color_list_arg ){ var color_count = color_list_arg.length, spec_map = {}, i; for ( i = 0, i < color_count; i++ ){ // ... 30 more lines } return spec_map; } // End /get_spec_map/ run_init = function () { var spec_map = get_spec_map( house_color_list ); $('#welocome').html('welcome_html'); $('#specs').text( JSON.stringify( spec_map ) ); }; run_init();
【給API和TODO添加文檔】
註釋也能為代碼提供更為正式的文檔。總體架構的文檔應該放在專門的架構文檔裡面。但是函數或者對象API的文檔,可以並且通常應該放在代碼的旁邊
1、解釋所有重要的函數,說明它的目的,使用的參教或者設置(setting),它的返回值,以及所有拋出的異常
2、如果禁用了代碼,要解釋為什麼,使用這種格式的註釋://TODO date username-comment。在判斷註釋新鮮度的時候,用戶名和日期是很有價值的,也可以使用自動化工具,在代碼庫中的TODO項上,自動填上用戶名和日期
// BEGIN DOM Method /toggleSlider/ // Purpose : Extends and retracts chat slider // Required Arguments : // * do_extend (boolean) true extends slider, false retracts // Optional Arguments : // * callback (function) executed after animation is complete // Settings : // * chat_extend_time, chat_retract_time // * chat_extend_height, chat_retract_height // Returns : boolean // * true - slider animation activated // * false - slider animation not activated // Throws : none // toggleSlider = function( do_extend, callback ) { // ... }; // END DOM Method /toggleSlider/
// BEGIN TODO 2018-01-11 xiaohuochai - debug code disabled // alert( warning_text ); // ... (lots more lines) ... // // END TODO 2018-01-11 xiaohuochai - debug code disabled
【使用命名約定,減少並改進註釋】
下麵是一個示例
var make_house = curry_build_item({ item_type : 'house' });
通過上面代碼可以得到以下信息
1、make_house是一個對象構造器。
2、調用的函數叫做柯里化函數,它使用閉包來維護狀態並返回一個函數
3、調用的函數接收字元串參數,表示類型(type)
4、變數的作用域是局部的
如果使用如下聲明,則需要添加許多註釋
var creator = maker('house');
// 'creator' is an object constructor we get by // calling 'maker'. The first positional argument // of 'maker' must be a string, and it directs // the type of object constructor to be returned. // 'maker' uses a closure to remember the type // of object the returned function is to // meant to create. var creator = maker('house');
加了註釋的示例,不但比簡易的示例顯得更為冗長,而且需要更多的時間編寫,很可能是因為我們設法傳遞和命名約定一樣多的信息量。情況會越來越糟糕:經過一段時間以後,註釋容易變得不准確,因為代碼改變了,開發人員變得懶惰了。假如幾個星期之後,我們決定更改了變數名,卻忘記更新註釋中引用這些變數名的地方。現在的註釋完全錯了並且容易誤導別人。不但是這樣,而且所有這些註釋使得代碼難以理解,因為代碼清單長了9倍。沒有註釋是最好的。相比之下,我們更想使用簡單示例中的變數名
變數
每個人在編碼的時候,都會使用命名約定,不管他們是否意識到這一點,就像不做決定也是一種決定。一個好的命名約定,當團隊的所有成員都理解並使用它的時候,能發揮巨大的價值。當他們這麼做的時候,就能從枯燥的代碼跟蹤和費力的註釋維護當中解放出來,把精力都集中在代碼的目標和邏輯上面
【使用常用字元】
1、變數名使用a~z、A~Z、0~9、下劃線和$符號
2、變數名不要以數字開頭
【傳送變數作用域】
1、當變數作用域是整個模塊時使用駝峰式(模塊名字空間的所有地方都可以訪問該變數)
2、當變數作用域不是整個模塊時使用下劃線 (模塊名字空間內的某個函數的局部變數)
3、確保所有模塊作用域內的變數至少有兩個音節,這樣作用域就清晰了。比如,不要使用叫做config的變數,可以使用更具描述性的和明顯是模塊作用域的configMap
【命名布爾變數】
當布爾值表示狀態的時候,我們使用單詞is,比如,is_retracted或者is_stale。當使用布爾值來表示行為的時候(如函數中的參數),我們使用單詞do,像do_retract或者do_extend。當使用布爾值來表示所有權的時候,我們使用has,比如,has_whiskers 或者 has_wheels
指示器 局部作用域 模塊作用域 bool[通用] bool_return boolReturn do(請求行為) do_retract doRetract has(表示包含) has_whiskers hasWhiskers is(表示狀態) is_retracted isRetracted
【命名字元串變數】
指示器 局部作用域 模塊作用域
str[通用] direction_str directionStr
id email_id emailld
date email_date emailDate
html body_html bodyHtml
msg employee_msg employeeMsg
name emp1oyee_name employeeName
text email_text emailText
type item_type itemType
【命名整型變數】
指示器 局部作用域 模塊作用域 int[通用] size_int sizeInt 無(約定) i , j , k (不允許出現在模塊作用域內) count employee_count employeeCount index employee_index employeeIndex time(毫秒) retract_time retractTime
【命名數字變數】
指示器 局部作用域 模塊作用域
num[通用] size_num sizeNum
無(約定) x, y, z (不允許出現在模塊作用域內)
coord(坐標) x_coord xCoord
ratio sales_ratio salesRatio
【命名正則變數】
指示器 局部作用域 模塊作用域
regex regex_filter regexFilter
【命名數組變數】
指示器 局部作用域 模塊作用域
list timestamp_list timestampList
list color_list colorList
【命名映射變數】
指示器 局部作用域 模塊作用域
map employee_map employeeMap
map receipt_timestamp_map receiptTimestampMap
【命名對象變數】
1、對象變數應該是名詞,加上可選的修飾符:emplyee或者receipt
2、確保模塊作用域的對象變數名具有兩個或者兩個以上的音節,這樣作用域就清晰了: storeEmployee 或者 salesReceipt
3、jQuery對象有首碼$。目前這種約定很常見,在單頁應用中,jQuery對象(有時候叫集合)很普遍
指示器 局部作用域 模塊作用域
無(單名詞) employee storeEmployee
無(單名詞) receipt salesReceipt
$ $area_tabs $areaTabs
【命名函數變數】
1、命名函數應始終遵循動詞加名詞的形式,比如,get_record或者empty_cache_map
2、模塊作用域的函數應始終包含兩個或兩個以上的音節,這樣作用域就清晰了:getRecord 或者 emptyCacheMap
3、動詞含義要一致
指示器 局部作用域 模塊作用域 指示器含義 fn[通用] fn_sync fnSync 通用函數指示器 curry curry_make_user curryMakeUser 返回指定參數的函數 destroy destroy_entry destroyEntry 移除數據結構,意味著必要時會回收數據引用 remove remove_element removeElement 移除數據結構的另一種寫法 empty empty_cache_map emptyCacheMap 移除數據結構的一些或者全部成員,不會移除容器 fetch fetch_user_list fetchUserList 返回從外部源獲取的數據 get get_user_list getUserList 返回對象或者其他內部數據結構中的數據 make make_user makeUser 返回新建對象(不使用new操作符) on on_mouseover onMouseover 事件處理程式。事件應是單字的,和HTML標記一致 save save_user_list saveUserList 把數據保存到對象或者其他內部數據結構中 set set_user_name setUserName 初始化或者更新通過參數提供的值 store store_user_list storeUserList 發送數據到外部源進行存儲,比如通過AJAX調用 update update_user_list updateUserList 和set類似,但有“先前己經初始化了”的暗含意思
【命名未知類型的變數】
有時候,我們實際上不知道變數包含的數據類型是什麼。有兩種情況很常見
1、編寫多態函數(接收多種數據類型的函數)
2、接收的數據來自外部數據源,比如AJAX或者Web Socket訂閱
局部作用域 模塊作用域 說明
http_data httpData 接收自HTTP訂閱的未知數據類型
socket_data socketData 接收自Web socket的未知數據類型
arg_data data 通過參數傳遞的未知數據類型
下麵是一個示例
dogPrototype = { body_temp_c : 36.5, dog_name : 'Guido', greet_text : 'Grrrr', speak_text : 'I am a dog', height_in_m : 1.0, leg_count : 4, check_destroy : checkDestroy, destroy_dog : destroyDog, print_greet : printGreet, print_name : printName, print_speak : printSpeak, show_flash : showFlash, redraw_dog : redrawDog };
【變數聲明和賦值】
1、創建新對象、映射或者數組的時候,使用{}或者[]]代替new Object()或者new Array()
2、使用工具方法複製對象和數組
3、一開始就在函數作用域內,使用單個var關鍵字,顯式地聲明所有的變數
4、不要使用塊
5、把所有函數賦給變數。這進一步鞏固了JavaScript把函數當作第一類對象的事實
6、 當函數需要三個以上的參數時,使用具名參數(named arguments),因為位置參數的含義很容易忘記,並且也不能進行自我說明
7、每條變數賦值語句占用一行。儘可能按字母或者邏輯來排序。多個聲明可以放在單行上
命名空間
很多早期的JavaScript代碼比較簡單,單獨在一張頁面上使用。這些腳本可以(而且經常就是這麼做的)使用全局變數,而不會有什麼影響。但是隨著JavaScript應用的蓬勃發展和第三方類庫的普遍使用,別人想要全局變數i的可能性會急劇上升。當兩個代碼庫聲明瞭相同的全局變數時,地獄之門也隨之打開
只使用單一的全局函數,把其他所有變數的作用域限制在該函數裡面,就可以極大地減少這種問題,如下所示:
var spa = (function () { // other code here var initModule = function () { console.log( 'hi there'); }; return { initModule : initModule }; }());
這個單一的全局函數(在這個示例中是spa)叫做名字空間。賦給它的函數. 在載入的時候就會執行,當然所有在該函數裡面賦值的局部變數,在全局名字空間中是不可訪問的。註意我們讓initModule方法對外可見。所以其他代碼可以調用初始化函數,但它不能訪問其他的東西。並且必須使用spa首碼
spa.initModule();
可以把命名空間再細分,這樣就不會被迫用單個文件來裝載50KB的應用。比如,可以創建spa、spa.shell和spa.slider這樣的命名空間:
// In the file spa.js: var spa = (function () { // some code here }()); // In the file spa.shell.js: var spa.shell = (function () { // some code here }()); // In the file spa.slider.js: var spa.slider = (function () { // some code here }());
命名空間是創建可維護的JavaScript代碼的關鍵所在
【文件命名】
1、根據命名空間來命名JavaScript文件,每個文件一個命名空間示例
spa.js // spa.* namespace spa.shell.js // spa.shell.* namespace spa.slider.js // spa.slider.* namespace
2、為會生成HTML的每個JavaScript文件創建一個CSS文件。示例:
spa.css // spa.* namespace spa.shell.css // spa.shell.* namespace spa.slider.css // spa.slider.* namespace
3、給CSS選擇器加上模塊名首碼。這種做法能極大地有助於避免和第三方模塊的意外衝突
spa.css defines #spa, .spa-x-clearall
spa.shell.css defines
#spa-shell-header, #spa-shell-footer, .spa-shell-main
JS模板
模塊按一致的區塊來劃分,是很有價值的做法。它能幫助我們理解和瀏覽代碼,提醒我們要以良好的方式來編碼
【使用IIFE創建命名空間】
使用自執行函數為模塊創建命名空間。這能防止意外地創建全局JavaScript變數。每個文件應該只定義一個命名空間,並且文件名正好和命名空間對應。比如,模塊的名字空間是spa.shell,則文件名應為spa.shell.js
spa.module = (function(){
})();
【聲明並初始化模塊作用域變數】
一般會使用configMap來保存模塊配置、使用stateMap來保存運行時的狀態值以及使用jqueryMap來緩存jQuery集合
var configMap = { settable_map : { color_name: true }, color_name : 'blue' }, stateMap = { $container : null }, jqueryMap = {};
// Begin DOM method /setJqueryMap/ setJqueryMap = function () { var $append_target = stateMap.$append_target, $slider = $append_target.find( '.spa-chat' ); jqueryMap = { $slider : $slider, $toggle : $slider.find( '.spa-chat-head-toggle' ), $window : $(window) }; }; // End DOM method /setJqueryMap/
【聲明工具方法】
把所有私有的工具方法聚集在它們自己的區塊裡面。這些方法不會操作DOM,因此不需要瀏覽器就行。如方法不是單個模塊的工具施,則應該把它移到共用的工具方法庫裡面,比如spa.util.js
//------------------- BEGIN UTILITY METHODS ------------------ // Cross-browser method to set __proto__ // Newer js engines (v1.8.5+) support Object.create() hasCreate = !! Object.create; createObject = function ( arg ){ if ( ! arg ) { return {}; } if ( hasCreate ){ return Object.create( arg ); } function obj() {}; obj.prototype = arg; return new obj; }; //-------------------- END UTILITY METHODS -------------------
【DOM方法】
把所有私有的DOM方法聚集在它們自己的區塊裡面。這些方法會訪問和修改DOM,因此需要瀏覽器才能運行。一個DOM方法的例子是移動CSS sprite。set JqueryMap方法用來緩存jQuery集合
//--------------------- BEGIN DOM METHODS -------------------- // functions used in dogPrototype printName = function (){ this.$name.text( this.dog_name ); }; showFlash = function (){ this.$bg.css({opacity: 1, display:'block'}) .fadeOut('slow'); };//---------------------- END DOM METHODS ---------------------
【事件處理】
把所有的私有事件處理程式聚集在它們自己的區塊裡面。這些方法會處理事件,比如按鈕點擊、按下按鍵、瀏覽器容器縮放、或者接收Websocket消息。事件處理程式理一般會調用DOM方法來修改DOM,而不是它們自己直接去修改DOM
// BEGIN EVENT HANDLERS onLogin = function ( event, login_user ) { configMap.set_chat_anchor( 'opened' ); }; onLogout = function ( event, logout_user ) { configMap.set_chat_anchor( 'closed' ); jqueryMap.$title.text( 'Chat' ); clearChat(); }; // END EVENT HANDLERS
【回調方法】
把所有的回調方法聚集在它們自己的區塊裡面。如果有回調函數,一般把它們放在事件處理程式和公開方法之間。它們是準公開方法,因為它們會被所服務的外部模塊使用
//---------------------- BEGIN CALLBACKS --------------------- setChatAnchor = function ( position_type ) { return changeAnchorPart({ chat : position_type }); }; //----------------------- END CALLBACKS ----------------------
【公開方法】
把所有的公開方法聚集在它們自己的區塊裡面。這些方法是模塊公開介面的部分。如果有的話,該區塊應該包括configModule和initModule
//------------------- BEGIN PUBLIC METHODS ------------------- configModule = function ( input_map ) { spa.util.setConfigMap({ input_map : input_map, settable_map : configMap.settable_map, config_map : configMap }); return true; }; initModule = function ( $container ) { setJqueryMap( $container ); $.gevent.subscribe( $container, 'spa-logout', onLogout ); $container .bind( 'utap', onTapNav ) .bind( 'uheldend', onHeldendNav ); return true; }; return { configModule : configModule, initModule : initModule }; //------------------- END PUBLIC METHODS ---------------------