Эта статья была рецензирована Мэттом Бернеттом , Саймоном Кодрингтоном и Нильсоном Жаком . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Вы когда-нибудь заканчивали проект за один запуск, и вам больше не приходилось смотреть на код снова? Я тоже. Когда вы работаете над старым проектом, вы, вероятно, захотите потратить немного времени на изучение того, как работает код. Читаемый код обязателен для обеспечения удобства сопровождения продукта и для того, чтобы вы и ваши коллеги или сотрудники были довольны.
Преувеличенные примеры нечитаемого кода можно найти в конкурсах JS1k , целью которых является написание лучших приложений JavaScript с 1024 символами или менее, и JSF * ck (кстати, NSFW), стиль эзотерического программирования, который использует только шесть различных символов написать код JavaScript. Просмотр кода на любом из этих сайтов заставит задуматься, что происходит. Представьте, что вы пишете такой код и пытаетесь исправить ошибку несколько месяцев спустя.
Если вы регулярно пользуетесь Интернетом или создаете интерфейсы, вы можете знать, что выйти из большой, громоздкой формы легче, чем простой и небольшой. То же самое можно сказать и о коде. Когда воспринимается как легче читать и работать, можно больше работать над этим. По крайней мере, это избавит вас от разочарования.
В этой статье я расскажу о советах и хитростях, которые сделают ваш код более читабельным, а также о подводных камнях, которых следует избегать.
Разделение кода
Придерживаясь аналогии с формой, формы иногда разделяются на части, что делает их менее сложными. То же самое можно сделать с кодом. Разделив его на части, читатели могут пропустить то, что им нужно, вместо того, чтобы пахать в джунглях.
Через файлы
В течение многих лет мы оптимизировали вещи для Интернета. Файлы JavaScript не являются исключением из этого. Подумайте о минимизации и до HTTP / 2 , мы сохранили HTTP-запросы, объединив сценарии в один. Сегодня мы можем работать так, как нам хочется, и у нас есть обработчик задач, такой как Gulp или Grunt, обрабатывающий наши файлы. Можно с уверенностью сказать, что мы можем программировать так, как нам нравится, и оставить оптимизацию (например, конкатенацию) инструментам.
// 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
});
Мы можем упростить приведенный выше код. Обратите внимание, как обе функции практически идентичны? Мы можем применять принцип « Не повторяйся» (СУХОЙ). Это предотвращает беспорядок.
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: '[email protected]'
})
.setResponseParser(JSON.parse)
.send(function(user) {
// Do something with new user
});
Созданный выше класс HttpRequest
Несмотря на более сложную реализацию — серию связанных вызовов методов — функции класса просты в обслуживании. Поиск баланса между реализацией и возможностью повторного использования может быть трудным и зависит от проекта.
При использовании ООП шаблоны проектирования являются отличным дополнением. Хотя они не улучшают читабельность как таковые, согласованность делает!
Синтаксис человека
Файлы, функции, объекты — это только грубые линии. Они делают ваш код легко сканируемым . Сделать код легко читаемым — гораздо более нюанс. Мельчайшие детали могут иметь большое значение. Например, ограничение длины строки до 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 действительно хороши в оптимизации кода для нас, и они продолжают улучшаться. Если не заметна разница между неоптимизированным и оптимизированным кодом, что часто происходит после тысяч или миллионов операций, рекомендуется перейти на более простое чтение.
Non-Code
Назовите это иронией, но лучший способ сохранить читабельность кода — это добавить синтаксис, который не выполняется. Давайте назовем это не кодом.
Пробелы
Я почти уверен, что у каждого разработчика был другой ресурс для разработчиков, или он проверял минимизированный код сайта — код, в котором удалено большинство пробелов. Встреча с этим в первый раз может быть довольно неожиданной. В различных областях визуального искусства, таких как дизайн и типографика, пустое пространство так же важно, как заполнение. Вы захотите найти тонкий баланс между двумя. Мнения об этом балансе варьируются в зависимости от компании, команды, разработчика. К счастью, есть несколько универсально согласованных правил:
- одно выражение на строку,
- сделать отступ в блоке,
- дополнительный разрыв может быть использован для разделения разделов кода.
Любое другое правило должно обсуждаться с кем бы вы ни работали. Независимо от стиля кода, с которым вы согласны, согласованность является ключевым фактором
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
});
Объект 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) { /* ... */ });
Хотя мы ввели дополнительный код, его легче правильно интерпретировать. Вы можете прочитать больше об Обещаниях здесь: JavaScript становится асинхронным (и это потрясающе)
ES6 / ES2015
Если вам известна спецификация ES2015, возможно, вы заметили, что все примеры кода в этой статье относятся к более старым версиям (за исключением объекта Promise
Несмотря на то, что ES6 предоставляет нам отличные возможности, есть некоторые проблемы с удобочитаемостью.
Синтаксис жирной стрелки определяет функцию, которая наследует значение this
По крайней мере, именно поэтому он был разработан. Соблазнительно использовать его для определения обычных функций.
var add = (a, b) => a + b;
console.log(add(1, 2)); // 3
Другой пример — синтаксис отдыха и распространения.
/**
* 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-линтер, чтобы сохранить код без ошибок
- Idiomatic — популярный стандарт стиля кода, но не стесняйтесь отклоняться
- EditorConfig — определение стилей кода кросс-редактора
Помимо инструментов качества и стиля кода, существуют также инструменты, облегчающие чтение любого кода. Попробуйте разные темы подсветки синтаксиса или мини-карту, чтобы увидеть нисходящий обзор вашего скрипта ( Atom , Brackets ).
Что вы думаете о написании читаемого и поддерживаемого кода? Я хотел бы услышать их в комментариях ниже.