[Из песочницы] Почему я все ещё использую function в JavaScript?
В далеких 90-х, когда я только изучал JavaScript, мы пытались писать «Hello World» с помощью оператора function
. Примерно так:
function helloWorld() {
return ‘Hello World!’;
}
В настоящее же время крутые ребята пишут функцию «Hello World» вот так:
const helloWorld = () => 'Hello World!';
Здесь используется стрелочная функция, добавленная в JavaScript в стандарте ES2015. Она выглядит чертовски прекрасно. Всё умещается в одну строку. Так кратко. Так замечательно.
Неудивительно, что стрелочные функции стали одной из самых популярных особенностей нового стандарта.
Когда я впервые их увидел, я выглядел примерно как Фрай из Футурамы
Даже если учесть, что Babel бесплатен
Так вот, спустя 20 лет изучения JavaScript и после начала использования ES2015 в некоторых проектах, как думаете, каким же образом я напишу Hello World сегодня? Вот так:
function helloWord() {
return ‘Hello World!’;
}
После того как я показал вам новый способ, вы едва ли станете даже смотреть на код «старой школы», представленный выше.
Целых 3 строчки на такую маленькую простую функцию?! Зачем здесь столько лишних символов?
Я знаю о чем вы думаете:
Ни у кого нет времени на это!
Не стоит считать, что мне не нравятся стрелочные функции. Но в случае, когда мне надо объявить функцию верхнего уровня в моем коде, я буду использовать старомодный оператор function.
Надеюсь из этой цитаты Мартина «Дяди Боба» станет понятно, зачем же я это делаю.
Отношение времени, потраченного на чтение кода, по отношению ко времени, потраченному на его написание, составляет 10 к 1. Мы постоянно читаем наш старый код во время работы над новым кодом.Из-за того, что это отношение слишком высоко, нам хочется, чтобы чтение нашего кода в последующем было легким и непринужденным, несмотря на то что для этого придется потратить больше усилий во время его написания.
Роберт Мартин: Чистый код: создание, анализ и рефакторинг
Оператор
function
имеет 2 явных преимущества по сравнению со стрелочными функциями.Преимущество №1: Ясность намерения
Когда мы просматриваем тысячи строк кода в день, будет полезным понимать намерения программиста так быстро и легко, как это только возможно.
Взгляните на это:
const maxNumberOfItemsInCart = ...;
Вы уже просмотрели почти всю строку, но вам все еще непонятно, что будет на месте многоточия: функция или какое-то значение. Это может быть как:
const maxNumberOfItemsInCart = 100;
… так и:
const maxNumberOfItemsInCart = (statusPoints) => statusPoints * 10;
Если же вы используете оператор function, то никакой двусмысленности уже не будет.
Взгляните на:
const maxNumberOfItemsInCart = 100;
… против:
function maxNumberOfItemsInCart(statusPoints) {
return statusPoints * 10;
}
Намерения программиста ясны с самого начала строки.
Возможно вы используете редактор кода с подсветкой синтаксиса. Может вы быстро читаете. Скорее вы просто думаете, что не получите большого преимущества от добавления пары строчек.
Я вас понимаю. Краткая запись все еще выглядит довольно привлекательно.
На самом деле, если бы это являлось моей единственной причиной, то я бы смог убедить себя, что читабельностью можно пожертвовать.
Но все же это не единственная моя причина.
Преимущество №2: Порядок объявления == Порядок выполнения
В идеальном случае, я хотел бы объявлять мой код примерно в том порядке, в котором я хочу, чтобы он выполнялся.
Но при использовании стрелочных функций это будет являться препятствием для меня: любое значение, объявленное через const
, недоступно до того момента, пока до него не дойдет исполнение кода.
Приготовьтесь к куче тарабарщины, которая должна доказать (надеюсь), что я разбираюсь в том, о чем говорю
Единственная вещь, которую вы должны уяснить в том, что написано ниже, это то, что вы не можете использовать const до того, как вы объявите её.
Данный код вызовет ошибку:
sayHelloTo(‘Bill’);
const sayHelloTo = (name) => `Hello ${name}`;
Все потому что в тот момент, когда движок JavaScript считает код, то он сделает привязку для функции «sayHelloTo», но не проинициализирует её.
Все объявления в JavaScript привязываются на ранней стадии, но инициализируются они в разное время. Другими словами, JavaScript привязывает объявление константы «sayHelloTo» — считывает ее, перемещает наверх и выделяет место в памяти под ее хранение —, но при этом не устанавливает ей какого-либо значения до того момента, пока не дойдет до нее в процессе выполнения.
Время между привязкой «sayHelloTo» и ее инициализацией называется «временная мертвая зона» (temporal dead zone — TDZ).
Если вы используете ES2015 прямо в браузере, не переводя код в ES5 с помощью Babel, представленный ниже пример также выдаст ошибку:
if(thing) {
console.log(thing);
}
const thing = 'awesome thing';
Если же заменить здесь const на var, то мы не получим ошибки. Дело в том, что переменные инициализируются со значением undefined сразу во время привязки, в отличие от констант. Но что-то я отвлекся…
Оператор function, в отличие от const, не страдает от проблемы TDZ. Данный код будет валидным:
sayHelloTo(‘Bill’);
function sayHelloTo(name) {
return `Hello ${name}`;
}
Это потому, что оператор function позволяет инициализировать функцию сразу же после привязки — до того, как будет выполнен какой-либо код.
Таким образом становится неважно, где размещен код функции, она становится доступна с самого начала исполнения.
Использование const заставляет нас писать код, который выглядит беспорядочно. Мы должны сначала начинать работать с низкоуровневыми функциями и затем подниматься все выше. Мой мозг не работает в этом направлении. Я хочу видеть общую картину до того, как стану углубляться в детали.
Большая часть кода написана людьми. Так что стоит учитывать то, что понимание большинства людей напрямую связано с порядком выполнения.
По факту, разве не будет прекрасно, если мы предоставим краткую сводку небольшой части нашего API в начале нашего кода? С оператором function мы легко можем это сделать.
Посмотрите на этот (отчасти выдуманный) модуль для корзины товаров…
export {
createCart,
addItemToCart,
removeItemFromCart,
cartSubTotal,
cartTotal,
saveCart,
clearCart,
}
function createCart(customerId) {...}
function isValidCustomer(customerId) {...}
function addItemToCart(item, cart) {...}
function isValidCart(cart) {...}
function isValidItem(item) {...}
...
Со стрелочными функциями он будет выглядеть как-то так:
const _isValidCustomer = (customerId) => ...
const _isValidCart = (cart) => ...
const _isValidItem = (item) => ...
const createCart = (customerId) => ...
const addItemToCart = (item, cart) => ...
...
export {
createCart,
addItemToCart,
removeItemFromCart,
cartSubTotal,
cartTotal,
saveCart,
clearCart,
}
А теперь представьте, что это большой модуль с огромным количеством небольших внутренних функций. Что вы предпочтете в таком случае?
Наверняка найдутся люди, которые станут утверждать, что использовать что-то до его объявления — это неестественно и может привести к неизвестным последствиям. Утверждающие это могут быть первоклассными специалистами и разбираться в своей области, но как ни крути — это всего-лишь ваше мнение, а не подтвержденный факт.
Я же считаю, что код является средством общения. Действительно хороший код может рассказать первоклассную историю. Я позволяю компиляторам, транспайлерам и минимизаторам оптимизировать код для машины. Но сам я хочу оптимизировать мой код для простого человека, который просто хочет понять что же у меня написано.
Так что же насчёт стрелочный функций?
Да. Они все еще прекрасны.
Я обычно использую стрелочные функции для передачи небольшой функции в качестве значения для функции уровнем выше. Я использую их с promise, с map, с filter, с reduce. Именно здесь они будут являться прекрасным выбором.
Вот некоторые примеры:
const goodSingers = singers.filter((singer) => singer.name !== 'Justin Bieber');
function tonyMontana() {
return getTheMoney().then((money) => power)
.then((power) => women);
}
На этом я пожалуй и закончу. Спасибо за чтение!