JavaScript 代码规范

1 0

  1.1 规范等级

  1.2 标记

2 代码风格

  2.1 文件

  2.2 缩进和空格

  2.3 换行

  2.4 语句

  2.5 命名

  2.6 注释

    2.6.1 单行注释

    2.6.2 文档注释

3 语言特性

  3.1 变量

  3.2 对象

  3.3 字符串

  3.4 数组

  3.5 流程控制

    3.5.1 条件

    3.5.2 类型检测

    3.5.3 循环

  3.6 函数

  3.7 类

  3.8 模块

  3.9 异步

4 jQuery

5 AngularJS

  5.1 文件

  5.2 Module

  5.3 Inject

  5.4 Controller

  5.5 Directive/Component

  5.6 Provider/Service/Factory

1 0

1.1 规范等级

每一条规范都有一个等级,说明该条规范的执行级别。规范等级有以下几种(按照等级排序):

1.2 标记

规范中有一些斜体标识的标记,各标记有含义如下:

A:AngularJS 相关规范。

E:该规范会在 .editorconfig 中体现。

6:该条目仅在 ES6 环境下有效,其他环境可以忽略。

2 代码风格

2.1 文件

这部分内容需要结合 Angular JS 部分一并阅读。

[强制] 每个文件中只存在一个可以向外公开的部分。

[强制] 文件使用 camelCase 的方式命名。

[强制] A 文件以文件中公开的部分调用方式命名。

解释:在使用 AngularJS 后,文件名与包含的 AngularJS 概念的调用方式一致,简单的说,即在文件中定义的部分在写代码时如何引入和使用,文件名称就是什么。具体请参考下面示例。

|-- js
|---- ProjectListController.js // 包含 Controller 定义:ProjectListController
|---- services
|------ projectService.js // 包含 Service 定义:projectService
|---- directives
|------ channel-select.js // 包含 Directive 定义:channelSelect
|---- filters
|------ toNo.js // 包含 Filter 定义:toNo

[强制] 如果文件中存放的是类,文件与类名同名。

①:对外公开的部分包括 Controller, Service, Directive, Component 等

// good
|-- js
|---- projectManage.js // angular controller
|---- datas
|------ User.js // export class

2.2 缩进和空格

[强制] E 使用 4空格缩进。

// good
function foo(options) {
    if (!options) {
        options = {};
    }
}

// bad
function baz(options) {
if (!options) {
    options = {};
}
}

[强制] if, else, for, while, function, switch, try, catch, finally, function 关键字后必须有一个空格。

// good
if (!options) {
    options = {};
}

// bad
if(options) {
    options = {};
}

[强制] 用作代码块起始的 { 前必须有一个空格。

// good
if (!options) {
    options = {};
}

// bad
if (options){
    options = {};
}

[强制] 创建对象时,: 之后必须有一个空格,: 之前不允许有空格。

// good
var john = {
    name: 'John'
};

// bad
var mike = {
    name:'Mike',
    age : 5
};

[强制] 函数声明和调用时,函数名和 ( 之间不允许有空格。

// good
function foo() {

}

// bad
function foo () {

}

[强制] ,; 之前不允许有空格;,; 如果不位于行尾,之后必须有空格。

// good
for (var i = 0; i < arr.length; i++) {

}
foo(10, 15);

// bad
for (var i = 0;i < arr.length;i++) {

}
foo(10,15);

[强制] ()[] 内紧贴括号的部分不允许有空格。

[强制] 6 解构时,{}[] 内紧贴括号的部分有一个空格。

// good
var arr = [1, 2, 3, 4, 5];

const { selectedProject } = options;
const [ , fileName ] = result;

// bad
var arr = [ 1, 2, 3, 4, 5 ];
const {selectedProject} = options;
const [filePath] = result;

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

[强制] 单行声明对象时,{} 内紧贴括号的部分必须有空格。

解释: 如果对象的值比较简单,可以使用单行声明,建议都使用多行声明对象。

// good
var obj = { enable: true };

// bad
var obj = {enable: true};

2.3 换行

[强制] E 使用 LF 作为行结束符。

[强制] E 文件以一个空行结束。

[强制] 在每个语句结束后换行。

// good
var age = 5;
var name = 'John';

// bad
var age = 5; var name = 'John';

[强制] S 每行不超过120个字符。

[建议] 不同的逻辑块之间使用换行隔开。

[强制] 在运算符处换行时,运算符必须在新行起始。

// good
if (user.isLogin()
    && user.isEnable()
    && user.hasRole('manage.role')) {

}

// bad
if (user.isLogin &&
    user.isEnable() &&
    user.hasRole('manage.role')) {

}

[强制] 函数声明(表达式)、函数调用、对象(数组)创建,for 等场景换行时,,; 不允许在新行起始。

// good
render(
    template,
    data1, data2, data3
);

// bad
render(
    template
    , data1, data2, data3
);

[强制] if, else, try, catch, finally 代码块的 } 之后换行。

[强制] 6 解构多个变量时,超出每行长度限制时,每个解构的变量单独一行,对象解构保持格式。

// good
const { name, age } = john;
const {
    options: {
        xAxis,
        legend
    }
} = chartOptions;

// bad
const { config: { dataLoaded, theme } } = chartOptions;

[建议] 进行链式调用时,每个函数换行,确保每个链式调用的函数垂直对齐。

// good
functionReturnsPromise()
    .then(data => {

    })
    .catch(error => {

    });

// bad
functionReturnsPromise().then(data => {

}).catch(error => {

})

[建议] 调用函数时,如果参数列表过长可以换行,每个或每个类型参数一行。

// good
logToFile(
    functionReturnFilePaths(),
    date,
    author,
    triggerEvent
);

// bad
logToFile(functionReturnFilePaths(), data, author, triggerEvent);

2.4 语句

[强制] 每个语句结束后必须添加 ;

[强制] if, else, try, catch, finally 语句不能省略块。

// good
if (cod) {
    foo();
}

// bad
if (cod) foo()
if (cod)
    foo();

[强制] 6 类声明不允许添加 ;

[强制] 函数定义不允许添加 ;

// good
function foo() {

}

export { foo }

// bad
function baz() {

};
export { baz };
export default baz;

[强制] 立即执行的匿名函数(Immediately-Invoked Function Expression, IIFE)外添加 (),其他函数不允许添加 ()

2.5 命名

[强制] 变量 使用 camelCase

// good
var age = 5;

// bad
var Age = 5;

[强制] 常量 使用 UPPER_CASE(即全部字母大写,单词之间使用下划线)。

// good
var MAX_STUDENT_PER_CLASS = 10;

// bad
var Max_Student_Per_Class = 10;

[建议] 常量 放置在一个对象中,常量对象使用 PascalCase,每个 key 使用 PascalCase

// good
var Constants = {
    Env: 'production',
    MaxProject: 5
};

// bad
var constants = {
    ENV: 'production',
    MAX_PROJECT: 5
};

[强制] 函数使用 camelCase,函数的参数使用 camelCase

// good
function doSomething(primary) {

}

// bad
function DoSomething(Primary) {

}

[强制] 类和构造函数使用 PascalCase

// good
function Person() {

}

// bad
function person() {
    // person is a constructor
}

[强制] 类的方法、属性使用 camelCase

// good
Person.prototype.sayHello = function() {

};

// bad
person.prototype.SayHello = function() {

};

[强制] 由多个单词组成的缩写词,在大小写上应该保持一致。

// good
function makeRequest(httpService) {
    
}

// bad
function makeRequest(hTTPService) {

}

[建议] boolean 类型的变量,以 hasis 开头。

// good
var isLoading = true;

// bad
var loading = false;

2.6 注释

2.6.1 单行注释

[强制] 注释必须位于独立的行,注释在代码上方。

// good
// options not null
if (options) {

}

// bad
if (options) { // options not null

}

[建议] 如果修改了非自己维护的代码,可以在改动的代码起始和结束位置注明改动日期和改动原因。

// 2017-03-21 WheelJS 新增一个判断逻辑,避免返回意外的值
if (condition
    && anotherCondition) {
    
}

2.6.2 文档注释

[建议] 在可复用代码中编写文档注释。

解释: 使用基于 jsdoc, ngdoc 的文档格式,编写文档注释,可以直接生成 API 文档。

/**
* 自动渲染模板,参数可以改变模板的行为。
*
* @param {object} options 模板配置项
* @param {boolean} [options.domDiff=false] 是否开启 virtual dom,默认 false
* @param {object} options.data 渲染模板使用的数据
* 
* @return Promise 渲染模板结束后会 resolve,遇到错误时 reject
*/
function templateAutoRender(options) {

}

3 语言特性

3.1 变量

[强制] 使用 var 声明变量,变量必须先声明后使用。

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

[建议] 变量声明在使用变量的代码附近。

解释:变量在真正使用前声明即可,在全局和函数开始时声明变量不利于查找。

// good
function object2List(source) {
    var list = [];

    for (var key in source) {
        if (source.hasOwnProperty(key)) {
            var item = {
                key: key,
                value: source[key]
            };
            list.push(item);
        }
    }

    return list;
}

// bad
function object2List(source) {
    var list = [],
        key, item;

    for (key in source) {
        if (source.hasOwnProperty(key)) {
            item = {
                key: key,
                value: source[key]
            };
            list.push(item;)
        }
    }
}

[建议] 6 ES6 环境中,不使用 var 声明变量。

解释:ES6 引入了 letconst 声明变量,使用 letconst 可以避免作用域等问题,所有使用 var 的场景理论上都可以使用 let 代替。如果变量声明后不需要重新赋值,使用 const

3.2 对象

[强制] 使用对象字面量声明对象。

// good
var john = {
    name: 'John',
    age: 27
};

// bad
var mike = new Object();
mike.name = 'Mike';
mike.age = 25;

[强制] 声明对象时,如果有一个属性名需要添加 ',则所有属性名都添加 ',否则所有属性都不添加 '

// good
var john = {
    'name': 'John',
    'age': 27,
    'class-name': 'Class 1'
};

// bad
var mike = {
    name: 'Mike',
    age: 25,
    'class-name': 'Class 3'
};

[强制] 声明对象时,如果有一个属性不指向同名变量,则所有属性都不使用缩写,否则所有属性使用缩写。

// good
const john = {
    name,
    age,
    gender
};

const mike = {
    name: name,
    age: age,
    gender: 1
};

// bad
const mike = {
    name,
    age,
    gender: 1
};

[强制] 对象的最后一个属性值之后不允许添加 ,

解释:现阶段部分浏览器支持在对象最后一个属性值之后添加 ,,但部分老旧浏览会报错。在找到解决方案前,不建议在最后添加 ,

[强制] 使用 for in 遍历对象的属性时,使用 hasOwnProperty 过滤非对象本身的属性。

解释:使用 for in 遍历对象时,原型上的属性名也会出现,使用 hasOwnProperty 可以判断,可以过滤掉原型上的属性。也可以使用 Object.keys 避免此问题。

// good
function Person(name) {
    this.name = name;
}
Person.prototype.instanceFrom = 'Person';
var john = new Person('John');
for (var key in john) {
    if (john.hasOwnProperty(key)) {
        console.log(key);
    }
}

// good
Object.keys(john).forEach(key => console.log(key));

// bad
function Person(name) {
    this.name = name;
}
Person.prototype.instanceFrom = 'Person';
var john = new Person('John');
for (var key in john) {
    console.log(key);
}

[强制] 禁止覆盖内置对象的方法。

[建议] 6 使用 Object.keys 遍历对象。

3.3 字符串

[强制] 使用 ' 声明字符串字面量,使用 ` 声明 ES6 模板字符串。

[建议] 使用数组拼接 HTML 字符串。

解释:使用数组拼接 HTML 字符串可以保持 HTML 的缩进结构。

// good
var htmlContent = [
    '<ul>',
        '<li>',
            '<a>Tab 1</a>',
        '</li>',
        '<li>',
            '<a>Tab 2</a>',
        '</li>',
    '</ul>'
].join('');

// bad
var htmlContent = '<ul><li><a>Tab 1</a></li><li><a>Tab 2</a></li></ul>';

3.4 数组

[强制] 使用数组字面量声明数组。

解释:使用 new Array() 时传入的参数会有歧义,使用字面量避免该问题。

new Array(3, 4, 5)
// [3, 4, 5]

new Array(3)
// [undefined, undefined, undefined];

[强制] 使用 Array.from 将伪数组转换为真实数组。

[建议] 使用数组提供的 API 完成对数组的操作。

解释:ES6 引入的新的数组 API 相比 for 循环更加直接,使用数组 API 可以提高代码可读性,更加直观。

// good
const selectedItem = list.filter(x => x._selected);

// bad
const selectedItem = [];
for (let i = 0; i < list.length; i++) {
    const item = list[i];
    if (item._selected) {
        selectedItem.push(item);
    }
}

3.5 流程控制

[建议] 使用 === 比较相等,使用 == null 判断为 nullundefined

解释:使用 === 避免隐式类型转换。

3.5.1 条件

[建议] 使用简洁的表达式。

解释:因为 JavaScript 的特性,在部分判断时可以使用简洁表达式,但部分场景使用简洁表达式可能会导致可读性降低。

[建议] 如果 else 之后不再有语句,可以删除 else

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

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

3.5.2 类型检测

[强制] 使用 typeof 检测基础数据类型,使用 instanceof 判断对象类型,检测 nullundefined 时使用 == null

解释:检测一个变量是否为 number, string 等基础类型时,使用 typeof;检测是否为某个类型的实例时使用 instanceof

[强制] 使用 typeof 检测类型时,必须使用 ===,右侧的类型字符串不能出现意外值。

var age = 5;
if (typeof age === 'number') {

}

3.5.3 循环

[建议] 在循环内部使用一个外部获取的值时,在循环外使用变量缓存。

// good
var width = wrap.offsetWidth + 'px';
var len = elements.length;
for (var i = 0; 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';
    // ......
}

3.6 函数

[强制] 6 箭头函数只有一个参数,并且不需要解构时,参数列表必须不添加括号。

[建议] 6 箭头函数函数体只有一个表达式,且作为返回值时,可以省略 {}return

[建议] 6 箭头函数函数体直接返回一个对象字面量时,可以省略 return 并使用 () 包裹。

// good
const getServerUrl = env => {

};

const selectedItems = displayList.filter(x => x.selected);

const getModalOptions = () => ({
    backdrop: 'static',
    keyboard: false
});

// bad
const sayHello = (name) => {
    console.log('Hello, my name is ' + name + '.');
};

[强制] 使用参数默认值代替条件判断默认值。

// good
function open(url, fileName = url) {

}

// bad
function open(url, fileName) {
    if (!fileName) {
        fileName = url;
    }
}

[强制] 使用 ...args 代替 arguments

解释:使用 ...args 获取可变长度的参数。

// good
function sum(...numbers) {
    return numbers.reduce((prevVal, x) => prevVal + x, 0);
}

// bad
function sum() {
    return Array.from(arguments).reduce((prevVal, x) => prevVal + x, 0);
}

[建议] 非数据输入参数使用 options 统一传递。

解释:一个函数的参数数量过多会导致参数顺序不好记忆,参数意义不直观等问题。使用 options 传递非数据类型,在方法传递时可以根据配置对象的 key 了解参数的意义,以及不需要记忆参数顺序。

3.7 类

[强制] 使用 class 定义类。

[强制] 使用 super 访问父类。

3.8 模块

[强制] 所有 import 写在模块开始处。

[强制] export 与内容定义放在一起。

// good
export function foo() {

}
export const bar = 3

// bad
function foo() {

}
const bar = 3;

export { foo, bar }

3.9 异步

[建议] 使用 Promise 代替 callback

解释:Promise 可以在大多数场景下有效避免 callback hell

[建议] 使用 BluebirdPromise.promisifyAll 方法处理 Node 标准库。

解释:在使用较多 Node 标准库(含有 callback 参数)的方法时,可以使用 Promise.promisifyAll 方法对标准库命名空间进行处理,处理后可以获得标准库方法的 Promise 版本。

import fs from 'fs';

// good
function readFileSafe(path) {
    return new Promise((resolve, reject) => {
        fs.stat(path, (err, status) => {
            if (err) {
                reject(err);
            }
            fs.readFile(path, (err, content) => {
                if (err) {
                    reject(err);
                }
                resolve(content);
            });
        });
    });
}

Promise.promifyAll(fs);
function readFileSafe(path) {
    fs.statAsync(path)
        .then(status => {
            return fs.readFileAsync(path);
        })
        .then(content => {
            console.log(content);
            return content;
        })
        .catch(err => {
            console.error(err);
        });
}

// bad
function readFileSafe(path) {
    fs.stat(path, (err, status) => {
        fs.readFile(path, (err, content) => {
            if (err) {
                throw err;
            }
            console.log(content);
        });
    });
}

[强制] 使用标准 Promise API。

解释:使用标准 Promise API,可以在运行环境都支持时,直接将 shim 去掉。在 Angular 1 环境中可以使用 $q

[强制] 运行环境中没有 Promise 时,将 Promise 时实现 shim 到 global 中。

4 jQuery

5 AngularJS

[强制] 按照变量声明、初始化回调、事件监听、函数的顺序组织 Controller, Directive, Component

// good
angular.module('myApp')
    .controller('TestController', function($scope) {
        $scope.queryParams = {};
        $scope.userList = [];
        $scope.isLoading = false;

        this.$onInit = function() {
            $scope.query();
        };

        $scope.$watch('queryParams', function(newVal, oldVal) {

        }, true);

        $scope.$on('change.ktDict', function() {

        });

        $scope.query = function() {

        };
    });

5.1 文件

[建议] 每个文件中只包含一个概念或逻辑单元。

“概念或逻辑单元”指的是:Directive 及其 ControllerService 和对应的 ProviderFilter视图 Controller 等。

解释:单一文件中内容过多时,代码可读性会降低。按照单元进行分割,保证了组件的完整性。注意:视图 Controller 和视图中弹框的 Controller 可能不为同一逻辑单元,因为弹框可能会被多个视图复用。

[建议] 公共常量在同一个文件中维护。组件使用的常量在组件文件中维护。

[建议] 组件的声明和配置使用单独的文件。

5.2 Module

[强制] 不使用变量缓存 angular.module() 的执行结果,直接进行链式调用。

// good
angular.module('myApp', [])
    .config(function($httpProvider) {

    });

// bad
var myApp = angular.module('myApp', []);
myApp.config(function($httpProvider) {

});

[建议] 声明 Module 的依赖时,优先声明 angular 官方依赖,再按照使用频率倒序进行排列。

5.3 Inject

[建议] 使用字符串数组声明依赖项。

解释:使用字符串数组指明依赖项,可以不依赖构造函数中参数的名称,方便的进行代码压缩和混淆。但要注意的是,需要高度关注依赖项数组和函数参数的位置

// good
angular.module('myApp')
    .controller('TodoListController', [
        '$scope', 
        'toastrService', 
        'todoListService', 
        function(scope, toastrService, todoListService) {

        }
    ]);

// bad
angular.module('myApp')
    .controller('TodoListController', function($scope, toastrService, todoListService) {

    });
angular.module('myApp')
    .controller('TodoListController', [
        '$scope', 
        '$window', 
        'toastrService', 
        'todoListService',
        function($scope, toastrService, todoListService) {

        }
    ])

[强制] 在项目配置使用了 ng-annotate 或类似插件时,使用插件支持的方式声明依赖项。

解释:使用插件支持的方式声明可以统一团队风格,在工作流中结合的插件不会影响代码的压缩和合并,且一般情况下不会出现错误。

// good
angular.module('myApp')
    .controller('TodoListController', function($scope, toastrService, todoListService) {
        'ngInject';
    });

[建议] 声明依赖项时,按照内置、第三方、自定义、常量的顺序声明。

// good
angular.module('myApp')
    .controller('DemoController', function($scope, toastrService, todoListService) {

    });

5.4 Controller

[建议] Controller 使用 PascalCase,并以 Controller 结尾。

// good
angular.module('myApp')
    .controller('TodoListController', function() {

    });

// bad
angular.module('myApp')
    .controller('TodoListCtrl', function() {

    });

[建议] 可复用弹框的 Controller 单独注册。

[建议] Controller 只编写与视图直接相关的逻辑。

解释:Controller 大部分情况下与视图紧密耦合,一些非视图逻辑可能在当前或未来是需要复用的。如:http 请求等不应该混杂在 Controller 代码中。

5.5 Directive/Component

[建议] Directive 或 Component 使用的 Controller 单独注册。

// good
angular.module('myApp')
    .controller('Ido85PagerController', function(ido85PagerConfig) {

    })
    .directive('ido85Pager', {
        restrict: 'A',
        controller: 'Ido85PagerController',
        templateUrl: 'ido85/pager.html'
    });

// bad
angular.module('myApp')
    .directive('ido85Pager', {
        restrict: 'A',
        controller: function(ido85PagerConfig) {

        },
        templateUrl: 'ido85/pager.html'
    });

// bad
function Ido85PagerController = function(ido85PagerConfig) {

}
angular.module('myApp')
    .directive('ido85Pager', {
        restrict: 'A',
        controller: Ido85PagerController,
        templateUrl: 'ido85/pager.html'
    });

5.6 Provider/Service/Factory

[强制] 允许通过配置改变行为的 Service,使用 .provider 方法注册。

通过定义 $get 方法使 Injector 获取真正的 Service。

解释:使用 .provider 注册的 Service,可以在 .config 函数中通过 ServiceNameProvider 获取到其本身,之后可以修改默认配置,调用配置方法。

angular.module('myApp')
    .provider('dataIsland', function() {
        this.selector = '[data-island]';

        this.$get = function($document) {

        };
    });