JavaScript часто называют самым простым языком для новичков, в программировании на котором сложнее всего достичь мастерства. Автор материала, перевод которого мы публикуем, говорит, что не может не согласиться с этим утверждением. Всё дело в том, что JS — это по-настоящему старый и по-настоящему гибкий язык. Он полон таинственных синтаксических конструкций и устаревших возможностей, всё ещё им поддерживаемых.
Сегодня мы поговорим о малоизвестных возможностях JavaScript и о вариантах их практического применения.
JavaScript — это всегда что-то новое
Я работаю с JavaScript уже много лет и мне постоянно попадается что-то такое, о существовании чего я и не подозревал. Здесь я попытался перечислить подобные малоизвестные возможности языка. В строгом режиме некоторые из них работать не будут, но в обычном режиме они представляют собой совершенно правильные образцы JS-кода. Надо отметить, что я не берусь советовать читателям брать всё это на вооружение. Хотя то, о чём пойдёт речь, кажется мне весьма интересным, вы, начав всем этим пользоваться, если вы работаете в команде, можете, мягко говоря, удивить коллег.
→ Код, который мы тут будем обсуждать, можно найти здесь
Обратите внимание на то, что я не включил сюда такие вещи, как поднятие переменных, замыкания, прокси-объекты, прототипное наследование, async/await, генераторы и прочее подобное. Хотя эти особенности языка и можно отнести к сложным для понимания, малоизвестными они не являются.
Оператор void
В JavaScript имеется унарный оператор void
. Возможно, вы сталкивались с ним в виде void(0)
или void 0
. Его единственная цель — вычислить выражение, находящееся справа от него и вернуть undefined
. 0
тут используется просто потому что так принято, хотя это и необязательно, и тут можно использовать любое правильное выражение. Правда, этот оператор в любом случае вернёт undefined
.
// Оператор void
void 0 // undefined
void (0) // undefined
void 'abc' // undefined
void {} // undefined
void (1 === 1) // undefined
void (1 !== 1) // undefined
void anyfunction() // undefined
Зачем добавлять в язык особое ключевое слово, служащее для возврата undefined
, если можно просто воспользоваться стандартным значением undefined
? Не правда ли, тут ощущается некоторая избыточность?
Как оказалось, до появления стандарта ES5 в большинстве браузеров стандартному значению undefined
можно было присвоить новое значение. Скажем, можно было успешно выполнить такую команду: undefined = "abc"
. В результате значение undefined
могло оказаться совсем не тем, чем оно должно быть. В те времена использование void
позволяло обеспечить уверенность в использовании именно настоящего undefined
.
Скобки при вызове конструкторов необязательны
Скобки, которые добавляют после имени класса, вызывая конструктор, совершенно необязательны (если только конструктору не надо передавать аргументы).
В следующем примере наличие или отсутствие скобок на правильность работы программы не влияет.
// Вызов конструктора со скобками
const date = new Date()
const month = new Date().getMonth()
const myInstance = new MyClass()
// Вызов конструктора без скобок
const date = new Date
const month = (new Date).getMonth()
const myInstance = new MyClass
Скобки при работе с IIFE можно не использовать
Синтаксис IIFE всегда казался мне странноватым. Зачем тут все эти скобки?
Как оказалось, скобки нужны лишь для того, чтобы сообщить JavaScript-парсеру о том, что некий код представляет собой функциональное выражение, а не неправильную попытку объявления функции. Знание этого факта позволяет понять то, что есть множество способов избавиться от скобок, в которые заключают IIFE, и при этом написать работающий код.
// IIFE
(function () {
console.log('Normal IIFE called')
})()
// Normal IIFE called
void function () {
console.log('Cool IIFE called')
}()
// Cool IIFE called
Здесь оператор void
сообщает парсеру о том, что следующий за ним код является функциональным выражением. Это даёт возможность избавиться от скобок вокруг объявления функции. И, кстати, тут можно использовать любой унарный оператор (void
, +
, !
, -
, и так далее), и код останется рабочим. Разве это не замечательно?
Однако если вы — внимательный читатель, то вы можете задаться вопросом о том, что унарный оператор воздействует на результат, возвращаемый из IIFE. На самом деле, так оно и есть. Но хорошо то, что если вам нужен результат выполнения IIFE, который вы, например, сохраняете в какой-нибудь переменной, тогда и скобки вокруг IIFE вам не нужны. Вот пример.
// IIFE, возвращающие некие значения
let result = (function () {
// ... какой-то код
return 'Victor Sully'
})()
console.log(result) // Victor Sully
let result1 = function () {
// ... какой-то код
return 'Nathan Drake'
}()
console.log(result1) // Nathan Drake
Скобки вокруг первого IIFE лишь улучшают читабельность кода, не влияя на его работу.
Если вы хотите лучше разобраться с IIFE — взгляните на этот материал.
Конструкция with
Знаете ли вы о том, что в JavaScript имеется конструкция with
, поддерживающая блоки выражений? Выглядит это так:
with (object)
statement
// для того чтобы выполнить несколько команд
with (object) {
statement
statement
...
}
Конструкция with
добавляет все свойства переданного ей объекта в цепочку областей видимости, используемую при выполнении команд.
// пример блока выражения with
const person = {
firstname: 'Nathan',
lastname: 'Drake',
age: 29
}
with (person) {
console.log(`${firstname} ${lastname} is ${age} years old`)
}
// Nathan Drake is 29 years old
Может показаться, что with
— это замечательный инструмент. Похоже, что он даже лучше чем новые возможности JS по деструктурированию объектов, но на самом деле это не так.
Конструкция with
признана устаревшей и пользоваться ей не рекомендуется. В строгом режиме её использование запрещено. Оказывается, блоки with
вызывают проблемы с производительностью и безопасностью.
Конструктор Function
Использование ключевого слова function
— это не единственный способ определить новую функцию. Определять функции можно динамически, используя конструктор Function
и оператор new
. Вот как это выглядит.
// Конструктор Function
const multiply = new Function('x', 'y', 'return x*y')
multiply(2,3)
// 6
Последний аргумент, передаваемый конструктору, представляет собой строку с кодом функции. Два других аргумента — это параметры функции.
Интересно отметить, что конструктор Function
является «родителем» всех конструкторов в JavaScript. Даже конструктор Object
— это конструктор Function
. И собственный конструктор Function
— это тоже Function
. В результате, вызов вида object.constructor.constructor...
, выполненный для любого JS-объекта достаточное количество раз вернёт в итоге конструктор Function
.
Свойства функций
Все мы знаем, что функции в JavaScript являются объектами первого класса. Следовательно, никто не мешает нам добавлять в функции новые свойства. Это совершенно нормально, но используется подобное редко.
Когда это может понадобиться?
На самом деле, есть несколько ситуаций, в которых эта возможность функций может оказаться кстати. Рассмотрим их.
▍Настраиваемые функции
Предположим, у нас имеется функция greet()
. Нам нужно, чтобы она выводила бы разные приветственные сообщения в зависимости от применяемых региональных настроек. Эти настройки могут храниться в некоей внешней по отношению к функции переменной. Кроме того, в функции может быть свойство, определяющие эти настройки, в частности — настройки языка пользователя. Мы воспользуемся вторым подходом.
// Свойства функций, задаваемые разработчиком
function greet () {
if (greet.locale === 'fr') {
console.log('Bonjour!')
} else if (greet.locale === 'es') {
console.log('Hola!')
} else {
console.log('Hello!')
}
}
greet()
// Hello!
greet.locale = 'fr'
greet()
// Bonjour!
▍Функции со статическими переменными
Вот ещё один похожий пример. Предположим, нам надо реализовать некий генератор, который выдаёт последовательность упорядоченных чисел. Обычно в подобных ситуациях, для того, чтобы хранить сведения о последнем сгенерированном числе, используются статические переменные-счётчики в классах или IIFE. При таком подходе мы ограничиваем доступ к счётчику и предотвращаем загрязнение глобальной области видимости дополнительными переменными.
Но что если нам нужна гибкость, если требуется читать или даже модифицировать значение подобного счётчика и при этом не засорять глобальную область видимости?
Конечно, можно создать класс с соответствующей переменной и с методами, позволяющими с ней работать. Или можно не утруждать себя подобными делами и просто использовать свойства функций.
// Свойства функций, задаваемые разработчиком
function generateNumber () {
if (!generateNumber.counter) {
generateNumber.counter = 0
}
return ++generateNumber.counter
}
console.log(generateNumber())
// 1
console.log(generateNumber())
// 2
console.log('current counter value: ', generateNumber.counter)
// current counter value: 2
generateNumber.counter = 10
console.log('current counter value: ', generateNumber.counter)
// current counter value: 10
console.log(generateNumber())
// 11
Свойства объекта arguments
Уверен, большинство из вас знают о том, что в функциях имеется объект arguments
. Это — массивоподобный объект, доступный внутри всех функций (за исключением стрелочных, у которых нет собственного объекта arguments
). Он содержит список аргументов, переданных функции при её вызове. Кроме того, в нём имеются некоторые интересные свойства:
arguments.callee
содержит ссылку на текущую функцию.arguments.caller
содержит ссылку на функцию которая вызвала текущую функцию.
Рассмотрим пример.
// свойства callee и caller объекта arguments
const myFunction = function () {
console.log('Current function: ', arguments.callee.name)
console.log('Invoked by function: ', arguments.callee.caller.name)
}
void function main () {
myFunction()
} ()
// Current function: myFunction
// Invoked by function: main
Стандарт ES5 запрещает использование свойств callee
и caller
в строгом режиме, но они всё ещё широко встречаются во многих скомпилированных в JavaScript текстах программ, например — в библиотеках. Поэтому о них полезно знать.
Тегированные шаблонные литералы
Наверняка вы, если имеете хоть какое-то отношение к программированию на JavaScript, слышали о шаблонных литералах. Шаблонные литералы — это одно из многих замечательных новшеств стандарта ES6. Однако знаете ли вы о тегированных шаблонных литералах?
// Обычный шаблонный литерал
`Hello ${username}!`
// Тегированный шаблонный литерал
myTag`Hello ${username}!`
Тегированные шаблонные литералы позволяют разработчику управлять тем, как шаблонный литерал превращается в строку. Делается это путём использования особых тегов. Тег — это всего лишь имя функции-парсера, которая получает массив строк и значений, интерпретированных строковым шаблоном. При использовании теговой функции ожидается, что она вернёт готовую строку.
В следующем примере наш тег — highlight
, интерпретирует данные шаблонного литерала и внедряет эти данные в готовую строку, помещая их в HTML-тег <mark>
, чтобы выделить их при выводе такого текста на веб-страницу.
// Объявление теговой функции
function highlight(strings, ...values) {
// здесь i - это итератор для массива строк
let result = ''
strings.forEach((str, i) => {
result += str
if (values[i]) {
result += `<mark>${values[i]}</mark>`
}
})
return result
}
const author = 'Henry Avery'
const statement = `I am a man of fortune & I must seek my fortune`
const quote = highlight`${author} once said, ${statement}`
// <mark>Henry Avery</mark> once said, <mark>I am a man of fortune
// & I must seek my fortune</mark>
Интересные способы использования этой возможности можно найти во многих библиотеках. Вот несколько примеров:
- styled-components — для использования в React-приложениях.
- es2015-i18n-tag — для перевода и интернационализации проектов.
- chalk — для вывода в консоль разноцветных сообщений.
Геттеры и сеттеры в стандарте ES5
JavaScript-объекты, по большей части, довольно просты. Предположим, у нас имеется объект user
, и мы пытаемся обратиться к его свойству age
, используя конструкцию user.age
. При таком подходе, если это свойство определено, мы получим его значение, а если не определено — получим undefined
. Всё очень просто.
Но работа со свойствами вовсе не должна быть настолько примитивной. JS-объекты реализуют концепцию геттеров и сеттеров. Вместо того чтобы напрямую возвращать значение некоего свойства объекта, мы можем написать собственную функцию-геттер, которая возвращает то, что мы сочтём нужным. То же самое касается и записи в свойства новых значений с использованием функций-сеттеров.
Геттеры и сеттеры позволяют реализовывать продвинутые схемы работы со свойствами. При чтении или записи свойств можно пользоваться концепциями виртуальных полей, можно проверять значения полей, при их записи или чтении могут происходить некие полезные побочные эффекты.
// Геттеры и сеттеры
const user = {
firstName: 'Nathan',
lastName: 'Drake',
// fullname - это виртуальное поле
get fullName() {
return this.firstName + ' ' + this.lastName
},
// проверка возраста перед записью значения
set age(value) {
if (isNaN(value)) throw Error('Age has to be a number')
this._age = Number(value)
},
get age() {
return this._age
}
}
console.log(user.fullName) // Nathan Drake
user.firstName = 'Francis'
console.log(user.fullName) // Francis Drake
user.age = '29'
console.log(user.age) // 29
// user.age = 'invalid text' // Error: Age has to be a number
Геттеры и сеттеры не относятся к новшествам стандарта ES5. Они всегда присутствовали в языке. В ES5 лишь добавлены удобные синтаксические средства для работы с ними. Подробности о геттерах и сеттерах можно почитать здесь.
Среди примеров использования геттеров можно отметить популярную Node.js-библиотеку Colors.
Эта библиотека расширяет класс String и добавляет в него множество методов-геттеров. Это позволяет преобразовывать строку в её «раскрашенный» вариант для того, чтобы эту строку потом использовать при логировании. Делается это путём работы со свойствами строки.
Оператор «запятая»
В JS есть оператор «запятая». Он позволяет записывать в одной строке несколько выражений, разделённых запятой, и возвращать результат вычисления последнего выражения. Вот как выглядят подобные конструкции.
let result = expression1, expression2,... expressionN
Здесь будут вычислены значения всех выражений, после чего в переменную result
попадёт значение выражения expressionN
.
Вполне возможно, что вы уже пользовались оператором «запятая» в циклах for
.
for (var a = 0, b = 10; a <= 10; a++, b--)
Иногда этот оператор оказывается очень кстати при необходимости записи нескольких выражений в одной строке.
function getNextValue() {
return counter++, console.log(counter), counter
}
Он может пригодиться и при конструировании небольших стрелочных функций.
const getSquare = x => (console.log (x), x * x)
Оператор «плюс»
Если вам нужно быстро превратить строку в число — вам пригодится оператор «плюс». Он способен работать с самыми разными числами, а не только, как может показаться, с положительными. Речь идёт об отрицательных, восьмеричных, шестнадцатеричных числах, и о числах в экспоненциальной записи. Более того, он способен преобразовывать во временные метки объекты Date
и объекты библиотеки Moment.js.
// Оператор "плюс"
+'9.11' // 9.11
+'-4' // -4
+'0xFF' // 255
+true // 1
+'123e-5' // 0.00123
+false // 0
+null // 0
+'Infinity' // Infinity
+'1,234' // NaN
+new Date // 1542975502981 (временная метка)
+momentObject // 1542975502981 (временная метка)
Двойной восклицательный знак
Надо отметить, что то, что иногда называют «оператор двойной восклицательный знак» (Bang Bang или Double Bang), на самом деле, не является оператором. Это — оператор «логическое НЕ», или оператор логического отрицания, выглядящий как восклицательный знак, повторённый два раза. Двойной восклицательный знак хорош тем, что позволяет конвертировать любое выражение в логическое значение. Если выражение, с точки зрения JS, истинно — после обработки его двойным восклицательным знаком будет возвращено true
. В противном случае будет возвращено false
.
// Применение двойного восклицательного знака
!!null // false
!!undefined // false
!!false // false
!!true // true
!!"" // false
!!"string" // true
!!0 // false
!!1 // true
!!{} // true
!![] // true
Оператор побитового отрицания
Давайте посмотрим правде в глаза: никому нет дела до побитовых операторов. Я уж не говорю о том, чтобы ими пользовались. Однако оператору побитового отрицания можно найти применение во многих ситуациях.
Когда этот оператор применяется к числам, он преобразует их следующим образом: из числа N
получается -(N+1)
. Такое выражение даёт 0
в том случае если N
равно -1
.
Эту возможность можно использовать с методом indexOf()
при выполнении с его помощью проверки существования в массиве или в строке некоего элемента, так как этот метод, не находя элемент, возвращает -1
.
// Оператор побитового отрицания и метод indexOf
let username = "Nathan Drake"
if (~username.indexOf("Drake")) {
console.log('Access denied')
} else {
console.log('Access granted')
}
Тут надо отметить, что в стандартах ES6 и ES7, соответственно, у строк и массивов, появился метод includes()
. Он, определённо, куда удобнее для определения наличия элементов, чем использование оператора побитового отрицания и indexOf()
.
Именованные блоки
В JavaScript есть концепция меток, используя которую, можно назначать имена (метки) циклам. Затем можно использовать эти метки для обращения к соответствующему циклу при применении инструкций break
или continue
. Метки можно назначать и обычным блокам кода.
Циклы с метками удобно использовать при работе с вложенными циклами. Но их можно применять и для удобной организации кода в блоках или при создании блоков, выполнение кода в которых можно прерывать.
// Работа с метками
declarationBlock: {
// этот подход можно использовать для группировки
// логически связанных блоков кода
var i, j
}
forLoop1: // Метка для первого цикла - "forLoop1"
for (i = 0; i < 3; i++) {
forLoop2: // Метка для второго цикла - "forLoop2"
for (j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
continue forLoop1
}
console.log('i = ' + i + ', j = ' + j)
}
}
/*
i = 0, j = 0
i = 0, j = 1
i = 0, j = 2
i = 1, j = 0
i = 2, j = 0
i = 2, j = 1
i = 2, j = 2
*/
// выполнение кода в блоке прерывается
loopBlock4: {
console.log('I will print')
break loopBlock4
console.log('I will not print')
}
// I will print
Обратите внимание на то, что, в отличие от некоторых других языков, в JS нет инструкции goto
. В результате метки используются лишь с инструкциями break
и continue
.
Итоги
В этом материале мы поговорили о малоизвестных возможностях JavaScript, знание о которых пригодится любому JS-программисту, по меньшей мере, для того, чтобы быть готовым к встрече с чем-то необычным в чужом коде. Если вам тема «неизвестного JS» интересна — можете взглянуть на эту нашу публикацию.
Уважаемые читатели! Если вы знаете о каких-нибудь малоизвестных возможностях JS и видите варианты их практического применения — просим о них рассказать.
Автор: ru_vds