[Перевод] JavaScript и TypeScript: 11 компактных конструкций, о которых стоит знать
Существует очень тонкая грань между чистым, эффективным кодом и кодом, который может понять только его автор. А хуже всего то, что чётко определить эту грань невозможно. Некоторые программисты в её поисках готовы зайти гораздо дальше других. Поэтому, если нужно сделать некий фрагмент кода таким, чтобы он был бы гарантированно понятен всем, в таком коде обычно стараются не использовать всяческие компактные конструкции вроде тернарных операторов и однострочных стрелочных функций.
Но правда, неприятная правда, заключается в том, что эти вот компактные конструкции часто оказываются очень кстати. И они, при этом, достаточно просты. А это значит, что каждый, кому интересен код, в котором они используются, может их освоить и понять такой код.
В этом материале я собираюсь разобрать некоторые весьма полезные (и иногда выглядящие достаточно таинственными) компактные конструкции, которые могут попасться вам в JavaScript и TypeScript. Изучив их, вы сможете пользоваться ими сами или, как минимум, сможете понять код тех программистов, которые их применяют.
1. Оператор ??
Оператор для проверки значений на null
и undefined
(nullish coalescing operator) выглядит как два знака вопроса (??
). С трудом верится в то, что это, с таким-то названием, самый популярный оператор. Правда?
Смысл этого оператора заключается в том, что он возвращает значение правого операнда в том случае, если значение левого равно null
или undefined
. Это не вполне чётко отражено в его названии, ну да ладно, что есть — то есть. Вот как им пользоваться:
function myFn(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"
Тут задействованы механизмы, очень похожие на те, что используются для организации работы оператора ||
. Если левая часть выражения равняется 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…elselet 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.8let 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) //1console.log(tail) //[2,3,4,5,6,7]console.log(name) //Fernandoconsole.log(age) //37console.log(others) //{country: "Spain", gender: "M"}
Обратите внимание на то, что три точки, используемые в левой части инструкции присвоения значения, должны применяться к самому последнему элементу. Нельзя сначала воспользоваться синтаксисом spread, а потом уже описывать индивидуальные переменные:
const [...values, lastItem] = [1,2,3,4]
Этот код работать не будет.
Итоги
Существует ещё очень много конструкций, подобных тем, о которых мы сегодня говорили. Но, используя их, стоит помнить о том, что чем компактнее код — тем сложнее его читать тому, кто не привык к применяемым в нём сокращённым конструкциям. И цель применения подобных конструкций — это не минификация кода и не его ускорение. Эта цель заключается лишь в том, чтобы убрать из кода ненужные структуры и облегчить жизнь того, кто будет читать этот код.
Поэтому, чтобы всем было хорошо, рекомендуется поддерживать здоровый баланс между компактными конструкциями и обычным читабельным кодом. Стоит всегда помнить о том, что вы — не единственный человек, который читает ваш код.
Какими компактными конструкциями вы пользуетесь в JavaScript- и TypeScript-коде?