Лекция
Это продолжение увлекательной статьи про идеальный код.
...
другая функция, которая использовала бы это имя, теперь это был бы массив, и он мог бы его сломать. let name = "Райан МакДермотт" ; функция splitIntoFirstAndLastName ( ) { имя = имя . сплит ( "" ) ; } splitIntoFirstAndLastName ( ) ; консоль . журнал ( имя ) ; // ['Райан', 'Макдермотт'];
Хороший:
функция splitIntoFirstAndLastName ( имя ) { возвращаемое имя . разделить ( "" ) ; } const name = "Райан МакДермотт" ; const newName = splitIntoFirstAndLastName ( имя ) ; консоль . журнал ( имя ) ; // 'Райан МакДермотт'; консоль . журнал ( новое имя ) ; // ['Райан', 'Макдермотт'];

В JavaScript некоторые значения неизменяемы (неизменяемы), а некоторые - изменяемы (изменяемы). Объекты и массивы - это два типа изменяемых значений, поэтому важно осторожно обращаться с ними, когда они передаются в качестве параметров функции. Функция JavaScript может изменять свойства объекта или изменять содержимое массива, что может легко вызвать ошибки в другом месте.
Предположим, есть функция, которая принимает параметр массива, представляющий корзину для покупок. Если функция вносит изменение в этот массив корзины покупок - например, добавляя элемент для покупки - то cartэто добавление повлияет на любую другую функцию, использующую тот же массив. Это может быть здорово, но может и плохо. Представим себе плохую ситуацию:
Пользователь нажимает кнопку «Купить», которая вызывает purchaseфункцию, которая порождает сетевой запрос и отправляетcart массив на сервер. Из-за плохого сетевого подключения purchaseфункция должна продолжать повторять запрос. А что, если тем временем пользователь случайно нажимает кнопку «Добавить в корзину» на предмете, который ему на самом деле не нужен, до того, как начнется сетевой запрос? Если это произойдет и начнется сетевой запрос, то функция покупки отправит случайно добавленный элемент, потому что cartмассив был изменен.
Отличным решением будет для addItemToCart функция всегда клонировала cart, редактировала и возвращала клон. Это гарантирует, что изменения не повлияют на функции, которые все еще используют старую корзину.
Следует упомянуть два предостережения относительно этого подхода:
Могут быть случаи, когда вы действительно хотите изменить входной объект, но когда вы воспользуетесь этой практикой программирования, вы обнаружите, что такие случаи довольно редки. Большинство вещей можно отремонтировать, чтобы не было побочных эффектов!
Клонирование больших объектов может быть очень дорогостоящим с точки зрения производительности. К счастью, на практике это не является большой проблемой, потому что существуют отличные библиотеки, которые позволяют использовать такой подход к программированию быстро и не так интенсивно использовать память, как если бы вы вручную клонировали объекты и массивы.
Плохой:
const addItemToCart = ( cart , item ) => { cart . push ( { элемент , дата : Дата . сейчас ( ) } ) ; } ;
Хороший:
const addItemToCart = ( cart , item ) => { return [ ... cart , { item , date : Date . now ( ) } ] ; } ;

Загрязнение глобальных переменных - это плохая практика в JavaScript, потому что вы можете столкнуться с другой библиотекой, и пользователь вашего API не будет ничего мудрого, пока не получит исключение в производственной среде. Давайте подумаем о примере: что, если вы хотите расширить собственный метод Array JavaScript, чтобы иметь diffметод, который мог бы показать разницу между двумя массивами? Вы можете написать свою новую функцию в Array.prototype, но она может конфликтовать с другой библиотекой, которая пытается сделать то же самое. Что, если эта другая библиотека просто использовала, diffчтобы найти разницу между первым и последним элементами массива? Вот почему было бы намного лучше просто использовать классы ES2015 / ES6 и просто расширить Arrayглобальный.
Плохой:
Массив . прототип . diff = функция diff ( compareArray ) { const hash = new Set ( compareArray ) ; верни это . фильтр ( elem => ! hash . has ( elem ) ) ; } ;
Хороший:
Класс SuperArray расширяет массив { диф ( comparisonArray ) { константный хэш = новый набор ( comparisonArray ) ; верни это . фильтр ( elem => ! hash . has ( elem ) ) ; } }

JavaScript не является функциональным языком в отличие от Haskell, но имеет функциональную окраску. Функциональные языки могут быть чище и проще для тестирования. По возможности отдайте предпочтение этому стилю программирования.
Плохой:
const programmerOutput = [ { имя : "Дядя Бобби" , linesOfCode : 500 } , { имя : "Сьюзи Кью" , linesOfCode : 1500 } , { имя : "Джимми Гослинг" , linesOfCode : 150 } , { имя : "Грейси Хоппер» , linesOfCode : 1000 } ] ; пусть totalOutput = 0 ; for ( пусть i = 0 ; i < programmerOutput . length ; i ++ ) { totalOutput + = programmerOutput [ i ] . linesOfCode ; }
Хороший:
const programmerOutput = [ { имя : "Дядя Бобби" , linesOfCode : 500 } , { имя : "Сьюзи Кью" , linesOfCode : 1500 } , { имя : "Джимми Гослинг" , linesOfCode : 150 } , { имя : "Грейси Хоппер» , linesOfCode : 1000 } ] ; const totalOutput = programmerOutput . уменьшить ( ( totalLines , output ) => totalLines + output . linesOfCode , 0 ) ;

Плохой:
if ( fsm . state === "выборка" && isEmpty ( listNode ) ) { // ... }
Хороший:
функция shouldShowSpinner ( fsm , listNode ) { возвращает fsm . state === "выборка" && isEmpty ( listNode ) ; } if ( shouldShowSpinner ( fsmInstance , listNodeInstance ) ) { // ... }

Плохой:
функция isDOMNodeNotPresent ( узел ) { // ... } if ( ! isDOMNodeNotPresent ( узел ) ) { // ... }
Хороший:
функция isDOMNodePresent ( узел ) { // ... } if ( isDOMNodePresent ( узел ) ) { // ... }

Это кажется невыполнимой задачей. Услышав это впервые, большинство людей спрашивают: «Как я могу что-то делать без ifзаявления?» Ответ заключается в том, что вы можете использовать полиморфизм для решения одной и той же задачи во многих случаях. Второй вопрос обычно звучит так: «Ну, это здорово, но зачем мне это делать?» Ответ - это предыдущая концепция чистого кода, которую мы усвоили: функция должна делать только одно. Когда у вас есть классы и функции с ifоператорами, вы говорите своему пользователю, что ваша функция выполняет несколько функций. Помните, просто делайте одно.
Плохой:
class Airplane { // ... getCruisingAltitude ( ) { switch ( this . type ) { case "777" : вернуть this . getMaxAltitude ( ) - это . getPassengerCount ( ) ; case "Air Force One" : верните это . getMaxAltitude ( ) ; дело «Цессна» : верните это . getMaxAltitude( ) - это . getFuelExpenditure ( ) ; } } }
Хороший:
class Airplane { // ... } class Boeing777 расширяет Airplane { // ... getCruisingAltitude ( ) { возвращает это . getMaxAltitude ( ) - это . getPassengerCount ( ) ; } } class AirForceOne расширяет Airplane { // ... getCruisingAltitude ( ) { возвращает это . getMaxAltitude ( ) ; } } class Cessna extends Airplane { // ... getCruisingAltitude ( ) { верните это . getMaxAltitude ( ) - это . getFuelExpenditure ( ) ; } }

JavaScript не типизирован, что означает, что ваши функции могут принимать аргументы любого типа. Иногда вас укусывает эта свобода, и возникает соблазн провести проверку типов в ваших функциях. Есть много способов избежать этого. Первое, что нужно учитывать, - это согласованные API.
Плохой:
Функция travelToTexas ( транспортного средства ) { если ( автомобиль InstanceOf велосипед ) { транспортного средства . педаль ( this . currentLocation , new Location ( "техас" ) ) ; } Еще если ( автомобиль InstanceOf автомобиль ) { автомобиль . диск ( this . currentLocation , new Location ( "texas" )); } }
Хороший:
функция travelToTexas ( транспортное средство ) { транспортное средство . move ( this . currentLocation , новое местоположение ( "Техас" ) ) ; }

Если вы работаете с базовыми примитивными значениями, такими как строки и целые числа, и не можете использовать полиморфизм, но по-прежнему чувствуете необходимость проверки типов, вам следует рассмотреть возможность использования TypeScript. Это отличная альтернатива обычному JavaScript, поскольку она предоставляет вам статическую типизацию поверх стандартного синтаксиса JavaScript. Проблема с ручной проверкой типов в обычном JavaScript заключается в том, что для правильного выполнения этого требуется столько лишнего словоблудия, что ложная «типобезопасность», которую вы получаете, не компенсирует потерю удобочитаемости. Держите свой JavaScript в чистоте, пишите хорошие тесты и проводите хорошие обзоры кода. В противном случае делайте все это, но с TypeScript (который, как я уже сказал, является отличной альтернативой!).
Плохой:
function comb ( val1 , val2 ) { if ( ( typeof val1 === "число" && typeof val2 === "число" ) || ( typeof val1 === "строка" && typeof val2 === "строка" ) ) { возвращение знач1 + знач2 ; } throw new Error ( «Должен быть типа String или Number» ) ; }
Хороший:
Функция объединить ( знач1 , знач2 ) { возвращение знач1 + знач2 ; }

Современные браузеры проводят большую внутреннюю оптимизацию во время выполнения. Часто, если вы оптимизируете, вы просто зря теряете время. Есть хорошие ресурсы, чтобы увидеть, где не хватает оптимизации. Тем временем нацельтесь на них, пока они не будут исправлены, если это возможно.
Плохой:
// В старых браузерах каждая итерация с некэшированным `list.length` была бы дорогостоящей // из-за пересчета` list.length`. В современных браузерах это оптимизировано. for ( let i = 0 , len = list . length ; i < len ; i ++ ) { // ... }
Хороший:
for ( let i = 0 ; i < list . length ; i ++ ) { // ... }

Мертвый код так же плох, как и дублированный код. Нет причин хранить его в своей кодовой базе. Если его не называют, избавьтесь от него! Он по-прежнему будет в безопасности в вашей истории версий, если он вам все еще понадобится.
Плохой:
функция oldRequestModule ( url ) { // ... } функция newRequestModule ( url ) { // ... } const req = newRequestModule ; inventoryTracker ( "яблоки" , req , "www.inventory-awesome.io" ) ;
Хороший:
функция newRequestModule ( url ) { // ... } const req = newRequestModule ; inventoryTracker ( "яблоки" , req , "www.inventory-awesome.io" ) ;

Использование методов получения и установки для доступа к данным об объектах может быть лучше, чем простой поиск свойства объекта. "Почему?" вы можете спросить. Что ж, вот неорганизованный список причин, почему:
set.Плохой:
function makeBankAccount ( ) { // ... return { баланс : 0 // ... } ; } const account = makeBankAccount ( ) ; аккаунт . баланс = 100 ;
Хороший:
function makeBankAccount ( ) { // это приватный let balance = 0 ; // "геттер", обнародованный через возвращенный объект ниже function getBalance ( ) { return balance ; } // "сеттер", обнародованный через возвращенный объект ниже function setBalance ( amount ) { // ... проверяем перед обновлением баланса balance = amount ; } return { // ... getBalance , setBalance } ; } const account = makeBankAccount ( ) ; аккаунт . setBalance ( 100 ) ;

Этого можно добиться путем закрытия (для ES5 и ниже).
Плохой:
const Сотрудник = функция ( имя ) { это . name = name ; } ; Сотрудник . прототип . getName = function getName ( ) { вернуть это . имя ; } ; const employee = new Employee ( "Джон Доу" ) ; консоль . log ( `Имя сотрудника: $ { employee . getName ( ) } ` ) ; // Имя сотрудника: Джон Доу удалить сотрудника . имя ; консоль . log ( `Имя сотрудника: $ { employee . getName ( ) } ` ) ; // Имя сотрудника: undefined
Хороший:
функция makeEmployee ( имя ) { return { getName ( ) { возвращаемое имя ; } } ; } const employee = makeEmployee ( "Джон Доу" ) ; консоль . log ( `Имя сотрудника: $ { employee . getName ( ) } ` ) ; // Имя сотрудника: Джон Доу удалить сотрудника . имя ; консоль . log ( `Имя сотрудника: $ { employee . getName ( ) } ` ) ; // Имя сотрудника: Джон Доу

Для классических классов ES5 очень сложно получить удобочитаемые определения классов, наследования, построения и методов. Если вам нужно наследование (и имейте в виду, что может и нет), то отдавайте предпочтение классам ES2015 / ES6. Однако предпочитайте небольшие функции классам, пока не обнаружите, что вам нужны более крупные и сложные объекты.
Плохой:
const Animal = function ( age ) { if ( ! ( this instanceof Animal ) ) { throw new Error ( "Создать экземпляр животного с помощью` new` " ) ; } это . age = возраст ; } ; Животное . прототип . move = функция move ( ) { } ; const Mammal = function ( age , furColor ) { if ( ! ( этот экземпляр Mammal ) ) { throw new Error ( "Создать экземпляр Mammal с помощью` new` " ) ; } Животное . вызов ( это , возраст ) ; это . furColor = furColor ; } ; Млекопитающее . prototype = Объект . создать ( Животное . прототип ) ; Млекопитающее . прототип . конструктор = Млекопитающее ; Млекопитающее . прототип . liveBirth = функция liveBirth ( ) { } ; const Human = function ( age , furColor , languageSpoken ) { if ( ! ( этот экземпляр Human ) ) { throw new Error ( "Создать экземпляр человека с помощью` new` " ) ; } Млекопитающее . call ( this , age , furColor ) ; это . languageSpoken = languageSpoken ; } ; Человек . prototype = Объект . создать ( Mammal . prototype ) ; Человек . прототип . конструктор = Человек ; Человек . прототип . Speak = функция Speak ( ) { } ;
Хороший:
class Animal { конструктор ( возраст ) { this . age = возраст ; } move ( ) { / * ... * / } } class Mammal extends Animal { constructor ( age , furColor ) { super ( age ) ; это . furColor = furColor ; } liveBirth ( ) { / * ... * / } } class Human расширяет Mammal { конструктор ( age , furColor , languageSpoken ) { super ( age , furColor ) ; это . languageSpoken = languageSpoken ; } Speak ( ) { / * ... * / } }

Этот шаблон очень полезен в JavaScript, и вы видите его во многих библиотеках, таких как jQuery и Lodash. Это позволяет вашему коду быть выразительным и менее подробным. По этой причине, я говорю, используйте цепочку методов и посмотрите, насколько чистым будет ваш код. В ваших функциях класса просто возвращайтесь thisв конце каждой функции, и вы можете связать с ней дополнительные методы класса.
Плохой:
class Car { конструктор ( марка , модель , цвет ) { this . make = сделать ; это . модель = модель ; это . color = цвет ; } setMake ( сделать ) { это . make = сделать ; } setModel ( модель ) { это . модель = модель ; } setColor ( цвет ) { это . color = цвет ; } save ( ) { console . журнал ( этот . марка , эта . модель , этот . цвет ) ; } } const car = new Car ( «Форд» , «F-150» , «красный» ) ; машина . setColor ( "розовый" ) ; машина . сохранить ( ) ;
Хороший:
class Car { конструктор ( марка , модель , цвет ) { this . make = сделать ; это . модель = модель ; это . color = цвет ; } setMake ( сделать ) { это . make = сделать ; // ПРИМЕЧАНИЕ. Возвращаем это для цепочки return this ; } setModel ( модель ) { это . модель = модель ; // ПРИМЕЧАНИЕ. Возвращаем это для цепочки return this ; } setColor ( цвет ) { это . color = цвет ; // ПРИМЕЧАНИЕ. Возвращаем это для цепочки return this ; } save ( ) { console . журнал ( этот . марка , эта . модель , этот . цвет ) ; // ПРИМЕЧАНИЕ. Возвращаем это для цепочки return this ; } } const car = new Car ( «Форд» , «F-150» , «красный» ) . setColor ( "розовый" ) . сохранить ( ) ;

Как известно в « Паттернах проектирования банды четырех», вам следует предпочесть композицию наследованию там, где это возможно. Есть много веских причин для использования наследования и множество веских причин для использования композиции. Суть этой максимы заключается в том, что если ваш разум инстинктивно стремится к наследованию, попробуйте подумать, может ли композиция лучше смоделировать вашу проблему. В некоторых случаях может.
Тогда вы можете спросить: «Когда мне следует использовать наследование?» Это зависит от вашей проблемы, но это достойный список случаев, когда наследование имеет больше смысла, чем композиция:
Плохой:
class Сотрудник { конструктор ( имя , адрес электронной почты ) { this . name = name ; это . электронная почта = электронная почта ; } // ... } // Плохо, потому что у сотрудников есть налоговые данные. EmployeeTaxData не тип Сотрудника класса EmployeeTaxData расширяет Employee { конструктор ( ПЛА , зарплаты ) { супер ( ) ; это . ssn = ssn ; это . зарплата = зарплата ; } // ... }
Хороший:
class EmployeeTaxData { конструктор ( ssn , зарплата ) { это . ssn = ssn ; это . зарплата = зарплата ; } // ... } class Сотрудник { конструктор ( имя , адрес электронной почты ) { this . name = name ; это . электронная почта = электронная почта ; } setTaxData ( ssn , зарплата ) { это . taxData = new EmployeeTaxData ( ssn , зарплата ) ; } // ... }

Как сказано в Чистом коде, «у класса не должно быть более одной причины для изменения». Заманчиво собрать класс с большим количеством функций, например, когда вы можете взять с собой в рейс только один чемодан. Проблема в том, что ваш класс не будет концептуально сплоченным, и это даст ему множество причин для изменений. Важно свести к минимуму количество раз, необходимое для смены класса. Это важно, потому что, если в одном классе содержится слишком много функций и вы изменяете его часть, может быть трудно понять, как это повлияет на другие зависимые модули в вашей кодовой базе.
Плохой:
class UserSettings { конструктор ( пользователь ) { this . пользователь = пользователь ; } changeSettings ( settings ) { if ( this . verifyCredentials ( ) ) { // ... } } verifyCredentials ( ) { // ... } }
Хороший:
class UserAuth { конструктор ( пользователь ) { this . пользователь = пользователь ; } verifyCredentials ( ) { // ... } } class UserSettings { конструктор ( пользователь ) { this . пользователь = пользователь ; это . auth = новый UserAuth ( пользователь ) ; } changeSettings ( settings ) { if ( this . auth . verifyCredentials ( ) ) { // ... } } }

Как заявил Бертран Мейер, «программные объекты (классы, модули, функции и т. Д.) Должны быть открыты для расширения, но закрыты для модификации». Что это значит? Этот принцип в основном гласит, что вы должны разрешать пользователям добавлять новые функции без изменения существующего кода.
Плохой:
класс AjaxAdapter расширяет адаптер { конструктор ( ) { супер ( ) ; это . name = "ajaxAdapter" ; } } класс NodeAdapter расширяет адаптер { конструктор ( ) { супер ( ) ; это . name = "nodeAdapter" ; } } класс HttpRequester { конструктор ( адаптер ) { это . адаптер = адаптер ; } fetch ( url ) { if ( this . adapter . name === "ajaxAdapter" ) { return makeAjaxCall ( url ) . then ( response => { // преобразовываем ответ и возвращаем } ) ; } else if ( this . adapter . name === "nodeAdapter" ) { return makeHttpCall ( url ) . тогда( response => { // преобразовываем ответ и возвращаем } ) ; } } } function makeAjaxCall ( url ) { // запрос и возврат обещания } function makeHttpCall ( url ) { // запрос и возврат обещания }
Хороший:
класс AjaxAdapter расширяет адаптер { конструктор ( ) { супер ( ) ; это . name = "ajaxAdapter" ; } request ( url ) { // запрос и возврат обещания } } класс NodeAdapter расширяет адаптер { конструктор ( ) { супер ( ) ; это . name = "nodeAdapter" ; } request ( url ) { // запрос и возврат обещания } } класс HttpRequester { конструктор ( адаптер ) { это . адаптер = адаптер ; } fetch ( url ) { вернуть это . адаптер . запрос ( URL ) . then ( response => { // преобразовываем ответ и возвращаем } ) ; } }

Это пугающий термин для обозначения очень простой концепции. Формально он определяется как «Если S является подтипом T, то объекты типа T могут быть заменены объектами типа S (т. Е. Объекты типа S могут заменять объекты типа T) без изменения каких-либо желаемых свойств этой программы. (правильность, выполненная задача и т. д.) ». Это еще более страшное определение.
Лучшее объяснение этого заключается в том, что если у вас есть родительский класс и дочерний класс, тогда базовый класс и дочерний класс могут использоваться взаимозаменяемо без получения неверных результатов. Это все еще может сбивать с толку, поэтому давайте взглянем на классический пример Square-Rectangle. Математически квадрат - это прямоугольник, но если вы смоделируете его с помощью отношения «is-a» через наследование, у вас быстро возникнут проблемы.
Плохой:
класс Rectangle { конструктор ( ) { это . ширина = 0 ; это . высота = 0 ; } setColor ( цвет ) { // ... } render ( область ) { // ... } setWidth ( ширина ) { это . ширина = ширина ; } setHeight ( высота ) { это . высота = высота ; } getArea ( ) { вернуть это . ширина * это . высота ; } } class Square расширяет Rectangle { setWidth ( width ) { this . ширина = ширина ; это . высота = ширина ; } setHeight ( высота ) { это . ширина = высота ; это . высота = высота ; } } функция renderLargeRectangles ( прямоугольники ) { прямоугольники . forEach ( rectangle => { rectangle . setWidth ( 4 ) ; rectangle . setHeight ( 5 ) ; const area = rectangle . getArea ( ) ; // ПЛОХО: возвращает 25 для Square. Должно быть 20. rectangle . render ( area ) ; } ) ; } const rectangles = [ новый прямоугольник ( ) , новый прямоугольник ( ) , новый квадрат ( ) ] ; renderLargeRectangles ( прямоугольники ) ;
Хороший:
class Shape { setColor ( color ) { // ... } render ( область ) { // ... } } class Rectangle extends Shape { constructor(width, height) { super(); this.width = width; this.height = height; } getArea() { return this.width * this.height; } } class Square extends Shape { constructor(length) { super(); this.length = length; } getArea() { return this.length * this.length; } } function renderLargeShapes(shapes) { shapes.forEach(shape => { const area = shape.getArea(); shape.render(area); }); } const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)]; renderLargeShapes(shapes);
У JavaScript нет интерфейсов, поэтому этот принцип не применяется так же строго, как другие. Однако это важно и актуально даже при отсутствии в JavaScript системы типов.
Интернет-провайдер заявляет, что «клиентов не следует заставлять зависеть от интерфейсов, которые они не используют». Интерфейсы - это неявные контракты в JavaScript из-за утиной печати.
Хороший пример, демонстрирующий этот принцип в JavaScript, - это классы, которым требуются большие объекты настроек. Не требовать от клиентов настройки огромного количества опций, это полезно, потому
продолжение следует...
Часть 1 Идеальный код на PHP и Javascript
Часть 2 1. Комментарии и Документация - Идеальный код на PHP и
Часть 3 Объекты и структуры данных - Идеальный код на PHP и
Часть 4 Тестирование - Идеальный код на PHP и Javascript
Комментарии