Доброго времени суток, друзья!
В этой статье я продолжаю делиться с Вами некоторыми находками, сделанными мной в процессе изучения JavaScript. Данная часть посвящена, преимущественно, новым и продвинутым фичам.
1. Частое обращение к одним и тем же элементам
Порой при написании кода приходится снова и снова обращаться к одним и тем же элементам. При работе с DOM, например, такими элементами являются document и document.body. Казалось бы, что тут такого? 8 и 13 символов, соответственно, да еще и emmet помогает. Однако, когда кода действительно много, автозавершение начинает предлагать неправильные варианты. Либо, когда работаешь не с html, а, например, с php без правильного синтаксического анализатора, многие привычные вещи приходится набирать вручную. Задумавшись о том, как решить указанную проблему, я вспомнил о canvas. Помните, с чего начинается работа с холстом? Правильно, с его определения и инициализации двумерного контекста рисования:
const C = document.querySelector('canvas') const $ = C.getContext('2d')
Также я подумал об объекте jQuery ($).
Так вот, могу предложить три варианта (один из вариантов я подглядел у разработчиков Facebook при изучении протокола Open Graph):
// внутри функции function foo() { const D = document const B = document.body const div = D.createElement('div') B.append(div) const p = D.createElement('p') p.textContent = 'Lorem ipsum dolor sit amet...' div.append(p) console.log(div) B.removeChild(div) } foo() // снаружи функции function bar(D, B) { const div = D.createElement('div') B.append(div) const p = D.createElement('p') p.textContent = 'Lorem ipsum dolor sit amet...' div.append(p) console.log(div) B.removeChild(div) } bar(document, document.body) // IIFE ;((D, B) => { const div = D.createElement('div') B.append(div) const p = D.createElement('p') p.textContent = 'Lorem ipsum dolor sit amet...' div.append(p) console.log(div) B.removeChild(div) })(document, document.body)
Это была разминка, переходим к тренировке.
2. Генератор
Генератор — это особая функция, которая работает как фабрика итераторов. Объект является итератором, если он умеет обращаться к элементам коллекции по одному за раз, при этом отслеживая свое текущее положение внутри этой последовательности. Генераторы позволяют определить алгоритм перебора элементов коллекции с помощью единственной функции, поддерживающей собственное состояние.
// пример 1 function* takeItem(arr) { for (let i = 0; i < arr.length; i++) { yield arr[i] } } const arr = ['foo', 'bar', 'baz', 'qux'] const generator = takeItem(arr) const timer = setInterval(() => { const item = generator.next() item.done ? clearInterval(timer) : console.log(item.value) }, 1000) // пример 2 async function* range(start, end) { for (let i = start; i <= end; i++) { yield Promise.resolve(i) } } ;(async () => { const generator = range(1, 4) for await (const item of generator) { console.log(item) } })()
3. Async/await + fetch
Async/await является альтернативой промисов, позволяя обеспечить синхронность выполнения асинхронных функций. В свою очередь, fetch является альтернативой XMLHttpRequest, представляя собой интерфейс для получения ресурсов (в том числе, по сети).
const url = 'https://jsonplaceholder.typicode.com/users' ;(async () => { try { const response = await fetch(url) const data = await response.json() console.table(data) } catch (er) { console.error(er) } finally { console.info('потрачено') } })()
4. For await
Выражение for await…of создает цикл, проходящий через асинхронные итерируемые объекты, а также синхронные итерируемые сущности. Он вызывает пользовательский итерационный хук с инструкциями, которые должны быть выполнены для значения каждого отдельного свойства объекта.
const delayedPromise = (id, ms) => new Promise(resolve => { const timer = setTimeout(() => { resolve(id) clearTimeout(timer) }, ms) }) const promises = [ delayedPromise(1, 1000), delayedPromise(2, 2000), delayedPromise(3, 3000) ] // старый стиль async function oldStyle() { for (const promise of await Promise.all(promises)) { console.log(promise) } } oldStyle() // все промисы через 3 секунды // новый стиль async function newStyle() { for await (const promise of promises) { console.log(promise) } } newStyle() // каждый промис в свой черед
5. Proxy
Прокси используются для объявления расширенной семантики JS объектов. Стандартная семантика реализована в движке JS, который обычно написан на низкоуровневом языке программирования, например C++. Прокси позволяют определить поведение объекта при помощи JS. Другими словами, они являются инструментом метапрограммирования.
const person = { firstname: 'Harry', lastname: 'Heman', city: 'Mountain View', company: 'Google' } const proxy = new Proxy(person, { get(target, property) { if (!(property in target)) { return property .split('_') .map(p => target[p]) .sort() .join(' ') } console.log(`получено свойство: ${property}`) return target[property] }, set(target, property, value) { if (property in target) { target[property] = value console.log(`изменено свойство: ${property}`) } else { console.error('нет такого свойства') } }, has(target, property) { // return property in target return Object.entries(target) .flat() .includes(property) }, deleteProperty(target, property) { if (property in target) { delete target[property] console.log(`удалено свойство: ${property}`) } else { console.error('нет такого свойства') } } }) console.log(proxy.company_city_firstname_lastname) // Google Harry Heman Mountain View proxy.firstname = 'John' // изменено свойство: firstname proxy.surname = 'Smith' // нет такого свойства console.log(proxy.city) // получено свойство: city Mountain View console.log('company' in proxy) // true delete proxy.age // нет такого свойства // proxy + cookie const getCookieObject = () => { const cookies = document.cookie.split(';').reduce((cks, ck) => ({ [ck.substr(0, ck.indexOf('=')).trim()]: ck.substr(ck.indexOf('=') + 1), ...cks }), {}) const setCookie = (name, value) => document.cookie = `${name}=${value}` const deleteCookie = name => document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:01 GTM;` return new Proxy(cookies, { set: (obj, prop, val) => (setCookie(prop, val), Reflect.set(obj, prop, val)), deleteProperty: (obj, prop) => (deleteCookie(prop), Reflect.deleteProperty(obj, prop)) }) }
6. Reduce
Метод reduce() применяет функцию reducer к каждому элементу массива, возвращая результирующее значение. Данный метод принимает четыре аргумента: начальное значение (или значение предыдущей функции обратного вызова), значение текущего элемента, текущий индекс и итерируемый массив (два последних являются необязательными). В простейшем случае это выглядит так:
const arr = [1, 2, 3] const total = arr.reduce((sum, cur) => sum + cur) console.log(total) // 6 // forEach let total2 = 0 arr.forEach(num => total2 += num) console.log(total2) // 6
Однако возможности reduce() этим далеко не исчерпываются:
const devs = [ { name: 'John', sex: 'm', age: 23 }, { name: 'Jane', sex: 'f', age: 24 }, { name: 'Alice', sex: 'f', age: 27 }, { name: 'Bob', sex: 'm', age: 28 } ] const men = devs.reduce((newArr, dev) => { if (dev.sex === 'm') newArr.push(dev.name) return newArr }, []) console.log(men) // ["John", "Bob"] // filter + map const olderThan25 = devs .filter(dev => dev.age > 25) .map(dev => dev.name) console.log(olderThan25) // ["Alice", "Bob"]
Сформируем список имен разработчиков одной строкой:
const devsNamesList = `<ul>${devs.reduce((html, dev) => html += `<li>${dev.name}</li>`, '')}</ul>` document.body.innerHTML = devsNamesList // map const devsNamesList2 = `<ul>${devs.map(dev => `<li>${dev.name}</li>`).join('')}</ul>` document.body.insertAdjacentHTML('beforeend', devsNamesList2)
Поговорим о группировке:
const groupBy = (arr, criteria) => arr.reduce((obj, item) => { const key = typeof criteria === 'function' ? criteria(item) : item[criteria] if (!obj.hasOwnProperty(key)) obj[key] = '' obj[key] = item return obj }, {}) const nums = [6.1, 4.2, 2.3] console.log(groupBy(nums, Math.floor)) // {2: 2.3, 4: 4.2, 6: 6.1} // forEach const groupBy2 = (arr, criteria, obj = {}) => { arr.forEach(item => { const key = typeof criteria === 'function' ? criteria(item) : item[criteria] if (!obj.hasOwnProperty(key)) obj[key] = '' obj[key] = item return obj }) return obj } const words = ['one', 'three', 'five'] console.log(groupBy2(words, 'length')) // {3: "one", 4: "five", 5: "three"}
Сделаем выборку:
const cash = { A: 1000, B: 2000 } const devsWithCash = devs.reduce((arr, dev) => { const key = dev.name.substr(0,1) if (cash[key]) { dev.cash = cash[key] arr.push(`${dev.name} => ${dev.cash}`) } else dev.cash = 0 return arr }, []) console.log(devsWithCash) // ["Alice => 1000", "Bob => 2000"] // map + filter const devsWithCash2 = devs.map(dev => { const key = dev.name.substr(0,1) if (cash[key]) { dev.cash = cash[key] } else dev.cash = 0 return dev }).filter(dev => dev.cash !== 0) console.log(devsWithCash2)
И последний пример. Помните, как мы формировали список имен разработчиков из массива объектов одной строкой? Но что если у нас имеется такой массив:
const users = [ { john: { name: 'John' } }, { jane: { name: 'Jane' } }, { alice: { name: 'Alice' } }, { bob: { name: 'Bob' } } ]
Как нам сделать тоже самое?
document.body.insertAdjacentHTML('afterbegin', `<ul>${users.reduce((html, el) => html + `<li>${Object.values(el)[0].name}</li>`, '')}</ul>`) // фух!
Давайте рассмотрим что-нибудь попроще.
7. Новые методы работы со строками
// trimStart() trimEnd() trim() const start = ' foo bar' const end = 'baz qux ' console.log(`${start.trimStart()} ${end.trimEnd()}`) // foo bar baz qux console.log((`${start} ${end}`).trim()) // тоже самое const startMiddleEnd = ' foo bar baz ' // три пробела в начале, два - между foo и bar, три - между bar и baz и два - в конце // при помощи регулярного выражения заменяем два и более пробела одним // затем посредством trim() удаляем пробелы в начале и конце const stringWithoutRedundantSpaces = startMiddleEnd.replace(/\s{2,}/g, ' ').trim() console.log(stringWithoutRedundantSpaces) // foo bar baz // padStart() padEnd() let str = 'google' str = str.padStart(14, 'https://') // первый аргумент - количество символов console.log(str) // https://google str = str.padEnd(18, '.com') console.log(str) // https://google.com
8. Новые методы работы с массивами
const arr = ['a', 'b', ['c', 'd'], ['e', ['f', 'g']]] console.log(arr.flat(2)) // ["a", "b", "c", "d", "e", "f", "g"] const arr2 = ['react vue', 'angular', 'deno node'] console.log(arr2.map(i => i.split(' '))) /* [Array(2), Array(1), Array(2)] 0: (2) ["react", "vue"] 1: ["angular"] 2: (2) ["deno", "node"] */ console.log(arr2.flatMap(i => i.split(' '))) // ["react", "vue", "angular", "deno", "node"]
9. Новые методы работы с объектами
const person = { name: 'John', age: 30 } console.log(Object.getOwnPropertyDescriptor(person, 'name')) // {value: "John", writable: true, enumerable: true, configurable: true} const arr = Object.entries(person) console.log(arr) // [["name", "John"], ["age", 30]] console.log(Object.fromEntries(arr)) for (const [key, value] of Object.entries(person)) { console.log(`${key} => ${value}`) // name => John age => 30 } console.log(Object.keys(person)) // ["name", "age"] console.log(Object.values(person)) // ["John", 30]
10. Приватные переменные в классах
class Person { // значения по умолчанию static type = 'человек' static #area = 'Земля' name = 'John' #year = 1990 get age() { return new Date().getFullYear() - this.#year } set year(age) { if (age > 0) { this.#year = new Date().getFullYear() - age } } get year() { return this.#year } static area() { return Person.#area } } const person = new Person() console.log(person) // Person {name: "John", #year: 1990} console.log(person.age) // 30 // console.log(person.#year) // error person.year = 28 console.log(person.year) // 1992 console.log(Person.type) // человек // console.log(Person.#area) // error console.log(Person.area()) // Земля
11. Еще парочка нововведений
// промисы const p1 = Promise.resolve(1) const p2 = Promise.reject('error') const p3 = Promise.resolve(3) ;(async () => { const result = await Promise.all([p1, p2, p3]) console.log(result) })() // Uncaught (in promise) error ;(async () => { const result = await Promise.allSettled([p1, p2, p3]) console.log(result) })() /* [{…}, {…}, {…}] 0: {status: "fulfilled", value: 1} 1: {status: "rejected", reason: "error"} 2: {status: "fulfilled", value: 3} */ // приведение к null (nullish coercion) const values = { undefined: undefined, null: null, false: false, zero: 0, empty: '' } console.log(values.undefined || 'default undefined') console.log(values.undefined ?? 'default undefined') // default undefined console.log(values.null || 'default null') console.log(values.null ?? 'default null') // default null console.log(values.false || 'default false') // default false console.log(values.false ?? 'default false') // false console.log(values.zero || 'default zero') // default zero console.log(values.zero ?? 'default zero') // 0 console.log(values.empty || 'default empty') // default empty console.log(values.empty ?? 'default empty') // '' // опциональная цепочка (optional chaining) const obj1 = { foo: { bar: { baz: { qux: 'veryDeepInside' } } } } const obj2 = { foo: {} } // старый стиль function getValueOld(obj) { if (obj.foo !== undefined && obj.foo.bar !== undefined && obj.foo.bar.baz !== undefined && obj.foo.bar.baz.qux !== undefined) { return obj.foo.bar.baz.qux } } console.log(getValueOld(obj1)) // veryDeepInside console.log(getValueOld(obj2)) // нет ошибки // новый стиль const getValueNew = obj => obj?.foo?.bar?.baz?.qux console.log(getValueNew(obj1)) // veryDeepInside console.log(getValueNew(obj2)) // нет ошибки
Благодарю за потраченное время. Надеюсь, оно было потрачено не зря.
Продолжение следует…
ссылка на оригинал статьи https://habr.com/ru/post/508494/
Добавить комментарий