Фичи JavaScript. Часть 2

от автора

Доброго времени суток, друзья!

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

См. Фичи JavaScript. Часть 1.

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/


Комментарии

Добавить комментарий

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