標籤:函數對象 瓶頸 kaa reg 簡單的 提交 多參數 相容 module
變數[強制] 變數、函數在使用前必須先定義。
// goodvar name = ‘MyName‘;// badname = ‘MyName‘;
[強制] 每個 var 只能聲明一個變數。
解釋:
一個 var 聲明多個變數,容易導致較長的行長度,並且在修改時容易造成逗號和分號的混淆。
// goodvar hangModules = [];var missModules = [];var visited = {};// badvar hangModules = [], missModules = [], visited = {};
[強制] 變數必須 即用即聲明,不得在函數或其它形式的代碼塊起始位置統一聲明所有變數。
解釋:
變數聲明與使用的距離越遠,出現的跨度越大,代碼的閱讀與維護成本越高。雖然JavaScript的變數是函數範圍,還是應該根據編程中的意圖,縮小變數出現的距離空間。
// goodfunction kv2List(source) { var list = []; for (var key in source) { if (source.hasOwnProperty(key)) { var item = { k: key, v: source[key] }; list.push(item); } } return list;}// badfunction kv2List(source) { var list = []; var key; var item; for (key in source) { if (source.hasOwnProperty(key)) { item = { k: key, v: source[key] }; list.push(item); } } return list;}
條件[強制] 在 Equality Expression 中使用類型嚴格的 ===。僅當判斷 null 或 undefined 時,允許使用 == null。
解釋:
使用 === 可以避免等於判斷中隱式的類型轉換。
// goodif (age === 30) { // ......}// badif (age == 30) { // ......}
[建議] 儘可能使用簡潔的運算式。
// 字串為空白// goodif (!name) { // ......}// badif (name === ‘‘) { // ......}
// 字串非空// goodif (name) { // ......}// badif (name !== ‘‘) { // ......}
// 數組非空// goodif (collection.length) { // ......}// badif (collection.length > 0) { // ......}
// 布爾不成立// goodif (!notTrue) { // ......}// badif (notTrue === false) { // ......}
// null 或 undefined// goodif (noValue == null) { // ......}// badif (noValue === null || typeof noValue === ‘undefined‘) { // ......}
[建議] 按執行頻率排列分支的順序。
解釋:
按執行頻率排列分支的順序好處是:
閱讀的人容易找到最常見的情況,增加可讀性。
提高執行效率。
[建議] 對於相同變數或運算式的多值條件,用 switch 代替 if。
// goodswitch (typeof variable) { case ‘object‘: // ...... break; case ‘number‘: case ‘boolean‘: case ‘string‘: // ...... break;}// badvar type = typeof variable;if (type === ‘object‘) { // ......}else if (type === ‘number‘ || type === ‘boolean‘ || type === ‘string‘) { // ......}
[建議] 如果函數或全域中的 else 塊後沒有任何語句,可以刪除 else。
// goodfunction getName() { if (name) { return name; } return ‘unnamed‘;}// badfunction getName() { if (name) { return name; } else { return ‘unnamed‘; }}
迴圈[建議] 不要在迴圈體中包含函數運算式,事先將函數提取到迴圈體外。
解釋:
迴圈體中的函數運算式,運行過程中會產生迴圈次數個函數對象。
// goodfunction clicker() { // ......}for (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; addListener(element, ‘click‘, clicker);}// badfor (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; addListener(element, ‘click‘, function () {});}
[建議] 對迴圈內多次使用的不變值,在迴圈外用變數緩衝。
// goodvar width = wrap.offsetWidth + ‘px‘;for (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; element.style.width = width; // ......}// badfor (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; element.style.width = wrap.offsetWidth + ‘px‘; // ......}
[建議] 對有序集合進行遍曆時,緩衝 length。
解釋:
雖然現代瀏覽器都對數組長度進行了緩衝,但對於一些宿主對象和老舊瀏覽器的數組對象,在每次 length 訪問時會動態計算元素個數,此時緩衝 length 能有效提高程式效能。
for (var i = 0, len = elements.length; i < len; i++) { var element = elements[i]; // ......}
[建議] 對有序集合進行順序無關的遍曆時,使用逆序遍曆。
解釋:
逆序遍曆可以節省變數,代碼比較最佳化。
var len = elements.length;while (len--) { var element = elements[len]; // ......}
類型類型檢測[建議] 類型檢測優先使用 typeof。物件類型檢測使用 instanceof。null 或 undefined 的檢測使用 == null。
// stringtypeof variable === ‘string‘// numbertypeof variable === ‘number‘// booleantypeof variable === ‘boolean‘// Functiontypeof variable === ‘function‘// Objecttypeof variable === ‘object‘// RegExpvariable instanceof RegExp// Arrayvariable instanceof Array// nullvariable === null// null or undefinedvariable == null// undefinedtypeof variable === ‘undefined‘
類型轉換[建議] 轉換成 string 時,使用 + ‘‘。
// goodnum + ‘‘;// badnew String(num);num.toString();String(num);
[建議] 轉換成 number 時,通常使用 +。
// good+str;// badNumber(str);
[建議] string 轉換成 number,要轉換的字串結尾包含非數字並期望忽略時,使用 parseInt。
var width = ‘200px‘;parseInt(width, 10);
[強制] 使用 parseInt 時,必須指定進位。
// goodparseInt(str, 10);// badparseInt(str);
[建議] 轉換成 boolean 時,使用 !!。
var num = 3.14;!!num;
[建議] number 去除小數點,使用 Math.floor / Math.round / Math.ceil,不使用 parseInt。
// goodvar num = 3.14;Math.ceil(num);// badvar num = 3.14;parseInt(num, 10);
字串[強制] 字串開頭和結束使用單引號 ‘。
解釋:
輸入單引號不需要按住 shift,方便輸入。
實際使用中,字串經常用來拼接 HTML。為方便 HTML 中包含雙引號而不需要轉義寫法。
var str = ‘我是一個字串‘;var html = ‘<div class="cls">拼接HTML可以省去雙引號轉義</div>‘;
[建議] 使用 數組 或 + 拼接字串。
解釋:
使用 + 拼接字串,如果拼接的全部是 StringLiteral,壓縮公用程式可以對其進行自動合并的最佳化。所以,靜態字串建議使用 + 拼接。
在現代瀏覽器下,使用 + 拼接字串,效能較數組的方式要高。
如需要兼顧老舊瀏覽器,應盡量使用數組拼接字串。
// 使用數組拼接字串var str = [ // 推薦換行開始並縮排開始第一個字串, 對齊代碼, 方便閱讀. ‘<ul>‘, ‘<li>第一項</li>‘, ‘<li>第二項</li>‘, ‘</ul>‘].join(‘‘);// 使用 `+` 拼接字串var str2 = ‘‘ // 建議第一個為空白字串, 第二個換行開始並縮排開始, 對齊代碼, 方便閱讀 + ‘<ul>‘, + ‘<li>第一項</li>‘, + ‘<li>第二項</li>‘, + ‘</ul>‘;
[建議] 使用字串拼接的方式產生HTML,需要根據語境進行合理的轉義。
解釋:
在 JavaScript 中拼接,並且最終將輸出到頁面中的字串,需要進行合理轉義,以防止安全性漏洞。下面的範例程式碼為情境說明,不能直接運行。
// HTML 轉義var str = ‘<p>‘ + htmlEncode(content) + ‘</p>‘;// HTML 轉義var str = ‘<input type="text" value="‘ + htmlEncode(value) + ‘">‘;// URL 轉義var str = ‘<a href="/?key=‘ + htmlEncode(urlEncode(value)) + ‘">link</a>‘;// JavaScript字串 轉義 + HTML 轉義var str = ‘<button onclick="check(\‘‘ + htmlEncode(strLiteral(name)) + ‘\‘)">提交</button>‘;
對象[強制] 使用對象字面量 {} 建立新 Object。
// goodvar obj = {};// badvar obj = new Object();
[建議] 對象建立時,如果一個對象的所有 屬性 均可以不添加引號,建議所有 屬性 不添加引號。
var info = { name: ‘someone‘, age: 28};
[建議] 對象建立時,如果任何一個 屬性 需要添加引號,則所有 屬性 建議添加 ‘。
解釋:
如果屬性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。
// goodvar info = { ‘name‘: ‘someone‘, ‘age‘: 28, ‘more-info‘: ‘...‘};// badvar info = { name: ‘someone‘, age: 28, ‘more-info‘: ‘...‘};
[建議] 屬性訪問時,盡量使用 .。
解釋:
屬性名稱符合 Identifier 的要求,就可以通過 . 來訪問,否則就只能通過 [expr] 方式訪問。
通常在 JavaScript 中聲明的對象,屬性命名是使用 Camel 命名法,用 . 來訪問更清晰簡潔。部分特殊的屬性(比如來自後端的 JSON ),可能採用不尋常的命名方式,可以通過 [expr] 方式訪問。
info.age;info[‘more-info‘];
[建議] for in 遍曆對象時, 使用 hasOwnProperty 過濾掉原型中的屬性。
var newInfo = {};for (var key in info) { if (info.hasOwnProperty(key)) { newInfo[key] = info[key]; }}
數組[強制] 使用數組字面量 [] 建立新數組,除非想要建立的是指定長度的數組。
// goodvar arr = [];// badvar arr = new Array();
[強制] 遍曆數組不使用 for in。
解釋:
數組對象可能存在數字以外的屬性, 這種情況下 for in 不會得到正確結果。
var arr = [‘a‘, ‘b‘, ‘c‘];// 這裡僅作示範, 實際中應使用 Object 類型arr.other = ‘other things‘;// 正確的遍曆方式for (var i = 0, len = arr.length; i < len; i++) { console.log(i);}// 錯誤的遍曆方式for (var i in arr) { console.log(i);}
[建議] 不因為效能的原因自己實現數組排序功能,盡量使用數組的 sort 方法。
解釋:
自己實現的常規排序演算法,在效能上並不優於數組預設的 sort 方法。以下兩種情境可以自己實現排序:
需要穩定的排序演算法,達到嚴格一致的排序結果。
資料特點鮮明,適合使用桶排。
[建議] 清空數組使用 .length = 0。函數函數長度[建議] 一個函數的長度控制在 50 行以內。
解釋:
將過多的邏輯單元混在一個大函數中,易導致難以維護。一個清晰易懂的函數應該完成單一的邏輯單元。複雜的操作應進一步抽取,通過函數的調用來體現流程。
特定演算法等不可分割的邏輯允許例外。
function syncViewStateOnUserAction() { if (x.checked) { y.checked = true; z.value = ‘‘; } else { y.checked = false; } if (a.value) { warning.innerText = ‘‘; submitButton.disabled = false; } else { warning.innerText = ‘Please enter it‘; submitButton.disabled = true; }}// 直接閱讀該函數會難以明確其主線邏輯,因此下方是一種更合理的表達方式:function syncViewStateOnUserAction() { syncXStateToView(); checkAAvailability();}function syncXStateToView() { y.checked = x.checked; if (x.checked) { z.value = ‘‘; }}function checkAAvailability() { if (a.value) { clearWarnignForA(); } else { displayWarningForAMissing(); }}
[建議] 一個函數的參數控制在 6 個以內。
解釋:
除去不定長參數以外,函數具備不同邏輯意義的參數建議控制在 6 個以內,過多參數會導致維護難度增大。
某些情況下,如使用 AMD Loader 的 require 載入多個模組時,其 callback 可能會存在較多參數,因此對函數參數的個數不做強制限制。
[建議] 通過 options 參數傳遞非資料輸入型參數。
解釋:
有些函數的參數並不是作為演算法的輸入,而是對演算法的某些分支條件判斷之用,此類參數建議通過一個 options 參數傳遞。
如下函數:
/** * 移除某個元素 * * @param {Node} element 需要移除的元素 * @param {boolean} removeEventListeners 是否同時將所有註冊在元素上的事件移除 */function removeElement(element, removeEventListeners) { element.parent.removeChild(element); if (removeEventListeners) { element.clearEventListeners(); }}
可以轉換為下面的簽名:
/** * 移除某個元素 * * @param {Node} element 需要移除的元素 * @param {Object} options 相關的邏輯配置 * @param {boolean} options.removeEventListeners 是否同時將所有註冊在元素上的事件移除 */function removeElement(element, options) { element.parent.removeChild(element); if (options.removeEventListeners) { element.clearEventListeners(); }}這種模式有幾個顯著的優勢:boolean 型的配置項具備名稱,從調用的代碼上更易理解其表達的邏輯意義。當配置項有增長時,無需無休止地增加參數個數,不會出現 removeElement(element, true, false, false, 3) 這樣難以理解的調用代碼。當部分配置參數可選時,多個參數的形式非常難處理重載邏輯,而使用一個 options 對象只需判斷屬性是否存在,實現得以簡化。
閉包[建議] 在適當的時候將閉包內大對象置為 null。
解釋:
在 JavaScript 中,無需特別的關鍵詞就可以使用閉包,一個函數可以任意訪問在其定義的範圍外的變數。需要注意的是,函數的範圍是靜態,即在定義時決定,與調用的時機和方式沒有任何關係。
閉包會阻止一些變數的記憶體回收,對於較老舊的 JavaScript 引擎,可能導致外部所有變數均無法回收。
首先一個較為明確的結論是,以下內容會影響到閉包內變數的回收:
嵌套的函數中是否有使用該變數。
嵌套的函數中是否有 直接調用eval。
是否使用了 with 運算式。
Chakra、V8 和 SpiderMonkey 將受以上因素的影響,表現出不盡相同又較為相似的回收策略,而 JScript.dll 和 Carakan 則完全沒有這方面的最佳化,會完整保留整個 LexicalEnvironment 中的所有變數綁定,造成一定的記憶體消耗。
由於對閉包內變數有回收最佳化策略的 Chakra、V8 和 SpiderMonkey 引擎的行為較為相似,因此可以總結如下,當返回一個函數 fn 時:
- 如果 fn 的 [[Scope]] 是 ObjectEnvironment(with 運算式產生 ObjectEnvironment,函數和 catch 運算式產生 DeclarativeEnvironment),則:
如果是 V8 引擎,則退出全過程。
如果是 SpiderMonkey,則處理該 ObjectEnvironment 的外層 LexicalEnvironment。
- 擷取當前 LexicalEnvironment 下的所有類型為 Function 的對象,對於每一個 Function 對象,分析其 FunctionBody:
如果 FunctionBody 中含有 直接調用 eval,則退出全過程。
否則得到所有的 Identifier。
對於每一個 Identifier,設其為 name,根據尋找變數引用的規則,從 LexicalEnvironment 中找出名稱為 name 的綁定 binding。
對 binding 添加 notSwap 屬性,其值為 true。
- 檢查當前 LexicalEnvironment 中的每一個變數綁定,如果該綁定有 notSwap 屬性且值為 true,則:
如果是 V8 引擎,刪除該綁定。
如果是 SpiderMonkey,將該綁定的值設為 undefined,將刪除 notSwap 屬性。
對於 Chakra 引擎,暫無法得知是按 V8 的模式還是按 SpiderMonkey 的模式進行。
如果有 非常龐大 的對象,且預計會在 老舊的引擎 中執行,則使用閉包時,注意將閉包不需要的對象置為空白引用。
[建議] 使用 IIFE 避免 Lift 效應。
解釋:
在引用函數外部變數時,函數執行時外部變數的值由運行時決定而非定義時,最典型的情境如下:
var tasks = [];for (var i = 0; i < 5; i++) { tasks[tasks.length] = function () { console.log(‘Current cursor is at ‘ + i); };}var len = tasks.length;while (len--) { tasks[len]();}
以上代碼對 tasks 中的函數的執行均會輸出 Current cursor is at 5,往往不符合預期。
此現象稱為 Lift 效應 。解決的方式是通過額外加上一層閉包函數,將需要的外部變數作為參數傳遞來解除變數的綁定關係:
var tasks = [];for (var i = 0; i < 5; i++) { // 注意有一層額外的閉包 tasks[tasks.length] = (function (i) { return function () { console.log(‘Current cursor is at ‘ + i); }; })(i);}var len = tasks.length;while (len--) { tasks[len]();}
[建議] 空函數不使用 new Function() 的形式。
var emptyFunction = function () {};
[建議] 對於效能有高要求的場合,建議存在一個空函數的常量,供多處使用共用。
var EMPTY_FUNCTION = function () {};function MyClass() {}MyClass.prototype.abstractMethod = EMPTY_FUNCTION;MyClass.prototype.hooks.before = EMPTY_FUNCTION;MyClass.prototype.hooks.after = EMPTY_FUNCTION;
物件導向[強制] 類的繼承方案,實現時需要修正 constructor
解釋:
通常使用其他 library 的類繼承方案都會進行 constructor 修正。如果是自己實現的類繼承方案,需要進行 constructor 修正。
/** * 構建類之間的繼承關係 * * @param {Function} subClass 子類函數 * @param {Function} superClass 父類函數 */function inherits(subClass, superClass) { var F = new Function(); F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass;}
[建議] 聲明類時,保證 constructor 的正確性。
function Animal(name) { this.name = name;}// 直接prototype等於對象時,需要修正constructorAnimal.prototype = { constructor: Animal, jump: function () { alert(‘animal ‘ + this.name + ‘ jump‘); }};// 這種方式擴充prototype則無需理會constructorAnimal.prototype.jump = function () { alert(‘animal ‘ + this.name + ‘ jump‘);};
[建議] 屬性在建構函式中聲明,方法在原型中聲明。
解釋:
原型對象的成員被所有執行個體共用,能節約記憶體佔用。所以編碼時我們應該遵守這樣的原則:原型對象包含程式不會修改的成員,如方法函數或配置項。
function TextNode(value, engine) { this.value = value; this.engine = engine;}TextNode.prototype.clone = function () { return this;};
[強制] 自訂事件的 事件名 必須全小寫。
解釋:
在 JavaScript 廣泛應用的瀏覽器環境,絕大多數 DOM 事件名稱都是全小寫。為了遵循大多數 JavaScript 開發人員的習慣,在設計自訂事件時,事件名也應該全小寫。
[強制] 自訂事件只能有一個 event 參數。如果事件需要傳遞較多資訊,應仔細設計事件對象。
解釋:
一個事件對象的好處有:
順序無關,避免事件監聽者需要記憶參數順序。
每個事件資訊都可以根據需要提供或者不提供,更自由。
擴充方便,未來添加事件資訊時,無需考慮會破壞
[建議] 設計自訂事件時,應考慮禁止預設行為。
解釋:
常見禁止預設行為的方式有兩種:
事件監聽函數中 return false。
事件對象中包含禁止預設行為的方法,如 preventDefault。
禁止使用evail推薦使用new Function()[建議] 減少 delete 的使用。
解釋:
如果沒有特別的需求,減少或避免使用 delete。delete 的使用會破壞部分 JavaScript 引擎的效能最佳化。
[建議] 處理 delete 可能產生的異常。
解釋:
對於有被遍曆需求,且值 null 被認為具有商務邏輯意義的值的對象,移除某個屬性必須使用 delete 操作。
在strict 模式或 IE 下使用 delete 時,不能被刪除的屬性會拋出異常,因此在不確定屬性是否可以刪除的情況下,建議添加 try-catch 塊。
try { delete o.x;}catch (deleteError) { o.x = null;}
避免修改外部傳入的對象。
解釋:
JavaScript 因其指令碼語言的動態特性,當一個對象未被 seal 或 freeze 時,可以任意添加、刪除、修改屬性值。
但是隨意地對 非自身控制的對象 進行修改,很容易造成代碼在不可預知的情況下出現問題。因此,設計良好的組件、函數應該避免對外部傳入的對象的修改。
下面代碼的 selectNode 方法修改了由外部傳入的 datasource 對象。如果 datasource 用在其它場合(如另一個 Tree 執行個體)下,會造成狀態的混亂。
function Tree(datasource) { this.datasource = datasource;}Tree.prototype.selectNode = function (id) { // 從datasource中找出節點對象 var node = this.findNode(id); if (node) { node.selected = true; this.flushView(); }};
對於此類情境,需要使用額外的對象來維護,使用由自身控制,不與外部產生任何互動的 selectedNodeIndex 對象來維護節點的選中狀態,不對 datasource 作任何修改。
function Tree(datasource) { this.datasource = datasource; this.selectedNodeIndex = {};}Tree.prototype.selectNode = function (id) { // 從datasource中找出節點對象 var node = this.findNode(id); if (node) { this.selectedNodeIndex[id] = true; this.flushView(); }};除此之外,也可以通過 deepClone 等手段將自身維護的對象與外部傳入的分離,保證不會相互影響。
DOM[建議] 對於單個元素,儘可能使用 document.getElementById 擷取,避免使用document.all。[建議] 對於多個元素的集合,儘可能使用 context.getElementsByTagName 擷取。其中 context 可以為 document 或其他元素。指定 tagName 參數為 * 可以獲得所有子項目。[建議] 遍曆元素集合時,盡量緩衝集合長度。如需多次操作同一集合,則應將集合轉為數組。[建議] 擷取元素的直接子項目時使用 children。避免使用childNodes,除非預期是需要包含文本、注釋和屬性類型的節點。
解釋:
原生擷取元素集合的結果並不直接引用 DOM 元素,而是對索引進行讀取,所以 DOM 結構的改變會即時反映到結果中。
<div></div><span></span><script>var elements = document.getElementsByTagName(‘*‘);// 顯示為 DIValert(elements[0].tagName);var div = elements[0];var p = document.createElement(‘p‘);docpment.body.insertBefore(p, div);// 顯示為 Palert(elements[0].tagName);</script>
[建議] 擷取元素實際樣式資訊時,應使用 getComputedStyle 或 currentStyle。
解釋:
通過 style 只能獲得內聯定義或通過 JavaScript 直接設定的樣式。通過 CSS class 設定的元素樣式無法直接通過 style 擷取。
[建議] 儘可能通過為元素添加預定義的 className 來改變元素樣式,避免直接操作 style 設定[強制] 通過 style 對象設定元素樣式時,對於帶單位非 0 值的屬性,不允許省略單位。
解釋:
除了 IE,標準瀏覽器會忽略不規範的屬性值,導致相容性問題。
[建議] 操作 DOM 時,盡量減少頁面 reflow。
解釋:
頁面 reflow 是非常耗時的行為,非常容易導致效能瓶頸。下面一些情境會觸發瀏覽器的reflow:
DOM 操作也是非常耗時的一種操作,減少 DOM 操作有助於提高效能。舉一個簡單的例子,構建一個列表。我們可以用兩種方式:
在迴圈體中 createElement 並 append 到父元素中。
在迴圈體中拼接 HTML 字串,迴圈結束後寫父元素的 innerHTML。
第一種方法看起來比較標準,但是每次迴圈都會對 DOM 進行操作,效能極低。在這裡推薦使用第二種方法。
JavaScript編碼規範(2)