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

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

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

1. Частое обращение к одним и тем же элементам


Порой при написании кода приходится снова и снова обращаться к одним и тем же элементам. При работе с DOM, например, такими элементами являются document и document.body.

Существует, как минимум, три варианта решения указанной задачи:

    // внутри функции
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)) // нет ошибки

Благодарю за потраченное время. Надеюсь, оно было потрачено не зря.

Продолжение следует…