Skip to content

Latest commit

 

History

History
1188 lines (730 loc) · 22.9 KB

es-next-style-guide.md

File metadata and controls

1188 lines (730 loc) · 22.9 KB

JavaScript编码规范 - ESNext补充篇(草案)

1 前言

2 代码风格

  2.1 文件

  2.2 结构

    2.2.1 缩进

    2.2.2 空格

    2.2.3 语句

3 语言特性

  3.1 变量

  3.2 解构

  3.3 模板字符串

  3.4 函数

  3.5 箭头函数

  3.6 对象

  3.7 类

  3.8 模块

  3.9 集合

  3.10 装饰器

  3.11 异步

4 环境

1 前言

随着ECMAScript的不断发展,越来越多更新的语言特性将被使用,给应用的开发带来方便。本文档的目标是使ECMAScript新特性的代码风格保持一致,并给予一些实践建议。

本文档仅包含新特性部分。基础部分请遵循JavaScript Style Guide

2 代码风格

2.1 文件

[建议] ESNext语法的JavaScript文件使用.js扩展名。
[强制] 当文件无法使用.js扩展名时,使用.es扩展名。

解释:

某些应用开发时,可能同时包含ES5和ESNext文件,运行环境仅支持ES5,ESNext文件需要经过预编译。部分场景下,编译工具的选择可能需要通过扩展名区分,需要重新定义ESNext文件的扩展名。此时,ESNext文件必须使用.es扩展名。

但是,更推荐使用其他条件作为是否需要编译的区分:

  1. 基于文件内容。
  2. 不同类型文件放在不同目录下。

2.2 结构

2.2.1 缩进

[建议] 使用多行字符串时遵循缩进原则。

解释:

4空格为一个缩进,换行后添加一层缩进。将起始和结束的```符号单独放一行,有助于生成HTML时的标签对齐。

示例:

// good
function foo() {
    let bar = `Hello
        World`;
    console.log(bar);
}

// good
function foo() {
    let html = `
        <div>
            <p></p>
            <p></p>
        </div>
    `;
}

2.2.2 空格

[强制] 使用generator时,*前面不允许有空格,*后面必须有一个空格。

示例:

// good
function* caller() {
    yield 'a';
    yield* callee();
    yield 'd';
}

// bad
function * caller() {
    yield 'a';
    yield *callee();
    yield 'd';
}

2.2.3 语句

[强制] 类声明结束不允许添加分号。

解释:

与函数声明保持一致。

[强制] 类成员定义中,方法定义后不允许添加分号,成员属性定义后必须添加分号。

解释:

成员属性是当前Stage 0的标准,如果使用的话,则定义后加上分号。

示例:

// good
class Foo {
    foo = 3;

    bar() {

    }
}

// bad
class Foo {
    foo = 3

    bar() {

    }
}
[强制] export语句后,不允许出现表示空语句的分号。

解释:

export关键字不影响后续语句类型。

示例:

// good
export function foo() {
}

export default bar() {
}


// bad
export function foo() {
};

export default bar() {
};
[强制] 属性装饰器后,可以不加分号的场景,不允许加分号。

解释:

只有一种场景是必须加分号的:当属性key是computed property key时,其装饰器必须加分号,否则修饰key的[]会做为之前表达式的property accessor。

上面描述的场景,装饰器后需要加分号。其余场景下的属性装饰器后不允许加分号。

示例:

// good
class Foo {
    @log('INFO')
    bar() {

    }

    @log('INFO');
    ['bar' + 2]() {

    }
}

// bad
class Foo {
    @log('INFO');
    bar() {

    }

    @log('INFO')
    ['bar' + 2]() {

    }
}
[强制] 箭头函数的参数只有一个时,参数部分的括号必须省略。

示例:

// good
list.map(item => item * 2);

// bad
list.map((item) => item * 2);
[强制] 如果箭头函数的函数体只有一个表达式语句并且被作为返回值,必须省略{}return

示例:

// good
list.map(item => item * 2);

// bad
list.map(item => {
    return item * 2;
});
[强制] 解构多个变量时,如果超过行长度限制,每个解构的变量必须单独一行。

解释:

太多的变量解构会让一行的代码非常长,极有可能超过单行长度控制,使代码可读性下降。

示例:

// good
let {
    name: personName,
    email: personEmail,
    sex: personSex,
    age: personAge
} = person;

// bad
let {name: personName, email: personEmail,
    sex: personSex, age: personAge
} = person;

3 语言特性

3.1 变量

[强制] 使用let和const定义变量,不使用var。

解释:

使用let和const定义时,变量作用域范围更明确。

示例:

// good
for (let i = 0; i < 10; i++) {
    
}

// bad
for (var i = 0; i < 10; i++) {
    
}

3.2 解构

[强制] 不要使用3层及以上的解构。

解释:

过多层次的解构会让代码变得难以阅读。

示例:

// bad
let {documentElement: {firstElementChild: {nextSibling}}} = window;

[建议] 使用解构减少中间变量。

解释:

常见场景如变量值交换,可能产生中间变量。这种场景推荐使用解构。

示例:

// good
[x, y] = [y, x];

// bad
let temp = x;
x = y;
y = temp;

[强制] 仅定义一个变量时不允许使用解构。

解释:

在这种场景下,使用结构将降低代码可读性。

示例:

// good
let len = myString.length;

// bad
let {length: len} = myString;

[强制] 不节省中间变量时,不允许因为使用解构产生中间对象。

解释:

纯因为使用解构产生无用的中间对象是一种浪费。

示例:

// good
let {first: firstName, last: lastName} = person;
let one = 1;
let two = 2;

// bad
// 下面的代码产生了数组对象[1, 2],但是这个数组对象并没什么用
let [one, two] = [1, 2];

[强制] 使用剩余运算符时,剩余运算符之前的所有元素必需具名。

解释:

剩余运算符之前的元素省略名称可能带来较大的程序阅读障碍。如果仅仅为了取数组后几项,请使用slice方法。

示例:

// good
let [one, two, ...anyOther] = myArray;
let other = myArray.slice(3);

// bad
let [,,, ...other] = myArray;

3.3 模板字符串

[建议] 当对行首空格敏感时,避免使用多行字符串。

解释:

由于会破坏整体的缩进,使代码整体上缺乏结构性,视觉上变丑。

示例:

// bad
function foo() {
    // 缩进再多几层会更丑
    let bar = `Hello
World`;
    console.log(bar);
}

[强制] 字符串内变量替换时,不要使用2次及以上的函数调用。

解释:

在变量替换符内有太多的函数调用等复杂语法会导致可读性下降。

示例:

// good
let fullName = getFullName(getFirstName(), getLastName());
let s = `Hello ${fullName}`;

// bad
let s = `Hello ${getFullName(getFirstName(), getLastName())}`;

3.4 函数

[强制] 使用变量默认语法代替基于条件判断的默认值声明。

解释:

添加默认值有助于引擎的优化,在未来strong mode下也会有更好的效果。

示例:

// good
function foo(text = 'hello') {
}

// bad
function foo(text) {
    text = text || 'hello';
}

[强制] 不要使用arguments对象,应使用...args代替。

解释:

在未来strong mode下arguments将被禁用。

示例:

// good
function foo(...args) {
    console.log(args.join(''));
}

// bad
function foo() {
    console.log([].join.call(arguments));
}

3.5 箭头函数

[强制] 不得在顶层作用域下使用箭头函数。

解释:

顶层作用域包括全局代码、模块代码等,这个作用域下this对象为undefined或者Global对象,因此函数不应该绑定this

[强制] 一个函数被设计为需要call和apply的时候,不能是箭头函数。

解释:

箭头函数会强制绑定当前环境下的this

3.6 对象

[强制] 定义对象时,如果所有键均指向同名变量,则所有键都使用缩写;如果有一个键无法指向同名变量,则所有键都不使用缩写。

解释:

目的在于保持所有键值对声明的一致性。

// good
let foo = {x, y, z};

let foo2 = {
    x: 1,
    y: 2,
    z: z
};


// bad
let foo = {
    x: x,
    y: y,
    z: z
};

let foo2 = {
    x: 1,
    y: 2,
    z
};

[强制] 定义方法时使用MethodDefinition语法,不使用PropertyName:FunctionExpression语法。

解释:

MethodDefinition语法更清晰简洁。

示例:

// good
let foo = {
    bar(x, y) {
        return x + y;
    }
};

// bad
let foo = {
    bar: function (x, y) {
        return x + y;
    }
};

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

解释:

不建议使用for .. in进行对象的遍历,以避免遗漏hasOwnProperty产生的错误。

示例:

// good
for (let key of Object.keys(foo)) {
    let value = foo[key];
}

[建议] 定义对象的方法不应使用箭头函数。

解释:

箭头函数将this绑定到当前环境,在obj.method()调用时容易导致不期待的this。除非明确需要绑定this,否则不应使用箭头函数。

示例:

// good
let foo = {
    bar(x, y) {
        return x + y;
    }
};

// bad
let foo = {
    bar: (x, y) => x + y
};

[建议] 尽量使用计算属性键在一个完整的字面量中完整地定义一个对象,避免对象定义后直接增加对象属性。

解释:

在一个完整的字面量中声明所有的键值,而不需要将代码分散开来,有助于提升代码可读性。

示例:

// good
const MY_KEY = 'bar';
let foo = {
    [MY_KEY + 'Hash']: 123
};

// bad
const MY_KEY = 'bar';
let foo = {};
foo[MY_KEY + 'Hash'] = 123;

3.7 类

[强制] 使用class关键字定义一个类。

解释:

直接使用class定义类更清晰。不要再使用functionprototype形式的定义。

// good
class TextNode {
    constructor(value, engine) {
        this.value = value;
        this.engine = engine;
    }

    clone() {
        return this;
    }
}

// bad
function TextNode(value, engine) {
    this.value = value;
    this.engine = engine;
}

TextNode.prototype.clone = function () {
    return this;
};

[强制] 使用super访问父类成员,而非父类的prototype

解释:

使用supersuper.foo可以快速访问父类成员,而不必硬编码父类模块而导致修改和维护的不便,同时更节省代码。

// good
class TextNode extends Node {
    constructor(value, engine) {
        super(value);
        this.engine = engine;
    }

    setNodeValue(value) {
        super.setNodeValue(value);
        this.textContent = value;
    }
}

// bad
class TextNode extends Node {
    constructor(value, engine) {
        Node.apply(this, arguments);
        this.engine = engine;
    }

    setNodeValue(value) {
        Node.prototype.setNodeValue.call(this, value);
        this.textContent = value;
    }
}

3.8 模块

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

解释:

何处声明要导出的东西,就在何处使用export关键字,不做声明后再统一导出。

示例:

// good
export function foo() {
}

export const bar = 3;


// bad
function foo() {
}

const bar = 3;

export foo;
export bar;

[建议] 相互之间无关联的内容使用命名导出。

解释:

举个例子,工具对象中的各个方法,相互之间并没有强关联,通常外部会选择几个使用,则应该使用命名导出。

简而言之,当一个模块只扮演命名空间的作用时,使用命名导出。

[建议] 导出时使用const定义。

解释:

ES6 Module导出的是引用而非值,为防止外部的修改,尽量导出const定义的常量。

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

解释:

import会被默认提升至模块最前面,同时具备依赖声明的作用。

浏览器如能尽早读取相关的import内容则有可能更早地请求相关的依赖模块,有一定的性能提升作用,在HTTP/2之后可能会具有更明显的效果。

示例:

// good
import {bar} from './bar';

function foo() {
    bar.work();
}

// bad
function foo() {
    import {bar} from './bar';
    bar.work();
}

3.9 集合

[建议] 对数组进行连接操作时,使用数组展开语法。

解释:

用数组展开代替concat方法,数组展开对Iterable有更好的兼容性。

示例:

// good
let foo = [...foo, newValue];
let bar = [...bar, ...newValues];

// bad
let foo = foo.concat(newValue);
let bar = bar.concat(newValues);

[建议] 不要使用数组展开进行数组的复制操作。

解释:

使用数组展开语法进行复制,代码可读性较差。推荐使用Array.from方法进行复制操作。

示例:

// good
let otherArr = Array.from(arr);

// bad
let otherArr = [...arr];

[建议] 尽可能使用for .. of进行遍历。

解释:

使用for .. of可以更好地接受任何的Iterable对象,如Map#values生成的迭代器,使得方法的通用性更强。

以下情况除外:

  1. 遍历确实成为了性能瓶颈,需要使用原生for循环提升性能。
  2. 需要遍历过程中的索引。

[强制] 当键值有可能不是字符串时,必须使用Map;当元素有可能不是字符串时,必须使用Set。

解释:

使用普通Object,对非字符串类型的key,需要自己实现序列化。并且运行过程中的对象变化难以通知Object。

[建议] 需要一个不可重复的集合时,应使用Set。

解释:

不要使用{foo: true}这样的普通Object。

示例:

// good
let members = new Set(['one', 'two', 'three']);

// bad
let members = {
    one: true,
    two: true,
    three: true
};

[建议] 当需要遍历功能时,使用Map和Set。

解释:

Map和Set是可遍历对象,能够方便地使用for...of遍历。不要使用使用普通Object。

示例:

// good
let membersAge = new Map([
    ['one', 10],
    ['two', 20],
    ['three', 30]
]);
for (let [key, value] of map) {
}

// bad
let membersAge = {
    one: 10,
    two: 20,
    three: 30
};
for (let key in membersAge) {
    if (membersAge.hasOwnProperty(key)) {
        let value = membersAge[key];
    }
}

[建议] 程序运行过程中有添加或移除元素的操作时,使用Map和Set。

解释:

使用Map和Set,程序的可理解性更好;普通Object的语义更倾向于表达固定的结构。

示例:

// good
let membersAge = new Map();
membersAge.set('one', 10);
membersAge.set('two', 20);
membersAge.set('three', 30);
membersAge.delete('one');

// bad
let membersAge = {};
membersAge.one = 10;
membersAge.two = 20;
membersAge.three = 30;
delete membersAge['one'];

3.10 装饰器

[建议] 不在直接声明的普通对象上使用装饰器。

解释:

一个普通对象应该是干净而不需要各种装饰器注入逻辑的。

3.11 异步

[强制] 回调函数的嵌套不得超过3层。

解释:

深层次的回调函数的嵌套会让代码变得难以阅读。

示例:

// bad
getUser(userId, function (user) {
    validateUser(user, function (isValid) {
        if (isValid) {
            saveReport(report, user, function () {
                notice('Saved!');
            });
        }
    });
});

[建议] 使用 Promise 代替 callback

解释:

相比callback,使用Promise能够使复杂异步过程的代码更清晰。

示例:

// good
let user;
getUser(userId)
    .then(function (userObj) {
        user = userObj;
        return validateUser(user);
    })
    .then(function (isValid) {
        if (isValid) {
            return saveReport(report, user);
        }

        return Promise.reject('Invalid!');
    })
    .then(
        function () {
            notice('Saved!');
        },
        function (message) {
            notice(message);
        }
    );

[强制] 使用标准的 Promise

解释:

  1. 不得使用非标准的Promise,如jQueryDeferred
  2. 不得使用非标准的Promise扩展API,如bluebirdPromise.any等。

[强制] 不允许直接扩展 Promise 对象的 prototype

解释:

理由和 不允许修改和扩展任何原生对象和宿主对象的原型 是一样的。如果想要使用更方便,可以用utility函数的形式。

[强制] 不得为了编写的方便,将可以并行的IO过程串行化。

解释:

并行IO消耗时间约等于IO时间最大的那个过程,串行的话消耗时间将是所有过程的时间之和。

示例:

requestData().then(function (data) {
    renderTags(data.tags);
    renderArticles(data.articles);
});

// good
function requestData() {
    return Promise.all(
        [
            requestTags(), 
            requestArticles()
        ]
    ).then(
        function (values) {
            return {
                tags: value[0], 
                articles: values[1]
            };
        }
    );
}

// bad
async function requestData() {
    let tags = await requestTags();
    let articles = await requestArticles();

    return Promise.resolve({tags, articles});
}

[建议] 使用 async/await 代替 generator + co

解释:

使用语言自身的能力可以使代码更清晰,也无需引入co库。

示例:

addReport(report, userId).then(
    function () {
        notice('Saved!');
    },
    function (message) {
        notice(message);
    }
);

// good
async function addReport(report, userId) {
    let user = await getUser(userId);
    let isValid = await validateUser(user);

    if (isValid) {
        let savePromise = saveReport(report, user);
        return savePromise();
    }

    return Promise.reject('Invalid');
}

// bad
function addReport(report, userId) {
    return co(function* () {
        let user = yield getUser(userId);
        let isValid = yield validateUser(user);

        if (isValid) {
            let savePromise = saveReport(report, user);
            return savePromise();
        }

        return Promise.reject('Invalid');
    });
}

4 环境

[建议] 持续跟进与关注运行环境对语言特性的支持程度。

解释:

查看环境对语言特性的支持程度

ES标准的制定还在不断进行中,各种环境对语言特性的支持也日新月异。了解项目中用到了哪些ESNext的特性,了解项目的运行环境,并持续跟进这些特性在运行环境中的支持程度是很有必要的。这意味着:

  1. 如果有任何一个运行环境(比如chrome)支持了项目里用到的所有特性,你可以在开发时抛弃预编译。
  2. 如果所有环境都支持了某一特性(比如Promise),你可以抛弃相关的shim,或无需在预编译时进行转换。
  3. 如果所有环境都支持了项目里用到的所有特性,你可以完全抛弃预编译。

[强制] 在运行环境中没有 Promise 时,将 Promise 的实现 shimglobal 中。

解释:

当前运行环境下没有Promise时,可以引入shim的扩展。如果自己实现,需要实现在global下,并且与标准API保持一致。

这样,未来运行环境支持时,可以随时把Promise扩展直接扔掉,而应用代码无需任何修改。

[建议] 需要预编译时,必须使用 babel 做为预编译工具。

解释:

由于不同环境的(如浏览器)JavaScript引擎对ESNext特性支持程度的问题,ESNext代码可能需要进行预编译。此时,必须使用babel做为预编译工具。由于babel最新的6暂时还不稳定,建议暂时使用5。

不同的产品,对于浏览器支持的情况不同,使用 babel 的时候,需要设置的参数也有一些区别。下面在示例中给出一些建议的参数。

示例:

# 建议的参数
--loose all --modules amd --blacklist strict

# 如果需要使用 es7.classProperties、es7.decorators 等一些特性,需要额外的 --stage 0 参数
--loose all --modules amd --blacklist strict --stage 0

[建议] 生成的代码在浏览器环境运行时,应生成AMD模块化代码。

解释:

添加--modules amd参数即可。

[建议] 使用 loose 生成更小、性能更高的代码。

解释:

使用--loose all将生成更直观、性能更高的代码,在绝大多数场景下能较好运行,但是生成代码与源代码的逻辑并不完全一致(如属性的可访问性)。具体请参照loose文档

[建议] 使用 external-helpers 减少生成文件的大小。

解释:

当babel在转换代码的过程中发现需要一些特性时,会在该文件头部生成对应的helper代码。默认情况下,对于每一个经由babel处理的文件,均会在文件头部生成对应需要的辅助函数,多份文件辅助函数存在重复,占用了不必要的代码体积。

因此推荐打开externalHelpers: true选项,使babel在转换后内容中不写入helper相关的代码,而是使用一个外部的.js统一提供所有的helper。对于external-helpers的使用,可以有两种方式:

  1. 默认方式:需要通过<script>自行引入babel-polyfill.jsbabel-external-helpers.js
  2. 定制方式:自己实现babel-runtime

示例:

# 默认方式
--loose all --modules amd --external-helpers
# `babelHelpers`的代码可以通过执行`babel-external-helpers -t var`得到所有相关API的实现

# 定制方式
--loose all --modules amd --optional runtime