1. 程式人生 > >JavaScript編碼規範

JavaScript編碼規範

工具 自文檔化 註釋 函數參數 .proto 初始 好處 表現 read

2 代碼風格

2.1 文件

[建議] JavaScript 文件使用無 BOMUTF-8 編碼。

解釋:

UTF-8 編碼具有更廣泛的適應性。BOM 在使用程序或工具處理文件時可能造成不必要的幹擾。

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

2.2 結構

2.2.1 縮進

[強制] 使用 4 個空格做為一個縮進層級,不允許使用 2 個空格 或 tab 字符。
[強制] switch 下的 casedefault 必須增加一個縮進層級。

2.2.2 空格

[強制] 二元運算符兩側必須有一個空格,一元運算符與操作對象之間不允許有空格。
[強制] 用作代碼塊起始的左花括號 {
前必須有一個空格。
[強制] if / else / for / while / function / switch / do / try / catch / finally 關鍵字後,必須有一個空格。
[強制] 在對象創建時,屬性中的 : 之後必須有空格,: 之前不允許有空格。
[強制] 函數聲明、具名函數表達式、函數調用中,函數名和 ( 之間不允許有空格。
[強制] ,; 前不允許有空格。如果不位於行尾,,; 後必須跟一個空格。
[強制] 在函數調用、函數聲明、括號表達式、屬性訪問、if / for / while / switch / catch 等語句中,()[] 內緊貼括號部分不允許有空格。
[強制] 單行聲明的數組與對象,如果包含元素,{}[] 內緊貼括號部分不允許包含空格。

聲明包含元素的數組與對象,只有當內部元素的形式較為簡單時,才允許寫在一行。元素復雜的情況,還是應該換行書寫。

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

2.2.3 換行

[強制] 每個獨立語句結束後必須換行。
[強制] 每行不得超過 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 語句等場景中,不允許在 ,; 前換行。
[建議] 不同行為或邏輯的語句集,使用空行隔開,更易閱讀。
[建議] 在語句的行長度超過 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 等語句,推薦使用在 } 號後添加一個換行 的風格,使代碼層次結構更清晰,閱讀性更好。

2.2.4 語句

[強制] 不得省略語句結束的分號。
[強制] 在 if / else / for / do / while 語句中,即使只有一行,也不得省略塊 {...}
[強制] 函數定義結束不允許添加分號。
[強制] IIFE 必須在函數表達式外添加 (,非 IIFE 不得在函數表達式外添加 (

IIFE = Immediately-Invoked Function Expression.

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

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

var func = function () {
};

2.3 命名

[強制] 變量 使用 Camel命名法
var loadingModules = {};
[強制] 常量 使用 全部字母大寫,單詞間下劃線分隔 的命名方式。
[強制] 函數 使用 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 類型的變量使用 is has 開頭。
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 文檔化註釋

[強制] 為了便於代碼閱讀和自文檔化,以下內容必須包含以 /**...*/ 形式的塊註釋中。
[強制] 文檔註釋前必須空一行。
[建議] 自文檔化的文檔說明 what,而不是 how。

2.4.4 類型定義

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

解釋:

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

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

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

2.4.5 文件註釋

[強制] 文件頂部必須包含文件註釋,用 @file 標識文件說明。
/**
 * @file Describe the file
 */
[建議] 文件註釋中可以用 @author 標識開發者信息。

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
        }
    }
);

.....部分省略

3 語言特性

3.1 變量

[強制] 變量、函數在使用前必須先定義。

不通過 var 定義變量將導致變量汙染全局環境。

原則上不建議使用全局變量,對於已有的全局變量或第三方框架引入的全局變量,需要根據檢查工具的語法標識。

[強制] 每個 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 中使用類型嚴格的 ===。僅當判斷 nullundefined 時,允許使用 == null

使用 === 可以避免等於判斷中隱式的類型轉換。

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

// bad
if (age == 30) {
    // ......
}
[建議] 盡可能使用簡潔的表達式。
// 字符串為空

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

// bad
if (name === ‘‘) {
    // ......
}
[建議] 按執行頻率排列分支的順序。

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

  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

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>‘;
[建議] 使用字符串拼接的方式生成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>‘;
[建議] 復雜的數據到視圖字符串的轉換過程,選用一種模板引擎。

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

  1. 在開發過程中專註於數據,將視圖生成的過程由另外一個層級維護,使程序邏輯結構更清晰。
  2. 優秀的模板引擎,通過模板編譯技術和高質量的編譯產物,能獲得比手工拼接字符串更高的性能。
  3. 模板引擎能方便的對動態數據進行相應的轉義,部分模板引擎默認進行HTML轉義,安全性更好。
  • 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] 方式訪問。

[建議] 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‘];

// 這裏僅作演示, 實際中應使用 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 方法。以下兩種場景可以自己實現排序:

  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 = ‘‘;
        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();
    }
}

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 中的所有變量綁定,造成一定的內存消耗。

3.8.4 空函數

[建議] 空函數不使用 new Function() 的形式。
var emptyFunction = function () {};
[建議] 對於性能有高要求的場合,建議存在一個空函數的常量,供多處使用共享。

由於對閉包內變量有回收優化策略的 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 的模式進行。

如果有 非常龐大 的對象,且預計會在 老舊的引擎 中執行,則使用閉包時,註意將閉包不需要的對象置為空引用。

JavaScript編碼規範