JavaScript语言通用编程规范

目的

规则并不是完美的,通过禁止在特定情况下有用的特性,可能会对代码实现造成影响。但是我们制定规则的目的“为了大多数程序员可以得到更多的好处”, 如果在团队运作中认为某个规则无法遵循,希望可以共同改进该规则。 参考该规范之前,希望您具有相应的JavaScript语言基础能力,而不是通过该文档来学习JavaScript语言。

总体原则

代码需要在保证功能正确的前提下,满足可读、可维护、安全、可靠、可测试、高效、可移植的特征要求。

约定

规则:编程时必须遵守的约定
建议:编程时必须加以考虑的约定

无论是“规则”还是“建议”,都必须理解该条目这么规定的原因,并努力遵守。

例外

在不违背总体原则,经过充分考虑,有充足的理由的前提下,可以适当违背规范中约定。
例外破坏了代码的一致性,请尽量避免。“规则”的例外应该是极少的。

下列情况,应风格一致性原则优先:
修改外部开源代码、第三方代码时,应该遵守开源代码、第三方代码已有规范,保持风格统一。

编程规范

命名规范

规则1.1 须使用正确的英文拼写进行命名,禁止使用拼音拼写。

反例:xingmingzhanghao

正例:usernameaccount

规则1.2 命名尽量少用缩写,除非是常见词或者业务线的领域词汇。比如:context可以简写成ctxrequest可简写成reqresponse可简写成resp

说明:完整的单词拼写可以避免不必要的阅读障碍。

例外:循环语种中可以使用ij循环条件变量名。

规则1.3 类名、枚举名、命名空间名采用upperCamelCase风格。

正例:

  1. // 类名
  2. classUser{
  3. constructor(username){
  4. this.username = username;
  5. }
  6. sayHi(){
  7. console.log(`hi, ${this.username}`);
  8. }
  9. }
  10. // 枚举名
  11. constUserType={
  12. TEACHER:0,
  13. STUDENT:1
  14. };
  15. // 命名空间
  16. constBase64Utils={
  17. encrypt:function(text){
  18. // todo encrypt
  19. },
  20. decrypt:function(text){
  21. // todo decrypt
  22. }
  23. };

规则1.4 变量名、方法名、参数名采用lowerCamelCase风格。

正例:

  1. let msg ='Hello world';
  2. function sendMsg(msg){
  3. // todo send message
  4. }
  5. function findUser(userID){
  6. // todo find user by user ID
  7. }

规则1.5 静态常量名、枚举值名采用全部大写,单词间使用下划线隔开。

正例:

  1. const MAX_USER_SIZE =10000;
  2. constUserType={
  3. TEACHER:0,
  4. STUDENT:1
  5. };

建议1.6 避免使用否定的布尔变量名,布尔型的局部变量或方法须加上表达是非意义的前缀。

反例:

  1. let isNoError =true;
  2. let isNotFound =false;
  3. function empty(){}
  4. function next(){}

正例:

  1. let isError =false;
  2. let isFound =true;
  3. function isEmpty(){}
  4. function hasNext(){}

代码格式

规则2.1 采用2个空格缩进,禁止使用tab字符

说明:只允许使用空格(space)进行缩进,每次缩进为2个空格。不允许使用Tab 符进行缩进。

正例:

  1. const dataSource =[
  2. {
  3. id:1,
  4. title:'Title 1',
  5. content:'Content 1'
  6. },
  7. {
  8. id:2,
  9. title:'Title 2',
  10. content:'Content 2'
  11. }
  12. ];
  13. function render(container, dataSource){
  14. if(!container ||!dataSource ||!dataSource.length){
  15. returnvoid0;
  16. }
  17. const fragment = document.createDocumentFragment();
  18. for(let data of dataSource){
  19. if(!data ||!data.id ||!data.title ||!data.content){
  20. continue;
  21. }
  22. const element = document.createElement("div");
  23. const textNode = document.createTextNode(`${data.title}: ${data.content}`);
  24. element.appendChild(textNode);
  25. element.setAttribute("id", data.id);
  26. fragment.appendChild(element);
  27. }
  28. container.appendChild(fragment);
  29. }

规则2.2 行宽不超过120个字符

说明:建议每行字符数不要超过 120 个。如果超过120个字符,请选择合理的方式进行换行。

例外:如果一行注释包含了超过120 个字符的命令或URL,则可以保持一行,以方便复制、粘贴和通过grep查找; 预处理的 error 信息在一行便于阅读和理解,即使超过 120 个字符。

规则3.3 大括号的使用须遵循约定:

  1. 如果大括号内为空,则允许简写成{},且无需换行;
  2. 左大括号前不换行,括号后换行;
  3. 右大括号前换行,括号后还有elsecatch等情况下不换行,其他情况都换行。

规则3.4 条件语句和循环语句的实现必须使用大括号包裹,即使只有一条语句。

反例:

  1. if(condition)
  2. console.log('success');
  3. for(let idx =0; idx <5;++idx)
  4. console.log(idx);

正例:

  1. if(condition){
  2. console.log('success');
  3. }
  4. for(let idx =0; idx <5;++idx){
  5. console.log(idx);
  6. }

规则2.5 条件语句和循环语句都不允许写在同一行。

反例:

  1. if(condition){/* todo something */}else{/* todo other */}
  2. let idx =0;
  3. while(idx <10) console.log(idx);

正例:

  1. if(condition){
  2. /* todo something */
  3. }else{
  4. /* todo other */
  5. }
  6. let idx =0;
  7. while(idx <10){
  8. console.log(idx);
  9. }

规则2.6 switch语句的casedefault 须缩进一层。

正例:

  1. switch(condition){
  2. case0:
  3. doSomething();
  4. break;
  5. case1:{// 可以带括号也可以不带
  6. doOtherthing();
  7. break;
  8. }
  9. default:
  10. break;
  11. }

规则2.7 表达式换行须保持一致性,运算符放行末。

说明:较长的表达式,不满足行宽要求时,需要在适当的位置进行换行。一般在较低优先级运算符或连接符后面阶段,运算符或连接符放行末。运算符、连接符放在行末,表示“未结束,后续还有”。

正例:

  1. // 假设条件语句超出行宽
  2. if(userCount > MAX_USER_COUNT ||
  3. userCount < MIN_USER_COUNT){
  4. doSomething();
  5. }
  6. const sum =
  7. number1 +
  8. number2 +
  9. number3 +
  10. number4 +
  11. number5 +
  12. number6 +
  13. number7 +
  14. number8 +
  15. number9;

规则2.8 多个变量定义和赋值语句不允许写在一行。

反例:

  1. let maxCount =10, isCompleted =false;
  2. let pointX, pointY;
  3. pointX =10; pointY =0;

正例:

  1. let maxCount =10;
  2. let isCompleted =false;
  3. let pointX =0;
  4. let pointY =0;

规则2.9 空格应该突出关键字和重要信息,避免不必要的空格。

说明:空格可以减低代码密度,增加代码的可读性。总体规则如下:

  1. ifelseifelseswitchwhilefor等关键字之后加空格;
  2. 小括号内部两侧不加空格;
  3. 大括号内部两侧须加空格,但{}等简单场景例外;
  4. 多重括号之间不加空格;
  5. 一元操作符(&*+-!等)之后不加空格;
  6. 二元操作符(=+-*/%|&||&&<><=>===!====!==等)左右两侧加空格;
  7. 三元运算符(?:)的两侧添加空格;
  8. 前置或后置的自增、自减(++--)和变量之间不加空格;
  9. 逗号(,)前面不加空格,后面加空格;
  10. 单行注释//后加空格;
  11. 行尾不加空格。

规则2.10 表达式语句须以分号结尾。

反例:

  1. let username ='jack'
  2. let birthday ='1997-09-01'
  3. console.log(`${username}'s birthday is ${birthday}`)

正例:

  1. let username ='jack';
  2. let birthday ='1997-09-01';
  3. console.log(`${username}'s birthday is ${birthday}`);

建议2.11 优先使用单引号包裹字符串。

正例:

  1. let message ='wolrd';
  2. console.log(message);

代码规范

规则3.1 声明变量时须使用varletconst关键字进行声明,避免变量暴露到全局作用域上。

说明:不使用varletconst关键字声明变量,会导致将变量暴露到全局作用域上,这样可能会覆盖全局作用域上的同名变量,进而引发GC无法有效回收内存的问题;另外,当变量中包含敏感信息时,暴露到全局作用域可能会导致信息泄露问题。另外,尽量对所有的引用使用const,不要使用 var;如果你一定需要可变动的引用,使用 let 代替 var。因为constlet的作用域更小,写代码更容易控制;const可确保无法对引用重新赋值,const引用的指针不可变,重新赋值会报错,避免了不小心的重新赋值给覆盖了。

反例:

  1. function open(){
  2. url ='http://127.0.0.1:8080';//url会暴露到全局作用域上
  3. //todo something
  4. }
  5. open();
  6. console.log(url);//url可以被访问到,输出内容:http://127.0.0.1:8080

正例:

  1. // ES5.1使用var声明变量
  2. function open(){
  3. var url ='http://127.0.0.1:8080';
  4. // todo something
  5. }
  6. open();
  7. console.log(url);//报错:Uncaught ReferenceError: url is not defined
  1. // ES6中,优先推荐使用let和const关键字来声明变量
  2. function open(){
  3. const url ='http://127.0.0.1:8080';
  4. //todo something
  5. }
  6. open();
  7. console.log(url);//报错:Uncaught ReferenceError: url is not defined

规则3.2 函数块内须使用函数表达式声明函数

说明:虽然很多 JS 引擎都支持块内声明函数,但它不属于 ECMAScript规范,各个浏览器糟糕的实现相互不兼容,有些也与未来 ECMAScript草案相违背。另外,ECMAScript5不支持块作用域,对于所有的控制流都不是独立的作用域,其内部声明的变量或者函数都是在其父函数或者脚本的作用域中,导致块内函数或者变量的声明会存在覆盖现象。如果确实需要在块中定义函数,应该使用函数表达式来初始化。

反例:

  1. function bar(name){
  2. if(name ==="hotel"){
  3. // 1、定义一个foo函数,其作用域不是if代码块,而是bar函数作用域。
  4. function foo(){
  5. console.log("hotel foo A");
  6. }
  7. }else{
  8. // 2、再重复定义一次foo函数,覆盖上面if条件分支下的foo函数定义。
  9. function foo(){
  10. console.log("hotel foo 2");
  11. }
  12. }
  13. foo && foo();
  14. }
  15. bar("hotel");// 输出结果总是显示"hotel foo 2"

正例:

  1. function bar(name){
  2. var foo;
  3. if(name =="hotel"){
  4. foo =function(){
  5. console.log("hotel foo 1");
  6. };
  7. }else{
  8. foo =function(){
  9. console.log("hotel foo 2");
  10. }
  11. }
  12. foo && foo();
  13. }
  14. bar("hotel");// 正确输出"hotel foo 1"

规则3.3 禁止封装基本类型

说明:JavaScript中有五种基本数据类型:Undefined、Null、Boolean、Number和String。基本数据类型的值是不可更改的。JavaScript中使用基本数据类型对象只是值,不包含器封装对象的方法和属性,在不需要使用属性和方法的时候,不需要使用其封装类型。

反例:

  1. var isShow =newBoolean(false);
  2. if(isShow){
  3. alert('hi');//被执行,界面弹出显示:hi
  4. }

正例:

  1. var isShow =false;
  2. if(isShow){
  3. alert('hi');
  4. }

规则3.4 禁止使用with

说明:使用 with让你的代码在语义上变得不清晰,因为with的对象,可能会与局部变量产生冲突,从而改变程序原本的用义。

反例:

  1. var foo ={ x:5};
  2. with(foo){
  3. var x =3;
  4. console.log(x);//输出:5
  5. }

规则3.5 this仅可在对象构造函数、方法、闭包中使用

说明:在JavaScript里面,this指针代表的是执行当前代码的对象的所有者。this具有特殊的语义:

  • 全局对象(大多数情况下)
  • 调用者的作用域(使用eval时)
  • DOM 树中的节点(添加事件处理函数时)
  • 新创建的对象(使用一个构造器)
  • 其他对象(如果函数被 call() 或 apply())
  1. varUser=function(username){
  2. this.username = username;
  3. };
  4. var user =newUser('John');
  5. console.log(user.username);// 输出:John
  6. varClickCounter={
  7. value:0,
  8. click:function(){
  9. ++this.value;
  10. },
  11. getValue(){
  12. returnthis.value;
  13. }
  14. };
  15. console.log(Counter.getValue());//输出:0
  16. Counter.click();
  17. console.log(Counter.getValue());//输出:1

规则3.6 禁止使用IE下的条件注释

说明:在IE下使用\@cc_on语句或使用\@if\@set语句可以激活条件编译。尽管可以通过注释来兼容IE以外的浏览器,但它妨碍自动化工具的执行,因为在运行时,它们会改变JavaScript 语法树。

反例:

  1. var f =function(){
  2. /*@cc_on @*/
  3. /*@if (@_jscript_version >= 4)
  4. alert("JavaScript version 4 or better");
  5. @else @*/
  6. alert("Conditional compilation not supported by this scripting engine.");
  7. /*@end @*/
  8. };

规则3.7 禁止修改内置对象的原型

说明:内置对象作为一套公共接口,具有约定俗成的行为方式,修改其原型,可能破坏接口语义,或导致调试时的诡异现象。

反例:

  1. Array.prototype.indexOf =function(){return-1}
  2. var arr =[1,1,1,1,1,2,1,1,1];
  3. console.log(aar.indexOf(2));// 输出:-1

规则3.8 禁止直接使用Object.prototype的内置属性

说明:ECMAScript5.1新增了Object.create,它创建一个新对象,使用现有的对象来提供新创建的对象的proto。Object.create(null)是用于创建用作map的对象的常用模式。当该对象具有Object.prototype同名的属性时,可能会导致意外行为或漏洞。例如,web服务器解析来自客户端的JSON输入并且使用hasOwnProperty直接调用生成的对象是不安全的,因为恶意客户端可能会发送类似的JSON值'{ "hasOwnProperty": 1 }' 并导致服务器崩溃。

反例:

  1. var hasBarProperty = foo.hasOwnProperty("bar");
  2. var isPrototypeOfBar = foo.isPrototypeOf(bar);
  3. var barIsEnumerable = foo.propertyIsEnumerable("bar");

正例:

  1. var hasBarProperty =Object.prototype.hasOwnProperty.call(foo,"bar");
  2. var isPrototypeOfBar =Object.prototype.isPrototypeOf.call(foo, bar);
  3. var barIsEnumerable ={}.propertyIsEnumerable.call(foo,"bar");

规则3.9 使用Object.getPrototypeOf函数而不要使用_proto_

说明:ES5引入Object.getPrototypeOf函数作为获取对象原型的标准API,但在这之前大量的JavaScript引擎早就使用一个特殊的proto属性来达到相同的目的。然而,proto它本质上是一个内部属性,而不是一个正式的对外的API,目前浏览器必须部署这个属性,但其他运行环境不一定部署,因此,该属性并不是完全兼容的。例如,对于拥有null原型的对象,不同的环境处理的不一样。

  1. var empty =Object.create(null);
  2. "_proto_" in empty;//有些环境返回false,有些环境返回true

所以无论从语义的角度,还是从兼容性的角度,都不要使用proto这个属性,而是使用Object.getPrototypeOf()来代替。无论在什么环境中,Object.getPrototypeOf函数都是有效的,而且也是提取对象原型更加标准、可移植的方法。

规则3.10 不要使用函数构造器创建函数

说明:定义函数的方法包括3种:函数声明、Function构造函数和函数表达式。不管用哪种方法定义函数,它们都是Function对象的实例,并将继承Function对象所有默认或自定义的方法和属性。以函数构造器创建函数的方式类似于字符串eval(),可以接受任何字符串形式作为它的函数体,这就会有安全漏洞的风险。

反例:

  1. var func =newFunction('a','b','return a + b');
  2. var func2 =newFunction('alert("Hello")');

正例:

  1. function func(a, b){
  2. return a + b;
  3. }
  4. var func2 =function(a, b){
  5. return a + b;
  6. }

建议3.11 在使用原型prototype实现继承时,尽量使用现有稳定的库方法而非自行实现

说明:多级原型结构是指JavaScript中的继承关系。当定义一个D类,且把B类作为其原型,那么就获得了一个多级原型结构。这些原型结构会变得很复杂。使用现有的稳定的库方法如the Closure库的goog.inherits()或其他类似的函数,可避免不必要的编码失误,将是更好的选择。

建议3.12 定义类时,应在原型下定义方法,在构造方法内定义属性

说明:JavaScript中有多种方法可以给构造函数添加方法或成员,但是使用原型定义方法,可以降低内存占用,提高运行效率。

反例:

  1. functionAnimals(){
  2. this.walk =function(){};// 这样会导致每个实例上都创建一个walk方法
  3. }

正例:

  1. functionAnimals(){}
  2. Animals.prototype.walk =function(){};

建议3.13 使用闭包时,应避免构成循环引用,导致内存泄露

说明:JavaScript是一种垃圾收集式语言,其对象的内存是根据对象的创建分配给该对象的,并且会在没有对该对象的引用时由浏览器收回。JavaScript的垃圾收集机制本身并没有问题,但浏览器在为DOM对象分配和恢复内存的方式上有些出入。IE和Firefox均使用引用计数来为DOM对象处理内存。在引用计数系统中,每个所引用的对象都会保留一个计数,以获悉有多少对象正在引用它。如果计数为零,那么该对象就会被销毁,其占用的内存也会返回给堆。虽然这种解决方案总的来说还算有效,但是在循环引用方面却存在一些盲点。 当两个对象互相引用时,就构成了循环引用,其中对象的引用计数值都被赋为1。在纯垃圾收集系统中,循环引用问题不大:如果涉及的两个对象中有一个对象被任何其他对象引用,那么这两个对象都将被垃圾收集。而在引用计数系统中,这两个对象都不能被销毁,原因是引用计数永远不能为0。在同时使用了垃圾收集和引用计数的混合系统中,将会发生泄漏,因为系统不能正确识别循环引用。在这种情况下,DOM对象和JavaScript对象均不能被销毁。 循环引用很容易创建。在JavaScript最为方便的编程结构之一——闭包中,循环引用尤其突出。闭包会持有其外部作用域(包括局部变量、参数及方法)的引用,当闭包本身又被作用域成员(常见于DOM对象)持有时便构成循环引用,进一步导致内存泄露。

反例:

  1. function setClickListener(element, a, b){
  2. element.onclick =function(){
  3. // 在这里用到a和b
  4. };
  5. };

在上述代码中,即使没有使用element,闭包也保留了element、a和b的引用。由于element也保留了对闭包的引用,因此产生了循环引用,不能被GC回收。

正例:

  1. function setClickListener(element, a, b){
  2. element.onclick = createHandler(a, b);
  3. }
  4. function createHandler(a, b){
  5. // 通过添加另外一个函数来避免闭包本身,进而组织内存泄露
  6. returnfunction(){
  7. // 在这里用到a和b
  8. }
  9. }

建议3.14 警惕JavaScript的浮点数

说明:JavaScript具有单一数字类型:IEEE 754双精度浮点数。拥有单一数字类型是JavaScript的最佳功能之一。多种数字类型可能是复杂性,混淆和错误的根源。但是,二进制浮点类型有一个最大的缺点是,它不能准确地表示小数部分,会导致让人意外的精度问题,见下面示例。

示例代码1:

  1. console.log(0.1+0.2===0.3);// 输出:false。所以通常禁止直接使用==或===来比较浮点数。

示例代码2:

  1. var sum1 =(0.1+0.2)+0.3;
  2. console.log(sum1);// 输出:0.6000000000000001
  3. var sum2 =0.1+(0.2+0.3);
  4. console.log(sum2);// 输出:0.6。所以对于二进制浮点数,(a + b) + c 不能保证产生于a + (b + c)相同的结果。

有效的解决方法有以下几种:

  1. 尽可能的采用整数值运算,因为整数在表示是不需要舍入;

  2. 使用JavaScript的原生方法Number.prototype.toFixed(digits),digist参数表示小数点后数字的个数,不使用指数法,在必要时会进行四舍五入。使用该方法,在判断浮点数运算结果前对计算结果进行精度缩小。示例代码如下所示:

    1. parseFloat(0.1+0.2).toFixed(1);//0.3
  3. ES6 新增了一个极小的常量Number.EPSILON =.220446049250313e-16,约等于0.00000000000000022204Number.EPSILON的出现是用来判断浮点数的计算误差,如果浮点数计算得到的误差不超过Number.EPSILON的值,就表示可以接受这样的误差。示例代码如下所示:

    1. function isNumberEquals(one, other){
    2. returnMath.abs(one - other)<Number.EPSILON;
    3. }
    4. var one =0.1+0.2;
    5. var other =0.3;
    6. console.log(isNumberEquals(one, other));// 输出:true
  4. 使用一些支持精确运算的类库方法,如math.js

    1. <!DOCTYPE html>
    2. <html>
    3. <head>
    4. <scriptsrc="https://cdnjs.cloudflare.com/ajax/libs/mathjs/5.0.0/math.js"></script>
    5. <scripttype="text/javascript">
    6. function fn_click(){
    7. math.config({
    8. number:"BigNumber",
    9. });
    10. var result = math.add(math.bignumber(0.1), math.bignumber(0.2));
    11. alert(result);
    12. }
    13. </script>
    14. </head>
    15. <body>
    16. <inputtype="button"value="mathjs(0.1+0.2)"onclick="fn_click();"/>
    17. </body>
    18. </html>

建议3.15 不要使用可变参数的Array构造函数说

说明:通常不鼓励使用构造函数new Array的方法来构造新数组,因为当构造函数只有一个参数的时候,可能会导致意外情况,另外Array的全局定义也可能被重新修改,所以提倡使用数组文字表示法来创建数组,也就是[]表示法。 反例:

  1. const arr1 =newArray(x1, x2, x3);
  2. const arr2 =newArray(x1, x2);
  3. const arr3 =newArray(x3);
  4. const arr4 =newArray();

除了第三种情况之外,其他都可以正常工作,如果x1是个整数,那么arr3就是长度为x1,值都为undefined的数组。如果x1是其他任何数字,那么将抛出异常,如果它是其他任何东西,那么它将是单元数组。

正例:

  1. const arr1 =[x1, x2, x3];
  2. const arr2 =[x1, x2];
  3. const arr3 =[x1];
  4. const arr4 =[];

这种写法,就会省去很多麻烦。

同理,对于对象,同样不要使用new Object(),而是使用{}来创建。

规则3.16 构建字符串时,优先使用字符串模板而不是字符串链接。

说明:模板字符串表达更简洁,更具可读性。

反例:

  1. function sayHi(name){
  2. console.log('hi, '+ name);
  3. }

正例:

  1. function sayHi(name){
  2. console.log(`hi, ${name}`);
  3. }

规则3.17 数组遍历采用for...of,对象遍历采用for...in

反例:

  1. let numbers =[1,2,3,4];
  2. let sum =0;
  3. for(let number in numbers){
  4. sum += number;
  5. }
  6. // sum === 00123;

正例:

  1. let numbers =[1,2,3,4];
  2. let sum =0;
  3. for(let number in numbers){
  4. sum += number;
  5. }
  6. // sum === 10