Хабрахабр

[Перевод] 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-коде?

Показать больше

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»