Доброго времени суток, друзья!
В этой статье я продолжаю делиться с Вами некоторыми находками, сделанными мной в процессе изучения 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)) // нет ошибки
Благодарю за потраченное время. Надеюсь, оно было потрачено не зря.
Продолжение следует…
dopusteam
О чем вообще первый пункт? Перечитал несколько раз, ни проблему ни решение не понял
anonymous
Первый пункт совсем элементарный. Насколько я понял речь идет про то, что не стоит многократно вызывать getElementById/querySelector для получение одного и того же элемента. Лучше завести переменную и обращаться к элементу через нее
В целом, тексту не хватает описаний тех самых «фичей». Если в начале текста хоть какие-то описания присутствовали, то к концу все свелось к мало информативным примерам.
Несмотря на это, спасибо автору за статью. Нашел для себя пару интересных моментов.
dopusteam
Мало того, что это не фича js, так это вообще не фича :)