Как стать автором
Обновить
2772.02
RUVDS.com
VDS/VPS-хостинг. Скидка 15% по коду HABR15

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

Время на прочтение12 мин
Количество просмотров46K
Автор оригинала: 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-коде?



Теги:
Хабы:
Всего голосов 37: ↑22 и ↓15+18
Комментарии17

Публикации

Информация

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