人类可以阅读的编写代码的重要性

本文由Matt BurnettSimon CodringtonNilson Jacques进行同行评审。 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

您是否曾经在一次运行中完成过一个项目,而无需再次查看代码? 我也没有。在进行较旧的项目时,您可能想花费很少甚至没有时间弄清楚代码是如何工作的。 必须保持可读的代码,以保持产品的可维护性,并使您自己以及您的同事或合作者感到高兴。

JS1k竞赛中可以找到夸大的不可读代码示例, 竞赛的目标是编写具有1024个字符或更少字符的最佳JavaScript应用程序,以及仅使用六个不同字符的深奥编程风格JSF * ck (顺便说一下,NSFW)编写JavaScript代码。 在这两个站点上查看代码都将使您感到疑惑。 想象一下编写此类代码并在几个月后尝试修复错误。

如果您定期浏览互联网或构建界面,您可能会知道,退出一个看起来又大又小的大型表单要容易一些。 关于代码也可以这样说。 当被认为更易于阅读和工作时,您可能会喜欢在其中进行更多的工作。 至少可以避免您无奈地扔掉计算机。

在本文中,我将介绍一些技巧和窍门,以使您的代码更具可读性,并避免一些陷阱。

代码分割

坚持使用形式比喻,形式有时会分成几部分,从而使它们看起来没有障碍。 使用代码也可以这样做。 通过将其拆分成多个部分,读者可以跳过与自己相关的内容,而不必在丛林中耕作。

跨文件

多年以来,我们一直在为网络优化事物。 JavaScript文件也不例外。 考虑到最小化和HTTP / 2之前的版本 ,我们通过将脚本合并为一个脚本来保存HTTP请求。 今天,我们可以根据需要进行工作,并有一个任务执行器(例如GulpGrunt)来处理文件。 可以肯定地说,我们可以按照自己喜欢的方式进行编程,而将优化(例如并置)留给工具。

// Load user data from API
var getUsersRequest = new XMLHttpRequest();
getUsersRequest.open('GET', '/api/users', true);
getUsersRequest.addEventListener('load', function() {
    // Do something with users
});

getUsersRequest.send();

//---------------------------------------------------
// Different functionality starts here. Perhaps
// this is an opportunity to split into files.
//---------------------------------------------------

// Load post data from API
var getPostsRequest = new XMLHttpRequest();
getPostsRequest.open('GET', '/api/posts', true);
getPostsRequest.addEventListener('load', function() {
    // Do something with posts
});

getPostsRequest.send();

功能

函数使我们可以创建可重用的代码块。 通常,函数的内容是缩进的,这样可以很容易地看到函数的开始和结束位置。 一个好习惯是使函数保持很小(10行或更少)。 正确命名函数后,也很容易理解调用函数时发生的情况。 稍后我们将讨论命名约定。

// Load user data from API
function getUsers(callback) {
    var getUsersRequest = new XMLHttpRequest();
    getUsersRequest.open('GET', '/api/users', true);
    getUsersRequest.addEventListener('load', function() {
        callback(JSON.parse(getUsersRequest.responseText));
    });

    getUsersRequest.send();
}

// Load post data from API
function getPosts(callback) {
    var getPostsRequest = new XMLHttpRequest();
    getPostsRequest.open('GET', '/api/posts', true);
    getPostsRequest.addEventListener('load', function() {
        callback(JSON.parse(getPostsRequest.responseText));
    });

    getPostsRequest.send();
}

// Because of proper naming, it’s easy to understand this code 
// without reading the actual functions
getUsers(function(users) {
    // Do something with users
});
getPosts(function(posts) {
    // Do something with posts
});

我们可以简化上面的代码。 请注意,两个功能几乎是相同的吗? 我们可以应用“ 不要重复自己做 (DRY)”原则。 这样可以防止混乱。

function fetchJson(url, callback) {
    var request = new XMLHttpRequest();
    request.open('GET', url, true);
    request.addEventListener('load', function() {
        callback(JSON.parse(request.responseText));
    });

    request.send();
}

// The below code is still easy to understand 
// without reading the above function
fetchJson('/api/users', function(users) {
    // Do something with users
});
fetchJson('/api/posts', function(posts) {
    // Do something with posts
});

如果我们想通过POST请求创建一个新用户怎么办? 此时,一种选择是向该函数添加可选参数,向该函数引入新的逻辑,从而使其对于一个函数而言过于复杂。 另一种选择是专门为POST请求创建一个新功能,这将导致重复的代码。

通过面向对象编程 ,我们可以充分利用两者,从而使我们能够创建可配置的一次性对象,同时保持其可维护性。

注意 :如果您需要专门针对面向对象的JavaScript的入门知识,建议您观看以下视频: 面向对象的JavaScript权威指南

面向对象编程

考虑对象(通常称为类),它们是上下文感知的功能簇。 一个对象非常适合放在专用文件中。 在我们的例子中,我们可以为XMLHttpRequest构建一个基本的包装器。

HttpRequest.js

function HttpRequest(url) {
    this.request = new XMLHttpRequest();

    this.body = undefined;
    this.method = HttpRequest.METHOD_GET;
    this.url = url;

    this.responseParser = undefined;
}

HttpRequest.METHOD_GET = 'GET';
HttpRequest.METHOD_POST = 'POST';

HttpRequest.prototype.setMethod = function(method) {
    this.method = method;
    return this;
};

HttpRequest.prototype.setBody = function(body) {
    if (typeof body === 'object') {
        body = JSON.stringify(body);
    }

    this.body = body;
    return this;
};

HttpRequest.prototype.setResponseParser = function(responseParser) {
    if (typeof responseParser !== 'function') return;

    this.responseParser = responseParser;
    return this;
};

HttpRequest.prototype.send = function(callback) {
    this.request.addEventListener('load', function() {
        if (this.responseParser) {
            callback(this.responseParser(this.request.responseText));
        } else {
            callback(this.request.responseText);
        }
    }, false);

    this.request.open(this.method, this.url, true);
    this.request.send(this.body);
    return this;
};

app.js

new HttpRequest('/users')
    .setResponseParser(JSON.parse)
    .send(function(users) {
        // Do something with users
    });

new HttpRequest('/posts')
    .setResponseParser(JSON.parse)
    .send(function(posts) {
        // Do something with posts
    });

// Create a new user
new HttpRequest('/user')
    .setMethod(HttpRequest.METHOD_POST)
    .setBody({
        name: 'Tim',
        email: 'info@example.com'
    })
    .setResponseParser(JSON.parse)
    .send(function(user) {
        // Do something with new user
    });

上面创建的HttpRequest类现在可以高度配置,因此可以应用于我们的许多API调用。 尽管实现(一系列链接方法调用)的实现更为复杂,但类的功能仍易于维护。 在实现和可重用性之间寻找平衡是困难的,并且是针对特定项目的。

使用OOP时,设计模式会增加很多功能。 尽管它们本身并不能提高可读性,但是一致性却可以!

人的语法

文件,函数,对象,这些只是粗略的线条。 它们使您的代码易于扫描 。 使代码易于阅读是一门更加细微的技巧。 最细微的细节可以带来很大的不同。 例如,将行长度限制为80个字符是一种简单的解决方案,通常由编辑人员通过垂直线来强制实施。 但是还有更多!

命名

适当的命名可以立即识别,从而省去了查找值或函数功能的麻烦。

功能通常是驼峰式的。 以动词开头,后跟主语通常会有所帮助。

function getApiUrl() { /* ... */ }
function setRequestMethod() { /* ... */ }
function findItemsById(n) { /* ... */ }
function hideSearchForm() { /* ... */ }

对于变量名,请尝试应用倒金字塔方法。 主题首先出现,属性随后出现。


var element = document.getElementById('body'),
    elementChildren = element.children,
    elementChildrenCount = elementChildren.length;

// When defining a set of colours, I prefix the variable with “color”
var colorBackground = 0xFAFAFA,
    colorPrimary = 0x663399;

// When defining a set of background properties, I use background as base
var backgroundColor = 0xFAFAFA,
    backgroundImages = ['foo.png', 'bar.png'];

// Context can make all the difference
var headerBackgroundColor = 0xFAFAFA,
    headerTextColor = 0x663399;

能够分辨出常规变量和特殊变量之间的区别也很重要。 例如,常量的名称通常用大写字母和下划线表示。

var URI_ROOT = window.location.href;

上课通常以驼峰式为例,以大写字母开头。

function FooObject {
    // ...
}

一个小细节是缩写。 有些人选择用大写字母写缩写,而另一些人选择坚持使用驼峰式写法。 使用前者可能会使识别后面的缩写更加困难。

紧凑性和优化

在许多代码库中,您可能会遇到“特殊”代码以减少字符数或提高算法性能。

单行代码是紧凑代码的示例。 不幸的是,他们经常依靠黑客或晦涩的语法。 嵌套三元运算符是一种常见的情况,如下所示。 尽管结构紧凑,但与常规的if语句相反,它也可能需要一两秒钟来了解其功能。 注意语法快捷方式。

// Yay, someone managed to make this a one-liner!
var state = isHidden ? 'hidden' : isAnimating ? 'animating' : '';

// Yay, someone managed to make this readable!
var state = '';
if (isAnimating) state = 'animating';
if (isHidden) state = 'hidden';

微观优化是性能优化,通常影响不大。 在大多数情况下,它们的可读性不如同等性能的同类产品。

// This may be most performant
$el[0].checked;

// But these are still fast, and are much easier to read
// Source: http://jsperf.com/prop-vs-ischecked/5
$el.prop('checked');
$el.is(':checked');
$el.attr('checked');

JavaScript编译器确实非常擅长为我们优化代码,而且它们还在不断改进。 除非未优化的代码与优化的代码之间的区别非常明显 (除非通常要经过数千或数百万次的操作),否则建议您更容易阅读。

非代码

讽刺的是,但是保持代码可读性的更好方法是添加未执行的语法。 我们称其为非代码。

空格

我很确定每个开发人员都有另一个开发人员供应,或者已经检查了站点的缩小代码-删除了大多数空白的代码。 第一次碰到这个可能会很令人惊讶。 在不同的视觉艺术领域,例如设计和版式中,空白空间与填充一样重要。 您将需要在两者之间找到微妙的平衡。 对于每个公司,每个团队,每个开发人员,对于这种平衡的观点有所不同。 幸运的是,有一些公认的规则:

  • 每行一个表达式
  • 缩进块的内容,
  • 可以使用额外的中断来分隔代码部分。

任何其他规则都应与您一起工作的人讨论。 无论您同意哪种代码样式,一致性都是关键。

function sendPostRequest(url, data, cb) {
    // A few assignments grouped together and neatly indented
    var requestMethod = 'POST',
        requestHeaders = {
            'Content-Type': 'text/plain'
        };

    // XMLHttpRequest initialisation, configuration and submission
    var request = new XMLHttpRequest();
    request.addEventListener('load', cb, false);
    request.open(requestMethod, url, false);
    request.send(data);
}

注释

就像空白一样,注释可以是一种很好的方式,使代码具有一定的气势,但同时也可以为代码添加细节。 确保添加注释以显示:

  • 非显而易见代码的解释和论证,
  • 修复程序可以解决哪些错误或怪异,并在可用时提供。

// Sum values for the graph’s range
var sum = values.reduce(function(previousValue, currentValue) { 
    return previousValue + currentValue;
});

并非所有修复程序都是显而易见的。 放置其他信息可以澄清很多:

if ('addEventListener' in element) {
    element.addEventListener('click', myFunc);
}
// IE8 and lower do not support .addEventListener, 
// so .attachEvent should be used instead
// http://caniuse.com/#search=addEventListener
// https://msdn.microsoft.com/en-us/library/ms536343%28VS.85%29.aspx
else {
    element.attachEvent('click', myFunc);
}

内联文档

编写面向对象的软件时,内联文档可以像常规注释一样为您的代码提供一些喘息的空间。 它们还有助于阐明属性或方法的目的和细节。 许多IDE使用它们作为提示,并且生成的文档工具也使用它们! 无论是什么原因,编写文档都是一种很好的实践。

/**
 * Create a HTTP request
 * @constructor
 * @param {string} url
 */
function HttpRequest(url) {
    // ...
}

/**
 * Set an object of headers
 * @param {Object} headers
 * @return {HttpRequest}
 */
HttpRequest.prototype.setHeaders = function(headers) {
    for (var header in headers) {
        this.headers[header] = headers[header];
    }

    // Return self for chaining
    return this;
};

回调难题

事件和异步调用是JavaScript的强大功能,但通常会使代码难以阅读。

异步调用通常随回调一起提供。 有时,您想按顺序运行它们,或等待它们准备就绪。

function doRequest(url, success, error) { /* ... */ }

doRequest('https://example.com/api/users', function(users) {
    doRequest('https://example.com/api/posts', function(posts) {
        // Do something with users and posts
    }, function(error) {
        // /api/posts went wrong
    });
}, function(error) {
    // /api/users went wrong
});

ES2015(也称为ES6)中引入了Promise对象以解决这两个问题。 它允许您展平嵌套的异步请求。

function doRequest(url) {
    return new Promise(function(resolve, reject) {
        // Initialise request
        // Call resolve(response) on success
        // Call reject(error) on error
    });
}

// Request users first
doRequest('https://example.com/api/users')
// .then() is executed when they all executed successfully
.then(function(users) { /* ... */ })
// .catch() is executed when any of the promises fired the reject() function
.catch(function(error) { /* ... */ });

// Run multiple promises parallel
Promise.all([
    doRequest('https://example.com/api/users'),
    doRequest('https://example.com/api/posts')
])
.then(function(responses) { /* ... */ })
.catch(function(error) { /* ... */ });

尽管我们引入了其他代码,但更易于正确解释。 您可以在此处阅读有关Promises的更多信息: JavaScript成为异步的(而且很棒)

ES6 / ES2015

如果您了解ES2015规范,则可能已经注意到本文中的所有代码示例均为旧版本( Promise对象除外)。 尽管ES6具有强大的功能,但在可读性方面还是存在一些问题。

所述脂肪箭头语法定义了一个函数,它继承的值this从它的父范围。 至少,这就是设计它的原因。 使用它来定义常规函数也很诱人。

var add = (a, b) => a + b;
console.log(add(1, 2)); // 3

另一个示例是rest和spread语法。

/**
 * Sums a list of numbers
 * @param {Array} numbers
 * @return {Number}
 */
function add(...numbers) {
    return n.reduce(function(previousValue, currentValue) {
        return previousValue + currentValue;
    }, 0);
}

add(...[1, 2, 3]);

/**
 * Sums a, b and c
 * @param {Number} a
 * @param {Number} b
 * @param {Number} c
 * @return {Number}
 */
function add(a, b, c) {
    return a + b + c;
}

add(1, 2, 3);

我的观点是,ES2015规范引入了许多有用的但晦涩难懂的语法,有时使它们容易被单行代码滥用。 我不想阻止使用这些功能。 我想鼓励谨慎使用它们。

结论

在项目的每个阶段都要记住保持代码的可读性和可维护性。 从文件系统到微小的语法选择,一切都很重要。 特别是在团队中,很难一直执行所有规则。 代码审查可以提供帮助,但仍然留有人为错误的余地。 幸运的是,有工具可以帮助您!

  • JSHint –保持代码无错误的JavaScript linter
  • 惯用语 -一种流行的代码样式标准,但可以随意更改
  • EditorConfig –定义跨编辑器的代码样式

除了代码质量和样式工具之外,还有一些工具可以使任何代码都易于阅读。 尝试使用不同的语法突出显示主题,或者尝试使用微型地图来查看脚本的自顶向下概述( AtomBrackets )。

您对编写可读和可维护的代码有何想法? 我希望在下面的评论中听到他们的声音。

From: https://www.sitepoint.com/importance-of-code-that-humans-can-read/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值