5 jQuery-фич, перешедших в нативный JavaScript

Сегодня у нас рассказ пойдет про jQuery и о том влиянии, которое он оказал на развитие современных браузеров. А рассмотрим мы это влияние на примере фич, которые, фактически, перекочевали из него в нативный JavaScript.

В каком-то смысле библиотека jQuery была полигоном для испытания новых подходов к написанию кода. Причем полигоном очень успешным, учитывая популярность этой библиотеки.

Фича #1: querySelector


Кто пользовался jQuery, тот знает, что если в функцию $, являющуюся коротким синонимом для более длинной переменной jQuery, передать строчку с любым валидным CSS-селектором, то на выходе мы получим коллекцию из найденных элементов, соответствующих этому селектору. Это будет нативная JS-коллекция, обернутая в jQuery-оболочку, у которой на прототипе есть полезные методы и свойства, которые, собственно описаны в документации библиотеки.

Конечно функция $ намного более функциональна, ведь она позволяет создавать DOM-ноды и делать другие полезные штуки. Но для нас интересно то, что с некоторых пор в нативном JS имеются не только исконные, содержащиеся на document методы getElementById, getElementByClassName и остальные, а еще и querySelector с querySelectorAll.

Первые очень специфичны и каждый раз нужно задумываться что применить, со второй группой все намного проще — передал селектор и получил ответ в виде коллекции DOM-нод. Очень удобно.

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

Вообще же, я не люблю писать длинные названия через document, поэтому использую такие обертки:

function qS (selector) {
    return document.querySelector(selector)
}

function qSA (selector) {
    return document.querySelectorAll(selector)
}

Или еще короче, в стиле ES6:

const qS = selector => document.querySelector(selector)
const qSA = selector => document.querySelectorAll(selector)

По сути мы передаем строку с селектором в нашу функцию и оттуда возвращаем результат работы нативной функции. Все просто.

Использовать такие функции становится очень удобно:

var myBtn = qS('.myBtn')

Вот это заселектит нам кнопку и сохранит в переменную myBtn.

Если это не пересекается с другими библиотеками, то можно вовсе переименовать qSA в символ доллара и будет «типа jQuery» =)

Фича #2: classList


classList имеет внутри себя каждая нода браузера. Я заселектил элемент и вижу у него classList. Кстати, в консоли инструментов разработчика в Chrome обратиться к последнему выделенному элементу в DOM-дереве можно через $0.

Подробно про инструменты Хрома я рассказывал в другом видео:

Пока список классов пуст, но если вызвать метод add, то класс можно добавить. С помощью remove его можно удалить, а через toggle можно его переключать — если был, то удалится и наоборот.

Все это произросло из jQuery-методов addClass, removeClass и toggleClass — как видите, названия тоже перекочевали.
У classList есть еще полезные методы — рекомендую вам с ними познакомиться, потому что операции с нодами это то, что в JavaScript делают наверное чаще всего и умение минимумом кода достигнуть максимум результата это полезный навык.

Фича #3: closest


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

Фича #4: fetch


Вы наверное слышали про AJAX, который является совокупностью подходов для выполнения асинхронных (и если надо, то синхронных) HTTP-запросов. Для этого в браузерах существовал издавна объект XMLHttpRequest или сокращенно XHR. Пользоваться им в чистом виде достаточно проблематично и некрасиво с точки зрения оформления кода, поэтому программисты всегда писали обертки, вызывающие нативные функции. Подобные тому, что я привел для querySelector.

Вот так выглядит простейший код с XHR-запросом:

var xhr = new XMLHttpRequest() // Создаем объект XHR

// Говорим что пойдем на адрес методом GET
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1')

xhr.send() // Реально шлём запрос

// Показываем то что пришло в ответ
xhr.onreadystatechange = () => {
  document.body.append(xhr.responseText)
}

Согласитесь, громоздко. А вот пример запроса, но с помощью jQuery-функции ajax:

$.ajax('https://jsonplaceholder.typicode.com/todos/1') // Куда слать запрос
    .done(function(response) {
        // Пришедший JSON-ответ прикрепляем к документу
        Object.keys(response).forEach(key => {
          document.body.append(key + ': ' + response[key] + '; ')
        })
    })

Однако в современных браузерах появился fetch, который выглядит вот так для того же запроса:

fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then(res => res.json())
    .then(res => console.log(res))

Не правда ли синтаксис очень похож? Тут ясно откуда растут ноги. Fetch, кстати работает внутри все равно используя тот самый XHR, но так ведь писать код намного удобнее и компактнее! Кстати, удобство в fetch доситигается за счет промисов.

Фича #5: Promise и Deferred


Про промисы в последнее время говорят очень много. Да и не только говорят, а используют при написании кода, потому что с приходом ES6 (он же ES2015), промисы стали частью нативного API браузерного JS. Короче говоря, о промисах вы скорее всего слышали, но вероятно не знали, что эта концепция намного старше JavaScript и не относится исключительно к нему.

Вот ссылка на статью в Википедии, из которой можно узнать, что промисы это понятие из информатики в целом, которое определяет как работать с асинхронными вычислениями таким образом, чтобы запустить их и по завершении выполнить какое-то нужное действие. Например, типичное для веб-программирования действие — сделать запрос к серверу на получение данных и продолжить дальше делать свои дела. А как только ответ с сервера придет, отреагировать на него каким-то действием. И предложено это было аж в дремучем 1976 году!

Другие языки также имеют свою реализацию промисов, например, Ruby, Python, Closure и другие. Но нас интересует сфера JS и jQuery.

В jQuery есть объект Deferred, как раз реализующий конкретную спецификацию промисов, о чем недвусмысленно сказано в документации.

Давайте посмотрим на простейший пример того, как работает Deferred:

var deferredOperation = function () {
    var dfd = $.Deferred()

    setTimeout(() => {
        if (Math.random() > 0.5) {
            dfd.resolve('Resolved')
        } else {
            dfd.reject('Rejected')
        }

    }, 1500)

    return dfd
}

var result = deferredOperation()

result.done(doneMsg => console.log(doneMsg))
result.fail(failMsg => console.log(failMsg))

Сначала я определяю функцию deferredOperation, внутри которой создаю Deferred-объект (изначально он в состоянии неопределенности или ожидания — pending) и говорю что через полторы секунды он должен будет перевестись в состояние resolved — успешно разрешен, или в состояние rejected — то есть отменен.
И этот экземпляр Deferred я из функции возвращаю.

Обращаю внимание, что return dfd выполнятся до того, как setTimeout отсчитает свои полторы секунды. То есть он сработает асинхронно, или иными словами параллельно синхронному коду. Внутри setTimeout генерируется дробное случайное значение от 0 до 1 и если получившееся больше 0.5, то на dfd вызываем функцию resolve, тем самым переводя промис в состояние resolved. Ну или в rejected с помощью метода reject, если Math.random() нам нагенерил число меньше 0.5.

Далее я возвращенное значение из выполненной функции deferredOperation присваиваю промежуточной переменной result. А затем говорю, что если dfd переведется в состояние resolved, то выполнится тот колбэк, что внутри done. А иначе — тот, что внутри fail.

Получается что мы можем расставить наши done и fail где угодно и они сработают как только Deferred разрешится в какое-то состояние. Мы ведь не знаем сколько например займет сетевой запрос — это зависит от задержек в сети. Поэтому такой синтаксис очень удобен в этом случае, как собственно и в любом другом случае, когда мы не знаем сколько точно займет та или иная операция.

На самом деле, уже рассмотренная нами jQuery-функция ajax также возвращает Deferred-объект. Так что можно будет сделать что-то когда запрос завершится. Совершенно по аналогии с предыдущим примером. В нем эмулятором задержки был setTimeout, кстати говоря.

А здесь задержка в запросе действительно будет непредсказуема:

$.ajax({ url: 'http://makeweb.me/api/courses/all' })
  .done(res => console.log(res))
  .fail(err => console.log(err))

Здесь мы делаем ajax-запрос с помощью jQuery и так как подразумевается что Deferred возвращется, то по цепочке можно выполнить done и fail. Но можно, как и до этого, присвоить результат запроса в промежуточную переменную и потом вызывать done и fail где-то дальше в коде.

const result = $.ajax({ url: 'http://makeweb.me/api/courses/all' })

// ... some code here ...

result.done(res => console.log(res))
result.fail(err => console.log(err))

Итак, сейчас я вам очень коротко показал как устроен Deferred в jQuery, хотя многие моменты мы не затронули, инчае бы все затянулось.

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

Вот так работают промисы в сегодняшнем нативном JavaScript:

const promisedOperation = () => new Promise((resolve, reject) => {
    setTimeout(_ => {
        (Math.random() > 0.5)
            ? resolve('Resolved')
            : reject('Rejected')
    }, 1500)
})

const result = promisedOperation()

result.then(thenMsg => console.log(thenMsg))
result.catch(catchMsg => console.log(catchMsg))

Тут чуть иначе. мы создаем новый промис, в который пробрасываются функции-переключатели состояния промиса. То есть внутри мы в зависимости от значения вызываем их и переключаем промис в одно из двух состояний.

Далее then будет срабатывать если промис разрешается успешно (если был вызван resolve), и catch, если промис был отменен через вызов rejected.

Если вас смущают стрелочные функции, что я использовал, рекомендую ролик из моего курса по React, где подробно рассказывается про них и про другие ES6-возможности:

А вот пример с fetch:

fetch('https://jsonplaceholder.typicode.com/todos')
  .then(res => res.json())
  .then(res => console.log(res))
  .catch(err => console.log(err))

Здесь мы делаем запрос на фейковый API, затем ответ интерпретируем как JSON и выводим результат в консоль. В противном случае сработает catch и мы выведем ошибку.

Fetch возвращет Promise, который умеет делать then и catch. Совершенно по аналогии с Deferred, который умеет делать done и fail. Так что можно сказать, что из jQuery это перекочевало в нативный JS.