Вкусности ES6: оператор расширения в подробностях

Перевод статьи ES6 Spread and Butter in Depth  с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.

Встречайте очередную часть «ES6 изнутри» на Pony Foo. В предыдущих частях мы рассмотрели деструктирование, литералы шаблона, и, в последний раз, стрелочные функции. Сегодня мы рассмотрим ещё несколько новых фич ES6  оставшиеся параметры, оператор расширения и параметры по умолчанию.

Мы уже рассматривали некоторые из этих фич, когда говорили о деструктировании, которое поддерживает значения по умолчанию, как пример того, как несколько фич в ES6 образуют одну суперфичу, о чем упомянул вчера. Эта статья в итоге может оказаться короче остальных, поскольку рассказывать об этих довольно простых фичах особенно нечего. Однако, как подмечено в начале серии «ES6 изнутри», самые простые фичи, обычно, оказываются наиболее полезными. Так что приступим!

Оставшиеся параметры

Вам знакома ситуация, когда есть множество аргументов и в итоге для работы с ними приходится использовать магическую переменную arguments? Рассмотрим следующий метод, объединяющий любые аргументы, переданные ему в качестве строки.

function concat () {
  return Array.prototype.slice.call(arguments).join(' ')
}
var result = concat('это', 'не', 'было', 'весело')
console.log(result)
// <- 'это не было весело'

Синтаксис оставшихся параметров позволяет вытащить настоящий Array из аргументов function путём добавления префикса … перед названием параметра. Явно проще, то, что это настоящий Array — тоже удобно, и лично я рад, что не надо больше возиться arguments.

function concat (...words) {
  return words.join(' ')
}
var result = concat('вот', 'теперь', 'хорошо')
console.log(result)
// <- 'вот теперь хорошо'

Когда в function есть ещё параметры, это работает немного по-другому. Всегда, когда я объявляю метод с оставшимися параметрами, я представляю их поведение так:

  • Оставшиеся параметры получают все arguments, переданные в вызов функции.
  • Всякий раз, когда параметр добавляется слева, это как если бы его значение присваивалось вызовом rest.shift().
  • Заметьте, что на самом деле нельзя размещать параметры справа: для оставшихся параметров отведён последний аргумент.

Это легче представить наглядно, чем выразить словами, поэтому так и поступим. Метод ниже вычисляет сумму sum для всех аргументов arguments, кроме первого, который затем используется в качестве множителя multiplier для sum. Если вы вдруг забыли, shift() возвращает первое значение в массиве, а также удаляет его из коллекции, что на мой взгляд, делает его полезным мнемоническим приёмом.

function sum () {
  var numbers = Array.prototype.slice.call(arguments) // переменная numbers получает все аргументы
  var multiplier = numbers.shift()
  var base = numbers.shift()
  var sum = numbers.reduce((accumulator, num) => accumulator + num, base)
  return multiplier * sum
}
var total = sum(2, 6, 10, 8, 9)
console.log(total)
// <- 66

Вот как выглядел бы этот метод, если бы мы воспользовались оставшимися параметрами, чтобы перебрать числа. Заметьте, что нам не нужно использовать arguments, или удалять что-либо из массива. Это здорово, поскольку значительно уменьшает сложность кода в методе — который теперь может сосредоточиться не столько на разборе arguments, сколько на самой функциональности.

function sum (multiplier, base, ...numbers) {
  var sum = numbers.reduce((accumulator, num) => accumulator + num, base)
  return multiplier * sum
}
var total = sum(2, 6, 10, 8, 9)
console.log(total)
// <- 66

Оператор расширения

Обычно вы вызываете функцию, передав ей аргументы.

console.log(1, 2, 3)
// <- '1 2 3'

Но иногда аргументы находятся в списке, и не хочется обращаться к каждому индексу только ради вызова метода — либо это невозможно, поскольку массив формируется динамически — поэтому мы используем .apply. Это выглядит как-то нескладно, поскольку .apply еще и перехватывает контекст для this, что в нашем случае оказывается совершенно не в тему, и приходится еще раз указывать хост-объект (либо использовать null).

console.log.apply(console, [1, 2, 3])
// <- '1 2 3'

Оператор расширения можно использовать в качестве безопасной альтернативы .apply. К тому же не требуется контекст. Просто добавьте три точки ... к массиву, как и в случае с оставшимся параметром.

console.log(...[1, 2, 3])
// <- '1 2 3'

Ещё один приятный бонус оператора расширения, который мы детально рассмотрим на следующей неделе в статье про итераторы в ES6, — его можно использовать со всем, что итерируется. Даже тем, что возвращают document.querySelectorAll('div') и ему подобные.

[...document.querySelectorAll('div')]
// <- [<div>, <div>, <div>]

Ещё приятный аспект безопасного оператора — с ним можно сочетать и чередовать обычные аргументы, которые распространятся по всему вызову функции, как вы и ожидаете от них. Это тоже может быть крайне полезно, когда у вас в ES5-коде распихиваются по функциям целые кучи аргументов.

console.log(1, ...[2, 3, 4], 5) // становится `console.log(1, 2, 3, 4, 5)`
// <- '1 2 3 4 5'

Пора привести пример из реальной жизни. Иногда я использую метод ниже в приложениях на фреймворке Express, чтобы morgan (регистратор запроса в Express) мог пересылать свои сообщения через winston, универсальный регистратор. Я удаляю концевые разрывы строк из сообщений message, поскольку winston уже заботится о них. Я также собираю метаданные о выполняющемся сейчас процессе, напр. хост и pid процесса, в списке аргументов, а затем я применяю всё с помощью .apply для механизма регистрации winston. Если внимательно посмотреть в код, то единственная строка кода, которая фактически что-то делает, отмечена соответствующим комментарием, а остальное — лишь возня с arguments.

function createWriteStream (level) {
  return {
    write: function () {
      var bits = Array.prototype.slice.call(arguments)
      var message = bits.shift().replace(/\n+$/, '') // удалить концевые переносы
      bits.unshift(message)
      bits.push({ hostname: os.hostname(), pid: process.pid })
      winston[level].apply(winston, bits) // единственная строчка по делу!
    }
  }
}
app.use(morgan(':status :method :url', {
  stream: createWriteStream('debug')
}))

С ES6 можно основательно упростить решение. Прежде всего, можно не полагаться на arguments, а использовать оставшейся параметр. Он уже предоставляет нам полноценный массив, так что ничего не надо ни к чему приводить. Можно перехватить сообщение message прямо первым параметром, а потом взять и применить всё прямо к winston[level], объединяя обычные аргументы с оставшимися кусочками ...bits. Код ниже выглядит гораздо лучше, потому что занимается именно тем, что нам нужно — вызовом winston[level] с несколькими изменёнными аргументами. Наш старый код, наоборот, в основном лишь жонглировал аргументами, и трудно было удержать логическую нить, продираясь сквозь хитросплетения самого JavaScriptв методе мало что оставалось от того кода, что был нам нужен.

function createWriteStream (level) {
  return {
    write: function (message, ...bits) {
      winston[level](message.replace(/\n+$/, ''), ...bits, {
        hostname: os.hostname(), pid: process.pid
      })
    }
  }
}

Можно еще упростить метод, вытащив наружу метаданные процесса, поскольку они не изменятся за время его жизни. Впрочем, это можно сделать и на ES5.

var proc = { hostname: os.hostname(), pid: process.pid }
function createWriteStream (level) {
  return {
    write: function (message, ...bits) {
      winston[level](message.replace(/\n+$/, ''), ...bits, proc)
    }
  }
}

Можно было бы еще сократить этот код, если воспользоваться стрелочными функциями. Однако, в данном случае это бы лишь усложнило дело. Вам пришлось бы сократить message до msg, чтобы уместить его в одну строку, а обращение к winston[level] с оставшимся параметром и операторами расширения выглядело бы невероятно запутанно для любого, кто не не раздумывал об этом методе последние 15 минут — будь то ваш напарник или вы сами через неделю после написания этой функции.

var proc = { hostname: os.hostname(), pid: process.pid }
function createWriteStream (level) {
  return {
    write: (msg, ...bits) => winston[level](msg.replace(/\n+$/, ''), ...bits, proc)
  }
}

Разумнее будет оставить нашу предыдущую версию. Хотя вполне очевидно, что в данном случае стрелочные функции только лишняя морока, в другом случае может быть иначе. Решать вам, и нужно уметь отличать, где использование фич ES6 по-настоящему улучшает ваш код и его поддерживаемость, а где вы по сути лишь жертвуете поддерживаемостью кода, переводя его на ES6 ради самого ES6.

Ниже описаны ещё несколько полезных применений. Понятно, что можно применить оператор расширения при создании нового массива, но можно также воспользоваться им при деструктировании, что будет напоминать ...rest, а также в случае, который не часто приходит на ум, но о котором стоит упомянуть — использовать оператор расширения в качестве этакого псевдо-.apply при использовании оператора new.

Задача ES5 ES6
Конкатенация [1, 2].concat(more) [1, 2, ...more]
Закинуть в конец массива list.push.apply(list, [3, 4]) list.push(...[3, 4])
Деструктирование a = list[0], rest = list.slice(1) [a, ...rest] = list
new + apply new (Date.bind.apply(Date, [null,2015,31,8])) new Date(...[2015,31,8])

Оператор «по умолчанию»

В статье про деструктирование мы уже рассматривали оператор «по умолчанию», но лишь мимоходом. Как и при деструктировании, есть возможность использовать значения по умолчанию, так же можно определить значение по умолчанию и для любого параметра в функции, как показано ниже.

function sum (left=1, right=2) {
  return left + right
}
console.log(sum())
// <- 3
console.log(sum(2))
// <- 4
console.log(sum(1, 0))
// <- 1

Рассмотрим код, который инициализирует параметры в dragula.

function dragula (options) {
  var o = options || {};
  if (o.moves === void 0) { o.moves = always; }
  if (o.accepts === void 0) { o.accepts = always; }
  if (o.invalid === void 0) { o.invalid = invalidTarget; }
  if (o.containers === void 0) { o.containers = initialContainers || []; }
  if (o.isContainer === void 0) { o.isContainer = never; }
  if (o.copy === void 0) { o.copy = false; }
  if (o.revertOnSpill === void 0) { o.revertOnSpill = false; }
  if (o.removeOnSpill === void 0) { o.removeOnSpill = false; }
  if (o.direction === void 0) { o.direction = 'vertical'; }
  if (o.mirrorContainer === void 0) { o.mirrorContainer = body; }
}

Думаете, было бы полезно перейти к параметрам по умолчанию в соответствии с синтаксисом ES6? Как бы вы это сделали?

P.S. Это тоже может быть интересно:

Оставить комментарий

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

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Получать новые комментарии по электронной почте. Вы можете подписаться без комментирования.