随着ECMAScript的不断发展,越来越多更新的语言特性将被使用,给应用的开发带来方便。本文档的目标是使ECMAScript新特性的代码风格保持一致,并给予一些实践建议。
本文档仅包含新特性部分。基础部分请遵循JavaScript Style Guide
解释:
某些应用开发时,可能同时包含ES5和ESNext文件,运行环境仅支持ES5,ESNext文件需要经过预编译。部分场景下,编译工具的选择可能需要通过扩展名区分,需要重新定义ESNext文件的扩展名。此时,ESNext文件必须使用.es
扩展名。
但是,更推荐使用其他条件作为是否需要编译的区分:
- 基于文件内容。
- 不同类型文件放在不同目录下。
解释:
4空格为一个缩进,换行后添加一层缩进。将起始和结束的```符号单独放一行,有助于生成HTML时的标签对齐。
示例:
// good
function foo() {
let bar = `Hello
World`;
console.log(bar);
}
// good
function foo() {
let html = `
<div>
<p></p>
<p></p>
</div>
`;
}
示例:
// good
function* caller() {
yield 'a';
yield* callee();
yield 'd';
}
// bad
function * caller() {
yield 'a';
yield *callee();
yield 'd';
}
解释:
与函数声明保持一致。
解释:
成员属性是当前Stage 0的标准,如果使用的话,则定义后加上分号。
示例:
// good
class Foo {
foo = 3;
bar() {
}
}
// bad
class Foo {
foo = 3
bar() {
}
}
解释:
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);
示例:
// 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;
解释:
使用let和const定义时,变量作用域范围更明确。
示例:
// good
for (let i = 0; i < 10; i++) {
}
// bad
for (var i = 0; i < 10; i++) {
}
解释:
过多层次的解构会让代码变得难以阅读。
示例:
// 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;
解释:
由于会破坏整体的缩进,使代码整体上缺乏结构性,视觉上变丑。
示例:
// bad
function foo() {
// 缩进再多几层会更丑
let bar = `Hello
World`;
console.log(bar);
}
解释:
在变量替换符内有太多的函数调用等复杂语法会导致可读性下降。
示例:
// good
let fullName = getFullName(getFirstName(), getLastName());
let s = `Hello ${fullName}`;
// bad
let s = `Hello ${getFullName(getFirstName(), getLastName())}`;
解释:
添加默认值有助于引擎的优化,在未来strong mode下也会有更好的效果。
示例:
// good
function foo(text = 'hello') {
}
// bad
function foo(text) {
text = text || 'hello';
}
解释:
在未来strong mode下arguments
将被禁用。
示例:
// good
function foo(...args) {
console.log(args.join(''));
}
// bad
function foo() {
console.log([].join.call(arguments));
}
解释:
顶层作用域包括全局代码、模块代码等,这个作用域下this
对象为undefined
或者Global
对象,因此函数不应该绑定this
。
解释:
箭头函数会强制绑定当前环境下的this
。
解释:
目的在于保持所有键值对声明的一致性。
// 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
语法更清晰简洁。
示例:
// good
let foo = {
bar(x, y) {
return x + y;
}
};
// bad
let foo = {
bar: function (x, y) {
return x + y;
}
};
解释:
不建议使用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;
解释:
直接使用class
定义类更清晰。不要再使用function
和prototype
形式的定义。
// 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
和super.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;
}
}
解释:
何处声明要导出的东西,就在何处使用export
关键字,不做声明后再统一导出。
示例:
// good
export function foo() {
}
export const bar = 3;
// bad
function foo() {
}
const bar = 3;
export foo;
export bar;
解释:
举个例子,工具对象中的各个方法,相互之间并没有强关联,通常外部会选择几个使用,则应该使用命名导出。
简而言之,当一个模块只扮演命名空间的作用时,使用命名导出。
解释:
ES6 Module导出的是引用而非值,为防止外部的修改,尽量导出const
定义的常量。
解释:
import
会被默认提升至模块最前面,同时具备依赖声明的作用。
浏览器如能尽早读取相关的import
内容则有可能更早地请求相关的依赖模块,有一定的性能提升作用,在HTTP/2之后可能会具有更明显的效果。
示例:
// good
import {bar} from './bar';
function foo() {
bar.work();
}
// bad
function foo() {
import {bar} from './bar';
bar.work();
}
解释:
用数组展开代替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
可以更好地接受任何的Iterable
对象,如Map#values
生成的迭代器,使得方法的通用性更强。
以下情况除外:
- 遍历确实成为了性能瓶颈,需要使用原生
for
循环提升性能。 - 需要遍历过程中的索引。
解释:
使用普通Object,对非字符串类型的key,需要自己实现序列化。并且运行过程中的对象变化难以通知Object。
解释:
不要使用{foo: true}
这样的普通Object。
示例:
// good
let members = new Set(['one', 'two', 'three']);
// bad
let members = {
one: true,
two: true,
three: true
};
解释:
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,程序的可理解性更好;普通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'];
解释:
一个普通对象应该是干净而不需要各种装饰器注入逻辑的。
解释:
深层次的回调函数的嵌套会让代码变得难以阅读。
示例:
// bad
getUser(userId, function (user) {
validateUser(user, function (isValid) {
if (isValid) {
saveReport(report, user, function () {
notice('Saved!');
});
}
});
});
解释:
相比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
,如jQuery
的Deferred
。 - 不得使用非标准的
Promise
扩展API,如bluebird
的Promise.any
等。
解释:
理由和 不允许修改和扩展任何原生对象和宿主对象的原型 是一样的。如果想要使用更方便,可以用utility函数的形式。
解释:
并行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});
}
解释:
使用语言自身的能力可以使代码更清晰,也无需引入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');
});
}
解释:
ES标准的制定还在不断进行中,各种环境对语言特性的支持也日新月异。了解项目中用到了哪些ESNext的特性,了解项目的运行环境,并持续跟进这些特性在运行环境中的支持程度是很有必要的。这意味着:
- 如果有任何一个运行环境(比如chrome)支持了项目里用到的所有特性,你可以在开发时抛弃预编译。
- 如果所有环境都支持了某一特性(比如Promise),你可以抛弃相关的shim,或无需在预编译时进行转换。
- 如果所有环境都支持了项目里用到的所有特性,你可以完全抛弃预编译。
解释:
当前运行环境下没有Promise
时,可以引入shim
的扩展。如果自己实现,需要实现在global
下,并且与标准API保持一致。
这样,未来运行环境支持时,可以随时把Promise
扩展直接扔掉,而应用代码无需任何修改。
解释:
由于不同环境的(如浏览器)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
解释:
添加--modules amd
参数即可。
解释:
使用--loose all
将生成更直观、性能更高的代码,在绝大多数场景下能较好运行,但是生成代码与源代码的逻辑并不完全一致(如属性的可访问性)。具体请参照loose文档。
解释:
当babel在转换代码的过程中发现需要一些特性时,会在该文件头部生成对应的helper代码。默认情况下,对于每一个经由babel处理的文件,均会在文件头部生成对应需要的辅助函数,多份文件辅助函数存在重复,占用了不必要的代码体积。
因此推荐打开externalHelpers: true
选项,使babel在转换后内容中不写入helper相关的代码,而是使用一个外部的.js
统一提供所有的helper。对于external-helpers的使用,可以有两种方式:
- 默认方式:需要通过
<script>
自行引入babel-polyfill.js
和babel-external-helpers.js
。 - 定制方式:自己实现babel-runtime。
示例:
# 默认方式
--loose all --modules amd --external-helpers
# `babelHelpers`的代码可以通过执行`babel-external-helpers -t var`得到所有相关API的实现
# 定制方式
--loose all --modules amd --optional runtime