7 рекомендаций по повышению надёжности JavaScript-кода +28



Автор статьи, перевод которой мы сегодня публикуем, решил поделиться с читателями семью рекомендациями по JavaScript. Эти рекомендации, как хочется надеяться автору, помогут писать более надёжные программы.



1. Используйте фабричные функции


Если кто не знает, фабричная функция — это обычная функция (не класс и не конструктор), которая возвращает объекты. Эта простая концепция позволяет нам воспользоваться полезными возможностями JavaScript для создания мощных и надёжных приложений.

Важно знать о том, что фабричная функция вызывается без использования ключевого слова new. Если вызвать её с данным ключевым словом, она перестанет вести себя так, как должна.

?Зачем пользоваться фабричными функциями?


Фабричные функции могут быть использованы для упрощения создания экземпляров объекта без необходимости связываться с классами или с ключевым словом new.

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

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

Вот простой пример фабричной функции:

function createFrog(name) {
  const children = []
  
  return {
    addChild(frog) {
      children.push(frog)
    },
  }
}

const mikeTheFrog = createFrog('mike')

Если некоторое время пользоваться фабричными функциями, можно прийти к пониманию того, что, в сравнении с их конкурентами в виде функций-конструкторов, фабричные функции способны повысить уровень повторного использования кода. В результате программисту приходится писать меньше кода. Фабричные функции облегчают рефакторинг, так как они способны возвращать произвольные объекты, и упрощают работу над кодовой базой.

2. При написании функций-конструкторов добавляйте методы к их прототипам


Если вы только недавно начали осваивать JavaScript, то работа с прототипом объекта может показаться вам чем-то новым. Так, в самом начале, было и со мной.

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

Вот пример функции-конструктора:

function Frog(name, gender) {
  this.name = name
  this.gender = gender
}

Frog.prototype.leap = function(feet) {
  console.log(`Leaping ${feet}ft into the air`)
}

Зачем пользоваться конструкцией Frog.prototype.leap вместо того, чтобы просто записать метод leap в создаваемый конструктором объект? Например — так:

function Frog(name, gender) {
  this.name = name
  this.gender = gender
  
  this.leap = function(feet) {
    console.log(`Leaping ${feet}ft into the air`)
  }
}

Дело в том, что если метод прикрепляется напрямую к свойству конструктора prototype — это значит, что данный метод будут совместно использовать все экземпляры объекта, созданные конструктором.

Другими словами, если опираться на предыдущий пример, в котором используется this.leap, то окажется, что при создании нескольких экземпляров объекта Frog у каждого из них будет собственный метод leap. То есть — будет создано несколько копий этого метода. В данном случае это говорит о нерациональном использовании системных ресурсов, так как во всех этих объектах будет присутствовать копия одного и того же метода, который везде ведёт себя одинаково. Создавать копии такого метода в каждом из экземпляров объекта не нужно.

В итоге это приведёт к ухудшению производительности программы. А ведь этого несложно избежать. Надо отметить, что свойства this.name и this.gender должны быть объявлены именно в таком виде, так как они должны принадлежать конкретному объекту. Если провести аналогию с настоящими лягушками, виртуальное представление которых описано с помощью конструктора Frog, то окажется, что у лягушек могут быть собственные имена, лягушки имеют разный пол. Как результат — для хранения в каждом из объектов уникальных сведений о лягушках свойства объектов имеет смысл объявлять так, чтобы они использовались бы именно на уровне экземпляров объектов.

Вот пример использования этой методики в популярном пакете request.

3. Добавляйте к объектам, которые нужно различать, свойство .type


Свойство .type, которое, по неофициальному соглашению, часто добавляют к объектам, нашло в наши дни чрезвычайно широкое применение. Если вы пишете React-приложения, то вы, возможно, сталкиваетесь с этим свойством постоянно. Особенно — если применяете Redux.

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

function createSpecies(type, name, gender) {
  if (type === 'frog') {
    return createFrog(name, gender)
  } else if (type === 'human') {
    return createHuman(name, gender)
  } else if (type == undefined) {
    throw new Error('Cannot create a species with an unknown type')
  }
}

const myNewFrog = createSpecies('frog', 'sally', 'female')

4. Пользуйтесь TypeScript


TypeScript получил широкое распространение в JavaScript-сообществе благодаря тому, что этот язык даёт в руки программиста мощное средство для безопасной работы с типами, а также позволяет выявлять ошибки ещё до того, как они проявятся в работающем коде.

Применение TypeScript позволяет находить потенциальные ошибки на этапе компиляции кода, ещё до того, как код будет запущен. Если в коде что-то не так — при его компиляции будет выведено соответствующее уведомление.

Но типобезопасность и раннее выявление ошибок — это далеко не полный список причин применения TypeScript в любых ситуациях. Одной из замечательных особенностей TypeScript является то, что этот язык позволяет пользоваться новыми возможностями JavaScript ещё до того, как их реализация появится в основных браузерах. Дело в том, что TypeScript-код компилируется в JavaScript-код, поддерживаемый как современными, так и не самыми новыми браузерами.

5. Пишите тесты


Если вы работаете над проектом и серьёзно настроены на эту работу, то вам практически необходимо писать тесты. Это позволит сделать код более предсказуемым, позволит сделать его менее подверженным ошибкам. Это даст более высокий уровень уверенности в качестве кода при внесении в него изменений. Другими словами, если вы хотите, чтобы ваш код выдержал бы проверку временем, нет лучшего способа подготовки кода к этой проверке, чем написание тестов. Чем больше тестов имеется в вашем проекте — тем больше уверенности в коде у вас будет при развёртывании его в продакшне.

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

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

6. Пишите настолько простые функции, насколько это возможно


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

Если вы — новичок в программировании, то вам это может показаться чем-то позитивным. О себе скажу, что в былые времена я прекрасно себя чувствовал, когда писал здоровенные куски кода, которые делали то, что мне было нужно. Это имело значение, преимущественно, для меня. Я увереннее чувствовал себя, когда видел, что мой код работает без проблем, уже не говоря о том, что мою уверенность в себе подкрепляло то, что я способен написать громадный блок хорошо работающего кода. Каким же наивным я был тогда!

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

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

7. Всегда помните о важности обработки ошибок при использовании JSON.parse и JSON.stringify


В JavaScript-программировании, при передаче JSON-данных методу JSON.parse, надо учитывать то, что этот метод ожидает получить, в качестве первого аргумента, правильно оформленный JSON-код. Если этот метод получит JSON-материалы, с которыми что-то не так, он выбросит ошибку.

Опасность тут заключается в том, что передача JSON.parse некорректного JSON-кода приводит к остановке приложения. На работе я недавно столкнулся с ситуацией, когда один из наших веб-проектов выдавал ошибки из-за того, что один из внешних пакетов не помещал JSON.parse в блок try/catch. Это заканчивалось сбоем при работе страницы, и мы никак не могли избавиться от проблемы до тех пор, пока не был исправлен код внешнего пакета. Происходило же всё это из-за того, что в коде, в процессе его работы, появлялась необработанная ошибка:

SyntaxError: Unexpected token } in JSON at position 107

Обрабатывая JSON-данные, поступающие в приложение из внешних источников, нельзя надеяться на то, что они будут правильно оформленными. Всегда нужно быть готовым к тому, что в них может встретиться что-то такое, что вызовет ошибку.

Итоги


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

Уважаемые читатели! Что вы посоветовали бы тем, кто хочет писать более качественный и надёжный код на JavaScript?




Комментарии (34):

  1. ganqqwerty
    /#21270462 / +1

    Лучше, чем обычно, но на мой вкус, как-то старовато. Все же сейчас мало кто реально пишет слово prototype, например

    • HawkeyePierce89
      /#21270898

      Это вы, видимо, на собеседования давно не ходили

      • AriesUa
        /#21272442

        Одно дело собеседование, где можно блеснуть знаниями, как это работает и что это за такой синтаксический сахар «class» в JS. И совсем другое дело, так делать в реальном проекте.

    • authoris
      /#21275490 / +1

      prototype довольно удобно до сих пор использовать в фабриках классов и миксинах.


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

      • Keyten
        /#21275934

        Я однажды делал наследование attrHooks, вот это было классно.

        function ShapeAttrHooks(){}
        Shape.prototype.attrHooks = ShapeAttrHooks.prototype = {};
        
        Rect.prototype.attrHooks = new ShapeAttrHooks();
        Circle.prototype.attrHooks = new ShapeAttrHooks();
        

        А дальше можно так:

        Shape.prototype.attrHooks.foo = 3333;
        // магия!
        new Rect().attrHooks.foo; // 3333
        

  2. AlexTheLost
    /#21270718 / +1

    Очень похоже на статью о Java.)
    Меня в JS привлекает как раз его простота и возможность не писать лишнего.
    Тем более что язык активно развивается, закрываются детские проблемы — отсутствие модульности или блочные переменные и т.п.
    ООП мягко говоря спорная концепция. Где то она применима, например компоненты в React адекватно используют эту концепцию. Но в прикладном коде я не вижу смысла писать классы в 99% случаев, есть отдельно данные и функции которые их обрабатывают но не все вместе.

    • DmitryKoterov
      /#21270962

      Компоненты в Реакте давным-давно используют хуки, а не классы. Переход с классов на хуки — это по магнитуде как был много лет назад переход с голого JS на jQuery.


      Но функции-прототипы тоже давно в прошлом, конечно. Тем более с typescript и его возможностью делать вещи типа:


      class Cls {
        constructor(public readonly x, public readonly y) {}
      }
      

      • AlexTheLost
        /#21273492

        Хуки действительно классная концепция.
        Я пока мало занимался разработкой с использованием react и не знал о них.
        Уже переписал все на хуки. Кода меньше и код проще.

      • evil_random
        /#21274418 / +1

        Давным-давно? Кажется активно год всего.

      • Clasen01
        /#21294880

        Так и без TS делать можно, только модификаторов доступа пока нет (обещают скоро подвезти)

  3. Goodzonchik123
    /#21270982

    «7 рекомендаций по повышению надёжности JavaScript-кода» — рекомендация «Используйте Typescript». Это разные языки программирования, хоть они и реализуют один стандарт, но все же формально разные языки. Довольно странно давать такой совет.

    • BerkutEagle
      /#21271724

      Ещё весело, когда говорят — «Начиная с сегодня все фронтовые проекты компании пишем на TS».
      Почему-то многие (почти все, кого я знаю) считают, что JS и TS — почти одно и то же. Питонистам же не предлагают вдруг пересесть на C# или Java! :)

      • Goodzonchik123
        /#21271946

        Работал на проекте, делал модуль на JS, а спустя половину реализации, весь проект перевели на TS. К счастью VueJS достаточно умный (умнее чем тот, кто переводит проект в процессе разработки на другой язык) и я смог дописать свой модуль на JS-е, и ничего не сломалось.

    • Finesse
      /#21275854

      Практика показывает, что нет. Когда пишешь на TS, нужно учитывать то, как это будет выполняться на JS, потому что (внезапно) весь TS пропадает в рантайме.


      TypeScript — это скорее надстройка над JS, чем отдельный язык. Можно использовать в одном проекте одновременно JS и TS файлы, и они будут уживаться друг с другом. Так можно постепенно переводить проект в JS на TS.

  4. kir_rik
    /#21272150

    Есть альтернативное мнение на тему «всегда пишите тесты»
    TL;DR: Надо все-таки думать головой, тесты это дорого, тесты могут цементировать архитектуру, покрытие тестами может быть «попугаями»

    На «Пользуйтесь TypeScript» могу ответить «Не пользуйтесь TypeScript» :)
    По моему опыту, при программировании чего-то сложнее сортировки очень большое время начинает отводиться на удовлетворение тайпскрипта, а не бизнеса.

    • KhodeN
      /#21274090

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

      Это с лихвой окупается за месяцы и годы поддержки и развития. Может быть для MVP он кому-то и кажется лишним, но для больших проектов с длинным циклом — must have.


      Да и если освоится, и в проекте типы и так есть, то на них не так уж много времени тратиться.

    • evil_random
      /#21274420 / +1

      Альтернативное мнение от лучших лапшеписателей?
      В текущем проекте я покрыл (пока что) тестами весь слой работы с данными, при рефакторинге и расширении функционала, не так давно, они мне помогли сэкономить кучу рабочего времени и указали на места в коде, в которых буду возникать баги.
      Про UI-тесты я даже писать не стану.

    • Finesse
      /#21275892

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

    • ganqqwerty
      /#21278126

      Слушайте, а как вы без тайпскрипта понимаете, что за тип ожидается функцией в параметре? Обязательный JSDoc со спецификацией типов?

      • Suntechnic
        /#21282940

        «Будем использовать TS чтобы писать хороший код, потому, что наш код состоит из настолько дерьмовых функций, что они не проверяют входящие параметры» — для меня это звучит как-то так. Хотя я вроде как бэкендщик и JS только осваиваю.

        • ganqqwerty
          /#21283192

          Как-как вы проверяете внутри функции входящие параметры? Вот предположим, что у меня функция ожидает в первом параметре объект rectangle, состоящий из точки левого верхнего угла и двух длин сторон:
          { upperLeft: {x:23, y:0}, height: 24, width: 60 }


          }

          • Suntechnic
            /#21283204

            Я же сказал, что только осваиваю, поэтому в данном случае я проверял бы в лоб каждое свойство объекта.
            А как это делается в TS? Там какой-то хитрый ходи для этого есть или просто сахар который в js развернется точно в такую же проверку?

            • ganqqwerty
              /#21283234

              Ну я вам так скажу, сейчас в js это делается никак. Вот прямо все берут и никак это не делают, потому что проверять надо будет очень многое: что весь rectangle не null, что у reactangle есть такие-то и такие-то свойства и эти свойства имеют ожидаемые нами типы, что у объекта отсутствуют другие свойства, и прочее. Кстати, это не сильно улучшит читабельности вашего кода — пользователь функции должен будет довольно долго соображать, какой же структуры передать объект внутрь нее — будет вчитываться в ваши ифы вместо того, чтобы мельком взглянуть на указанный тип. TS для рантайма тоже не генерирует никаких проектов потому что это же будет ужас, а не код. Зато TS на этапе компиляции позволяет задать тип Reactangle и в процессе компиляции проверить, не передаёте ли вы в вашу функцию какую-нибудь фигню вместо него.

              • Suntechnic
                /#21283248

                Но ведь TS просто компилируется в JS, я правильно понимаю? И там эти проверки все равно будут такими же как у меня?

                • ganqqwerty
                  /#21283270

                  Нет, в сгенерированном коде не будет никаких проверок.

                  • Suntechnic
                    /#21283284

                    Т.е. он проверит тип на этапе компиляции и просто не сгенерит код в случае ошибки?

                    И вот еще о чем я последнее время думаю:
                    В детстве мы мечтали как в будущем производительность компьютеров вырастит настолько, что мы сможем писать на языках высокого уровня почти всё. Мы наконец забудем компиляторы как страшный сон, ведь человеческое время будет дороже машинного.
                    Мы будем писать преимущественно на Java, JavaScript, Tcl и тому подобных языках, которые будут выполнятся сразу, без необходимости предварительной сборки или компиляции.
                    2020 год, и теперь у нас есть Babel, Webpack и TypeScript, для того чтобы «компилировать» наши программы в интерпретируемый (!) JS.
                    Как так получилось? Почему силы сообщества брошены не на то, чтобы реализовать поддержку всех этих возможностей в интерпретаторе, а на то чтобы создать прослойку сборки между редактором и интерпретатором? Что пошло не так? Где мы ошиблись?

                    • ganqqwerty
                      /#21283302

                      Да, таким образом тс спасёт вас в случае, если вы опечатались в имени свойства объекта, который сами же создали. Однако если объект пришёл с API, и оказался не той структуры, которой вы ожидали, то TS тут не спасёт. Есть несколько стратегий как минимизировать эти риски.

  5. AriesUa
    /#21272238

    Итак по пунктам

    1. Фабричные функции.
    В чем приимущество перед классами? Не писать new? Как делать extends? Различать по типу? И в чем проблема связываться с классом? В общем, давайте без фанатизма. Против фабричный функций ничего не имею против, но всему свое место. Не надо это лепить куда попало.

    2. Прототип.
    Ну ребят, ES6 не вчера же появился, и даже не позавчера. А вы продолжаете это лепить. Откройте для себя уже «class» в конце концов. Даже если вас заботит поддержка старых браузеров, то и TS и Babel давно уже могут собрать ваш код в ES5.

    PS Да, классы не отменяют того, что вы должны понимать как работают прототипы под капотом и как работает наследование в JS.

    7. Откройте для себя схемы и валидацию данных. Это избавит вас от ручной манипуляции. Посмотрите, к примеру, подход MongooseJS. Как там объявляются и валидируются данные.

    Совет для новичков. Не стоит эти советы принимать к работе. Ознакомится — да, но не более.

    • Bori5
      /#21276772

      С классами нужно быть очень внимательным с this

      class Person{
        constructor(name){
          this.name = name;
        }
        logName(){
          console.log(this.name);
        }
      }
      let p = new Person('Ivan');
      btn.addEventListener('click', p.logName); // проблема this не person а уже button
      // нужно явно указать контекст 
      btn.addEventListener('click', p.logName.bind(p));                                                          
      

      С фабричными функциями нет проблем с this
      const Person = function(name){
        let state = {
          name: name;
        };
        // Насчет extends наследование заменим композицией
        return Object.assign({ logName: () => state.name }, 
           Programmer(state), 
           Sportsmen(state)
        );
      }
      const sportsmenIvan = Person('Ivan');
      btn.addEventListener('click', p.logName); // никаких проблем с this
      


      • AriesUa
        /#21277556

        Одна из причин, почему появились «arrow functions», это потеря контекста. И именно их рекомендуют использовать, как callback функции.

        В вашем примере необходимо использовать:

        class Person{
          constructor(name){
            this.name = name;
          }
          logName(){
            console.log(this.name);
          }
        }
        
        let p = new Person('Ivan');
        
        btn.addEventListener('click', (e) => p.logName()); // нет магии, нет привязки.
        


        Плюс, повышается читабельность кода.

    • Kozack
      /#21290502

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


      function createSpecies(type, name, gender) {
        if (type === 'frog') {
          return new Frog(name, gender)
      
        } else if (type === 'human') {
          return new Human(name, gender)
      
        } else if (type == undefined) {
          throw new Error('Cannot create a species with an unknown type')
        }
      }
      
      const species = createSpecies('frog', 'sally', 'female');
      
      // Проверка типов
      species instanceof Frog // true
      species instanceof Human // false

  6. Tolomuco
    /#21273756

    Название не соответствует содержанию.

    И, кстати, я не понял из текста, почему так важно обрабатывать именно JSON ошибки? Они как-то особо загадочно рушат приложения? И как try/catch помог бы автору избавиться от проблемы без исправления кода «внешнего пакета»?

    • evil_random
      /#21274422

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

      Конструкция response.data.record.id без дополнительных проверок в 99% случаев приведёт к багу рано или поздно.

  7. potan
    /#21274750

    Самый надежный javasctipt, это сгенерированный компилятором Elm, Purescript, Idris или Haskell.