JavaScript quality guide
С помощью советов предложенных в данном руководстве вы будете писать код, понятный любой команде разработчиков.
От переводчика
Всем привет, с вами Максим Иванов, и сегодня мы поговорим о правилах оформления кода на языке JavaScript. Николя Бэвакуа (Nicolás Bevacqua), автор книги «Дизайн JavaScript-приложений» (JavaScript Application Design), разработчик из Аргентины, опубликовал данное руководство достаточно давно, первая запись появилась еще в 2014 году, многое написано по стандарту ES5, однако, в наши дни это все равно актуально, сейчас, когда ES6 еще нигде полноценно не работает без babel и прочих транспайлеров. Хотя мы видим прогресс в топовых десктопных браузерах (Google Crhome, Firefox), где уже реализовано 70-90% задуманного, мы видим, что они стремятся поддерживать новый стандарт, но, к сожалению, ещё нет браузеров, которые полностью могли бы поддерживать ES6. К слову, я буду очень рад вашим комментариям. В общем, удачи и давайте начнем.
Введение
Эти рекомендации не высечены на камне, это базовые правила, которые помогут вам научиться писать более последовательный и понятный код. Чтобы эффективно применять на практике предложенные правила, начните прямо сейчас, делитесь ими со своими коллегами и внедряйте в производство. Однако, не зацикливайтесь на этих правилах, это может быть бесплодным и контрпродуктивным. Попробуйте найти золотую середину, чтобы всем было комфортно в вашем коллективе. Автоматизированная проверка стиля кода это довольно полезная вещь, так как стиль написания кода это дело каждого.
Содержание
- Модули
- Строгий режим
- Форматирование пробелов
- Точка с запятой
- Стиль кода
- Анализ кода на ошибки
- Строки
- Инициализация переменных
- Условные конструкции
- Сравнения
- Тернарный оператор
- Функции
- Прототипы и наследования
- Объекты
- Массивы
- Регулярные выражения
- Консоль разработчика
- Комментарии
- Именование
- Polyfill-библиотеки
- Ежедневные хитрости
- Руководство по ES6
Модули
Этот пункт предполагает, что вы используете модульные системы такие как CommonJS, AMD, ES6 Modules, или любые другие. Модульные системы работают с отдельной областью видимости, не затрагивая глобальные объекты, также они обеспечивают более организованную структуру кода за счет автоматической генерации зависимостей, освобождая вас от самостоятельной вставки тега <script>.
Модульные системы могут подгружать шаблоны, которые в свою очередь полезны для локальных решений, когда речь идет о тестировании отдельного изолированного компонента.
К прочтению:
1. Что такое модуль?
2. Модули CommonJS
3. Модули в JS
Строгий режим
Всегда используйте 'use strict'; в верхней части своего модуля. Строгий режим позволяет отлавливать ошибки языка, хоть это и немного страшное название, такой режим позволяет интерпретатору следить за качеством вашего кода, тем самым вы тратите меньше времени на исправление ошибок.
К прочтению
1. Директива use strict
2. Strict Mode
Форматирование пробелов
Контролируйте количество пробелов при форматировании кода. Для этого рекомендуется использовать файл конфигурации .editorconfig. Я предлагаю использовать такие настройки.
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
Использование табуляции и пробелов ваше личное дело, однако я рекомендую делать отступы в два пробела. Файл .editorconfig позаботится обо всем, процесс форматирования будет контролироваться и тогда, когда вы нажмете клавишу Tab.
Можно настроить не только правильную табуляцию, можно также настроить и отступы в нужных местах, например, до, после или между чем-либо. Правда выполнить контроль над такими вещами довольно трудоемкое занятие.
function () {}
function( a, b ){}
function(a, b) {}
function (a,b) {}
Старайтесь свести к минимуму различия в форматировании своего кода.
Там, где это возможно, улучшайте читабельность кода, старайтесь писать в одной строке максимум 80 символов.
К прочтению:
1. EditorConfig
2. Одни настройки для всех редакторов
3. Установка EditorConfig в Sublime text 3
4. 80-characters
5. Хороший стиль программирования
Точка с запятой
Большинство JavaScript-программистов предпочитают использовать точку с запятой. Точку с запятой необходимо ставить всегда для того, чтобы избежать автоматической подстановки (ASI). Есть языки, в которых точка с запятой не обязательна, и её там никто не ставит. В JavaScript перевод строки её заменяет, но лишь частично, поэтому лучше её ставить, если вы понимаете правила ASI.
Независимо от того, что вы уверены в своем коде, используйте валидатор (linter), чтобы отлавливать ненужные точки с запятой.
К прочтению:
1. JavaScript Semicolon Insertion
2. Советы по стилю кода
3. Повышение качества javascript кода
4. Всё, что надо знать о точке с запятой
5. Открытое письмо лидерам JS касательно точек с запятой
Стиль кода
Не стоит заморачиваться на этом, ведь это сверхболезненный и напряженный момент, не приносящий заметного успеха при соблюдении такой же строгой политики в других областях.
Разумеется, некий стандарт оформления кода (стиль программирования) (англ. coding convention) существует. В мире JavaScript есть даже инструменты для проверки вашего кода на соблюдение этого стандарта — JSCS (JavaScript Code Style). Наличие общего стиля программирования облегчает понимание и поддержание исходного кода, написанного более чем одним программистом, а также упрощает взаимодействие нескольких человек при разработке программного обеспечения.
К таким стандартам приходят как программисты, так и лучшие команды, так и целые компании:
1. Airbnb
2. Дуглас Крокфорд
3. Google
4. Grunt
5. Idiomatic
6. jQuery
7. MDCS
8. Node.js
9. Wikimedia
10. WordPress
11. Яндекс
К прочтению:
1. Стиль программирования
2. JavaScript Code Style
Анализ кода на ошибки
С другой стороны, валидация иногда необходима. Если вы не используете валидатор, значит вы точно уверены в том как работает ваш код, и вам не нужно использовать, к примеру, JSLint (инструмент проверки качества программного кода). И все же, я рекомендую пользоваться JSHint или ESLint.
Несколько советов в использовании JSHint:
- Объявите .jshintignore файл в node_modules, bower_components и других
- Используйте правила ниже в файле .jshintrc
{
"curly": true,
"eqeqeq": true,
"newcap": true,
"noarg": true,
"noempty": true,
"nonew": true,
"sub": true,
"undef": true,
"unused": true,
"trailing": true,
"boss": true,
"eqnull": true,
"strict": true,
"immed": true,
"expr": true,
"latedef": "nofunc",
"quotmark": "single",
"indent": 2,
"node": true
}
Конечно, вы не обязаны строго придерживаться этих правил, но важно найти золотую середину между валидатором и иными стилями программирования.
К прочтению:
1. JSLint
2. JSHint
3. Несколько советов по настройке ESlint
4. Настройка JSHint в sublime text 3
5. JavaScript-линтинг
Строки
Все строки должны иметь один и тот же тип кавычек. Используйте одинарные или двойные кавычки во всем коде.
// плохо
var message = 'oh hai ' + name + "!";
// хорошо
var message = 'oh hai ' + name + '!';
Это будет работать быстрее, если воспользуетесь функцией форматирования похожей на util.format в Node.js. Таким образом, вам будет проще форматировать строки, а код будет выглядеть намного чище.
// лучше
var message = util.format('oh hai %s!', name);
Вы можете реализовать что-то подобное, воспользовавшись куском кода ниже.
function format () {
var args = [].slice.call(arguments);
var initial = args.shift();
function replacer (text, replacement) {
return text.replace('%s', replacement);
}
return args.reduce(replacer, initial);
}
Для объявления мультистрок (многострочных), особенно когда речь идет об HTML сниппетах, иногда лучше использовать массив в качестве буфера данных, элементы которого в дальнейшем можно объединить в строку. Конечно, конкатенировать (объединять) строки гораздо легче в привычном стиле и, конечно, это будет работает быстрее, но иногда это труднее отслеживать.
var html = [
'<div>',
format('<span class="monster">%s</span>', name),
'</div>'
].join('');
В объединяемый массив можно вставлять свои сниппеты и в конце объединять их для лучшего чтения кода. Именно так работают некоторые шаблонизаторы, например Jade.
К прочтению:
1. Строки
2. Работа со строками
3. Производительность конкатенации строк
4. C-подобное представление строк на основе типизированных массивов
Инициализация переменных
Всегда объявляйте переменные по смыслу и семантике, фиксируя их в верхней части области видимости. Приветствуется инициализация переменных в каждой строке по одиночке. Конечно, можно один раз объявить оператор var, а затем через запятую указывать переменные, однако будьте последовательны в масштабах целого проекта.
// плохо
var foo = 1,
bar = 2;
var baz;
var pony;
var a
, b;
// плохо
var foo = 1;
if (foo > 1) {
var bar = 2;
}
Во всяком случае, вы точно видите, где объявлена конкретная переменная
// хорошо
var foo = 1;
var bar = 2;
var baz;
var pony;
var a;
var b;
// хорошо
var foo = 1;
var bar;
if (foo > 1) {
bar = 2;
}
Объявлять пустые переменные можно одной строкой через запятую.
// приемлемо
var a = 'a';
var b = 2;
var i, j;
Условные конструкции
Используйте фигурные скобки. В некоторых случаях это поможет вам избежать критического бага у продукции Apple во время работы протокола безопасного соединения SSL/TLS.
// плохо
if (err) throw err;
// хорошо
if (err) { throw err; }
Ради понимания содержания, старайтесь держать содержимое блока условия не на одной строке
// лучше
if (err) {
throw err;
}
К прочтению:
1. Apple's SSL/TLS bug
2. Вот это fail
Сравнения
Избегайте использования операторов == и !=, используйте более благоприятные операторы сравнения === и! ==. Эти операторы называются жесткими операторами сравнения, в то время как их альтернативы (== и !=) преобразуют операнды к одному и тому же типу данных.
// плохо
function isEmptyString (text) {
return text == '';
}
isEmptyString(0);
// <- true
// хорошо
function isEmptyString (text) {
return text === '';
}
isEmptyString(0);
// <- false
Тернарный оператор
Тернарный оператор отчетливо подходит для конкретных условий и совершенно не подходит для многокомпонетных. Как правило, если ваш наметанный глаз не может разобрать условие в тернарном операторе настолько быстро, насколько ваш
jQuery является ярким примером, который наполнен неприятными трехкомпонентными операторами.
// плохо
function calculate (a, b) {
return a && b ? 11 : a ? 10 : b ? 1 : 0;
}
// хорошо
function getName (mobile) {
return mobile ? mobile.name : 'Generic Player';
}
В случаях, когда условие может оказаться запутанным просто используйте if и else.
Функции
При инициализации функции, используйте функциональную декларацию вместо функциональных выражений. Все это влияет на «поднятие» переменных и объявлении функций.
// плохо
var sum = function (x, y) {
return x + y;
};
// хорошо
function sum (x, y) {
return x + y;
}
Нет ничего плохого в функциональных выражениях, если вы их используете при карринге.
Имейте в виду, что функциональная декларация функций будет доступна и в области видимости выше, так что все это не имеет значения, в каком порядке была объявлена эта функция. Как правило, вы всегда должны объявлять функции в глобальной области видимости, при этом вы должны стараться избегать размещения таких функций внутри условных операторов.
// плохо
if (Math.random() > 0.5) {
sum(1, 3);
function sum (x, y) {
return x + y;
}
}
// хорошо
if (Math.random() > 0.5) {
sum(1, 3);
}
function sum (x, y) {
return x + y;
}
// хорошо
function sum (x, y) {
return x + y;
}
if (Math.random() > 0.5) {
sum(1, 3);
}
Если вам нужен пустой метод (no-op) вы можете использовать либо расширение с помощью прототипа Function.prototype или использовать функцию function noop () {}. Также, в идеале, у вас должна быть одна ссылка на собственный метод, которая используется во всем приложении. Вместо того, чтобы писать однотипные конструкции кода, ваш метод будет шаблоном для подобных конструкции.
// плохо
var divs = document.querySelectorAll('div');
for (i = 0; i < divs.length; i++) {
console.log(divs[i].innerHTML);
}
// хорошо
var divs = document.querySelectorAll('div');
[].slice.call(divs).forEach(function (div) {
console.log(div.innerHTML);
});
Привязка может быть осуществлена посредством функции .call() из прототипа функции Function.prototype, также запись может быть сокращена до [].slice.call(arguments) вместо использования Array.prototype.slice.call(). В любом случае, она может быть упрощена посредством использования функции bind().
Тем не менее, следует помнить, что существует значительное снижение производительности в движке V8 при использовании такого подхода.
// плохо
var args = [].slice.call(arguments);
// хорошо
var i;
var args = new Array(arguments.length);
for (i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
Не объявляйте функции внутри циклов
// плохо
var values = [1, 2, 3];
var i;
for (i = 0; i < values.length; i++) {
setTimeout(function () {
console.log(values[i]);
}, 1000 * i);
}
// плохо
var values = [1, 2, 3];
var i;
for (i = 0; i < values.length; i++) {
setTimeout(function (i) {
return function () {
console.log(values[i]);
};
}(i), 1000 * i);
}
// хорошо
var values = [1, 2, 3];
var i;
for (i = 0; i < values.length; i++) {
setTimeout(function (i) {
console.log(values[i]);
}, 1000 * i, i);
}
// хорошо
var values = [1, 2, 3];
var i;
for (i = 0; i < values.length; i++) {
wait(i);
}
function wait (i) {
setTimeout(function () {
console.log(values[i]);
}, 1000 * i);
}
Или еще лучше, просто используйте .forEach, который оптимизирован для случаев использования функции в цикле.
// лучше
[1, 2, 3].forEach(function (value, i) {
setTimeout(function () {
console.log(value);
}, 1000 * i);
});
Для того, чтобы просмотр стека вызовов был значительно проще необходимо сократить использование анонимных функций. То есть, если вы привыкли назначать все ваши обратные вызовы в качестве анонимных функций, вы можете попробовать присвоить им имена. Это позволит определить основную причину исключения при анализе трассировки стека.
// плохо
function once (fn) {
var ran = false;
return function () {
if (ran) { return };
ran = true;
fn.apply(this, arguments);
};
}
// хорошо
function once (fn) {
var ran = false;
return function run () {
if (ran) { return };
ran = true;
fn.apply(this, arguments);
};
}
Избегайте вложенных условий в функции, иногда лучше использовать пустое возвращаемое значение, тем самым ваш код будет читабельнее.
// плохо
function foo (car, black, turbine) {
if (car) {
if (black) {
if (turbine) {
return 'batman!';
}
}
}
}
// плохо
function fn (condition) {
if (condition) {
// 10+ строк кода
}
}
// хорошо
function foo (car, black, turbine) {
if (!car) {
return;
}
if (!black) {
return;
}
if (!turbine) {
return;
}
return 'batman!';
}
Если функция ничего не возвращает или возвращает пустой return, то она возвращает значение undefined.
// хорошо
function fn (condition) {
if (!condition) {
return;
}
// 10+ строк кода
}
К прочтению:
1. Область видимости в JavaScript и «поднятие» переменных и объявлений функций
2. Каррирование
3. Использование пустой функции, на примере jQuery
4. Массивоподобные объекты
5. Трассировка стека
6. Советы по отладке JavaScript в асинхронных стеках вызовов
7. JavaScript return в функции возвращает undefined
Прототипы и наследования
Любой ценой избегайте расширения стандартных прототипов. Если вам необходимо расширить функциональные возможности нативных типов данных (структур) языка, воспользуйтесь библиотекой poser или разработайте свою собственную реализацию структуры данных.
К стандартным типам данных относятся:
- String
- Number
- Boolean
К составным типам данных относятся:
- Object
- Function
- Array
// плохо
String.prototype.half = function () {
return this.substr(0, this.length / 2);
};
// хорошо
function half (text) {
return text.substr(0, text.length / 2);
}
Избегайте прототипную модель наследования, ведь это может сказаться на производительности. Одной из частых ошибок при программировании на языке JavaScript как раз является расширение базовых прототипов. Эта технология, называемая monkey patching, она нарушает принцип инкапсуляции. Несмотря на то, что она используется в широко распространенных фреймворках, таких как Prototype.js, на настоящий момент не существует разумных причин для ее использования, так как в данном случае встроенные типы «захламляются» дополнительной нестандартной функциональностью. Единственным оправданием расширения базовых прототипов является лишь эмуляция новых возможностей, таких как Array.forEach, для неподдерживающих их старых версий языка.
- Прототипное наследование заставляет вас использовать ключевое слово this всегда
- Тут больше абстракции, чем при использование простых объектов
- Это причина головной боли при создании новых объектов
- Старайтесь использовать простые объекты
К прочтению:
1. Типы данных JavaScript и структуры данных
2. Определение типа данных
3. Что такое прототип?
4. Прототипно-ориентированное программирование
5. Наследование и цепочка прототипов
6. Что такое this и определение контекста на практике
Объекты
Для инициализации объекта мы можем использовать фигурные скобки { }, которые будут являться литералом объекта. Используйте подход создания фабрики вместо использования чистого конструктора для создания объекта.
Создание объекта:
var World = {}; // пустой объект, создается при помощи литерала фигурных скобок
console.log(World); // Object {}
var World = {
people: "~ 7 млрд.",
country: "~ 258 стран"
};
console.log(World); // Object {people: "~ 7 млрд.", country: "~ 258 стран"}
Создание объекта при помощи пользовательского прототипа (подход в использовании конструктора):
// плохо
// вспоминаем, что прототип - это изначально пустой объект
var TemplateWorld = function(_people, _country) {
// в общем смысле, как только вы используете this внутри функции или создаете внутренние методы
// TemplateWorld перестает быть обычной функцией и становится прототипом
this.people = _people;
this.country = _country;
} // конструктор (и в тоже время прототип)
var World = new TemplateWorld("~ 7 млрд.", "~ 258 стран");
console.log(World); // TemplateWorld {people: "~ 7 млрд.", country: "~ 258 стран"}
Создание объекта при помощи пользовательского прототипа (подход в использовании фабрик):
// хорошо
var TemplateWorld = function(_people, _country) {
return {
people: _people,
country: _country
};
}
var World = TemplateWorld("~ 7 млрд.", "~ 258 стран");
console.log(World); // Object {people: "~ 7 млрд.", country: "~ 258 стран"}
Наглядный пример создания прототипа:
// хорошо
function util (options) {
// приватные методы и свойства прототипа
var foo;
function add () {
return foo++;
}
function reset () { // обратите внимание, что этот метод не становится публичным
foo = options.start || 0;
}
reset();
return {
// публичные методы и свойства прототипа
uuid: add
};
}
Копирование по ссылке и передача по значению
Более наглядно и прозрачно, как данные передаются между ячейками памяти видно в С/С++. Но в JavaScript, очень желательно помнить про механизм работы объектов и тогда отпадут некоторые вопросы сразу.
Переменные
// это называется передача по значению
// один кусок памяти, копируется в другую ячейку памяти
var a = 'text';
var b = a; // передача по значению
a = 'update';
console.log(a); // update
console.log(b); // text
Объекты
// это называется передача по ссылке
// в JavaScript это делаетcя неявно
var a = {foo: 'bar'};
var b = a; // передача по ссылке
// b - ссылка на a
a.foo = 'foo';
console.log(a); // Object {foo: 'foo'}
console.log(b); // Object {foo: 'foo'}
b.foo = 'bar';
console.log(a); // Object {foo: 'bar'}
console.log(b); // Object {foo: 'bar'}
К прочтению:
1. Объекты: передача по ссылке
Массивы
Для инициализации массива мы можем использовать квадратные скобки [ ], которые будут являться литералом массива. Для увеличения производительности, вы можете создать массив при помощи конструкции new Array(length) с указанием его длины (размера массива).
Для того, чтобы правильно манипулировать с элементами массива необходимо знать стандартные методы. Все гораздо проще, чем вы можете себе представить.
Стандартные методы:
Более продвинутые методы:
Копирование по ссылке
// Массив - это тот же объект
var a = [1, 2, 3];
var b = a; // передача по ссылке
// b - ссылка на a
a[2] = 4;
console.log(a); // [1, 2, 4]
console.log(b); // [1, 2, 4]
b[2] = 3;
console.log(a); // [1, 2, 3]
console.log(b); // [1, 2, 3]
Приступим к изучению стандартных методов:
- Перебор элементов делается при помощи .forEach
- Проверки при помощи .some и .every
- Объединение при помощи .join и .concat
- Работа со стеком и очередью при помощи .pop, .push, .shift, и .unshift
- Перебирающий метод (mapping) .map
- Выполнение запросов .filter
- Сортировка при помощи .sort
- Вычисления совместно с .reduce, .reduceRight
- Копирование при помощи .slice
- Более мощное средство, для удаления и добавление элементов .splice
- Поиск элемента в массиве .indexOf
- В помощь оператор in
- Перебор в обратном порядке или переворот массива с помощью .reverse
Перебор элементов делается при помощи .forEach
Это один из самых простых методов в JavaScript. Не поддерживается в IE7 и IE8.
Принимает функцию обратного вызова (сallback), которая вызывается один раз для каждого элемента в массиве, и пропускается через три основных аргумента:
- value — значение, которое содержит текущий элемент массива
- index — позиция элемента в массиве
- array — представляет собой ссылку на массив
Кроме того, мы можем передать необязательный второй аргумент (объект, массив, переменную, что угодно), этот аргумент будет доступен в callback-функции и представлен в виде контекста (this).
['_', 't', 'a', 'n', 'i', 'f', ']'].forEach(function (value, index, array) {
this.push(String.fromCharCode(value.charCodeAt() + index + 2))
}, out = [])
out.join('')
// <- 'awesome'
Мы схитрили, .join мы еще не рассматривали, но скоро дойдем до этого. В нашем случае, он объединяет различные элементы в массиве, этот метод гораздо эффективней работает, чем делать это вручную out[0] + '' + out[1] + '' + out[2] + '' + out[n]. Кстати, мы не можем разорвать foreach-цикл, как это делается в обычных переборах при помощи break. К счастью, у нас есть другие способы.
Проверки при помощи .some и .every
Эти методы относятся к assert-функциям. Если вы когда -либо работали с перечислениями в .NET, то поймете, что они похожи на своих кузенов .Any(x => x.IsAwesome) и .All(x => x.IsAwesome).
Эти методы аналогичны .forEach тем, что они также принимают callback-функцию с такими же аргументами, но немного отличаются по своей природе.
Метод some() вызывает переданную функцию callback один раз для каждого элемента, присутствующего в массиве до тех пор, пока не найдет такой, для которого callback вернет истинное значение (значение, становящееся равным true при приведении его к типу Boolean). Если такой элемент найден, метод some() немедленно вернёт true. В противном случае, если callback вернёт false для всех элементов массива, метод some() вернёт false. Функция callback вызывается только для индексов массива, имеющих присвоенные значения; она не вызывается для индексов, которые были удалены или которым значения никогда не присваивались.
max = -Infinity
satisfied = [10, 12, 10, 8, 5, 23].some(function (value, index, array) {
if (value > max) max = value
return value < 10
})
console.log(max)
// <- 12
satisfied
// <- true
Метод .every() работает таким же образом, но замыкания случаются лишь тогда, когда ваша callback-функция возвращает ложь, а не правду.
Объединение при помощи .join и .concat
Метод .join часто путают с методом .concat, как вы знаете, метод принимает разделитель в аргументе .join(separator), он создает строку, в результате чего он берет каждый элемент массива и разделяет их разделителем в этой строке. Если разделитель не указан, по умолчанию separator = ', '. Метод concat() возвращает новый массив, состоящий из массива, на котором он был вызван, соединённого с другими массивами и/или значениями, переданными в качестве аргументов.
- .concat работает по такому принципу: array.concat(val, val2, val3, valn)
- .concat возвращает новый массив
- array.concat() без аргументов возвращает неполную копию массива
Неполная копия означает, что копия будет содержать те же ссылки на объекты, что и исходный массив.
Как правило, и оригинал, и новый массив ссылаются на один и тот же объект.
То есть, если объект по ссылке будет изменён, изменения будут видны и в новом, и в исходном массивах.
var a = { foo: 'bar' }
var b = [1, 2, 3, a]
var c = b.concat()
console.log(b === c)
// <- false
a.foo = 'foo';
console.log(b); // [1, 2, 3, Object {foo: 'foo'}]
console.log(c); // [1, 2, 3, Object {foo: 'foo'}]
b[3] === a && c[3] === a
// <- true
Работа со стеком и очередью при помощи .pop, .push, .shift, и .unshift
В настоящее время, все знают, что добавление элементов в конец массива осуществляется с помощью .push. А можете ли вы сразу добавить несколько элементов, используя такую конструкцию [].push('a', 'b', 'c', 'd', 'z')?
var a = [].push('a', 'b', 'c', 'd', 'z');
console.log(a); // 5
var a = [];
a.push('a', 'b', 'c', 'd', 'z');
console.log(a); // ['a', 'b', 'c', 'd', 'z']
Метод pop() удаляет последний элемент из массива и возвращает его значение. Если массив пуст, возвращается undefined (void 0).
Используя .push и .pop мы можем разработать свою структура LIFO стека.
function Stack () {
this._stack = []
}
Stack.prototype.next = function () {
return this._stack.pop()
}
Stack.prototype.add = function () {
return this._stack.push.apply(this._stack, arguments)
}
stack = new Stack()
stack.add(1, 2, 3)
stack.next()
// <- 3
stack.next()
// <- 2
stack.next()
// <- 1
stack.next()
// <- undefined
Наоборот, мы можем создать FIFO очередь используя .unshift и .shift.
function Queue () {
this._queue = []
}
Queue.prototype.next = function () {
return this._queue.shift()
}
Queue.prototype.add = function () {
return this._queue.unshift.apply(this._queue, arguments)
}
queue = new Queue()
queue.add(1,2,3)
queue.next()
// <- 1
queue.next()
// <- 2
queue.next()
// <- 3
queue.next()
// <- undefined
Использование .shift (или .pop ) в цикле для набора элементов массива, мы можем очистить его в процессе работы.
list = [1,2,3,4,5,6,7,8,9,10]
while (item = list.shift()) {
console.log(item)
}
list
// <- []
Перебирающий метод (mapping) .map
Метод map() создаёт новый массив с результатом вызова указанной функции для каждого элемента массива.
Метод map вызывает переданную функцию callback один раз для каждого элемента в порядке их появления и конструирует новый массив из результатов её вызова. Функция callback вызывается только для индексов массива, имеющих присвоенные значения, включая undefined. Она не вызывается для пропущенных элементов массива (то есть для индексов, которые никогда не были заданы, которые были удалены или которым никогда не было присвоено значение).
Метод Array.prototype.map имеет ту же сигнатуру, что и у .forEach, .some, .every:
.map(fn(value, index, array), thisArgument).
values = [void 0, null, false, '']
values[7] = 'text'
result = values.map(function(value, index, array){
console.log(value)
return value
})
// <- [undefined, null, false, '', undefined × 3, undefined]
console.log(result[7]); // text
undefined × 3 — объясняют тем, что метод .map не отработал для удаленных или нераспределенных элементов массива, но они все равно по-прежнему включены в результирующий массив. Метод .map используется для трансформации массива.
// перебор
[1, '2', '30', '9'].map(function (value) {
return parseInt(value, 10)
})
// 1, 2, 30, 9
[97, 119, 101, 115, 111, 109, 101].map(String.fromCharCode).join('')
// <- 'awesome'
// используемый шаблон отображается в новых объектах
items.map(function (item) {
return {
id: item.id,
name: computeName(item)
}
})
Выполнение запросов .filter
Метод filter() создаёт новый массив со всеми элементами, прошедшими проверку, задаваемую в передаваемой функции.
Метод filter() вызывает переданную функцию callback один раз для каждого элемента, присутствующего в массиве, и конструирует новый массив со всеми значениями, для которых функция callback вернула true или значение, становящееся true при приведении в boolean. Функция callback вызывается только для индексов массива, имеющих присвоенные значения; она не вызывается для индексов, которые были удалены или которым значения никогда не присваивались. Элементы массива, не прошедшие проверку функцией callback, просто пропускаются и не включаются в новый массив.
Как обычно: .filter(fn(value, index, array), thisArgument). Учитывая, что .filter возвращает только те элементы, которые проходят проверку callback-функции, есть некоторые интересные варианты использования.
[void 0, null, false, '', 1].filter(function (value) {
return value
})
// <- [1]
[void 0, null, false, '', 1].filter(function (value) {
return !value
})
// <- [void 0, null, false, '']
Сортировка при помощи .sort
Метод sort() на месте сортирует элементы массива и возвращает отсортированный массив. Сортировка необязательно устойчива. Порядок cортировки по умолчанию соответствует порядку кодовых точек Unicode.
Сигнатура такая же как и большинство сортировочных функций: Array.prototype.sort(fn(a,b)), метод принимает обратный вызов, который проверяет два элемента, в итоге в зависимости от задачи возвращаемое значение может быть:
- return value < 0, a предшествует b
- return value === 0, если оба а и Ь считаются эквивалентны
- return value > 0, если a после b
[9,80,3,10,5,6].sort()
// <- [10, 3, 5, 6, 80, 9]
[9,80,3,10,5,6].sort(function (a, b) {
return a - b
})
// <- [3, 5, 6, 9, 10, 80]
Вычисления совместно с .reduce, .reduceRight
Метод reduce() выполняет функцию callback один раз для каждого элемента, присутствующего в массиве, за исключением пустот, принимая четыре аргумента: начальное значение (или значение от предыдущего вызова callback), значение текущего элемента, текущий индекс и массив, по которому происходит итерация.
В целом, методы reduce() и reduceRight() идентичны, за исключением того, что перебор элементов в методе reduce() идет слева направо, а в методе reduceRight() справа налево.
Оба метода имеют следующую сигнатуру: reduce(callback(previousValue, currentValue, index, array), initialValue).
При первом вызове функции параметры previousValue и currentValue могут принимать одно из двух значений. Если при вызове reduce() передан аргумент initialValue, то значение previousValue будет равным значению initialValue, а значение currentValue будет равным первому значению в массиве. Если аргумент initialValue не задан, то значение previousValue будет равным первому значению в массиве, а значение currentValue будет равным второму значению в массиве.
Один из типичных случаев использования .reduce функция суммы:
Array.prototype.sum = function () {
return this.reduce(function (partial, value) {
return partial + value
}, 0)
};
[3,4,5,6,10].sum()
// <- 28
Скажем, мы хотим конкатенировать несколько строк вместе. Мы могли бы использовать .join для этой цели. Но в случае объектов это не сработает, поэтому:
function concat (input) {
return input.reduce(function (partial, value) {
if (partial) {
partial += ', '
}
return partial + value.name
}, '')
}
concat([
{ name: 'George' },
{ name: 'Sam' },
{ name: 'Pear' }
])
// <- 'George, Sam, Pear'
Копирование при помощи .slice
Подобно .concat, вызов .slice без каких-либо аргументов произведет неполную копию исходного массива. Функция принимает два аргумента — начальное положение и конечное. Array.prototype.slice может быть использован для преобразования массивов в объекты и наоборот в массивы.
Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })
// <- ['a', 'b']
Но это не будет работать с .concat:
Array.prototype.concat.call({ 0: 'a', 1: 'b', length: 2 })
// <- [{ 0: 'a', 1: 'b', length: 2 }]
Кроме этого, еще один распространенный способ использования метода .slice — удаление нескольких элементов по списку аргументов:
function format (text, bold) {
if (bold) {
text = '<b>' + text + '</b>'
}
var values = Array.prototype.slice.call(arguments, 2)
values.forEach(function (value) {
text = text.replace('%s', value)
})
return text
}
format('some%sthing%s %s', true, 'some', 'other', 'things')
// <- <b>somesomethingother things</b>
Более мощное средство для удаления и добавление элементов .splice
Метод splice() изменяет содержимое массива, удаляя существующие элементы и/или добавляя новые. Если количество указанных вставляемых элементов будет отличным от количества удаляемых элементов, массив изменит длину после вызова. Обратите внимание, что эта функция изменяет исходный массив, в отличие от .concat или .slice.
var source = [1,2,3,8,8,8,8,8,9,10,11,12,13]
var spliced = source.splice(3, 4, 4, 5, 6, 7)
console.log(source)
// <- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ,13]
spliced
// <- [8, 8, 8, 8]
Возможно, уже отмечалось, что она также возвращает удаленные элементы и иногда это может пригодиться.
var source = [1,2,3,8,8,8,8,8,9,10,11,12,13]
var spliced = source.splice(9)
spliced.forEach(function (value) {
console.log('removed', value)
})
// <- removed 10
// <- removed 11
// <- removed 12
// <- removed 13
console.log(source)
// <- [1, 2, 3, 8, 8, 8, 8, 8, 9]
Поиск элемента в массиве .indexOf
Метод indexOf() сравнивает искомый элемент searchElement с элементами в массиве, используя строгое сравнение (тот же метод используется оператором ===, тройное равно).
var a = { foo: 'bar' }
var b = [a, 2]
console.log(b.indexOf(1))
// <- -1
console.log(b.indexOf({ foo: 'bar' }))
// <- -1
console.log(b.indexOf(a))
// <- 0
console.log(b.indexOf(a, 1))
// <- -1
b.indexOf(2, 1)
// <- 1
b.indexOf(2, a)
// <- 1
b.indexOf(a, 2)
// <- -1
В помощь оператор in
Распространенной ошибкой новичков на собеседовании является их запутанное мнение об одинаковой работе метода .indexOf и оператора in.
var a = [1, 2, 5]
1 in a
// <- true
// так как существует элемент a[1], который равен 2!
5 in a
// <- false
// так как a[5] не существует
Дело в том, что в оператор in проверяет ключ объекта, а не делает поиск по значению. Такой вариант гораздо быстрее работает, чем при использовании .indexOf:
var a = [3, 7, 6]
1 in a === !!a[1]
// <- true
Перебор в обратном порядке или переворот массива с помощью .reverse
Этот метод будет принимать элементы в массиве и располагать их в обратном порядке.
var a = [1, 1, 7, 8]
a.reverse()
// [8, 7, 1, 1]
К прочтению:
1. Array
2. Array vs. Object
3. Производительность массивов в JavaScript
4. Заставляем массивы работать быстрее
5. Производительность Objects/Arrays в JavaScript
6. Перебирающие методы
7. Библиотека, реализующая дополнительную функциональность для работы с массивами, объектами и функциями
Регулярные выражения
Регулярные выражения создаются при помощи двух слэшей / /, которые являются литералом регулярного выражения. Внутри вы указываете шаблон поиска. Старайтесь хранить регулярные выражения в переменных, не встраивайте их непосредственно в код (inline), это позволит значительно улучшить читаемость.
// плохо
if (/d+/.test(text)) { // non-precompiled, but faster
console.log('so many numbers!');
}
// хорошо
var numeric = /d+/; // precompiled
if (numeric.test(text)) {
console.log('so many numbers!');
}
Кроме того, вы можете ознакомиться с тем, как составлять регулярные выражения и узнать для чего они нужны. Вы можете также визуализировать их, посмотрев в онлайн режиме.
К прочтению:
1. Регулярные выражения
2. Класс RegExp
3. Cпособ поиска и замены для строк
4. Методы RegExp и String
5. JavaScript производительность precompiled регулярного выражения
6. Основные понятия и проблемы производительности
7. Задачи на работу с регулярными варажениями
Консоль разработчика
Для проверки состояния веб-приложения очень удобно использовать объект console, который в дальнейшем можно убрать при продакшене. Эта возможность не является стандартной и стандартизировать её пока никто не собирается. Также могут присутствовать большие несовместимости между реализациями, и её поведение может в будущем измениться. Поэтому не используйте её на сайтах, выкладывая свой код в интернет. При отладке своего веб-приложения старайтесь не записывать все подряд в журнал вывода (console.log
К прочтению:
1. Отладка JavaScript для начинающих
2. Использование console
3. Стек вызова. Трассировка
4. Вся правда о Chrome. Web Inspector
5. Chrome console API
Комментарии
Комментарии не предназначены для объяснения того, что делает ваш код. Хороший код должен говорить сам за себя. Если вы задумались о написании комментария в том месте, где хотите объяснить, что делает кусок вашего кода, скорее всего, вам необходимо изменить сам код. Исключением из этого правила является объяснение сложных участков кода, нагруженных синтаксическими конструкциями по типу регулярных выражений. Хороший комментарий должен рассказать о том, почему кусок кода работает именно так и в чем его конкретная цель.
// плохо
// создаем центрированный контейнер
var p = $('<p/>');
p.center(div);
p.text('foo');
// хорошо
var container = $('<p/>');
var contents = 'foo';
container.center(parent);
container.text(contents);
megaphone.on('data', function (value) {
container.text(value); // megaphone периодически обновляет данные в контейнере
});
var numeric = /d+/; // поиск одной или более цифр в строке
if (numeric.test(text)) {
console.log('so many numbers!');
}
Комментировать огромные блоки кода не следует, ведь у вас есть система контроля версий!
К прочтению:
1. 16 приёмов написания суперчитабельного кода
2. Лучшие практики комментирования кода
3. Комментирование функций
4. Генераторы JavaScript-документации
Именование
Переменные должны иметь осмысленные названия. Вы ведь не хотите прибегать к чтению комментариев или документации, чтобы понять, что делает часть данного кода.
// плохо
function a (x, y, z) {
return z * y / x;
}
a(4, 2, 6);
// <- 3
// хорошо
function ruleOfThree (had, got, have) {
return have * got / had;
}
ruleOfThree(4, 2, 6);
// <- 3
К прочтению:
1. JavaScript Style Guide and Coding Conventions
Polyfill-библиотеки
Там, где невозможно использовать нативные функции браузера, то есть они попросту не реализованы по умолчанию, включают polyfill-библиотеки, которые эмулируют необходимое нам поведение для поддержки этих функций в старых браузерах. В итоге, нам легче работать с кодом, теперь не нужно нагромождать его условными и прочими конструкциями для поддержания стабильности.
Если вам не хватает возможностей polyfill-библиотеки, вы можете написать плагин для такой библиотеки или просто самостоятельно расширить возможности стандартных функций, объектов, обернув их в свой polyfill.
К прочтению:
1. Современный DOM: полифиллы
2. 10 лучших polyfill-библиотек
3. Список готовых polyfill-библиотек
Ежедневные хитрости
1. Используйте логический оператор ||, чтобы определить значение по умолчанию. Если значение слева от оператора будет ложным, то будет использоваться значение справа по отношению к оператору (false || true). Примите к сведению, из-за слабого сравнения типа значения, таких как false, 0, null или '', которые являются ложными, будет использоваться значение по умолчанию, которое вы укажете справа по отношению к логическому оператору. Для строгой проверки типов используйте такую конструкцию: if (value === void 0) { value = defaultValue }
function a (value) {
var defaultValue = 33;
var used = value || defaultValue;
}
2. Для частичного использования функции, можете воспользоваться методом .bind
function sum (a, b) {
return a + b;
}
var addSeven = sum.bind(null, 7);
addSeven(6);
// <- 13
3. Используйте Array.prototype.slice.call, если хотите получить массив из объекта
var args = Array.prototype.slice.call(arguments);
4. Используйте слушатели событий на все что угодно!
var emitter = contra.emitter(); // example, event emitters: jQuery, Angular, React ..
body.addEventListener('click', function () {
emitter.emit('click', e.target);
});
emitter.on('click', function (elem) {
console.log(elem);
});
// эмитация клика
emitter.emit('click', document.body);
5. Если вам нужна пустая функция (no-op), используйте Function()
function (cb) {
setTimeout(cb || Function(), 2000);
}
К прочтению:
1. browserhacks
2. 5 популярных JavaScript-хаков
3. Несколько JavaScript хаков для хипстеров
4. Создание и вызов событий
5. Магия JavaScript: arguments
Руководство по ES6
Язык JavaScript стандартизируется организацией ECMA (наподобие W3C), а сам стандарт носит название ECMAScript. ECMAScript определяет следующее:
- Синтаксис языка — правила парсинга, ключевые слова, операторы, выражения и прочее
- Типы — числа, строки, объекты и прочее
- Прототипы и наследование
- Стандартную библиотеку встроенных объектов и функций — JSON, Math, методы массивов, методы объектов и тому подобное
В 2009 году ES5 был официально закончен (позже ES5.1 в 2011), и стал широко распространенным стандартом для браузеров, таких как Firefox, Chrome, Opera, Safari и многих других. В ожидании следующей версии JS (в 2013, потом в 2014, а затем уже и в 2015) самой обсуждаемой новой веткой была — ES6.
ES6 — это радикальный скачок вперед. Даже если вы думаете, что знаете JS (ES5), то ES6 полон новых вещей, о которых вам еще только предстоит узнать. Так что будьте готовы! ES6 довольно сильно отличается. Это результат долгих лет слаженной работы. И это клад новых возможностей языка. Наиболее значимое обновление JS, которое было когда-либо. Новые возможности варьируются от простых удобств вроде функций-стрелок и интерполяции строк до мозговзрывающих концепций вроде прокси и генераторов, но уже сейчас вы можете приступить к его изучению (ссылки в приведенной литературе ниже).
К прочтению:
1. Современный Style Guide по ES6
2. Шпаргалка по ES6
3. Все необходимое для изучения ES6
4. ES6 в деталях: введение
5. ES6 и за его пределами
6. Примеры ECMAScript 6
7. 350 аспектов ES6
Автор: splincodewd