WarpPrism / Blog

Do Epic Things
52 stars 7 forks source link

数说前端js代码规范 #18

Open WarpPrism opened 7 years ago

WarpPrism commented 7 years ago

文档说明

本文档的目标是使 JavaScript 代码风格保持一致,容易被理解和被维护。

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

1. 代码风格

1.1 文件

[建议] JavaScript 文件使用无 BOM 的 UTF-8 编码。

1.2 结构

1.2.1 缩进

[强制] 使用 4 个空格做为一个缩进层级,不允许使用 2 个空格 或 tab 字符。

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

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

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

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

[建议] 在语句的行长度超过 120 时,根据逻辑条件合理缩进。

示例:

// 按一定长度截断字符串,并使用 + 运算符进行连接。
// 分隔字符串尽量按语义进行,如不要在一个完整的名词中间断开。
// 特别的,对于 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('');

// 当函数调用时,如果有一个或以上参数跨越多行,应当每一个参数独立一行。
// 这通常出现在匿名函数或者对象初始化等作为参数时,如 `setTimeout` 函数等。
setTimeout(
    function () {
        alert('hello');
    },
    200
);

// 链式调用较长时采用缩进进行调整。
$('#items')
    .find('.selected')
    .highlight()
    .end();

// 三元运算符由3部分组成,因此其换行应当根据每个部分的长度不同,形成不同的情况。
var result = thisIsAVeryVeryLongCondition
    ? resultA : resultB;

var result = condition
    ? thisIsAVeryVeryLongResult
    : resultB;

1.2.4 语句

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

示例:

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

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

1.3 命名

[强制] 变量函数,函数的参数方法/属性 使用 Camel命名法。 示例:

var loadingModules = {};

function stringFormat(sourceString) {
}

[强制] 常量使用 全部字母大写,单词间下划线分隔 的命名方式。 示例:

var HTML_ENTITY = {};

[强制] 枚举变量 使用 Pascal命名法。枚举的属性 使用 全部字母大写,单词间下划线分隔 的命名方式。 示例:

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

1.4 注释

1.4.1 文档化注释

[建议] Sublime Text的同学,可以安装docBlkr文档编写插件。 [强制] 为了便于代码阅读和自文档化,以下内容必须包含以 /*.../ 形式的块注释中。

解释:

文件 namespace 类 函数或方法 类属性 事件 全局变量 常量 AMD 模块 [强制] 文档注释前必须空一行。

[建议] 自文档化的文档说明 what,而不是 how。

示例:

/**
 * 函数描述
 *
 * @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
    };
}

2 语言特征

2.1 变量

原则上不建议使用全局变量,对于已有的全局变量或第三方框架引入的全局变量,需要根据检查工具的语法标识。

[强制] 每个 var 只能声明一个变量。

解释:

一个 var 声明多个变量,容易导致较长的行长度,并且在修改时容易造成逗号和分号的混淆。

示例:

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

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

2.2 条件

[强制] 在 Equality Expression 中使用类型严格的 ===。仅当判断 null 或 undefined 时,允许使用 == null。

解释:

使用 === 可以避免等于判断中隐式的类型转换。

2.3 循环

[建议] 对有序集合进行遍历时,缓存 length。

解释:

虽然现代浏览器都对数组长度进行了缓存,但对于一些宿主对象和老旧浏览器的数组对象,在每次 length 访问时会动态计算元素个数,此时缓存 length 能有效提高程序性能。

示例:

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

2.4 类型

2.4.1 类型转换

[建议] 转换成 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);

2.5 字符串

[强制] 字符串开头和结束使用单引号 '。

解释:

输入单引号不需要按住 shift,方便输入。 实际使用中,字符串经常用来拼接 HTML。为方便 HTML 中包含双引号而不需要转义写法。 示例:

var str = '我是一个字符串';
var html = '<div class="cls">拼接HTML可以省去双引号转义</div>';

2.6 动态特征

2.6.1 eval

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

解释:

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

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

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

3 浏览器环境

3.1 模块化

3.1.1 AMD

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

解释:

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

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

官方实现的 requirejs

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

解释:

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

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

3.1.2 define

[建议] 定义模块时不要指明 iddependencies

解释:

在 AMD 的设计思想里,模块名称是和所在路径相关的,匿名的模块更利于封包和迁移。模块依赖应在模块定义内部通过 local require 引用。

所以,推荐使用 define(factory) 的形式进行模块定义。

示例:

define(
    function (require) {
    }
);

[建议] 使用 return 来返回模块定义。

解释:

使用 return 可以减少 factory 接收的参数(不需要接收 exportsmodule),在没有 AMD Loader 的场景下也更容易进行简单的处理来伪造一个 Loader。

示例:

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

        // ...

        return exports;
    }
);

3.1.3 require

[强制] 全局运行环境中,require 必须以 async require 形式调用。

解释:

模块的加载过程是异步的,同步调用并无法保证得到正确的结果。

示例:

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

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

3.1.4 UMD

如果你打算编写通用模块(支持Common.js、AMD等),你可能需要参考一下如何用UMD来包装你的模块:

UMD

3.2 DOM

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

在循环体中 createElementappend 到父元素中。 在循环体中拼接 HTML 字符串,循环结束后写父元素的 innerHTML。 第一种方法看起来比较标准,但是每次循环都会对 DOM 进行操作,性能极低。在这里推荐使用第二种方法。

liangtongzhuo commented 7 years ago

写的非常好, 最后尽量减少DOM操作,好难啊。
拼写HTML字符串 然后直接用innerHTML这个好

有一个问题, 读取元素的某些属性为什么会触发reflow?

WarpPrism commented 7 years ago

@hi363138911 Reflow(重流)的定义就是计算DOM节点在页面上展示的位置和空间等等。而读取offsetLeft/Top/Width/Height等属性,其背后进行的正是计算节点和其父节点位置,宽高等属性,所以会触发reflow,这是我的理解。