Темная сторона Javascript: избегайте данных трех функций

Оператор Void

Вы, вероятно, уже сталкивались с этим оператором. Раньше при наличии ссылки, запускающей функцию JavaScript, нужно было добавлять href=”javascript:void(0)”, чтобы действие по умолчанию не запустилось. Но что же это значило на самом деле?

Оператор void в JavaScript позволяет сгенерировать значение undefined. Он принимает любое выражение и каждый раз возвращает undefined.

Возможно, вы подумали: почему бы просто не воспользоваться ключевым словом undefined? Дело в том, что до ECMAScript 5 undefined не было постоянным значением, которое нельзя изменять. Раньше это было возможно, и оператор void позволял получать к нему доступ, даже если константа не работала.

По сути, чтобы переопределить константу только для локального пространства имен и избежать проблем со сторонними библиотеками, лучше всего создавать собственные IIFE (Immediately Invoked Function Expressions), где один из полученных параметров будет undefined.

(function (window, undefined) {
  // Логика находится здесь, где undefined действует, как ожидается
})(window, void(0))

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

Такая функция возвращает результат одной строки, даже если оператор return не используется.

const double = x => x * 2; // Возвращает результат X, умноженный на 2

const callAfunction = () => myFunction(); // Возвращает то же, что и myFunction, даже если это не нужно

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

const callAfunction = () => void myFunction(); // Возвращает то же, что и myFunction, даже если это не нужно

Возвращаемое значение будет скрыто  —  его заменит undefined.

Однако такое поведение приносит минимальную выгоду в современном JavaScript.

Оператор with

Вы, вероятно, даже не слышали об этой конструкции в JavaScript, так как она не особо широко используется. Даже официальная документация MDN призывать избегать ее, потому что она может сильно запутать код.

Оператор with позволяет расширить цепь областей видимости (scope chain) для определенного оператора. Другими словами, с его помощью можно встроить выражение в область видимости определенного оператора, в идеале при этом упрощая его.

Ниже показан пример.

function greet(user) {
  
  with(user) {
    console.log(`Hello there ${name}, how are you and your ${kids.length} kids today?`)
  }
}

greet({
  name: "Fernando",
  kids: ["Brian", "Andrew"]
})

Обратите внимание на то, как действует оператор with внутри функции greet. Теперь рассмотрим другой случай, в котором все становится немного сложнее.

function greet(user, message) {
  with(user) {
    console.log(`Hey ${name}, here is a message for you: ${message}`)
  }
}

// Счастливый путь
greet({
  name: "Fernando"
}, "You got 2 emails")

// Неудачный путь
greet({
  name: "Fernando",
  message: "Unrelated message"
}, "you got email")

Как думаете, каким будет результат этого выполнения?

Hey Fernando, here is a message for you: You got 2 emails
Hey Fernando, here is a message for you: Unrelated message

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

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

Label

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

Оператор goto позволяет поместить маркер в любом фрагменте кода и переместиться в него из любого другого места. Он был своего рода магическим порталом: с его помощью можно было перейти в середину функции или внутрь оператора IF.

В JavaScript есть подобная конструкция и называется она “метки” (labels). Метки помещаются перед оператором, который затем можно как продолжить (continue), так и покинуть (break). Обратите внимание, что никаких goto не требуется  —  и это определенно плюс.

Записываем следующее:

label1: {
  console.log(1)
  let condition = true
  if(condition) {
  	break label1
  }
  console.log(2)
}
console.log("end")

Вывод будет таким:

1
end

Конечно, этот пример очень похож на оператор if..else, и, в целом, выглядит не так уж плохо. Однако вы выпадаете из нормального потока кода и пропускаете операторы. Если вы хотите реализовать подобное, то лучше использовать if..else  —  другим разработчикам будет гораздо легче его прочитать.

Недостатки меток становятся более очевидными при взаимодействии с циклами и оператором continue:

let i, j;

loop1:
for(i = 0; i < 10; i++) {
  loop2:
  for(j = 0; j < 10; j++) {

    if(j == 3 && i == 2) {
      continue loop2;
    }
    console.log({i, j})
    if(j % 2 == 0) {
      continue loop1;
    }
  }
}

Можете ли вы проанализировать приведенный выше код и сказать, каким будет вывод? Это вполне реально, но потребует времени. Этот сценарий выведет следующее:

{ i: 0, j: 0 }
{ i: 1, j: 0 }
{ i: 2, j: 0 }
{ i: 3, j: 0 }
{ i: 4, j: 0 }
{ i: 5, j: 0 }
{ i: 6, j: 0 }
{ i: 7, j: 0 }
{ i: 8, j: 0 }
{ i: 9, j: 0 }

По сути, второй if оценивает true как 0, поэтому оператор continue влияет на внешний цикл, заставляя его перейти к следующему значению индекса. Он, в свою очередь, перезапускает внутренний цикл, возвращая его к нулю. И то же самое происходит снова и снова. В то же время первый if никогда не получит значения true, потому что j не достигнет никакого другого значения, кроме 0.

Метки часто вызывают трудности. Даже если вы сможете их применить, они будет гораздо понятнее интерпретатору, а не человеку, для которого вы и пишите код. Тому, кто будет читать ваш код (или даже вам самим через несколько недель), потребуется гораздо больше времени, чтобы понять его основной поток.

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Fernando Doglio: The Dark Side of Javascript: A Look at 3 Features You Never Want to Use

Предыдущая статьяКак разработать логотип: руководство для новичков
Следующая статьяАвтоматический анализ текста с использованием Streamlit