Статьи

Методы заимствования из сильно типизированных языков в JavaScript

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

Система типов JavaScript

Давайте сначала сделаем краткий обзор того, как работает система типов JavaScript. JavaScript разделяет свои значения на две категории:

  • Примитивные типы, такие как String , Number и Boolean . Когда вы присваиваете примитивный тип переменной, вы всегда создаете новое значение, которое является копией присваиваемого вами значения.
  • Типы ссылок, такие как Object и Array . Присвоение ссылочных типов всегда копирует одну и ту же ссылку. Чтобы прояснить это, давайте посмотрим на следующий пример кода:
 var a = []; var b = a; a.push('Hello'); 

Переменная b изменится, когда мы изменим a , поскольку они обе являются ссылками на один и тот же массив. Так работают все ссылочные типы.

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

Введение в правило последовательных типов

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

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

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

Типы в переменных

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

 var text = 'Hello types'; // This is wrong! Don't do it! text = 1; 

Приведенный выше пример показывает проблему. Это правило требует от нас сделать вид, что последняя строка кода в этом примере выдаст ошибку, потому что, когда мы впервые определили переменную text , мы присвоили ей значение типа string и теперь мы присваиваем ей number . Правило согласованных типов означает, что мы не можем изменять тип переменной таким образом.

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

Типы в параметрах функций

То же правило применяется здесь. Параметры для функций также должны быть согласованными. Пример неправильного поведения:

 function sum(a, b) { if (typeof a === 'string') { a = 1; } return a + b; } 

Что с этим не так? Обычно считается плохой практикой ветвление логики на основе проверки типа. Есть исключения из этого, но обычно лучше использовать полиморфизм.

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

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

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

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

Типы в функциях Возвращаемые значения

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

Мы можем взять пример из AngularJS здесь. AngularJS предоставляет функцию строчного текста, которая называется angular.lowercase . Для этого также есть стандартная функция String.prototype.toLowerCase . Мы можем сравнить их поведение, чтобы лучше понять эту часть правила:

 var a = angular.lowercase('Hello Types'); var b = angular.lowercase(null); 

Переменная a будет содержать то, что вы ожидаете: 'hello types' . Однако, что будет содержать? Это будет пустая строка? Будет ли функция генерировать исключение? Или, может быть, он просто будет null ? В этом случае значение b равно null . Обратите внимание, как сразу было трудно догадаться, каким будет результат — у нас было сразу три возможных результата. В случае функции Angular для нестроковых значений она всегда будет возвращать входные данные.

Теперь давайте посмотрим, как ведет себя встроенный:

 var a = String.prototype.toLowerCase.call('Hello Types'); var b = String.prototype.toLowerCase.call(null); 

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

Давайте рассмотрим типичный вариант использования такой функции. Мы используем его в какой-то момент в нашем коде для преобразования строк в нижний регистр. Как часто бывает в коде JavaScript, мы не уверены на 100%, что наш ввод всегда будет строкой. Это не имеет значения, поскольку, поскольку мы хорошие программисты, мы предполагаем, что в нашем коде нет ошибок.

Что произойдет, если мы используем функцию из AngularJS, которая не соблюдает эти правила? Нестроковое значение проходит через это без каких-либо проблем. Может пройти через еще пару функций, возможно, мы даже отправим его через вызов XMLHttpRequest . Теперь неправильное значение находится на нашем сервере, и оно попадает в базу данных. Вы можете видеть, куда я иду с этим, верно?

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

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

 function foo(a) { if(a === 'foo') { return 'bar'; } return false; } 

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

 function foo(a) { if(a === 'foo') { return 'bar'; } return ''; } 

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

null и undefined являются специальными

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

При работе со ссылочными типами иногда необходимо указать, что значения нет. Хорошим примером этого является document.getElementById . Если он не найдет соответствующий элемент, он вернет null .

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

undefined может рассматриваться как «нет значения» для ссылок. Для большинства целей его можно рассматривать как равное null , но предпочтение отдается null из-за его семантики в других объектно-ориентированных языках.

Массивы и null

При работе с массивами также следует учитывать, что пустой массив часто является лучшим выбором, чем null . Хотя массивы являются ссылочными типами, и вы можете использовать с ними null , обычно имеет смысл возвращать пустой массив. Давайте посмотрим на следующий пример:

 var list = getListOfItems(); for(var i = 0; i < list.length; i++) { //do something } 

Это, вероятно, один из самых распространенных стилей использования массивов. Вы получаете массив из функции, а затем перебираете его, чтобы сделать что-то еще. Что произойдет в приведенном выше коде, если getListOfItems вернул getListOfItems когда нет элементов? Это выдаст ошибку, потому что null не имеет length (или любого другого свойства в этом отношении). Когда вы рассматриваете типичное использование массивов, подобных этому, или даже list.forEach или list.map , вы можете увидеть, как это вообще хорошая идея — возвращать пустой массив, когда значений нет.

Проверка типов и преобразование типов

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

Преобразование типов

Первая цель при преобразовании типов должна заключаться в том, чтобы убедиться, что ваши значения имеют правильный тип. Числовые значения должны быть Number s, а не String s и так далее. Вторая цель должна заключаться в том, что вам нужно конвертировать значение только один раз.

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

Парсинг данных из DOM является очень распространенным примером того, как что-то начинает идти не так. Допустим, у вас есть текстовое поле, которое содержит число, и вы хотите его прочитать. Или это может быть просто атрибут в каком-то элементе HTML, он даже не должен вводиться пользователем.

 //This is always going to be a string var num = numberInput.value; //This is also always a string var num2 = myElement.getAttribute('numericAttribute'); 

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

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

 //We can parse ints and floats like so var num = parseInt(numberInput.value, 10); var num2 = parseFloat(myElement.getAttribute('numericAttribute')); //But if you need to convert a string to a boolean, you need to do a string comparison var bool = booleanString === 'true'; 

typeof и Тип Проверки

Вы должны использовать только typeof для проверки, а не ветвление логики на основе типа. Есть исключения из этого, но это хорошее правило для подражания.

Давайте рассмотрим два примера для этого:

 function good(a) { if(typeof a !== 'number') { throw new TypeError('a must be a number'); } //do something } 

Это пример использования typeof для проверки. Мы гарантируем, что параметр, переданный функции, имеет правильный тип. Однако в следующем примере показано, что означает ветвление логики по типу.

 function bad(a) { if(typeof a === 'number') { //do something } else if(typeof a === 'string') { //do something } else if(typeof a === 'boolean') { //do something } } 

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

Если в вашем коде много типов typeof , это может быть признаком того, что вам может понадобиться преобразовать значение, с которым вы сравниваете. Типичные проверки типов распространяются, и это часто является хорошим признаком плохого дизайна в отношении типов.

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

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

То же правило применяется к instanceof как typeof : вы должны стараться избегать его, так как это может быть признаком плохого дизайна. Есть один случай, когда это неизбежно:

 try { // some code that throws exceptions } catch(ex) { if (ex instanceof TypeError) { } else if (ex instanceof OtherError) { } } 

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

Вывод

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

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

Это может показаться сложным, если вы не использовали языки со строгой типизацией, но это окупается, когда вам нужно отладить или поддерживать код.

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