JavaScript编程风格指南


JavaScript编程风格指南

介绍

作者根据Robert C. Martin《代码整洁之道》总结了适用于JavaScript的188金博宝bet原则《Clean Code JavaScript》。

本文是对其的翻译。

不必严格遵守本文的所有原则,有时少遵守一些效果可能会更好,具体应根据实际情况决定。这是根据《代码整洁之道》作者多年经验整理的代码优化建议,但也仅仅只是一份建议。

188金博宝bet已经发展了50多年,至今仍在不断前进。现在,把这些原则当作试金石,尝试将他们作为团队代码质量考核的标准之一吧。

最后你需要知道的是,这些东西不会让你立刻变成一个优秀的工程师,长期奉行他们也并不意味着你能够高枕无忧不再犯错。千里之行,始于足下。我们需要时常和同行们进行代码评审,不断优化自己的代码。不要惧怕改善代码质量所需付出的努力,加油。

变量

使用有意义,可读性好的变量名

反例:

var yyyymmdstr = moment().format('YYYY/MM/DD');

正例:

var yearMonthDay = moment().format('YYYY/MM/DD');

使用ES6的const定义常量

反例中使用”var”定义的”常量”是可变的。

在声明一个常量时,该常量在整个程序中都应该是不可变的。

反例:

var FIRST_US_PRESIDENT = "George Washington";

正例:

const FIRST_US_PRESIDENT = "George Washington";

对功能类似的变量名采用统一的命名风格

反例:

getUserInfo();

正例:

getUser();

使用易于检索名称

我们需要阅读的代码远比自己写的要多,使代码拥有良好的可读性且易于检索非常重要。阅读变量名晦涩难懂的代码对读者来说是一种相当糟糕的体验。 让你的变量名易于检索。

反例:

// 525600 是什么?for (var i = 0; i < 525600; i++) {

正例:

// Declare them as capitalized `var` globals.var MINUTES_IN_A_YEAR = 525600;for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {

使用说明变量(即有意义的变量名)

反例:

const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;

正例:

const
cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;const match =
cityStateRegex.match(cityStateRegex)const city = match[1];const state =
match[2];

不要绕太多的弯子

显式优于隐式。

反例:

var locations = ['Austin', 'New York', 'San Francisco'];

正例:

var locations = ['Austin', 'New York', 'San Francisco'];

避免重复的描述

当类/对象名已经有意义时,对其变量进行命名不需要再次重复。

反例:

var Car = {

正例:

var Car = {

避免无意义的条件判断

反例:

function createMicrobrewery(name) { var breweryName; if (name) {

正例:

function createMicrobrewery(name) { var breweryName = name || 'Hipster Brew Co.'}

函数

函数参数 (理想情况下应不超过2个)

限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。过多的参数将导致难以采用有效的测试用例对函数的各个参数进行测试。

应避免三个以上参数的函数。通常情况下,参数超过两个意味着函数功能过于复杂,这时需要重新优化你的函数。当确实需要多个参数时,大多情况下可以考虑这些参数封装成一个对象。

JS定义对象非常方便,当需要多个参数时,可以使用一个对象进行替代。

反例:

function createMenu(title, body, buttonText, cancellable) {

正例:

var menuConfig = {

函数功能的单一性

这是软件功能中最重要的原则之一。

功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。

反例:

function emailClients(clients) {

正例:

function emailClients(clients) {

函数名应明确表明其功能

反例:

function dateAdd(date, month) { // ...}let date = new Date();// 很难理解dateAdd(date, 1)是什么意思

正例:

function dateAddMonth(date, month) { // ...}let date = new Date();

函数应该只做一层抽象

当函数的需要的抽象多余一层时通常意味着函数功能过于复杂,需将其进行分解以提高其可重用性和可测试性。

反例:

function parseBetterJSAlternative(code) {

正例:

function tokenize(code) {

移除重复的代码

永远、永远、永远不要在任何循环下有重复的代码。

这种做法毫无意义且潜在危险极大。重复的代码意味着逻辑变化时需要对不止一处进行修改。JS弱类型的特点使得函数拥有更强的普适性。好好利用这一优点吧。

反例:

function showDeveloperList(developers) {

正例:

function showList(employees) {

采用默认参数精简代码

反例:

function writeForumComment(subject, body) {

正例:

function writeForumComment(subject = 'No subject', body = 'No text') {

使用Object.assign设置默认对象

反例:

var menuConfig = {

正例:

var menuConfig = {

不要使用标记(Flag)作为函数参数

这通常意味着函数的功能的单一性已经被破坏。此时应考虑对函数进行再次划分。

反例:

function createFile(name, temp) { if (temp) {

正例:

function createTempFile(name) {

避免副作用

当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。

程序在某些情况下确实需要副作用这一行为,如先前例子中的写文件。这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个service完成这一需求。

反例:

//
Global variable referenced by following function.// If we had another
function that used this name, now it'd be an array and it could break
it.var name = 'Ryan McDermott';function splitIntoFirstAndLastName() {

正例:

function splitIntoFirstAndLastName(name) { return name.split(' ');

不要写全局函数

在JS中污染全局是一个非常不好的实践,这么做可能和其他库起冲突,且调用你的API的用户在实际环境中得到一个exception前对这一情况是一无所知的。

想象以下例子:如果你想扩展JS中的Array,为其添加一个diff函数显示两个数组间的差异,此时应如何去做?你可以将diff写入Array.prototype,但这么做会和其他有类似需求的库造成冲突。如果另一个库对diff的需求为比较一个数组中收尾元素间的差异呢?

使用ES6中的class对全局的Array做简单的扩展显然是一个更棒的选择。

反例:

Array.prototype.diff = function(comparisonArray) { var values = []; var hash = {}; for (var i of comparisonArray) {

正例:

class SuperArray extends Array { constructor(...args) { super(...args);

采用函数式编程

函数式的编程具有更干净且便于测试的特点。尽可能的使用这种风格吧。

反例:

const programmerOutput = [

正例:

const programmerOutput = [

封装判断条件

反例:

if (fsm.state === 'fetching' && isEmpty(listNode)) { /// ...}

正例:

function shouldShowSpinner(fsm, listNode) { return fsm.state === 'fetching' && isEmpty(listNode);

避免“否定情况”的判断

反例:

function isDOMNodeNotPresent(node) { // ...}if (!isDOMNodeNotPresent(node)) { // ...}

正例:

function isDOMNodePresent(node) { // ...}if (isDOMNodePresent(node)) { // ...}

避免条件判断

这看起来似乎不太可能。

大多人听到这的第一反应是:“怎么可能不用if完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。

第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。

反例:

class Airplane { //...

正例:

class Airplane { //...}class Boeing777 extends Airplane { //...

避免类型判断(part 1)

JS是弱类型语言,这意味着函数可接受任意类型的参数。

有时这会对你带来麻烦,你会对参数做一些类型判断。有许多方法可以避免这些情况。

反例:

function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) {

正例:

function travelToTexas(vehicle) {

避免类型判断(part 2)

如果需处理的数据为字符串,整型,数组等类型,无法使用多态并仍有必要对其进行类型检测时,可以考虑使用TypeScript。

反例:

function
combine(val1, val2) { if (typeof val1 == "number" && typeof
val2 == "number" || typeof val1 == "string" && typeof val2 ==
"string") { return val1 + val2;

正例:

function combine(val1, val2) { return val1 + val2;

避免过度优化

现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。

这里可以找到许多真正需要优化的地方

反例:

//
这里使用变量len是因为在老式浏览器中,// 直接使用正例中的方式会导致每次循环均重复计算list.length的值,//
而在现代浏览器中会自动完成优化,这一行为是没有必要的for (var i = 0, len = list.length; i < len;
i++) { // ...}

正例:

for (var i = 0; i < list.length; i++) { // ...}

删除无效的代码

不再被调用的代码应及时删除。

反例:

function oldRequestModule(url) { // ...}function newRequestModule(url) { // ...}var req = newRequestModule;

正例:

function newRequestModule(url) { // ...}var req = newRequestModule;

对象和数据结构

使用getters和setters

JS没有接口或类型,因此实现这一模式是很困难的,因为我们并没有类似publicprivate的关键词。

然而,使用getters和setters获取对象的数据远比直接使用点操作符具有优势。为什么呢?

  1. 当需要对获取的对象属性执行额外操作时。

  2. 执行set时可以增加规则对要变量的合法性进行判断。

  3. 封装了内部逻辑。

  4. 在存取时可以方便的增加日志和错误处理。

  5. 继承该类时可以重载默认行为。

  6. 从服务器获取数据时可以进行懒加载。

反例:

class BankAccount { constructor() { this.balance = 1000;

正例:

class BankAccount { constructor() { this.balance = 1000;

让对象拥有私有成员

可以通过闭包完成

反例:

var Employee = function(name) { this.name = name;

正例:

var Employee = (function() { function Employee(name) { this.getName = function() { return name;

单一职责原则 (SRP)

如《代码整洁之道》一书中所述,“修改一个类的理由不应该超过一个”。

将多个功能塞进一个类的想法很诱人,但这将导致你的类无法达到概念上的内聚,并经常不得不进行修改。

最小化对一个类需要修改的次数是非常有必要的。如果一个类具有太多太杂的功能,当你对其中一小部分进行修改时,将很难想象到这一修够对代码库中依赖该类的其他模块会带来什么样的影响。

反例:

class UserSettings { constructor(user) { this.user = user;

正例:

class UserAuth { constructor(user) { this.user = user;

开/闭原则 (OCP)

“代码实体(类,模块,函数等)应该易于扩展,难于修改。”

这一原则指的是我们应允许用户方便的扩展我们代码模块的功能,而不需要打开js文件源码手动对其进行修改。

反例:

class AjaxRequester {

正例:

class AjaxRequester { constructor() { this.HTTP_METHODS = ['POST', 'PUT', 'GET'];

利斯科夫替代原则 (LSP)

“子类对象应该能够替换其超类对象被使用”。

也就是说,如果有一个父类和一个子类,当采用子类替换父类时不应该产生错误的结果。

反例:

class Rectangle { constructor() { this.width = 0; this.height = 0;

正例:

class Shape { constructor() {}

接口隔离原则 (ISP)

“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。”

在JS中,当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量的需求是有益的。

反例:

class DOMTraverser { constructor(settings) { this.settings = settings; this.setup();

正例:

class DOMTraverser { constructor(settings) { this.settings = settings; this.options = settings.options; this.setup();

依赖反转原则 (DIP)

该原则有两个核心点:

1. 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口。 2. 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。

反例:

class
InventoryTracker { constructor(items) { this.items = items; // BAD: We
have created a dependency on a specific request implementation.

正例:

class InventoryTracker { constructor(items, requester) { this.items = items; this.requester = requester;

使用ES6的classes而不是ES5的Function

典型的ES5的类(function)在继承、构造和方法定义方面可读性较差。

当需要继承时,优先选用classes。

但是,当在需要更大更复杂的对象时,最好优先选择更小的function而非classes。

反例:

var Animal = function(age) { if (!(this instanceof Animal)) { throw new Error("Instantiate Animal with `new`");

正例:

class Animal { constructor(age) { this.age = age;

使用方法链

这里我们的理解与《代码整洁之道》的建议有些不同。

有争论说方法链不够干净且违反了德米特法则,也许这是对的,但这种方法在JS及许多库(如JQuery)中显得非常实用。

因此,我认为在JS中使用方法链是非常合适的。在class的函数中返回this,能够方便的将类需要执行的多个方法链接起来。

反例:

class Car { constructor() { this.make = 'Honda'; this.model = 'Accord'; this.color = 'white';

正例:

class Car {

优先使用组合模式而非继承

在著名的设计模式一书中提到,应多使用组合模式而非继承。

这么做有许多优点,在想要使用继承前,多想想能否通过组合模式满足需求吧。

那么,在什么时候继承具有更大的优势呢?这取决于你的具体需求,但大多情况下,可以遵守以下三点:

  1. 继承关系表现为”是一个”而非”有一个”(如动物->人 和 用户->用户细节)

  2. 可以复用基类的代码(“Human”可以看成是”All animal”的一种)

  3. 希望当基类改变时所有派生类都受到影响(如修改”all animals”移动时的卡路里消耗量)

反例:

class Employee { constructor(name, email) { this.name = name; this.email = email;

正例:

class Employee { constructor(name, email) { this.name = name; this.email = email;

测试

一些好的覆盖工具.

一些好的JS测试框架

单一的测试每个概念

反例:

const assert = require('assert');

正例:

const assert = require('assert');

并发

用Promises替代回调

回调不够整洁并会造成大量的嵌套。ES6内嵌了Promises,使用它吧。

反例:

require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin',
function(err, response) { if (err) { console.error(err);

正例:

require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')

Async/Await是较Promises更好的选择

Promises是较回调而言更好的一种选择,但ES7中的async和await更胜过Promises。

在能使用ES7特性的情况下可以尽量使用他们替代Promises。

反例:

require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')

正例:

async
function getCleanCodeArticle() { try { var request = await
require('request-promise') var response = await
request.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); var
fileHandle = await require('fs-promise'); await
fileHandle.writeFile('article.html', response); console.log('File
written');

错误处理

错误抛出是个好东西!这使得你能够成功定位运行状态中的程序产生错误的位置。

别忘了捕获错误

对捕获的错误不做任何处理是没有意义的。

代码中try/catch的意味着你认为这里可能出现一些错误,你应该对这些可能的错误存在相应的处理方案。

反例:

try {

正例:

try {

不要忽略被拒绝的promises

理由同try/catch.

反例:

getdata()

正例:

getdata()

格式化

格式化是一件主观的事。如同这里的许多规则一样,这里并没有一定/立刻需要遵守的规则。可以在这里完成格式的自动化。

大小写一致

JS是弱类型语言,合理的采用大小写可以告诉你关于变量/函数等的许多消息。

这些规则是主观定义的,团队可以根据喜欢进行选择。重点在于无论选择何种风格,都需要注意保持一致性。

反例:

var
DAYS_IN_WEEK = 7;var daysInMonth = 30;var songs = ['Back In Black',
'Stairway to Heaven', 'Hey Jude'];var Artists = ['ACDC', 'Led Zeppelin',
'The Beatles'];function eraseDatabase() {}function restore_database()
{}class animal {}class Alpaca {}

正例:

var DAYS_IN_WEEK = 
7;var DAYS_IN_MONTH = 30;var songs = ['Back In Black', 'Stairway to
Heaven', 'Hey Jude'];var artists = ['ACDC', 'Led Zeppelin', 'The
Beatles'];function eraseDatabase() {}function restoreDatabase() {}class
Animal {}class Alpaca {}

调用函数的函数和被调函数应放在较近的位置

当函数间存在相互调用的情况时,应将两者置于较近的位置。

理想情况下,应将调用其他函数的函数写在被调用函数的上方。

反例:

class PerformanceReview { constructor(employee) { this.employee = employee;

正例:

class PerformanceReview { constructor(employee) { this.employee = employee;

注释

只对存在一定业务逻辑复制性的代码进行注释

注释并不是必须的,好的代码是能够让人一目了然,不用过多无谓的注释。

反例:

function hashIt(data) { // The hash

正例:

function hashIt(data) {

不要在代码库中遗留被注释掉的代码

版本控制的存在是有原因的。让旧代码存在于你的history里吧。

反例:

doStuff();

正例:

doStuff();

不需要版本更新类型注释

记住,我们可以使用版本控制。废代码、被注释的代码及用注释记录代码中的版本更新说明都是没有必要的。

需要时可以使用git log获取历史版本。

反例:

/**

正例:

function combine(a, b) { return a + b;

避免位置标记

这些东西通常只能代码麻烦,采用适当的缩进就可以了。

反例:

//////////////////////////////////////////////////////////////////////////////////
Scope Model
Instantiation////////////////////////////////////////////////////////////////////////////////let
$scope.model = {

正例:

let $scope.model = {

避免在源文件中写入法律评论

将你的LICENSE文件置于源码目录树的根目录。

反例:

/*

正例:

function calculateBill() { // ...}

学习IT编程技术,就到职坐标,如果你喜欢我们的文章,可以点击右上角关注我们。想看到更多IT界的资讯,关注职坐标公众号,每天为你奉上最新行业动态及技术干货。

公众号:Zhizuobiao_Online(职坐标在线)

本文为头条号作者发布,不代表今日头条立场。

智能推荐

注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2019 ITdaan.com 粤ICP备14056181号  

赞助商广告