Открыть список
Как стать автором
Обновить
2707,69
Рейтинг
RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR

JavaScript и TypeScript: 11 компактных конструкций, о которых стоит знать

Блог компании RUVDS.comРазработка веб-сайтовJavaScriptTypeScript
Перевод
Автор оригинала: Fernando Doglio
Существует очень тонкая грань между чистым, эффективным кодом и кодом, который может понять только его автор. А хуже всего то, что чётко определить эту грань невозможно. Некоторые программисты в её поисках готовы зайти гораздо дальше других. Поэтому, если нужно сделать некий фрагмент кода таким, чтобы он был бы гарантированно понятен всем, в таком коде обычно стараются не использовать всяческие компактные конструкции вроде тернарных операторов и однострочных стрелочных функций.

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



В этом материале я собираюсь разобрать некоторые весьма полезные (и иногда выглядящие достаточно таинственными) компактные конструкции, которые могут попасться вам в JavaScript и TypeScript. Изучив их, вы сможете пользоваться ими сами или, как минимум, сможете понять код тех программистов, которые их применяют.

1. Оператор ??


Оператор для проверки значений на null и undefined (nullish coalescing operator) выглядит как два знака вопроса (??). С трудом верится в то, что это, с таким-то названием, самый популярный оператор. Правда?

Смысл этого оператора заключается в том, что он возвращает значение правого операнда в том случае, если значение левого равно null или undefined. Это не вполне чётко отражено в  его названии, ну да ладно, что есть — то есть. Вот как им пользоваться:

function myFn(variable1, variable2) {
  let var2 = variable2 ?? "default value"
  return variable1 + var2
}

myFn("this has ", "no default value") //возвращает "this has no default value"
myFn("this has no ") //возвращает "this has no default value"
myFn("this has no ", 0) //возвращает "this has no 0"

Тут задействованы механизмы, очень похожие на те, что используются для организации работы оператора ||. Если левая часть выражения равняется null или undefined, то возвращена будет правая часть выражения. В противном случае будет возвращена левая часть. В результате оператор ?? отлично подходит для использования в ситуациях, когда некоей переменной может быть назначено всё что угодно, но при этом нужно принять какие-то меры в том случае, если в эту переменную попадёт null или undefined.

2. Оператор ??=


Оператор, используемый для назначения значения переменной только в том случае, если она имеет значение null или undefined (logical nullish assignment operator), выглядит как два вопросительных знака, за которыми идёт знак «равно» (??=). Его можно счесть чем-то вроде расширения вышеописанного оператора ??.

Посмотрим на предыдущий фрагмент кода, переписанный с использованием ??=.

function myFn(variable1, variable2) {
  variable2 ??= "default value"
  return variable1 + variable2
}

myFn("this has ", "no default value") //возвращает "this has no default value"
myFn("this has no ") //возвращает "this has no default value"
myFn("this has no ", 0) //возвращает "this has no 0"

Оператор ??= позволяет проверить значение параметра функции variable2. Если оно равняется null или undefined, он запишет в него новое значение. В противном случае значение параметра не изменится.

Учитывайте то, что конструкция ??= может показаться непонятной тем, кто с ней не знаком. Поэтому, если вы её используете, вам, возможно, стоит добавить в соответствующем месте кода короткий комментарий с пояснениями.

3. Сокращённое объявление TypeScript-конструкторов


Эта возможность имеет отношение исключительно к TypeScript. Поэтому если вы — поборник чистоты JavaScript, то вы многое упускаете. (Шучу, конечно, но к обычному JS такое, и правда, неприменимо).

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

Вот как это выглядит:

//Старый подход...
class Person {
  
  private first_name: string;
  private last_name: string;
  private age: number;
  private is_married: boolean;
  
  constructor(fname:string, lname:string, age:number, married:boolean) {
    this.first_name = fname;
    this.last_name = lname;
    this.age = age;
    this.is_married = married;
  }
}

//Новый подход, позволяющий сократить код...
class Person {

  constructor( private first_name: string,
               private last_name: string,
               private age: number,
               private is_married: boolean){}
}

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

Тут главное — не забыть добавить {} сразу после описания конструктора, так как это — представление тела функции. После того, как компилятор встретит такое описание, он всё поймёт и всё остальное сделает сам. Фактически, речь идёт о том, что и первый и второй фрагменты TS-кода будут в итоге преобразованы в один и тот же JavaScript-код.

4. Тернарный оператор


Тернарный оператор — это конструкция, которая читается достаточно легко. Этот оператор часто используют вместо коротких инструкций if…else, так как он позволяет избавиться от лишних символов и превратить многострочную конструкцию в однострочную.

// Исходная инструкция if…else
let isEven = ""
if(variable % 2 == 0) {
  isEven = "yes"
} else {
  isEven = "no"
}

//Использование тернарного оператора
let isEven = (variable % 2 == 0) ? "yes" : "no"

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

let variable = true;

(variable) ? console.log("It's TRUE") : console.log("It's FALSE")

Обратите внимание на то, что структура оператора выглядит так же, как и в предыдущем примере. Минус использования тернарного оператора заключается в том, что если в будущем понадобится расширить одну из его частей (либо ту, что относится к истинному значению логического выражения, либо ту, что относится к его ложному значению), это будет означать необходимость перехода к обычной инструкции if…else.

5. Использование короткого цикла вычислений, применяемого оператором ||


В JavaScript (и в TypeScript тоже) логический оператор ИЛИ (||) реализует модель сокращённых вычислений. То есть — он возвращает первое выражение, оцениваемое как true, и не выполняет проверку оставшихся выражений.

Это значит, что если имеется следующая инструкция if, где выражение expression1 содержит ложное значение (приводимое к false), а expression2 — истинное (приводимое к true), то вычисленными будут лишь expression1 и expression2. Выражения espression3 и expression4 вычисляться не будут.

if( expression1 || expression2 || expression3 || expression4)

Мы можем воспользоваться этой возможностью и за пределами инструкции if, там, где присваиваем переменным некие значения. Это позволит, в частности, записать в переменную значение, задаваемое по умолчанию, в том случае, если некое значение, скажем, представленное параметром функции, оказывается ложным (например — равно undefined):

function myFn(variable1, variable2) {
  let var2 = variable2 || "default value"
  return variable1 + var2
}

myFn("this has ", " no default value") //возвращает "this has no default value"
myFn("this has no ") //возвращает "this has no default value"

В этом примере продемонстрировано то, как можно пользоваться оператором || для записи в переменную либо значения второго параметра функции, либо значения, задаваемого по умолчанию. Правда, если присмотреться к этому примеру, в нём можно увидеть небольшую проблему. Дело в том, что если в variable2 будет значение 0 или пустая строка, то в var2 будет записано значение, задаваемое по умолчанию, так как и 0 и пустая строка приводятся к false.

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

6. Двойной побитовый оператор ~


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

Если говорить о побитовом операторе НЕ (~), то он берёт число, преобразует его в 32-битное целое число (отбрасывая «лишние» биты) и инвертирует биты этого числа. Это приводит к тому, что значение x превращается в значение -(x+1). Чем нам интересно подобное преобразование чисел? А тем, что если воспользоваться им дважды, это даст нам тот же результат, что и вызов метода Math.floor.

let x = 3.8
let y = ~x // x превращается в -(3 + 1), не забывайте о том, что число становится целым
let z = ~y //тут преобразуется y (равное -4) в -(-4 + 1) то есть - в 3

//Поэтому можно поступить так:

let flooredX = ~~x //оба вышеописанных действия выполняются в одной строке

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

7. Назначение значений свойствам объектов


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

Вот — пример, написанный на TypeScript.

let name:string = "Fernando";
let age:number = 36;
let id:number = 1;

type User = {
  name: string,
  age: number,
  id: number
}

//Старый подход
let myUser: User = {
  name: name,
  age: age,
  id: id
}

//Новый подход
let myNewUser: User = {
  name,
  age,
  id
}

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

8. Неявный возврат значений из стрелочных функций


Знаете о том, что однострочные стрелочные функции возвращают результаты вычислений, выполненных в их единственной строке?

Использование этого механизма позволяет избавиться от ненужного выражения return. Этот приём часто применяют в стрелочных функциях, передаваемых методам массивов, таким, как filter или map. Вот TypeScript-пример:

let myArr:number[] = [1,2,3,4,5,6,7,8,9,10]

//Использование длинных конструкций:
let oddNumbers:number[] = myArr.filter( (n:number) => {
  return n % 2 == 0
})

let double:number[] = myArr.map( (n:number) => {
  return n * 2;
})

//Применение компактных конструкций:
let oddNumbers2:number[] = myArr.filter( (n:number) => n % 2 == 0 )

let double2:number[] = myArr.map( (n:number) =>  n * 2 )

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

Единственная особенность, которую придётся тут учитывать, заключается в том, что то, что содержится в единственной строке рассматриваемых здесь коротких стрелочных функций, должно быть выражением (то есть — должно выдавать некий результат, который можно вернуть из функции). В противном случае подобная конструкция окажется неработоспособной. Например, вышеописанные однострочные функции нельзя писать так:

const m = _ => if(2) console.log("true")  else console.log("false")

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

9. Параметры функций, которые могут иметь значения, назначаемые по умолчанию


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

Но теперь та же задача решается очень просто:

//Функцию можно вызвать без 2 последних параметров
//в них могут быть записаны значения, задаваемые по умолчанию
function myFunc(a, b, c = 2, d = "") {
  //тут будет логика функции...
}

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

const mandatory = _ => {
  throw new Error("This parameter is mandatory, don't ignore it!")
}

function myFunc(a, b, c = 2, d = mandatory()) {
  // тут будет логика функции...
}

//Отлично работает!
myFunc(1,2,3,4)

//Выдаёт ошибку
myFunc(1,2,3)

Вот, собственно говоря, та самая однострочная стрелочная функция, при создании которой не обойтись без фигурных скобок. Дело тут в том, что функция mandatory использует инструкцию throw. Обратите внимание — «инструкцию», а не «выражение». Но, полагаю, это — не самая высокая плата за возможность оснащать функции обязательными параметрами.

10. Приведение любых значений к логическому типу с использованием !!


Этот механизм работает по тому же принципу, что и вышерассмотренная конструкция ~~. А именно, речь идёт о том, что для приведения любого значения к логическому типу можно воспользоваться двумя логическими операторами НЕ (!!):

!!23 // TRUE
!!"" // FALSE
!!0 // FALSE
!!{} // TRUE

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

Эта короткая конструкция может оказаться полезной в различных ситуациях. Во-первых — когда нужно обеспечить присвоение некоей переменной настоящего логического значения (например, если речь идёт о TypeScript-переменной типа boolean). Во-вторых — когда нужно выполнить строгое сравнение (с помощью ===) чего-либо с true или false.

11. Деструктурирование и синтаксис spread


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

▍Деструктурирование объектов


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

Применение деструктурирования объектов позволяет решать подобные задачи, используя минимальные объёмы кода:

const myObj = {
  name: "Fernando",
  age: 37,
  country: "Spain"
}

//Старый подход:
const name = myObj.name;
const age = myObj.age;
const country = myObj.country;

//Использование деструктурирования
const {name, age, country} = myObj;

Тот, кто пользовался TypeScript, видел этот синтаксис в инструкциях import. Он позволяет импортировать отдельные методы библиотек и при этом не загрязнять пространство имён проекта множеством ненужных функций:

import { get } from 'lodash'

Например, эта инструкция позволяет импортировать из библиотеки lodash лишь метод get. При этом в пространство имён проекта не попадают другие методы этой библиотеки. А их в ней очень и очень много.

▍Синтаксис spread и создание новых объектов и массивов на основе существующих


Использование синтаксиса spread () позволяет упростить задачу создания новых массивов и объектов на основе существующих. Теперь эту задачу можно решить, написав буквально одну строку кода и не обращаясь к каким-то особым методам. Вот пример:

const arr1 = [1,2,3,4]
const arr2 = [5,6,7]

const finalArr = [...arr1, ...arr2] // [1,2,3,4,5,6,7]

const partialObj1 = {
  name: "fernando"
}
const partialObj2 = {
  age:37
}

const fullObj = { ...partialObj1, ...partialObj2 } // {name: "fernando", age: 37}

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

▍Совместное использование деструктурирования и синтаксиса spread


Деструктурирование можно использовать вместе с синтаксисом spread. Это позволяет достичь интересного эффекта. Например — убрать первый элемент массива, а остальные не трогать (как в распространённом примере с первым и последним элементом списка, реализацию которого можно найти на Python и на других языках). А ещё, например, можно даже извлечь некоторые свойства из объекта, а остальные оставить нетронутыми. Рассмотрим пример:

const myList = [1,2,3,4,5,6,7]
const myObj = {
  name: "Fernando",
  age: 37,
  country: "Spain",
  gender: "M"
}

const [head, ...tail] = myList

const {name, age, ...others} = myObj

console.log(head) //1
console.log(tail) //[2,3,4,5,6,7]
console.log(name) //Fernando
console.log(age) //37
console.log(others) //{country: "Spain", gender: "M"}

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

const [...values, lastItem] = [1,2,3,4]

Этот код работать не будет.

Итоги


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

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

Какими компактными конструкциями вы пользуетесь в JavaScript- и TypeScript-коде?



Теги:JavaScriptTypeScriptразработка
Хабы: Блог компании RUVDS.com Разработка веб-сайтов JavaScript TypeScript
Всего голосов 48: ↑33 и ↓15 +18
Просмотры18.4K

Комментарии 17

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

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
ruvds.com
Численность
11–30 человек
Дата регистрации
Представитель
ruvds

Блог на Хабре