1. 程式人生 > >前端讀者 | 百度前端編碼規範(JS)

前端讀者 | 百度前端編碼規範(JS)

本文來自:百度FEX

1 前言

JavaScript在百度一直有著廣泛的應用,特別是在瀏覽器端的行為管理。本文件的目標是使JavaScript程式碼風格保持一致,容易被理解和被維護。

雖然本文件是針對JavaScript設計的,但是在使用各種JavaScript的預編譯語言時(如TypeScript等)時,適用的部分也應儘量遵循本文件的約定。

2 程式碼風格

2.1 檔案

[建議] JavaScript 檔案使用無 BOMUTF-8 編碼。

解釋:UTF-8 編碼具有更廣泛的適應性。BOM 在使用程式或工具處理檔案時可能造成不必要的干擾。

[建議] 在檔案結尾處,保留一個空行。

2.2 結構

[強制] 使用 4 個空格做為一個縮排層級,不允許使用 2 個空格 或 tab 字元。

[強制] switch 下的 casedefault 必須增加一個縮排層級。

// good
switch (variable) {

    case '1':
        // do...
        break;

    case '2':
        // do...
        break;

    default:
        // do...

}

// bad
switch (variable) {

case '1':
    // do...
    break;

case '2':
    // do...
    break;

default:
    // do...

}

[強制] 二元運算子兩側必須有一個空格,一元運算子與操作物件之間不允許有空格。

var a = !arr.length;
a++;
a = b + c;

[強制] 用作程式碼塊起始的左花括號 { 前必須有一個空格。

示例:

// good
if (condition) {
}

while (condition) {
}

function funcName() {
}

// bad
if (condition){
}

while (condition){
}

function funcName(){
}

[強制] if / else / for / while / function / switch / do / try / catch / finally

關鍵字後,必須有一個空格。

// good
if (condition) {
}

while (condition) {
}

(function () {
})();

// bad
if(condition) {
}

while(condition) {
}

(function() {
})();

[強制] 在物件建立時,屬性中的 : 之後必須有空格,: 之前不允許有空格。

// good
var obj = {
    a: 1,
    b: 2,
    c: 3
};

// bad
var obj = {
    a : 1,
    b:2,
    c :3
};

[強制] 函式宣告、具名函式表示式、函式呼叫中,函式名和 ( 之間不允許有空格。

// good
function funcName() {
}

var funcName = function funcName() {
};

funcName();

// bad
function funcName () {
}

var funcName = function funcName () {
};

funcName ();

[強制] ,; 前不允許有空格。

// good
callFunc(a, b);

// bad
callFunc(a , b) ;

[強制] 在函式呼叫、函式宣告、括號表示式、屬性訪問、if / for / while / switch / catch 等語句中,()[] 內緊貼括號部分不允許有空格。

// good

callFunc(param1, param2, param3);

save(this.list[this.indexes[i]]);

needIncream && (variable += increament);

if (num > list.length) {
}

while (len--) {
}


// bad

callFunc( param1, param2, param3 );

save( this.list[ this.indexes[ i ] ] );

needIncreament && ( variable += increament );

if ( num > list.length ) {
}

while ( len-- ) {
}

[強制] 單行宣告的陣列與物件,如果包含元素,{}[] 內緊貼括號部分不允許包含空格。

解釋:宣告包含元素的陣列與物件,只有當內部元素的形式較為簡單時,才允許寫在一行。元素複雜的情況,還是應該換行書寫。

// good
var arr1 = [];
var arr2 = [1, 2, 3];
var obj1 = {};
var obj2 = {name: 'obj'};
var obj3 = {
    name: 'obj',
    age: 20,
    sex: 1
};

// bad
var arr1 = [ ];
var arr2 = [ 1, 2, 3 ];
var obj1 = { };
var obj2 = { name: 'obj' };
var obj3 = {name: 'obj', age: 20, sex: 1};

[強制] 行尾不得有多餘的空格。

[強制] 每個獨立語句結束後必須換行。

[強制] 每行不得超過 120 個字元。

解釋:超長的不可分割的程式碼允許例外,比如複雜的正則表示式。長字串不在例外之列。

[強制] 運算子處換行時,運算子必須在新行的行首。

// good
if (user.isAuthenticated()
    && user.isInRole('admin')
    && user.hasAuthority('add-admin')
    || user.hasAuthority('delete-admin')
) {
    // Code
}

var result = number1 + number2 + number3
    + number4 + number5;


// bad
if (user.isAuthenticated() &&
    user.isInRole('admin') &&
    user.hasAuthority('add-admin') ||
    user.hasAuthority('delete-admin')) {
    // Code
}

var result = number1 + number2 + number3 +
    number4 + number5;

[強制] 在函式宣告、函式表示式、函式呼叫、物件建立、陣列建立、for語句等場景中,不允許在 ,; 前換行。

// good
var obj = {
    a: 1,
    b: 2,
    c: 3
};

foo(
    aVeryVeryLongArgument,
    anotherVeryLongArgument,
    callback
);


// bad
var obj = {
    a: 1
    , b: 2
    , c: 3
};

foo(
    aVeryVeryLongArgument
    , anotherVeryLongArgument
    , callback
);

[建議] 不同行為或邏輯的語句集,使用空行隔開,更易閱讀。

// 僅為按邏輯換行的示例,不代表setStyle的最優實現
function setStyle(element, property, value) {
    if (element == null) {
        return;
    }

    element.style[property] = value;
}

[建議] 在語句的行長度超過 120 時,根據邏輯條件合理縮排。

// 較複雜的邏輯條件組合,將每個條件獨立一行,邏輯運算子放置在行首進行分隔,或將部分邏輯按邏輯組合進行分隔。
// 建議最終將右括號 ) 與左大括號 { 放在獨立一行,保證與 if 內語句塊能容易視覺辨識。
if (user.isAuthenticated()
    && user.isInRole('admin')
    && user.hasAuthority('add-admin')
    || user.hasAuthority('delete-admin')
) {
    // Code
}

// 按一定長度截斷字串,並使用 + 運算子進行連線。
// 分隔字串儘量按語義進行,如不要在一個完整的名詞中間斷開。
// 特別的,對於HTML片段的拼接,通過縮排,保持和HTML相同的結構。
var html = '' // 此處用一個空字串,以便整個HTML片段都在新行嚴格對齊
    + '<article>'
    +     '<h1>Title here</h1>'
    +     '<p>This is a paragraph</p>'
    +     '<footer>Complete</footer>'
    + '</article>';

// 也可使用陣列來進行拼接,相對 + 更容易調整縮排。
var html = [
    '<article>',
        '<h1>Title here</h1>',
        '<p>This is a paragraph</p>',
        '<footer>Complete</footer>',
    '</article>'
];
html = html.join('');

// 當引數過多時,將每個引數獨立寫在一行上,並將結束的右括號 ) 獨立一行。
// 所有引數必須增加一個縮排。
foo(
    aVeryVeryLongArgument,
    anotherVeryLongArgument,
    callback
);

// 也可以按邏輯對引數進行組合。
// 最經典的是baidu.format函式,呼叫時將引數分為“模板”和“資料”兩塊
baidu.format(
    dateFormatTemplate,
    year, month, date, hour, minute, second
);

// 當函式呼叫時,如果有一個或以上引數跨越多行,應當每一個引數獨立一行。
// 這通常出現在匿名函式或者物件初始化等作為引數時,如setTimeout函式等。
setTimeout(
    function () {
        alert('hello');
    },
    200
);

order.data.read(
    'id=' + me.model.id, 
    function (data) {
        me.attchToModel(data.result);
        callback();
    }, 
    300
);

// 鏈式呼叫較長時採用縮排進行調整。
$('#items')
    .find('.selected')
    .highlight()
    .end();

// 三元運算子由3部分組成,因此其換行應當根據每個部分的長度不同,形成不同的情況。
var result = thisIsAVeryVeryLongCondition
    ? resultA : resultB;

var result = condition
    ? thisIsAVeryVeryLongResult
    : resultB;

// 陣列和物件初始化的混用,嚴格按照每個物件的 { 和結束 } 在獨立一行的風格書寫。
var array = [
    {
        // ...
    },
    {
        // ...
    }
];

[建議] 對於 if...else...try...catch...finally 等語句,推薦使用在 } 號後新增一個換行 的風格,使程式碼層次結構更清晰,閱讀性更好。

if (condition) {
    // some statements;
}
else {
    // some statements;
}

try {
    // some statements;
}
catch (ex) {
    // some statements;
}

[強制] 不得省略語句結束的分號。

[強制] 在 if / else / for / do / while 語句中,即使只有一行,也不得省略塊 {...}

// good
if (condition) {
    callFunc();
}

// bad
if (condition) callFunc();
if (condition)
    callFunc();

[強制] 函式定義結束不允許新增分號。

// good
function funcName() {
}

// bad
function funcName() {
};

// 如果是函式表示式,分號是不允許省略的。
var funcName = function () {
};

[強制] IIFE 必須在函式表示式外新增 (,非 IIFE 不得在函式表示式外新增 (

解釋:IIFE = Immediately-Invoked Function Expression.

額外的 ( 能夠讓程式碼在閱讀的一開始就能判斷函式是否立即被呼叫,進而明白接下來程式碼的用途。而不是一直拖到底部才恍然大悟。

// good
var task = (function () {
   // Code
   return result;
})();

var func = function () {
};


// bad
var task = function () {
    // Code
    return result;
}();

var func = (function () {
});

2.3 命名

下面提到的 Camel命名法:駝峰命名法;Pascal命名法:帕斯卡命名法,又叫大駝峰命名法。

[強制] 變數 使用 Camel命名法

var loadingModules = {};

[強制] 常量 使用 全部字母大寫,單詞間下劃線分隔 的命名方式。

var HTML_ENTITY = {};

[強制] 函式 使用 Camel命名法

function stringFormat(source) {
}

[強制] 函式的 引數 使用 Camel命名法

function hear(theBells) {
}

[強制] 使用 Pascal命名法

function TextNode(options) {
}

[強制] 類的 方法 / 屬性 使用 Camel命名法

function TextNode(value, engine) {
    this.value = value;
    this.engine = engine;
}

TextNode.prototype.clone = function () {
    return this;
};

[強制] 列舉變數 使用 Pascal命名法列舉的屬性 使用 全部字母大寫,單詞間下劃線分隔 的命名方式。

var TargetState = {
    READING: 1,
    READED: 2,
    APPLIED: 3,
    READY: 4
};

[強制] 名稱空間 使用 Camel命名法

equipments.heavyWeapons = {};

[強制] 由多個單片語成的縮寫詞,在命名中,根據當前命名法和出現的位置,所有字母的大小寫與首字母的大小寫保持一致。

function XMLParser() {
}

function insertHTML(element, html) {
}

var httpRequest = new HTTPRequest();

[強制] 類名 使用 名詞

function Engine(options) {
}

[建議] 函式名 使用 動賓短語

function getStyle(element) {
}

[建議] boolean 型別的變數使用 ishas 開頭。

var isReady = false;
var hasMoreCommands = false;

[建議] Promise物件動賓短語的進行時 表達。

var loadingData = ajax.get('url');
loadingData.then(callback);

2.4 註釋

2.4.1 單行註釋

[強制] 必須獨佔一行。// 後跟一個空格,縮排與下一行被註釋說明的程式碼一致。

2.4.2 多行註釋

[建議] 避免使用 /*...*/ 這樣的多行註釋。有多行註釋內容時,使用多個單行註釋。

2.4.3 文件化註釋

[強制] 為了便於程式碼閱讀和自文件化,以下內容必須包含以 /**...*/ 形式的塊註釋中。

解釋:

  1. 檔案
  2. namespace
  3. 函式或方法
  4. 類屬性
  5. 事件
  6. 全域性變數
  7. 常量
  8. AMD 模組

[強制] 文件註釋前必須空一行。

[建議] 自文件化的文件說明 what,而不是 how。

2.4.4 型別定義

[強制] 型別定義都是以{開始, 以}結束。

解釋:常用型別如:{string}, {number}, {boolean}, {Object}, {Function}, {RegExp}, {Array}, {Date}。

型別不僅侷限於內建的型別,也可以是自定義的型別。比如定義了一個類 Developer,就可以使用它來定義一個引數和返回值的型別。

[強制] 對於基本型別 {string}, {number}, {boolean},首字母必須小寫。

型別定義 語法示例 解釋
String {string} --
Number {number} --
Boolean {boolean} --
Object {Object} --
Function {Function} --
RegExp {RegExp} --
Array {Array} --
Date {Date} --
單一型別集合 {Array.<string>} string 型別的陣列
多型別 {(number|boolean)} 可能是 number 型別, 也可能是 boolean 型別
允許為null {?number} 可能是 number, 也可能是 null
不允許為null {!Object} Object 型別, 但不是 null
Function型別 {function(number, boolean)} 函式, 形參型別
Function帶返回值 {function(number, boolean):string} 函式, 形參, 返回值型別
引數可選 @param {string=} name 可選引數, =為型別字尾
可變引數 @param {...number} args 變長引數, ...為型別字首
任意型別 {*} 任意型別
可選任意型別 @param {*=} name 可選引數,型別不限
可變任意型別 @param {...*} args 變長引數,型別不限

2.4.5 檔案註釋

[強制] 檔案頂部必須包含檔案註釋,用 @file 標識檔案說明。

/**
 * @file Describe the file
 */

[建議] 檔案註釋中可以用 @author 標識開發者資訊。

解釋:

開發者資訊能夠體現開發人員對檔案的貢獻,並且能夠讓遇到問題或希望瞭解相關資訊的人找到維護人。通常情況檔案在被建立時標識的是建立者。隨著專案的進展,越來越多的人加入,參與這個檔案的開發,新的作者應該被加入 @author 標識。

@author 標識具有多人時,原則是按照 責任 進行排序。通常的說就是如果有問題,就是找第一個人應該比找第二個人有效。比如檔案的建立者由於各種原因,模組移交給了其他人或其他團隊,後來因為新增需求,其他人在新增程式碼時,新增 @author 標識應該把自己的名字新增在建立人的前面。

@author 中的名字不允許被刪除。任何勞動成果都應該被尊重。

業務專案中,一個檔案可能被多人頻繁修改,並且每個人的維護時間都可能不會很長,不建議為檔案增加 @author 標識。通過版本控制系統追蹤變更,按業務邏輯單元確定模組的維護責任人,通過文件與wiki跟蹤和查詢,是更好的責任管理方式。

對於業務邏輯無關的技術型基礎專案,特別是開源的公共專案,應使用 @author 標識。

/**
 * @file Describe the file
 * @author author-name([email protected])
 *         author-name2([email protected])
 */

2.4.6 名稱空間註釋

[建議] 名稱空間使用 @namespace 標識。

/**
 * @namespace
 */
var util = {};

2.4.7 類註釋

[建議] 使用 @class 標記類或建構函式。

解釋:對於使用物件 constructor 屬性來定義的建構函式,可以使用 @constructor 來標記。

/**
 * 描述
 *
 * @class
 */
function Developer() {
    // constructor body
}

[建議] 使用 @extends 標記類的繼承資訊。

/**
 * 描述
 *
 * @class
 * @extends Developer
 */
function Fronteer() {
    Developer.call(this);
    // constructor body
}
util.inherits(Fronteer, Developer);

[強制] 使用包裝方式擴充套件類成員時, 必須通過 @lends 進行重新指向。

解釋:沒有 @lends 標記將無法為該類生成包含擴充套件類成員的文件。

/**
 * 類描述
 *
 * @class
 * @extends Developer
 */
function Fronteer() {
    Developer.call(this);
    // constructor body
}

util.extend(
    Fronteer.prototype,
    /** @lends Fronteer.prototype */{
        _getLevel: function () {
            // TODO
        }
    }
);

[強制] 類的屬性或方法等成員資訊使用 @public / @protected / @private 中的任意一個,指明可訪問性。

解釋:生成的文件中將有可訪問性的標記,避免使用者直接使用非 public 的屬性或方法。

/**
 * 類描述
 *
 * @class
 * @extends Developer
 */
var Fronteer = function () {
    Developer.call(this);

    /**
     * 屬性描述
     *
     * @type {string}
     * @private
     */
    this._level = 'T12';

    // constructor body
};
util.inherits(Fronteer, Developer);

/**
 * 方法描述
 *
 * @private
 * @return {string} 返回值描述
 */
Fronteer.prototype._getLevel = function () {
};

2.4.8 函式/方法註釋

[強制] 函式/方法註釋必須包含函式說明,有引數和返回值時必須使用註釋標識。

[強制] 引數和返回值註釋必須包含型別資訊和說明。

[建議] 當函式是內部函式,外部不可訪問時,可以使用 @inner 標識。

/**
 * 函式描述
 *
 * @param {string} p1 引數1的說明
 * @param {string} p2 引數2的說明,比較長
 *     那就換行了.
 * @param {number=} p3 引數3的說明(可選)
 * @return {Object} 返回值描述
 */
function foo(p1, p2, p3) {
    var p3 = p3 || 10;
    return {
        p1: p1,
        p2: p2,
        p3: p3
    };
}

[強制] 對 Object 中各項的描述, 必須使用 @param 標識。

/**
 * 函式描述
 *
 * @param {Object} option 引數描述
 * @param {string} option.url option項描述
 * @param {string=} option.method option項描述,可選引數
 */
function foo(option) {
    // TODO
}

[建議] 重寫父類方法時, 應當新增 @override 標識。如果重寫的形參個數、型別、順序和返回值型別均未發生變化,可省略 @param@return,僅用 @override 標識,否則仍應作完整註釋。

解釋:簡而言之,當子類重寫的方法能直接套用父類的方法註釋時可省略對引數與返回值的註釋。

2.4.9 事件註釋

[強制] 必須使用 @event 標識事件,事件引數的標識與方法描述的引數標識相同。

/**
 * 值變更時觸發
 *
 * @event
 * @param {Object} e e描述
 * @param {string} e.before before描述
 * @param {string} e.after after描述
 */
onchange: function (e) {
}

[強制] 在會廣播事件的函式前使用 @fires 標識廣播的事件,在廣播事件程式碼前使用 @event 標識事件。

[建議] 對於事件物件的註釋,使用 @param 標識,生成文件時可讀性更好。

/**
 * 點選處理
 *
 * @fires Select#change
 * @private
 */
Select.prototype.clickHandler = function () {
    /**
     * 值變更時觸發
     *
     * @event Select#change
     * @param {Object} e e描述
     * @param {string} e.before before描述
     * @param {string} e.after after描述
     */
    this.fire(
        'change',
        {
            before: 'foo',
            after: 'bar'
        }
    );
};

2.4.10 常量註釋

[強制] 常量必須使用 @const 標記,幷包含說明和型別資訊。

/**
 * 常量說明
 *
 * @const
 * @type {string}
 */
var REQUEST_URL = 'myurl.do';

2.4.11 複雜型別註釋

[建議] 對於型別未定義的複雜結構的註釋,可以使用 @typedef 標識來定義。

// `namespaceA~` 可以換成其它 namepaths 字首,目的是為了生成文件中能顯示 `@typedef` 定義的型別和連結。
/**
 * 伺服器
 *
 * @typedef {Object} namespaceA~Server
 * @property {string} host 主機
 * @property {number} port 埠
 */

/**
 * 伺服器列表
 *
 * @type {Array.<namespaceA~Server>}
 */
var servers = [
    {
        host: '1.2.3.4',
        port: 8080
    },
    {
        host: '1.2.3.5',
        port: 8081
    }
];

2.4.12 AMD 模組註釋

[強制] AMD 模組使用 @module@exports 標識。

解釋:@exports 與 @module 都可以用來標識模組,區別在於 @module 可以省略模組名稱。而只使用 @exports 時在 namepaths 中可以省略 module: 字首。

define(
    function (require) {

        /**
         * foo description
         *
         * @exports Foo
         */
        var foo = {
            // TODO
        };

        /**
         * baz description
         *
         * @return {boolean} return description
         */
        foo.baz = function () {
            // TODO
        };

        return foo;

    }
);

也可以在 exports 變數前使用 @module 標識:

define(
    function (require) {

        /**
         * module description.
         *
         * @module foo
         */
        var exports = {};


        /**
         * bar description
         *
         */
        exports.bar = function () {
            // TODO
        };

        return exports;
    }
);

如果直接使用 factory 的 exports 引數,還可以:

/**
 * module description.
 *
 * @module
 */
define(
    function (require, exports) {

        /**
         * bar description
         *
         */
        exports.bar = function () {
            // TODO
        };
        return exports;
    }
);

[強制] 對於已使用 @module 標識為 AMD模組 的引用,在 namepaths 中必須增加 module: 作字首。

解釋:namepaths 沒有 module: 字首時,生成的文件中將無法正確生成連結。

/**
 * 點選處理
 *
 * @fires module:Select#change
 * @private
 */
Select.prototype.clickHandler = function () {
    /**
     * 值變更時觸發
     *
     * @event module:Select#change
     * @param {Object} e e描述
     * @param {string} e.before before描述
     * @param {string} e.after after描述
     */
    this.fire(
        'change',
        {
            before: 'foo',
            after: 'bar'
        }
    );
};

[建議] 對於類定義的模組,可以使用 @alias 標識構建函式。

/**
 * A module representing a jacket.
 * @module jacket
 */
define(
    function () {

        /**
         * @class
         * @alias module:jacket
         */
        var Jacket = function () {
        };

        return Jacket;
    }
);

[建議] 多模組定義時,可以使用 @exports 標識各個模組。

// one module
define('html/utils',
    /**
     * Utility functions to ease working with DOM elements.
     * @exports html/utils
     */
    function () {
        var exports = {
        };

        return exports;
    }
);

// another module
define('tag',
    /** @exports tag */
    function () {
        var exports = {
        };

        return exports;
    }
);

[建議] 對於 exports 為 Object 的模組,可以使用@namespace標識。

解釋:使用 @namespace 而不是 @module 或 @exports 時,對模組的引用可以省略 module: 字首。

[建議] 對於 exports 為類名的模組,使用 @class@exports 標識。


// 只使用 @class Bar 時,類方法和屬性都必須增加 @name Bar#methodName 來標識,與 @exports 配合可以免除這一麻煩,並且在引用時可以省去 module: 字首。
// 另外需要注意類名需要使用 var 定義的方式。

/**
 * Bar description
 *
 * @see foo
 * @exports  Bar
 * @class
 */
var Bar = function () {
    // TODO
};

/**
 * baz description
 *
 * @return {(string|Array)} return description
 */
Bar.prototype.baz = function () {
    // TODO
};

2.4.13 細節註釋

對於內部實現、不容易理解的邏輯說明、摘要資訊等,我們可能需要編寫細節註釋。

[建議] 細節註釋遵循單行註釋的格式。說明必須換行時,每行是一個單行註釋的起始。

function foo(p1, p2, opt_p3) {
    // 這裡對具體內部邏輯進行說明
    // 說明太長需要換行
    for (...) {
        ....
    }
}

[強制] 有時我們會使用一些特殊標記進行說明。特殊標記必須使用單行註釋的形式。下面列舉了一些常用標記:

解釋:

  1. TODO: 有功能待實現。此時需要對將要實現的功能進行簡單說明。
  2. FIXME: 該處程式碼執行沒問題,但可能由於時間趕或者其他原因,需要修正。此時需要對如何修正進行簡單說明。
  3. HACK: 為修正某些問題而寫的不太好或者使用了某些詭異手段的程式碼。此時需要對思路或詭異手段進行描述。
  4. XXX: 該處存在陷阱。此時需要對陷阱進行描述。

3 語言特性

3.1 變數

[強制] 變數在使用前必須通過 var 定義。

解釋:不通過 var 定義變數將導致變數汙染全域性環境。

// good
var name = 'MyName';

// bad
name = 'MyName';

[強制] 每個 var 只能宣告一個變數。

解釋:一個 var 宣告多個變數,容易導致較長的行長度,並且在修改時容易造成逗號和分號的混淆。

// good
var hangModules = [];
var missModules = [];
var visited = {};

// bad
var hangModules = [],
    missModules = [],
    visited = {};

[強制] 變數必須 即用即宣告,不得在函式或其它形式的程式碼塊起始位置統一宣告所有變數。

解釋: 變數宣告與使用的距離越遠,出現的跨度越大,程式碼的閱讀與維護成本越高。雖然JavaScript的變數是函式作用域,還是應該根據程式設計中的意圖,縮小變量出現的距離空間。

// good
function 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;
}

// bad
function 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;
}

3.2 條件

[強制] 在 Equality Expression 中使用型別嚴格的 ===。僅當判斷 null 或 undefined 時,允許使用 == null

解釋:使用 === 可以避免等於判斷中隱式的型別轉換。

// good
if (age === 30) {
    // ......
}

// bad
if (age == 30) {
    // ......
}

[建議] 儘可能使用簡潔的表示式。

// 字串為空

// good
if (!name) {
    // ......
}

// bad
if (name === '') {
    // ......
}
// 字串非空

// good
if (name) {
    // ......
}

// bad
if (name !== '') {
    // ......
}
// 陣列非空

// good
if (collection.length) {
    // ......
}

// bad
if (collection.length > 0) {
    // ......
}
// 布林不成立

// good
if (!notTrue) {
    // ......
}

// bad
if (notTrue === false) {
    // ......
}
// null 或 undefined

// good
if (noValue == null) {
  // ......
}

// bad
if (noValue === null || typeof noValue === 'undefined') {
  // ......
}

[建議] 按執行頻率排列分支的順序。

解釋:按執行頻率排列分支的順序好處是:

  1. 閱讀的人容易找到最常見的情況,增加可讀性。
  2. 提高執行效率。

[建議] 對於相同變數或表示式的多值條件,用 switch 代替 if

// good
switch (typeof variable) {
    case 'object':
        // ......
        break;
    case 'number':
    case 'boolean':
    case 'string':
        // ......
        break;
}

// bad
var type = typeof variable;
if (type === 'object') {
    // ......
} 
else if (type === 'number' || type === 'boolean' || type === 'string') {
    // ......
}

[建議] 如果函式或全域性中的 else 塊後沒有任何語句,可以刪除 else

示例:

// good
function getName() {
    if (name) {
        return name;
    }

    return 'unnamed';
}

// bad
function getName() {
    if (name) {
        return name;
    }
    else {
        return 'unnamed';
    }
}

3.3 迴圈

[建議] 不要在迴圈體中包含函式表示式,事先將函式提取到迴圈體外。

解釋:迴圈體中的函式表示式,執行過程中會生成迴圈次數個函式物件。

// good
function clicker() {
    // ......
}

for (var i = 0, len = elements.length; i < len; i++) {
    var element = elements[i];
    addListener(element, 'click', clicker);
}


// bad
for (var i = 0, len = elements.length; i < len; i++) {
    var element = elements[i];
    addListener(element, 'click', function () {});
}

[建議] 對迴圈內多次使用的不變值,在迴圈外用變數快取。

// good
var width = wrap.offsetWidth + 'px';
for (var i = 0, len = elements.length; i < len; i++) {
    var element = elements[i];
    element.style.width = width;
    // ......
}


// bad
for (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];
    // ......
}

3.4 型別

3.4.1 型別檢測

[建議] 型別檢測優先使用 typeof。物件型別檢測使用 instanceofnullundefined 的檢測使用 == null

// string
typeof variable === 'string'

// number
typeof variable === 'number'

// boolean
typeof variable === 'boolean'

// Function
typeof variable === 'function'

// Object
typeof variable === 'object'

// RegExp
variable instanceof RegExp

// Array
variable instanceof Array

// null
variable === null

// null or undefined
variable == null

// undefined
typeof variable === 'undefined'

3.4.2 型別轉換

[建議] 轉換成 string 時,使用 + ''

// good
num + '';

// bad
new String(num);
num.toString();
String(num);

[建議] 轉換成 number 時,通常使用 +

// good
+str;

// bad
Number(str);

[建議] string 轉換成 number,要轉換的字串結尾包含非數字並期望忽略時,使用 parseInt

var width = '200px';
parseInt(width, 10);

[強制] 使用 parseInt 時,必須指定進位制。

// good
parseInt(str, 10);

// bad
parseInt(str);

[建議] 轉換成 boolean 時,使用 !!

var num = 3.14;
!!num;

[建議] number 去除小數點,使用 Math.floor / Math.round / Math.ceil,不使用 parseInt

// good
var num = 3.14;
Math.ceil(num);

// bad
var num = 3.14;
parseInt(num, 10);

3.5 字串

[強制] 字串開頭和結束使用單引號 '

解釋:

  1. 輸入單引號不需要按住 shift,方便輸入。
  2. 實際使用中,字串經常用來拼接 HTML。為方便 HTML 中包含雙引號而不需要轉義寫法。
var str = '我是一個字串';
var html = '<div class="cls">拼接HTML可以省去雙引號轉義</div>';

[建議] 使用 陣列+ 拼接字串。

解釋:

  1. 使用 + 拼接字串,如果拼接的全部是 StringLiteral,壓縮工具可以對其進行自動合併的優化。所以,靜態字串建議使用 + 拼接。
  2. 在現代瀏覽器下,使用 + 拼接字串,效能較陣列的方式要高。
  3. 如需要兼顧老舊瀏覽器,應儘量使用陣列拼接字串。

示例:

// 使用陣列拼接字串
var str = [
    // 推薦換行開始並縮排開始第一個字串, 對齊程式碼, 方便閱讀.
    '<ul>',
        '<li>第一項</li>',
        '<li>第二項</li>',
    '</ul>'
].join('');

// 使用 + 拼接字串
var str2 = '' // 建議第一個為空字串, 第二個換行開始並縮排開始, 對齊程式碼, 方便閱讀
    + '<ul>',
    +    '<li>第一項</li>',
    +    '<li>第二項</li>',
    + '</ul>';

[建議] 複雜的資料到檢視字串的轉換過程,選用一種模板引擎。

解釋:使用模板引擎有如下好處:

  1. 在開發過程中專注於資料,將檢視生成的過程由另外一個層級維護,使程式邏輯結構更清晰。
  2. 優秀的模板引擎,通過模板編譯技術和高質量的編譯產物,能獲得比手工拼接字串更高的效能。
  • artTemplate: 體積較小,在所有環境下效能高,語法靈活。
  • dot.js: 體積小,在現代瀏覽器下效能高,語法靈活。
  • etpl: 體積較小,在所有環境下效能高,模板複用性高,語法靈活。
  • handlebars: 體積大,在所有環境下效能高,擴充套件性高。
  • hogon: 體積小,在現代瀏覽器下效能高。
  • nunjucks: 體積較大,效能一般,模板複用性高。

3.6 物件

[強制] 使用物件字面量 {} 建立新 Object

// good
var obj = {};

// bad
var obj = new Object();

[強制] 物件建立時,如果一個物件的所有 屬性 均可以不新增引號,則所有 屬性 不得新增引號。

var info = {
    name: 'someone',
    age: 28
};

[強制] 物件建立時,如果任何一個 屬性 需要新增引號,則所有 屬性 必須新增 '

解釋:如果屬性不符合 Identifier 和 NumberLiteral 的形式,就需要以 StringLiteral 的形式提供。

// good
var info = {
    'name': 'someone',
    'age': 28,
    'more-info': '...'
};

// bad
var info = {
    name: 'someone',
    age: 28,
    'more-info': '...'
};

[強制] 不允許修改和擴充套件任何原生物件和宿主物件的原型。

// 以下行為絕對禁止
String.prototype.trim = function () {
};

[建議] 屬性訪問時,儘量使用 .

解釋:屬性名符合 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];
    }
}

3.7 陣列

[強制] 使用陣列字面量 [] 建立新陣列,除非想要建立的是指定長度的陣列。

// good
var arr = [];

// bad
var arr = new Array();

[強制] 遍歷陣列不使用 for in

解釋:陣列物件可能存在數字以外的屬性, 這種情況下 for in 不會得到正確結果.

var arr = ['a', 'b', 'c'];
arr.other = 'other things'; // 這裡僅作演示, 實際中應使用Object型別

// 正確的遍歷方式
for (var i = 0, len = arr.length; i < len; i++) {
    console.log(i);
}

// 錯誤的遍歷方式
for (i in arr) {
    console.log(i);
}

[建議] 不因為效能的原因自己實現陣列排序功能,儘量使用陣列的 sort 方法。

解釋:自己實現的常規排序演算法,在效能上並不優於陣列預設的 sort 方法。以下兩種場景可以自己實現排序:

  1. 需要穩定的排序演算法,達到嚴格一致的排序結果。
  2. 資料特點鮮明,適合使用桶排。

[建議] 清空陣列使用 .length = 0

3.8 函式

3.8.1 函式長度

[建議] 一個函式的長度控制在 50 行以內。

解釋:將過多的邏輯單元混在一個大函式中,易導致難以維護。一個清晰易懂的函式應該完成單一的邏輯單元。複雜的操作應進一步抽取,通過函式的呼叫來體現流程。

特定演算法等不可分割的邏輯允許例外。

function syncViewStateOnUserAction() {
    if (x.checked) {
        y.checked = true;
        z.value = '';
    }
    else {
        y.checked = false;
    }

    if (!a.value) {
        warning.innerText = 'Please enter it';
        submitButton.disabled = true;
    }
    else {
        warning.innerText = '';
        submitButton.disabled = false;
    }
}

// 直接閱讀該函式會難以明確其主線邏輯,因此下方是一種更合理的表達方式:

function syncViewStateOnUserAction() {
    syncXStateToView();
    checkAAvailability();
}

function syncXStateToView() {
    if (x.checked) {
        y.checked = true;
        z.value = '';
    }
    else {
        y.checked = false;
    }
}

function checkAAvailability() {
    if (!a.value) {
        displayWarningForAMissing();
    }
    else {
        clearWarnignForA();
    }
}

3.8.2 引數設計

[建議] 一個函式的引數控制在 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 物件只需判斷屬性是否存在,實現得以簡化。

3.8.3 閉包

[建議] 在適當的時候將閉包內大物件置為 null

解釋:

在 JavaScript 中,無需特別的關鍵詞就可以使用閉包,一個函式可以任意訪問在其定義的作用域外的變數。需要注意的是,函式的作用域是靜態的,即在定義時決定,與呼叫的時機和方式沒有任何關係。

閉包會阻止一些變數的垃圾回收,對於較老舊的JavaScript引擎,可能導致外部所有變數均無法回收。

首先一個較為明確的結論是,以下內容會影響到閉包內變數的回收:

  • 巢狀的函式中是否有使用該變數。
  • 巢狀的函式中是否有 直接呼叫eval
  • 是否使用了 with 表示式。

Chakra、V8 和 SpiderMonkey 將受以上因素的影響,表現出不盡相同又較為相似的回收策略,而JScript.dll和Carakan則完全沒有這方面的優化,會完整保留整個 LexicalEnvironment 中的所有變數繫結,造成一定的記憶體消耗。

由於對閉包內變數有回收優化策略的 Chakra、V8 和 SpiderMonkey 引擎的行為較為相似,因此可以總結如下,當返回一個函式 fn 時:

  1. 如果 fn 的 [[Scope]] 是ObjectEnvironment(with 表示式生成 ObjectEnvironment,函式和 catch 表示式生成 DeclarativeEnvironment),則:
    1. 如果是 V8 引擎,則退出全過程。
    2. 如果是 SpiderMonkey,則處理該 ObjectEnvironment 的外層 LexicalEnvironment。
  2. 獲取當前 LexicalEnvironment 下的所有型別為 Function 的物件,對於每一個 Function 物件,分析其 FunctionBody:
    1. 如果 FunctionBody 中含有 直接呼叫eval,則退出全過程。
    2. 否則得到所有的 Identifier。
    3. 對於每一個 Identifier,設其為 name,根據查詢變數引用的規則,從 LexicalEnvironment 中找出名稱為 name 的繫結 binding。
    4. 對 binding 新增 notSwap 屬性,其值為 true。
  3. 檢查當前 LexicalEnvironment 中的每一個變數繫結,如果該繫結有 notSwap 屬性且值為 true,則:
    1. 如果是V8引擎,刪除該繫結。
    2. 如果是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]();
}

3.8.4 空函式

[建議] 空函式不使用 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;

3.9 面向物件

[強制] 類的繼承方案,實現時需要修正 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等於物件時,需要修正constructor
Animal.prototype = {
    constructor: Animal,

    jump: function () {
        alert('animal ' + this.name + ' jump');
    }
};

// 這種方式擴充套件prototype則無需理會constructor
Animal.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 引數。如果事件需要傳遞較多資訊,應仔細設計事件物件。

解釋:一個事件物件的好處有:

  1. 順序無關,避免事件監聽者需要記憶引數順序。
  2. 每個事件資訊都可以根據需要提供或者不提供,更自由。
  3. 擴充套件方便,未來新增事件資訊時,無需考慮會破壞監聽器引數形式而無法向後相容。

[建議] 設計自定義事件時,應考慮禁止預設行為。

解釋:常見禁止預設行為的方式有兩種:

  1. 事件監聽函式中 return false。
  2. 事件物件中包含禁止預設行為的方法,如 preventDefault。

3.10 動態特性

3.10.1 eval

[強制] 避免使用直接 eval 函式。

解釋:直接 eval,指的是以函式方式呼叫 eval 的呼叫方法。直接 eval 呼叫執行程式碼的作用域為本地作用域,應當避免。

如果有特殊情況需要使用直接 eval,需在程式碼中用詳細的註釋說明為何必須使用直接 eval,不能使用其它動態執行程式碼的方式,同時需要其他資深工程師進行 Code Review。

[建議] 儘量避免使用 eval 函式。

3.10.2 動態執行程式碼

[建議] 使用 new Function 執行動態程式碼。

解釋:通過 new Function 生成的函式作用域是全域性使用域,不會影響噹噹前的本地作用域。如果有動態程式碼執行的需求,建議使用 new Function。

var handler = new Function('x', 'y', 'return x + y;');
var result = handler($('#x').val(), $('#y').val());

3.10.3 with

[建議] 儘量不要使用 with

解釋:使用 with 可能會增加程式碼的複雜度,不利於閱讀和管理;也會對效能有影響。大多數使用 with 的場景都能使用其他方式較好的替代。所以,儘量不要使用 with。

3.10.4 delete

[建議] 減少 delete 的使用。

解釋:如果沒有特別的需求,減少或避免使用deletedelete的使用會破壞部分 JavaScript 引擎的效能優化。

[建議] 處理 delete 可能產生的異常。

解釋:

對於有被遍歷需求,且值 null 被認為具有業務邏輯意義的值的物件,移除某個屬性必須使用 delete 操作。

在嚴格模式或IE下使用 delete 時,不能被刪除的屬性會丟擲異常,因此在不確定屬性是否可以刪除的情況下,建議新增 try-catch 塊。

try {
    delete o.x;
}
catch (deleteError) {
    o.x = null;
}

3.10.5 物件屬性

[建議] 避免修改外部傳入的物件。

解釋:

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 等手段將自身維護的物件與外部傳入的分離,保證不會相互影響。

[建議] 具備強型別的設計。

解釋:

  • 如果一個屬性被設計為 boolean 型別,則不要使用 1 / 0 作為其值。對於標識性的屬性,如對程式碼體積有嚴格要求,可以從一開始就設計為 number 型別且將 0 作為否定值。
  • 從 DOM 中取出的值通常為 string 型別,如果有物件或函式的接收型別為 number 型別,提前作好轉換,而不是期望物件、函式可以處理多型別的值。

4 瀏覽器環境

4.1 模組化

4.1.1 AMD

[強制] 使用 AMD 作為模組定義。

解釋:

AMD 作為由社群認可的模組定義形式,提供多種過載提供靈活的使用方式,並且絕大多數優秀的 Library 都支援 AMD,適合作為規範。

目前,比較成熟的 AMD Loader 有:

  • 百度自己實現的 esl

[強制] 模組 id 必須符合標準。

解釋:模組 id 必須符合以下約束條件:

  1. 型別為 string,並且是由 / 分割的一系列 terms 來組成。例如:this/is/a/module
  2. term 應該符合 [a-zA-Z0-9_-]+ 規則。
  3. 不應該有 .js 字尾。
  4. 跟檔案的路徑保持一致。

4.1.2 define

[建議] 定義模組時不要指明 iddependencies

解釋:

在 AMD 的設計思想裡,模組名稱是和所在路徑相關的,匿名的模組更利於封包和遷移。模組依賴應在模組定義內部通過 local require 引用。

所以,推薦使用 define(factory) 的形式進行模組定義。

define(
    function (require) {
    }
);

[建議] 使用 return 來返回模組定義。

解釋:使用 return 可以減少 factory 接收的引數(不需要接收 exports 和 module),在沒有 AMD Loader 的場景下也更容易進行簡單的處理來偽造一個 Loader。

define(
    function (require) {
        var exports = {};

        // ...

        return exports;
    }
);

4.1.3 require

[強制] 全域性執行環境中,require 必須以 async require 形式呼叫。

解釋:模組的載入過程是非同步的,同步呼叫並無法保證得到正確的結果。

// good
require(['foo'], function (foo) {
});

// bad
var foo = require('foo');

[強制] 模組定義中只允許使用 local require,不允許使用 global require

解釋:

  1. 在模組定義中使用 global require,對封裝性是一種破壞。
  2. 在 AMD 裡,global require 是可以被重新命名的。並且 Loader 甚至沒有全域性的 require 變數,而是用 Loader 名稱做為 global require。模組定義不應該依賴使用的 Loader。

[強制] Package在實現時,內部模組的 require 必須使用 relative id

解釋:對於任何可能通過 釋出-引入 的形式複用的第三方庫、框架、包,開發者所定義的名稱不代表使用者使用的名稱。因此不要基於任何名稱的假設。在實現原始碼中,require 自身的其它模組時使用 relative id。

define(
    function (require) {
        var util = require('./util');
    }
);

[建議] 不會被呼叫的依賴模組,在 factory 開始處統一 require

解釋:有些模組是依賴的模組,但不會在模組實現中被直接呼叫,最為典型的是 css / js / tpl 等 Plugin 所引入的外部內容。此類內容建議放在模組定義最開始處統一引用。

define(
    function (require) {
        require('css!foo.css');
        require('tpl!bar.tpl.html');

        // ...
    }
);

4.2 DOM

4.2.1 元素獲取

[建議] 對於單個元素,儘可能使用 document.getElementById 獲取,避免使用document.all

[建議] 對於多個元素的集合,儘可能使用 context.getElementsByTagName 獲取。其中 context 可以為 document 或其他元素。指定 tagName 引數為 * 可以獲得所有子元素。

[建議] 遍歷元素集合時,儘量快取集合長度。如需多次操作同一集合,則應將集合轉為陣列。

解釋:原生獲取元素集合的結果並不直接引用 DOM 元素,而是對索引進行讀取,所以 DOM 結構的改變會實時反映到結果中。

<div></div>
<span></span>

<script>
var elements = document.getElementsByTagName('*');

// 顯示為 DIV
alert(elements[0].tagName);

var div = elements[0];
var p = document.createElement('p');
document.body.insertBefore(p, div);

// 顯示為 P
alert(elements[0].tagName);
</script>

[建議] 獲取元素的直接子元素時使用 children。避免使用childNodes,除非預期是需要包含文字、註釋和屬性型別的節點。

4.2.2 樣式獲取

[建議] 獲取元素實際樣式資訊時,應使用 getComputedStylecurrentStyle

解釋:通過 style 只能獲得內聯定義或通過 JavaScript 直接設定的樣式。通過 CSS class 設定的元素樣式無法直接通過 style 獲取。

4.2.3 樣式設定

[建議] 儘可能通過為元素新增預定義的 className 來改變元素樣式,避免直接操作 style 設定。

[強制] 通過 style 物件設定元素樣式時,對於帶單位非 0 值的屬性,不允許省略單位。

解釋:除了 IE,標準瀏覽器會忽略不規範的屬性值,導致相容性問題。

4.2.4 DOM 操作

[建議] 操作 DOM 時,儘量減少頁面 reflow

解釋:頁面 reflow 是非常耗時的行為,非常容易導致效能瓶頸。下面一些場景會觸發瀏覽器的reflow:

  • DOM元素的新增、修改(內容)、刪除。
  • 應用新的樣式或者修改任何影響元素佈局的屬性。
  • Resize瀏覽器視窗、滾動頁面。
  • 讀取元素的某些屬性(offsetLeft、offsetTop、offsetHeight、offsetWidth、scrollTop/Left/Width/Height、clientTop/Left/Width/Height、getComputedStyle()、currentStyle(in IE)) 。

[建議] 儘量減少 DOM 操作。

解釋:DOM 操作也是非常耗時的一種操作,減少 DOM 操作有助於提高效能。舉一個簡單的例子,構建一個列表。我們可以用兩種方式:

  1. 在迴圈體中 createElement 並 append 到父元素中。
  2. 在迴圈體中拼接 HTML 字串,迴圈結束後寫父元素的 innerHTML。

第一種方法看起來比較標準,但是每次迴圈都會對 DOM 進行操作,效能極低。