JavaScript quality guide


С помощью советов предложенных в данном руководстве вы будете писать код, понятный любой команде разработчиков.

От переводчика


Всем привет, с вами Максим Иванов, и сегодня мы поговорим о правилах оформления кода на языке JavaScript. Николя Бэвакуа (Nicolas Bevacqua), автор книги «Дизайн JavaScript-приложений» (JavaScript Application Design), разработчик из Аргентины, опубликовал данное руководство достаточно давно, первая запись появилась еще в 2014 году, многое написано по стандарту ES5, однако, в наши дни это все равно актуально, сейчас, когда ES6 еще нигде полноценно не работает без babel и прочих транспайлеров. Хотя мы видим прогресс в топовых десктопных браузерах (Google Crhome, Firefox), где уже реализовано 70-90% задуманного, мы видим, что они стремятся поддерживать новый стандарт, но, к сожалению, ещё нет браузеров, которые полностью могли бы поддерживать ES6. К слову, я буду очень рад вашим комментариям. В общем, удачи и давайте начнем.


Введение


Эти рекомендации не высечены на камне, это базовые правила, которые помогут вам научиться писать более последовательный и понятный код. Чтобы эффективно применять на практике предложенные правила, начните прямо сейчас, делитесь ими со своими коллегами и внедряйте в производство. Однако, не зацикливайтесь на этих правилах, это может быть бесплодным и контрпродуктивным. Попробуйте найти золотую середину, чтобы всем было комфортно в вашем коллективе. Автоматизированная проверка стиля кода это довольно полезная вещь, так как стиль написания кода это дело каждого.

Содержание


  1. Модули
  2. Строгий режим
  3. Форматирование пробелов
  4. Точка с запятой
  5. Стиль кода
  6. Анализ кода на ошибки
  7. Строки
  8. Инициализация переменных
  9. Условные конструкции
  10. Сравнения
  11. Тернарный оператор
  12. Функции
  13. Прототипы и наследования
  14. Объекты
  15. Массивы
  16. Регулярные выражения
  17. Консоль разработчика
  18. Комментарии
  19. Именование
  20. Polyfill-библиотеки
  21. Ежедневные хитрости
  22. Руководство по 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).

// плохо
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


image

Перебор элементов делается при помощи .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 (x+y+z) / 3;
}
a(1, 3, 5);
// <- 3


// хорошо
function average(a, b, c) {
  return (a + b + c) / 3;
}

average(1, 3, 5);
// <- 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

Комментарии (35)


  1. kahi4
    07.04.2016 02:12
    +8

    Прототипы и наследования

    Забавно видеть полное противоречие вашего текста тексту по приведенной вами же ссылке. Например, функции в js типом данных не являются, а так же нет "простых" и "составных" типов, а есть "примитивы" и "объекты". Формально — в js массив — тоже объект. Да и неформально тоже, если бы язык позволял обращаться к свойствам через точку и для имен, начинающихся с чисел, конструкция была бы валидной
    var a = [3,4,5];
    a[0] === a.0

    Но он не позволяет, но от этого массивы объектами быть не перестали (к слову, вы сами об этом дальше пишите).
    Ну и потеряны null и undefined.
    жесткими операторами сравнения

    Строгими только.
    Имейте в виду, что функциональная декларация функций будет доступна и в области видимости выше, так что все это не имеет значения, в каком порядке была объявлена эта функция. Как правило, вы всегда должны объявлять функции в глобальной области видимости, при этом вы должны стараться избегать размещения таких функций внутри условных операторов.

    Чего? Я бы отправил личным сообщением как просьбу переписать абзац, однако тут есть жуткий bad practices: "вы всегда должны объявлять функции в глобальной области видимости" — вы никогда и ни при каких обстоятельствах не должны объявлять функции в глобальной области видимости. Даже если совсем совсем прижмет. Очень мало исключений, как правило, библиотеки.
    function ruleOfThree (had, got, have) {
      return have * got / had;
    }
    ruleOfThree(4, 2, 6);
    // <- 3

    Просто великолепно! Идея понятна, конечно же, но пример отвратный. В данном случае, что функцию a, что функцию ruleOfThree я полезу искать, покуда совершенно не очевидно, что она делает.
    Вообще у вас талант давать плохие примеры под комментарием "хорошо".
    Например, в секции "комментарии"
    хорошо: [… не дублировать комментариями то, что понятно из кода...]
    var numeric = /\d+/; // поиск одной или более цифр в строке

    Так то это и так понятно.
    Вообще в целом: видно, работа проделана большая и хорошая. Не без огрехов, но все равно. Хотя я бы все же рекомендовал в 2016 году использовать исключительно es6 и выше. Гораздо проще и меньше хлопот. Один раз потратить пару часов на настройку бейбела или запускать в новой ноде гораздо проще и выгоднее, чем потом набивать шишки на всплытии переменных и куче других причуд.


    1. Ahineya
      07.04.2016 10:24
      +3

      Поддерживаю. Некоторые примеры действительно чудовищны — reduce для склейки свойств объектов в массиве просто нечитаем по сравнению с более лаконичным someCollection.map(function(n) {return n.name}).join(", ");, или ещё более прекрасным ES2015-вариантом someCollection.map(n => n.name).join(", ");


  1. IvanPanfilov
    07.04.2016 09:47
    -6

    зачем это говно мамонта? если есть es6
    а все нормальные люди так вообще используют транспилеры с последних стандартов.


  1. AngReload
    07.04.2016 10:24

    Автор зачастую сам себе противоречит, так например призывают везде использовать точку с запятой и тутже ссылка на jQuery codestyle, призыв не расширять стандартные объекты и Array.prototype.sum =…


  1. bingo347
    07.04.2016 11:05
    +4

    До листал до середины, почти в каждом примере помеченным «хорошо» показаны bad-practices

    [].slice.call(arguments)
    

    Создали новый массив, только ради того чтоб обратится к прототипу, утечка аргументов это главный вырубатель оптимизаторов в v8 и SpiderMonkey, но к сожалению этим пестрит каждая 3я библиотека на npm, благодаря вот таким вот туториалам…
    Сложно скопировать в функцию простой шаблон
    var args; for(let i = arguments.length, a = args = new Array(i); i--; a[i] = arguments[i]);
    

    Тогда скопируйте этот:
    var args = Array.prototype.concat.apply(Array.prototype, arguments);
    


    Создание новых функций для каждого инстанса быстрее чем копирование ссылок из прототипа? да ну?
    Все методы, гетеры, сетеры должны быть в прототипе, все изменяемые свойства создаются в конструкторе, прототип обязан содержать свойство constructor — ссылку на свой конструктор.
    Сложно запомнить? Используйте class из es2015

    Ну и напоследок, функциональные выражения, которые живут дольше 1 вызова, должны быть именованны
    someEmitter.on('event', function onSomeEmitterEvent() {
        //some code
    });
    

    не раз поможет при отладке


    1. TheShock
      07.04.2016 14:41

      var args = Array.prototype.concat.apply(Array.prototype, arguments);

      Тут же ж тоже утечка аргументов. Вообще не понимаю, неужели нельзя делать такой контракт функций, чтобы не было необходимости в arguments? Это ведь очевиднее для использования.


      1. bingo347
        07.04.2016 16:48

        Причина деоптимизации «утечки аргументов» — связь между индексами объекта arguments и переменными в аргументах функции, при изменении одного — меняется другое, и это не ссылачная связь, а скорее биндинг, поэтому на машинном уровне такой код сложно анализировать.
        Когда мы передаем объект arguments во вне функции, он передается по ссылке, внешний код может изменить значения объекта.
        Поэтому оптимизатор отказывается менять код функции с подобным поведением, а так же функции подобные такой:

        function f(arg) {
           arg = arg || DEFAULT; //Присвоили аргументу новое значение
        }
        

        2й аргумент нативного Function.prototype.apply исключение, так как вызываемая функция на наш arguments гарантировано воздействовать не сможет


        1. Riim
          07.04.2016 17:18

          Разве здесь [].slice.call(arguments) есть утечка аргументов? slice же тоже ничего не меняет в arguments.


          1. Shannon
            07.04.2016 17:23
            +4

            function bad2() {
              return [].call.slice(arguments, 1);
            }
            

            Если же вы пытаетесь передать arguments в какую-нибудь другую функцию (например, делаете [].slice.call(arguments, 1)), то Crankshaft вообще откажется оптимизировать вашу функцию.

            http://mrale.ph/blog/2015/04/12/jsunderhood.html


            1. bingo347
              07.04.2016 18:36
              +2

              благодарю за ссылку, нашел для себе пару новшеств


        1. TheShock
          07.04.2016 18:18

          2й аргумент нативного Function.prototype.apply исключение, так как вызываемая функция на наш arguments гарантировано воздействовать не сможет

          Да, про apply вы правы. Забыл про эту особенность


        1. Large
          08.04.2016 23:44

          >>связь между индексами объекта arguments и переменными в аргументах функции, при изменении одного — меняется другое

          это не так со включенным флагом 'use strict';


          1. Large
            08.04.2016 23:51
            -1

            Да и вообще сейчас лучше вообще

            Array.from(arguments)
            
            использовать. Или даже ...args


            1. Shannon
              09.04.2016 19:32

              «это не так со включенным флагом 'use strict';»
              Не поможет

              «Или даже ...args»
              Поможет

              «Да и вообще сейчас лучше вообще Array.from(arguments) использовать.»
              Так делать не стоит


              1. Large
                09.04.2016 20:01

                Если включен флаг use strict то связи между объектом arguments и аргументами функции нет — откуда утечка?


                1. Shannon
                  09.04.2016 20:12

                  «Если же вы пытаетесь передать arguments в какую-нибудь другую функцию, то Crankshaft вообще откажется оптимизировать вашу функцию.»


                  1. Large
                    09.04.2016 20:16

                    Статье уже год как, думаю это стоит проверить тем более Array.from как раз создан для работы с итерируемыми объектами и от ...args не слишком то отличается.


                    1. Shannon
                      09.04.2016 20:34

                      То есть вы не проверили и советуете только основываясь на своё внутреннее ощущение, что за год в движке уж точно этот момент доработали?

                      Я вот в свою очередь думаю, что год назад как была эта информация актуальна, так и сейчас актуальна, потому что зачем допиливать оптимизацию устаревшнего arguments, раз уже активно внедряют es2015-es2016 и rest parameters


                      1. Large
                        09.04.2016 20:38

                        Нет, я исходил из соображений, что утечка отсутсвует и потому оптимизация должна быть. Теперь обязательно проверю. Тем более интересен тот момент, что Array.from не может менять arguments, так что это не совсем обычная функция.


                      1. Large
                        09.04.2016 23:09
                        +1

                        Итого проверил в node 5.10.1 и chrome 49.0.2623.112 со включенным use strict:

                        function(...args) {var a = args; return a;} // не оптимизируется 
                        

                        function() {var a = Array.from(arguments); return a;} // не оптимизируется 
                        

                        function() {var a = []; for(let el of arguments) a.push(el); return a;} // оптимизируется TurboFan
                        

                        function() {var a = []; for(var i = 0, l = arguments.length; i < l; i++) a.push(arguments[i]); return a;} // оптимизируется
                        

                        function() {var a = Array.prototype.concat.apply(Array.prototype, arguments); return a;} // оптимизируется 
                        


                        Итого оптимизация случается крайне редко, ES6 пока оптимизируется плохо или не оптимизируется.
                        Так получается, что критически важные по скорости задачи все равно пишутся с использованием классического for, а в остальном нет смысла отказываться от удобных и читаемых конструкций будь то ...args, Array.from или for of.


                        1. Shannon
                          10.04.2016 00:05

                          1. здесь нечего оптимизировать, код инлайнится в простой return args, так как ...args в отличии от arguments просто массив
                          2. теперь и вы убедились, что ваш совет и вывод про статью были ошибочны
                          3-5. Невозможно просто заинлайнить, оптимизируется как и ожидалось

                          Итого. Используйте es6 + babel и никого не слушайте


                          1. Large
                            10.04.2016 00:19

                            Ну так тоже не оптимизируется:

                            function testFunction(...args) {
                              const l = args.length;
                              var a = new Array(l);
                              for(let i = 0; i < l; i++) a[i] = args[i];
                              return a;
                            }
                            


                            C use strict arguments это тоже просто объект. Проблемы с внутренней кухней оптимизации не предсказуемы. Я думаю как только будет оптимизироваться ...args будет оптимизироваться и Array.form (механизм работы у них схожий), хотя опять же, это все очень не предсказуемо.

                            Общий вывод поддерживаю — есть смысл пользоваться es6 с бабелем или без, а там где нужна оптимизация переходить к старому доброму for или уже переходить на asmjs и/или glsl (зависит от задач).


                          1. Large
                            10.04.2016 00:28

                            Кстати for-of только с 41 версии хрома оптимизируется, раньше тоже не хотел.


                            1. Shannon
                              10.04.2016 01:09

                              Если есть желание, можно проверить еще и на 6.0.0 ноде, где более актуальная версия v8 4.9.385.27 (chrome ~49), против 4.6.85.31 (chrome ~45) в node 5.10.1 (в 5 ветке вообще v8 не менялся с 5.1.0)
                              Ночные сборки 6.0.0 ноды: https://nodejs.org/download/nightly/


                              1. Large
                                10.04.2016 01:13

                                Ну я прямо в браузере проверял так что наверное смысла нет. Скорей всего они эти оптимизации проведут когда полностью заменят кранкшафт на турбофан.


                                1. Shannon
                                  10.04.2016 16:42
                                  +1

                                  Спасибо за инструкцию как запускать проверку в браузере

                                  Вот еще до кучи вариант со стрелочными функциями (которые, как известно, дешевле и быстрее создаются и вызываются, и вовсе не имеют arguments):

                                  var testFunction = (...args) => {
                                  	let a = Array.from(args);
                                  	args = 4;
                                  	return a;
                                  }
                                  

                                  Результат:
                                  Function is optimized by TurboFan

                                  После работы бабеля получается тоже оптимизированная функция:
                                  Function is optimized

                                  Проверил так же различные варианты стрелочных функций (в том числе и в прототипах), они всегда оптимизировались (по крайней мере не удалось найти вариант, чтобы оптимизации не было)


      1. Shannon
        07.04.2016 17:16

        Например, функцию console.log() не получится сделать так, чтобы можно было обойтись без arguments, ну или max(21, 34, 54, 345, 1) какой-нибудь

        Просто сейчас можно перейти на ...rest параметры, и очевиднее, и деоптимизаций не будет, так как Rest parameters создают обычный массив, хоть как его используй

        При этом babel при преобразовании в es5 сам оптимизирует этот момент, чтобы деоптимизаций не было

        function test(...rest) {
          [].slice.call(rest)
        }
        

        Станет:
        function test() {
          for (var _len = arguments.length, rest = Array(_len), _key = 0; _key < _len; _key++) {
            rest[_key] = arguments[_key];
          }
        
          [].slice.call(rest);
        }
        

        Поэтому, кстати, в 2016 году куда полезнее учить связке es2015+babel, чем держать в голове все эти особенности с оптимизациями. По крайней мере на странных вещах, вроде arguments, попасться будет сложнее


        1. bingo347
          07.04.2016 18:13

          Вы не вкурсе, в последних версиях бабеля поправили преобразование

          [...arguments]
          

          в
          [].concat(Array.prototype.slice.call(arguments))
          

          В исходном варианте утечки нет, после бабеля появилась


          1. Shannon
            07.04.2016 18:53

            Проверил на последней версии (или что-то тут не так?):

            $ ./node_modules/.bin/babel -V
            6.6.5 (babel-core 6.7.4)
            

            На выходе получается правильная версия, без утечки:
            $ cat ./src/index.js 
            function test(...rest) {
              [].slice.call(rest)
            }
            
            $ cat ./lib/index.js 
            "use strict";
            
            function test() {
              for (var _len = arguments.length, rest = Array(_len), _key = 0; _key < _len; _key++) {
                rest[_key] = arguments[_key];
              }
            
              [].slice.call(rest);
            }
            

            Try it out выдает тот же результат


            1. bingo347
              08.04.2016 00:57

              Я тут не про рест аргументы, а про спред оператор в литерале массива, имеющий сходный синтаксис

              var arr = [1, 2];
              console.log([3, 4, 5, ...arr]); // [3, 4, 5, 1, 2]
              

              Вместо arr допускается любой массиво-подобный объект


              1. Shannon
                08.04.2016 05:55

                Тогда о чем и к чему речь, если я предлагаю вовсе отказаться от arguments и заменить его на Rest parameters + babel, чтобы утечек оптимизации не было? А вы предлагает вариант, где снова тянуть за собой arguments, да еще и как оператор spread. Es6 вообще подразумевает, что вы забудете как пишется arguments.

                И кстати, Babel тут наоборот правильно всё делает, конструкция [...arguments] и до преобразования не «В исходном варианте утечки нет, после бабеля появилась», а наоборот, потому что это сложение с arguments, что приводит к деоптимизации. И как раз babel не вправе делать из версии с утечкой версию где нет утечки оптимизации (даже если умеет и знает что такая проблема есть), потому что в исходной версии она есть.

                И если цель использовать arguments в качестве оператора spread
                То в случае перехода на rest параметры:
                function test(...args) {
                  var newArgs = [...args]
                }
                

                babel преобразует его правильным образом:
                function test() {
                  for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
                    args[_key] = arguments[_key];
                  }
                
                  var newArgs = [].concat(args);
                }
                


    1. Aingis
      07.04.2016 18:34
      +3

      Вариант через Array.prototype.concat «сплющит» переданные массивы в отдельные аргументы (а ля _.flatten). Надёжнее будет push, но вариант с циклом, говорят, быстрее работает.


  1. rboots
    07.04.2016 16:52

    При всём уважении, рано вам ещё руководства писать. Разработчик проходит три этапа развития: концентрация на синтаксисе языка, концетрация на паттернах, концентрация на архитектуре. Вы сейчас на первом, когда перейдёте на следующий — поменяете своё мнение о предыдущем и будете видеть каждый из 500 ляпов этой статьи. Вот тогда милости просим от вас руководство.


  1. dmrt
    08.04.2016 16:02

    Вопрос на засыпку, по методу массива reduce

    if (!Array.prototype.reduce) {
      Array.prototype.reduce = function(callback/*, initialValue*/) {
        'use strict';
        if (this == null) {
          throw new TypeError('Array.prototype.reduce called on null or undefined');
        }
        if (typeof callback !== 'function') {
          throw new TypeError(callback + ' is not a function');
        }
        var t = Object(this), len = t.length >>> 0, k = 0, value;
        if (arguments.length == 2) {
          value = arguments[1];
        } else {
          while (k < len && ! k in t) {
            k++; 
          }
          if (k >= len) {
            throw new TypeError('Reduce of empty array with no initial value');
          }
          value = t[k++];
        }
        for (; k < len; k++) {
          if (k in t) {
            value = callback(value, t[k], k, t);
          }
        }
        return value;
      };
    }
    

    Почему вот этот кусок кода:
    while (k < len && ! k in t) {
        k++; 
    }
    if (k >= len) {
        throw new TypeError('Reduce of empty array with no initial value');
    }
    value = t[k++];
    

    Нельзя было заменить на этот:
    if (len == 0) {
        throw new TypeError('Reduce of empty array with no initial value');
    }
    value = t[k++];
    

    Когда вот в этот цикл тут можно войти, при каких условиях в данной функции:
    while (k < len && ! k in t) {
        k++; 
    }
    


    1. rock
      08.04.2016 20:16
      +2

      Array(7).reduce((a, b)=> a + b)