zoukankan      html  css  js  c++  java
  • js编码规范

    1 前言

    JavaScript在百度一直有着广泛的应用,特别是在浏览器端的行为管理。本文档的目标是使JavaScript代码风格保持一致,容易被理解和被维护。

    虽然本文档是针对JavaScript设计的,但是在使用各种JavaScript的预编译语言时(如TypeScript等)时,适用的部分也应尽量遵循本文档的约定。

    2 代码风格

    1. 结构

    • [强制] 使用 4 个空格做为一个缩进层级,不允许使用 2 个空格 或 tab 字符。
      
    • [强制] switch 下的 case 和 default 必须增加一个缩进层级。
      

    2. 空格

    • [强制] 二元运算符两侧必须有一个空格,一元运算符与操作对象之间不允许有空格。
      

    示例:

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

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

    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语句等场景中,不允许在 , 或 ; 前换行。

    示例:

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

    4. 语句

    [强制] 不得省略语句结束的分号。

    [强制] 在 if / else / for / do / while 语句中,即使只有一行,也不得省略块 {...}。

    示例:

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

    [强制] 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 () {
    });
    

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

    2. 条件

    • 对象 被计算为 true
    • Undefined 被计算为 false
    • Null 被计算为 false
    • 布尔值 被计算为 布尔的值
    • 数字 如果是 +0、-0 或 NaN 被计算为 false,否则为 true
    • 字符串 如果是空字符串 '' 被计算为 false,否则为 true

    [强制] 在 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.循环

    [建议] 不要在循环体中包含函数表达式,事先将函数提取到循环体外。

    解释:

    循环体中的函数表达式,运行过程中会生成循环次数个函数对象。

    示例:

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

    4.块

    • 使用大括号包裹所有的多行代码块。
    /bad
    if (test)
      return false;
    
    // good
    if (test) return false;
    
    // good
    if (test) {
      return false;
    }
    
    // bad
    function () { return false; }
    
    // good
    function () {
      return false;
    }
    
    • 如果通过 if 和 else 使用多行代码块,把 else 放在 if 代码块关闭括号的同一行。
    // bad
    if (test) {
      thing1();
      thing2();
    }
    else {
      thing3();
    }
    
    // good
    if (test) {
      thing1();
      thing2();
    } else {
      thing3();
    }
    

    4. 类型

    4.1 类型检测

    [建议] 类型检测优先使用 typeof。对象类型检测使用 instanceof。null 或 undefined 的检测使用 == 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'
    

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

    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: 体积较大,性能一般,模板复用性高。

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

    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 方法。以下两种场景可以自己实现排序:

    需要稳定的排序算法,达到严格一致的排序结果。
    数据特点鲜明,适合使用桶排。
    [建议] 清空数组使用 .length = 0。

    8. 函数

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

    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 对象只需判断属性是否存在,实现得以简化。

    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),则:
    • 如果是 V8 引擎,则退出全过程。
    • 如果是 SpiderMonkey,则处理该 ObjectEnvironment 的外层 LexicalEnvironment。
    1. 获取当前 LexicalEnvironment 下的所有类型为 Function 的对象,对于每一个 Function 对象,分析其 FunctionBody:
    • 如果 FunctionBody 中含有 直接调用eval,则退出全过程。
    • 否则得到所有的 Identifier。
    • 对于每一个 Identifier,设其为 name,根据查找变量引用的规则,从 LexicalEnvironment 中找出名称为 name 的绑定 binding。
    • 对 binding 添加 notSwap 属性,其值为 true。
    1. 获取当前 LexicalEnvironment 下的所有类型为 Function 的对象,对于每一个 Function 对象,分析其 FunctionBody
    • 如果是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]();
    }
    

    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;
    

    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. 扩展方便,未来添加事件信息时,无需考虑会破坏监听器参数形式而无法向后兼容。

    [建议] 设计自定义事件时,应考虑禁止默认行为。

    解释:

    常见禁止默认行为的方式有两种:

    • 事件监听函数中 return false。
    • 事件对象中包含禁止默认行为的方法,如 preventDefault。

    10. 动态特性

    10.1 eval

    [强制] 避免使用直接 eval 函数。

    解释:

    直接 eval,指的是以函数方式调用 eval 的调用方法。直接 eval 调用执行代码的作用域为本地作用域,应当避免。

    如果有特殊情况需要使用直接 eval,需在代码中用详细的注释说明为何必须使用直接 eval,不能使用其它动态执行代码的方式,同时需要其他资深工程师进行 Code Review。

    [建议] 尽量避免使用 eval 函数。

    10.2 动态执行代码

    [建议] 使用 new Function 执行动态代码。

    解释:

    通过 new Function 生成的函数作用域是全局使用域,不会影响当当前的本地作用域。如果有动态代码执行的需求,建议使用 new Function。

    示例:

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

    10.3 with

    [建议] 尽量不要使用 with。

    解释:

    使用 with 可能会增加代码的复杂度,不利于阅读和管理;也会对性能有影响。大多数使用 with 的场景都能使用其他方式较好的替代。所以,尽量不要使用 with。

    10.4 delete

    [建议] 减少 delete 的使用。
    解释:

    如果没有特别的需求,减少或避免使用delete。delete的使用会破坏部分 JavaScript 引擎的性能优化。

    [建议] 处理 delete 可能产生的异常。
    解释:

    对于有被遍历需求,且值 null 被认为具有业务逻辑意义的值的对象,移除某个属性必须使用 delete 操作。

    在严格模式或IE下使用 delete 时,不能被删除的属性会抛出异常,因此在不确定属性是否可以删除的情况下,建议添加 try-catch 块。

    示例:

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

    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 等手段将自身维护的对象与外部传入的分离,保证不会相互影响。

    [建议] 具备强类型的设计。

    解释:

    1. 如果一个属性被设计为 boolean 类型,则不要使用 1 / 0 作为其值。对于标识性的属性,如对代码体积有严格要求,可以从一开始就设计为 number 类型且将 0 作为否定值。
    2. 从 DOM 中取出的值通常为 string 类型,如果有对象或函数的接收类型为 number 类型,提前作好转换,而不是期望对象、函数可以处理多类型的值。

    4.浏览器环境

    4.1 模块化

    4.1.1 AMD

    [强制] 使用 AMD 作为模块定义。

    解释:

    AMD 作为由社区认可的模块定义形式,提供多种重载提供灵活的使用方式,并且绝大多数优秀的 Library 都支持 AMD,适合作为规范。

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

    [强制] 模块 id 必须符合标准。

    解释:

    模块 id 必须符合以下约束条件:

    • 类型为 string,并且是由 / 分割的一系列 terms 来组成。例如:this/is/a/module。
    • term 应该符合 [a-zA-Z0-9_-]+ 规则。
    • 不应该有 .js 后缀。
    • 跟文件的路径保持一致。

    4.1.2 define

    [建议] 定义模块时不要指明 id 和 dependencies。

    解释:

    在 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 样式获取

    [建议] 获取元素实际样式信息时,应使用 getComputedStyle 或 currentStyle。

    解释:

    通过 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 操作有助于提高性能。举一个简单的例子,构建一个列表。我们可以用两种方式:

    • 在循环体中 createElement 并 append 到父元素中。
    • 在循环体中拼接 HTML 字符串,循环结束后写父元素的 innerHTML。

    第一种方法看起来比较标准,但是每次循环都会对 DOM 进行操作,性能极低。在这里推荐使用第二种方法。

    4.2.5 DOM 事件

    [建议] 优先使用 addEventListener / attachEvent 绑定事件,避免直接在 HTML 属性中或 DOM 的 expando 属性绑定事件处理。

    解释:

    expando 属性绑定事件容易导致互相覆盖。

    [建议] 使用 addEventListener 时第三个参数使用 false。

    解释:

    标准浏览器中的 addEventListener 可以通过第三个参数指定两种时间触发模型:冒泡和捕获。而 IE 的 attachEvent 仅支持冒泡的事件触发。所以为了保持一致性,通常 addEventListener 的第三个参数都为 false。

    [建议] 在没有事件自动管理的框架支持下,应持有监听器函数的引用,在适当时候(元素释放、页面卸载等)移除添加的监听器。

  • 相关阅读:
    Maven 集成Tomcat插件
    dubbo 序列化 问题 属性值 丢失 ArrayList 解决
    docker 中安装 FastDFS 总结
    docker 从容器中拷文件到宿主机器中
    db2 相关命令
    Webphere WAS 启动
    CKEDITOR 4.6.X 版本 插件 弹出对话框 Dialog中 表格 Table 自定义样式Style 问题
    SpringMVC JSONP JSON支持
    CKEDITOR 3.4.2中 按钮事件中 动态改变图标和title 获取按钮
    git回退到远程某个版本
  • 原文地址:https://www.cnblogs.com/Hsong/p/9011234.html
Copyright © 2011-2022 走看看