Язык JavaScript претерпел много изменений с момента своего создания. Поэтому сейчас не так просто отслеживать хорошие практики с таким количеством новых функций, изменений и фреймворков.

В этой статье мы рассмотрим некоторые общие правила, которые лучше всего использовать в JavaScript. Эти советы помогут нам писать код лучше. Они подходят как для новичков, так и для опытных разработчиков.


1. Объявление одной переменной на одной линии

В JavaScript вы можете объявить сразу несколько переменных на одной линии. Это то, что я не рекомендовал бы делать. Объявление переменных на разных линиях делает код более простым для чтения и понимания.

Посмотрим пару примеров:

// ❌ нежелательно
const x = 0, y = 10, z = 20;

// ✅ предпочтительно
const x = 0;
const y = 10;
const z = 20;

Объявление нескольких переменных на одной строке может выглядеть "круче", но это непрактично. Код становится менее читабельным. Этот подход также может привести к нежелательным результатам.

В нижеприведенном примере может показаться, что xy, и z - все константы. Но это неправда, константой является только x.

const x = y = z = 10;

// ❌ y и z не константы, им можно присвоить новые значения
y = 15; // works
z = 15; // works

// только x константа
x = 15; // error: Uncaught TypeError: Assignment to constant variable.

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

Давайте посмотрим на код:

const point = { x: 10, y: 15, z: 20 };

// ✅ желательно
const { x, y } = point;

// ✅ выведет 10
console.log(x);

// ✅ выведет 15
console.log(y);

Пример выше - лучшее решение.


2. Понимание браузерной оптимизации

Поскольку JavaScript не компилируется, единственный способ оптимизации, которую делает движок — во время выполнения. Он придумывает способы оптимизации на ходу.

Эти способы скрыты и не всегда понятны. Каждый движок имеет собственную реализацию оптимизаций. Например, у движка Хромаv8 этот механизм называется TurboFan. Понимая некоторую базу его внутреннего устройства, мы может писать более эффективный код.

Если мы будем игнорировать движок, производительность может пострадать. Наше приложение будет работать медленнее, что явно не понравится пользователю.

Посмотрим на несколько ситуаций, откуда мы можем извлечь пользу:

Прототипы

Движок JavaScript понимает, что какие-либо изменения прототипов довольно редки. Прототип объекта стабилен и предсказуем. Вот почему движок делает некоторые оптимизации на ранних этапах прототипов.

Однако, тут две стороны одной медали. При изменении прототипа объекта движок пересчитывает все оптимизации. Из-за этих изменений приложение работает медленнее. Это даже может замедлить код, который взаимодействует с прототипом.

Рассмотрим пример плохого использования:

// объявление прототипа
function Item() {}
Item.prototype.save = () => {};
Item.prototype.delete = () => {};

...
...

function foo() {
  // ❌ плохо, мы не должны менять прототип
  delete Item.prototype.save;
  // выведете undefined: save метод больше недоступен
  console.log(Item.prototype.save);
}

Если вам правда необходим похожий функционал, просто используйте свойства самого объекта. Это не приведет к пересчету оптимизаций его прототипа.

Посмотрим на исправленный пример:

// объявление прототипа
function Item() {
  this.save = () => {};
}
Item.prototype.delete = () => {};

...
...

function foo() {
  const newItem = new Item();
  // ✅ желательно, удаление свойства без изменения самого прототипа
  delete newItem.save;
  // выведет undefined: save метод больше недоступен
  console.log(newItem.save);
}

Больше можно узнать в статье от MDN.

Типизация

Так как JavaScript использует JIT компиляцию, ему необходимо делать много проверок перед тем как выполнить какую-либо функцию. Во многом он зависит от оптимизаций.

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

Одна из таких стратегий - типизация. Функция создаст заглушку для каждой комбинации типа и параметров. Это значит, если наша функция мономорфная (с одинаковым типом параметров), потребуется всего одна заглушка. Если она полиморфная, потребуется одна заглушка для каждой комбинации параметра и типа.

Соответственно, если следить за типами параметров, можно улучшить производительность.

Рассмотрим пример:

function add(a, b) {
  return a + b;
}

// ✅ "горячую" функцию JIT оптимизирует эффективно
add(1,2);
add(1,1);
add(2,3);
add(4,5);

Если бы мы выполняли эту функцию с разными типами параметров, код не выполнялся бы так быстро.

Как раз посмотрим на такой вариант:

function sum(a, b) {
  return a + b;
}

// ❌ если "горячая" функция будет храниться
// будет создано много заглушек для каждой комбинации
add(1,2);
add(1,'3');
add(2,true);
add(4,5);

TypeScript, например, может помочь нам сделать наши методы максимально эффективными.


3. Ранний вызов return

Мы привыкли к шаблону if/else, и не подвергали его сомнению. Однако, с опытом вы могли понять, что использование полного ветвления это:

  • неэффективно;

  • трудно читать;

  • трудно поддерживать.

Как мы можем улучшить наш код? Просто используем шаблон раннего вызова return.

"Ранний вызов return" - это шаблон, в котором рекомендуется возвращать какой-либо результат настолько рано, насколько это возможно, не прибегая к else выражению.

Взглянем на классическую реализацию FizzBuzz функции.

Код ниже мог бы быть решением:

function FizzBuzz(i) {
    let result = undefined;
    if (i % 15 == 0) {
        result = 'FizzBuzz';
    } else if (i % 3 == 0) {
        result = 'Fizz';
    } else if (i % 5 == 0) {
        result = 'Buzz';
    } else {
        result = i;
    }
    return result;
}

Однако, его можно улучшить, используя шаблон раннего вызова return:

function FizzBuzz(i) {
    if (i % 15 == 0) {
        return 'FizzBuzz';
    }
    if (i % 3 == 0) {
        return 'Fizz';
    }
    return  (i % 5 == 0) ? 'Buzz' : i;
}

В итоге наш код стал:

  • более продуманным;

  • более читабельным;

  • более эффективным.


4. Принятие функционального программирования

JavaScript это мультипарадигменный язык программирования. Мы можем выбирать между объектно-ориентированным программированием и функциональным. Первый подход стал более доступным с появлением классов в ES6.

Правда, это все еще синтаксический сахар для обычного прототипного наследования в JS. Это может привести к конфликтам.

По моему мнению, функциональный подход предполагает модульность и очень легок при тестировании.

Мы можем наблюдать как создатели React упростили процесс разработки, отказавшись от классового подхода. Даже если вы никогда не писали на React, сразу сможете заметить большую разницу.

Посмотрим на пример с классами на React:

class ClassComponent extends React.Component{
    constructor(){
        super();
        this.state={
            count :0
        };
    }

    increase = () => {
        this.setState({count : this.state.count + 1});
    }
 
    render(){
        return (
            <div>
               <p> {this.state.count}</p> 
               <button onClick={this.increase}> Add</button>
            </div>
        )
    }
}

А теперь перепишем тот же компонент, используя функциональное программирование:

const functionComponent = () => {
    const [count, setCount] = useState(0);
    const increase = () => setState(count + 1);

    return (
        <div>
           <p> {count}</p> 
           <button onClick={increase}> Add</button>
        </div>
    );
}

В результате мы тратим меньше кода, что делает его более простым.

У функционального подхода есть много преимуществ. Он спасает нас от большого количества проблем, связанных с изменениями сущностей.


5. Использование '===' для проверки на равенство

Оператор == - оператор сравнения, который приводит операнды к одному и тому же типу.

Он использует приведение типов, чтобы сравнить их значения. Например, когда сравниваются число и строка, движок преобразует строку в число и после этого сравнивает их.

Оператор == сравниваниет значения, но не типы.

Посмотрим несколько примеров:

'1' == 1 // ✅ true

true == 1 // ✅ true

false == 0 // ✅ true

'0' == false // ✅ true

Это может привести к некоторым "веселым" ситуациям, изображенным на картинке ниже:

Чтобы предотвратить нежелательное поведение, лучше проверять и тип, и значение. Это возможно с помощью оператора === .

При строгом сравнении сначала будут сравниваться типы значений. Если они не равны, то вернется false. Только если они совпадут, будут проверяться сами значения.

⚠️ Есть такая особенность, что NaN ничему не равно при строгом сравнении. Для этого мы можем использовать isNan() .

Посмотрим на оператор === в действии:

1 === 1 // ✅ true
1 === '1' // ❌ false

const obj = {};

obj === obj // ✅ true
'x' === 'x' // ✅ true

NaN === NaN // ❌ false
isNaN(NaN) // ✅ true

6. Await вместо промисов

До промисов работа с асинхронными операциями была довольно утомительной. Все делалось через колбэки. Это приводило к, так называемому, "аду колбэков" (callback hell). Код было трудно читать и поддерживать. Промисы помогли писать код лучше, но они все еще далеки от совершенства, и могут привести к "аду промисов" (promise hell).

Async/await функционал был представлен в ES7. Он упростил работу с промисами в языке. Работать с асинхронностью стало удобнее, ведь ведь всю тяжелую работу на себя взял движок. А выпуск await верхнего уровня в ES12 добавил последнюю часть, отсутствующую в этом функционале.

Сейчас осталось не так много случаев, когда нам надо использовать промисы вместо async/await. Использование async/await повысит читабельность нашего кода.

Рассмотрим пример:

const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);

function addItem() {
    createItem()
        .then(({ id }) => updateStock(id))
        .then(() => console.log('success'))
        .catch(() => console.error('oops error'));
}

Если мы перепишем его с async/await, то его станет легче прочитать:

const createItem = (id) => Promise.resolve(true);
const updateStock = () => Promise.resolve(true);

async function addItem() {
    try {
      const { id } = await createItem();
      await updateStock(id);
      console.log('success');
    } catch {
      console.error('oops error');
    }
}

Async/await хорошо работает с новыми методами промисов такими как Promise.allPromise.anyPromise.allSettled и тд.

Посмотрим пример с промисами и async/await в ES12:

(async () => {
    const result = await Promise.any([
        Promise.reject('Error 1'),
        Promise.reject('Error 2'),
        Promise.resolve('success'),
    ]);
    console.log(`result: ${result}`);
})();
// result: success

Итог

Это были мои заметки по разработке, когда дело доходит до JavaScript. Конечно, их намного больше, но эти шесть - мои фавориты. Важность использования Strict также очень близка к ним. К счастью, нам не нужно об этом сильно беспокоиться, поскольку есть инструменты, которые уже сделают все за нас.

Обязательно используйте ESLint. Он автоматизирует цикл разработки, помогает нам писать код лучше и ускоряет процесс проверки кода.

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


  1. rock
    21.03.2022 08:53
    +4

    1. Пример к функциональному программированию почти никакого отношения не имеет.

    2. isNaN('x') -> true - прекрасная строгая проверка -) Есть Number.isNaN и Object.is.

    3. Правильно использовать не "вместо", а "вместе". async/await был представлен в ES2017 (ES8), top-level await попадет только в ES2022 (ES13).


    1. radtie
      21.03.2022 10:33

      Пример к функциональному программированию почти никакого отношения не имеет

      И к тому же лукавит про количество кода, пример с классом написан и сформатирован избыточно.


  1. UnknownQq
    21.03.2022 09:18
    -3

    Я не претендую на гуру JS, но по-моему, удобнее объявлять переменные вот так (по крайнем мере, в web(php)storm'e это очень удобно):

    const a = 1
        , b = 2
        , c = 3
    ;
    

    По поводу async/await, кажется, что это вещь ситуативная. Сам автор как бы намекая, вспоминает callback hell, потом говорит про promise hell. А что мешает, при желании, напилить async/await hell? Проблема, кажется, не совсем с инструментом, а скорее с тем, как его пользуют.
    В целом, статья показалась местами весьма субъективной, нежели объективной.


    1. rock
      21.03.2022 10:49
      +3

      по-моему, удобнее объявлять переменные вот так

      Если вам понадобится удалить из кода переменную a, вам придется изменить и строку, где задается переменная b - а это лишний шум в системе контроля версий - история изменений, пул-реквесты...

      А что мешает, при желании, напилить async/await hell?

      Как вы это себе представляете? Да, это возможно, но нужно ещё постараться.


  1. domix32
    21.03.2022 09:44
    +2

    Забавно, что отучают менять прототипы, но не используют классы.


  1. vladvul
    21.03.2022 09:51
    -8

    Не делайте так, это неправильно

    if (i % 15 == 0) {
        return 'FizzBuzz';
    }
    if (i % 3 == 0) {
        return 'Fizz';
    }
    

    Правильно:

    if (i % 15 == 0)   return 'FizzBuzz';     
    if (i % 3 == 0)    return 'Fizz';
    

    Ну и согласен с комментатором выше что async await может вытечь из-под капота сахарной протечкой и ты даже не поймешь почему. В то время как коллбэки гораздо прозрачнее, хотя не такие читабельные.

    Лекарство от коллбэк хелла - отказ от сложных анонимных функций.

    Вместо do_something_long ( data, () => {

    bla-bla на 20 строк с дальнейшими коллбэками

    });

    нужно писать do_something_long(data, cb_something_long_done, cb_something_long_error);

    Анонимные функции (кроме однострочников) такое же зло как переменные с именами a,b,c

    Не говоря уже о том адище когда они цепочками написаны как у Маяковского


    1. Rsa97
      21.03.2022 10:12
      +1

      Не делайте так, это неправильно
      Что стиль Google, что стиль Airbnb допускают для if оба варианта написания.


      1. evgeniyPP
        22.03.2022 17:46

        Главное, выбрать один вариант на весь проект, а не пилить везде по-разному. Я лично предпочитаю с фигурными скобками.

        Говорить, что один из этих вариантов неправильный, это просто глупость. Это как говорить, что писать надо без точек с запятой, с ними – неправильно. Это же чистая вскусовщина.


    1. mayorovp
      21.03.2022 11:21
      +1

      Но ведь отказ от анонимных функций ещё больше усугубляет callback hell!


      Когда вызовы функций написаны "цепочкой" или там "лесенкой" — вполне можно разобраться в алгоритме, проблема тут скорее в том, что приходится учить два разных языка с разными управляющими конструкциями для синхронного и асинхронного кода. А вот в вашем стиле алгоритм размазывается по всему файлу (и хорошо ещё если по одному файлу)…


  1. Holix
    21.03.2022 12:50

    1. Вкусовщина. По мне код в меньше строчек часто легче читается. Объявление нескольких дружественных переменных часто лучше делать в одну строчку;

    2. Полезно узнать новичкам;

    3. Хорошая практика, но во всем надо знать меру;

    4. Жесть! Если первый пример ещё хоть как-то можно понять(хотя мы все знаем, что классы в JS фальшивка из сахара), но якобы "функциональный" пример -- вообще не читаем, от слова абсолютно!

    5. Не понял посыла: Pro или Cons? Если пишешь на JS ты просто обязан это знать и использовать по необходимости, а не тупо - везде;

    6. Ужас Callback hell очень сильно преувеличен. Обязательно надо помнить, что async/await -- такая же сладкая фальшивка, как и class-ы. И непонимание этого может породить больше проблем, чем пользы. Стоит применять только, если это действительно уместно.

    P.S. На мой взгляд, за последние годы в JS надобавляли много лишнего сахара, чтобы гости из других языков не плакали. Но те всё равно плачут, ибо результат не такой, как у них на родине.


  1. Krotolesya
    22.03.2022 11:23

    По четвертому пункту, хорошо бы увидеть пример без использования хуков React, а сравнение функционального и ООП стиля на примерах "чистого" JavaScript, на мой взгляд, спорно утверждение: "...мы тратим меньше кода, что делает его более простым"

    За остальные советы -- спасибо, мне как новичку было интересно.


  1. evgeniyPP
    22.03.2022 18:09
    -1

    Объявление одной переменной на одной линии.
    Бесспорно. Это общепринятая практика писать каждую переменную отдельно. Иначе путает. Плюс для гита меньше правок. Деструктуризация не в счет, там, как раз, наоборот, так понятнее.

    Понимание браузерной оптимизации.
    В принципе, использовать прототипы, а не классы, это уже какой-то ретростиль. Сложен для восприятия, поэтому никогда не пишите так (если только не нужно залезть очень низкоуровнево, но к ключевой аудитории этой статьи это не относится).
    Типизация тоже полезна для удобочитаемости, остальное неважно.

    В целом, если вы пилите сайты, забейте на оптимизацию, думайте о читабельности. Оптимизацию оставьте более опытным ребятам, которые пишут для вас либы. У вас там такие, скорее всего, абстракции, что ваши пять копеек роли не сыграют. Либо вы пишете на ванильном JS и с современными мощностями компов там нечему тормозить, если вы только while (true) {} не напишете.

    Ранний вызов return.
    Если вы до сих пор пишете else {...} и не чувствуете после этого себя плохим разработчиком, то вы плохой разработчик.

    Принятие функционального программирования.
    Очевидно, что сегодня писать React на классах не надо, потому что этот синтаксис негласно считается deprecated. Но это касается конкретно React, про функциональное программирование тут ни слова. Да и это продвинутые темы не на одну статью. Вон, авторка сама понятия не имеет, что это такое.

    Использование '===' для проверки на равенство.
    Должно быть очевидно. Единственное исключение – foo == null, чтобы не писать foo === null || foo === undefined.

    Await вместо промисов.
    Поддержка браузеров уже давно не хуже, поэтому дефолтное решение для асинхронки. Промисы – более низкоуровневые для более низкоуровневых задач, не CRUD запросы посылать.


    Итог.
    Еще раз повторю, сконцентрируйтесь на читабельности. Не пишите коротко, пишите понятно. Можно еще 100500 раз повторить, всё равно ведь среди тех, кто прочитал мой коммент найдется придурок, который пойдет и сразу же у себя в коде вложенный тернарик запилит ????.


  1. RaShe
    22.03.2022 20:46
    -1

    Ранний вызов return это так себе совет. На маленьком примере очень даже ничего, но если есть чуть больше логики начинаешь искать что там возвращается и где.


    1. mayorovp
      22.03.2022 20:48
      +1

      А если не использовать ранний return — придётся искать то же самое, но ещё и добавятся уровни вложенности фигурных скобок.


  1. bopoh13
    23.03.2022 02:20

    @DKozachenko для чего создавать анонимную функцию? Читаемость кода выше если ответ try / catch записывать в константу, результат которой нужно передать Promise.all().

    const getRequests = jsonfiles.map(async (filename) => {
    	try {
    		const response = await fetch(`${filename}`);			
    		const json = await response.json();
    		return json;
    	} catch (e) {
    		e.message = `${e.message}`;
    		throw e;
    	}
    });
      
    const result = Promise.all(getRequests).then((data) => {
    	data.forEach((obj) => {
    		// Обработка каждого Promise
    	});
    }).finally(() => {
    	// Оценить обработанные Promise
    }).then(() => {
    	// Действие после получния данных
    }).catch((e) => {
    	console.error(e);
    });


  1. Zipri
    23.03.2022 15:15

    const obj = {};

    obj === obj // ✅ true

    Равзе можно сравнивать объекты в JS ?

    Даже если в консольке написать {} === {} - выдаст false в любом случае


    1. Rsa97
      23.03.2022 15:54

      Можно. Сравнивается не содержимое объектов, а ссылки на них.

      const obj1 = {};
      const obj2 = {};
      obj1 === obj2 // false
      — здесь сравниваются два разных объекта, ссылки на которые различаются.

      const obj1 = {};
      const obj2 = obj1;
      obj1 === obj2  // true
      — здесь объект один и тот же, при присвоении копируется ссылка, а не содержимое объекта.


      1. Zipri
        23.03.2022 21:55

        понял, спасибо)