Русский
Русский
English
Статистика
Реклама

Возможность

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

19.06.2020 08:12:45 | Автор: admin


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

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

Настоятельно рекомендую применить к body следующие стили:

body {    margin: 0;    min-height: 100vh;    overflow: hidden;}

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

const clear = () => document.body.innerHTML = ''clear()

1. Активный элемент


Свойство activeElement позволяет получить элемент, находящийся в фокусе.

const input = document.createElement('input')input.setAttribute('type', 'text')input.setAttribute('placeholder', 'Введите свое имя')input.className = 'username'document.body.append(input)input.focus()console.log(document.activeElement)// <input type="text" placeholder="Введите свое имя" class="username">

2. Редактирование страницы


Свойство designMode позволяет редактировать страницу, открытую в браузере.

document.designMode = 'on'



3. Стили элемента


Метод getComputedStyle() позволяет получить стили элемента. Для получения определенного свойства следует использовать getPropertyValue().

// напишем вспомогательную функцию для получения определенного свойства элемента// мы будем использовать ее в нескольких примерахconst getStyle = (element, property) => getComputedStyle(element).getPropertyValue(property)// возьмем инпут из первого примера// и определим его ширину и высотуconst inputWidth = getStyle(input, 'width')const inputHeight = getStyle(input, 'height')console.log(`Ширина: ${inputWidth}\nВысота: ${inputHeight}`)// Ширина: 156.8px// Высота: 16px// позиционируем элемент, используя полученные данные// предположим, что мы собираемся анимировать элемент// поэтому не хотим использовать transform: translate(-50%, -50%)// допустим также, что мы не знаем размеров элемента// поэтому не можем использовать calc(50% - ширина/высота элемента)input.setAttribute('style',    `position: absolute; top: calc(50% - ${inputHeight.replace('px', '') / 2}px); left: calc(50% - ${inputWidth.replace('px', '') / 2}px);`)

4. Определение браузера


Объект Navidator, в числе прочего, позволяет получить информацию о браузере пользователя.

let browserconst agent = navigator.userAgentif (agent.indexOf('Google')) {    browser = 'Google Chrome'} else if (agent.indexOf('Safari')) {    browser = 'Apple Safari'} else if (agent.indexOf('Opera')) {    browser = 'Opera'} else if (agent.indexOf('Firefox')) {    browser = 'Mozilla Firefox'} else if (agent.indexOf('MSIE')) {    browser = 'Microsoft Interner Explorer'}console.log(browser) // Google Chrome// вероятно, в данном случае надо было использовать switchif (browser === 'Google Chrome' || browser === 'Mozilla Firefox') {    console.log('ok') // ok} else if (browser === 'Opera' || browser === 'Apple Safari') {    console.log('50/50')} else if (browser === 'Microsoft Interner Explorer') {    console.log('!ok')}

5. Получение координат


Объект Navigator также позволяет получить информацию о местонахождении пользователя, т.е. его координаты.

const success = position => {    // деструктурируем объект    const {        latitude,        longitude,        altitude,        speed    } = position.coords    console.log(`${latitude.toFixed(2)}\n${longitude.toFixed(2)}\n${altitude}\n${speed}`)    // об этом ниже    getCityAndWeather(latitude, longitude)}navigator.geolocation.getCurrentPosition(success)/*    56.90    60.63    null    null*/// вот как мы можем использовать полученные данные// определим город пользователя и погодуconst getCityAndWeather = (latitude, longitude) => {    // прокси для преодоления CORS    const proxy = 'https://cors-anywhere.herokuapp.com/'    // данный сервис был куплен Facebook и станет платным в 2021 году    const api = `${proxy}https://api.darksky.net/forecast/fd9d9c6418c23d94745b836767721ad1/${latitude}, ${longitude}`    fetch(api)        .then(response => response.json())        .then(data => {            console.log(data) // много всего            // получаем город            const city = data.timezone            // получаем температуру            const { temperature, summary } = data.currently            // переводим фаренгейт в цельсий            const celsius = Math.floor((temperature - 32) * (5 / 9)).toFixed()            // выводим результат            console.log(                `${city}\n${celsius}C\n${summary}`            )            /*                Asia/Yekaterinburg                15C                Overcast            */        })}

6. Получение элементов


Как нам получить все элементы DOM? Использовать рекурсию.

const template = `<div>    <p> Lorem ispum        <span>dolor sit amet</span>    </p></div><a href="#">link</a>`document.body.innerHTML = templateconst getElements = element => {    for (const i of element.children) {        console.log(i.tagName)        // 0 -> ! -> false -> ! -> true        if (!!i.children.length) {            console.log('дочерний элемент')            getElements(i)        }    }}getElements(document.body)/*    DIV    дочерний элемент    P      дочерний элемент    SPAN    A*/

7. Разбор URL


Как нам получить отдельные части URL? Это можно сделать двумя способами.

// с помощью регуляркиconst regex = /(\w+):\/\/([\w.]+)\/(\S*)/const url = 'https://example.com/index.html'const result = url.match(regex)// полный адрес (абсолютный путь), протокол, хост, страницаconsole.log(result[0], result[1], result[2], result[3])// https://example.com/index.html https example.com index.html// с помощью конструктора URLconst url2 = new URL('https://example.com/search?query=fetch&page=2#awesome-page')console.log(url2) // много всегоconst {    origin,    protocol,    host,    pathname} = url2console.log(    `${origin} ${protocol} ${host} ${pathname}`)// https://example.com https: example.com /search// рекомендую почитать про свойство searchParams// searchParams.get(), searchParams.append(), searchParams.has(), searchParams.delete() и т.д.

8. Позиционирование одного элемента относительно другого


const toCenter = (element, parent) => {    element.style.position = 'relative'    element.style.left = (parent.clientWidth - element.offsetWidth) / 2 + 'px'    element.style.top = (parent.clientHeight - element.offsetHeight) / 2 + 'px'}const div = document.createElement('div')div.setAttribute('style', 'width: 150px; height: 150px; background: red;')document.body.append(div)const div2 = document.createElement('div')div2.setAttribute('style', 'width: 100px; height: 100px; background: green;')div.append(div2)const div3 = document.createElement('div')div3.setAttribute('style', 'width: 50px; height: 50px; background: blue;')div2.append(div3)toCenter(div, document.body)toCenter(div2, div)toCenter(div3, div2)


9. Ширина и высота документа


Как нам получить полную ширину и высоту документа?

const pageWidth = Math.max(    document.body.scrollWidth, document.documentElement.scrollWidth,    document.body.offsetWidth, document.documentElement.offsetWidth,    document.body.clientWidth, document.documentElement.clientWidth)const pageHeight = Math.max(    document.body.scrollHeight, document.documentElement.scrollHeight,    document.body.offsetHeight, document.documentElement.offsetHeight,    document.body.clientHeight, document.documentElement.clientHeight)// один из вариантов использования// определяем центр страницыconst pageCenter = [pageWidth / 2, pageHeight / 2]console.log(pageCenter)// создаем элемент для позиционированияconst p = document.createElement('p')p.textContent = 'Lorem ipsum dolor sit amet'document.body.append(p)p.style.position = 'absolute'// получаем ширину и высоту элемента, используя getStyleconst elementWidth = getStyle(p, 'width').replace('px', '')const elementHeight = getStyle(p, 'height').replace('px', '')// определяем центр элементаconst elementCenter = [elementWidth / 2, elementHeight / 2]console.log(elementCenter)// позиционируем элементp.style.top = pageCenter[1] - elementCenter[1] + 'px'p.style.left = pageCenter[0] - elementCenter[0] + 'px'

10. Координаты элемента в контексте документа


Метод getBoundingClientRect() возвращает размер элемента и его позицию относительно области просмотра.

// возьмем p из предыдущего примераconsole.log(p.getBoundingClientRect()) // много всегоconsole.log(    `Отступ сверху => ${p.getBoundingClientRect().top.toFixed()}\nОтступ слева => ${p.getBoundingClientRect().left.toFixed()}`)/*    Отступ сверху => 352    Отступ слева => 288*/// создадим два элемента// и определим, в какой части страницы находится каждый из нихconst div = document.createElement('div')div.setAttribute('style', 'width: 100px; height: 100px; background: #222; position: absolute; top: calc(50% - 50px); left: calc(25% - 50px);')document.body.append(div)const div2 = document.createElement('div')div2.setAttribute('style', 'width: 100px; height: 100px; background: #222; position: absolute; top: calc(50% - 50px); left: calc(75% - 50px);')document.body.append(div2)document.querySelectorAll('div').forEach(div => div.addEventListener('click', event => {    const x = event.target.getBoundingClientRect().x    const width = event.target.getBoundingClientRect().width    // расчеты приблизительные    x + width < innerWidth / 2    ? console.log('Элемент находится в левой части страницы.')    : console.log('Элемент находится в правой части страницы.')}))div.click() // Элемент находится в левой части страницы.div2.click() // Элемент находится в правой части страницы.// определим расстояние между нимиconst distanceBetweenDivs = (div, div2) =>console.log((div2.getBoundingClientRect().x - div.getBoundingClientRect().x + div.getBoundingClientRect().width).toFixed())distanceBetweenDivs(div, div2) // 477

11. Координаты курсора


Как нам получить координаты курсора? Очень просто.

// document.addEventListener('click', ev => console.log(`X => ${ev.clientX}\nY => ${ev.clientY}`))/*    X => 348    Y => 304*/// вот как мы можем это использовать// создаем холст и получаем его контекстconst canvas = document.createElement('canvas')document.body.append(canvas)const $ = canvas.getContext('2d')// размер холста - область просмотраcanvas.width = innerWidthcanvas.height = innerHeight// создаем вспомогательную функцию для получения случайного целого числа в заданном диапазонеconst randomInt = (min, max) => Math.floor(min + Math.random() * (max + 1 - min))// создаем вспомогательную функцию для получения случайного цветаconst randomColor = () => `#${((Math.random()*0xfff)<<0).toString(16)}`// давайте порисуем// рисование фигур осуществляется по клику// центр фигуры - место клика// форма фигуры - круг или квадратlet i = 0canvas.addEventListener('click', ev => {    $.beginPath()    // если i - четное число, то рисуем круг    // если нечетное - квадрат    if (i % 2 === 0) {        // $.arc(x, y, радиус, угол)        $.arc(ev.clientX, ev.clientY, randomInt(10, 30), 0, 2 * Math.PI)        $.fillStyle = randomColor()        $.fill()    } else {        let randomSize = randomInt(20, 60)        $.fillStyle = randomColor()        // $.fillRect(x, y, ширина, высота)        $.fillRect(ev.clientX - randomSize / 2, ev.clientY - randomSize / 2, randomSize, randomSize)    }    $.closePath()    i++})

Напоследок реализуем функцию рисования определенного количества фигур.

const manyShapes = number => {    // очищаем холст    $.clearRect(0, 0, canvas.width, canvas.height)    for (let i = 0; i < number; i++) {        let randomX = randomInt(0, innerWidth)        let randomY = randomInt(0, innerHeight)        if (i % 2 === 0) {            $.beginPath()            $.arc(randomX, randomY, randomInt(10, 30), 0, 2 * Math.PI)            $.fillStyle = randomColor()            $.fill()        } else {            let randomSize = randomInt(20, 60)            $.beginPath()            $.rect(randomX, randomY, randomSize, randomSize)            $.fillStyle = randomColor()            $.fill()        }    }}manyShapes(100)



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

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

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

27.06.2020 10:22:34 | Автор: admin


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

В этой статье я продолжаю делиться с Вами некоторыми находками, сделанными мной в процессе изучения 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. Генератор


Генератор это особая функция, которая работает как фабрика итераторов. Объект является итератором, если он умеет обращаться к элементам коллекции по одному за раз, при этом отслеживая свое текущее положение внутри этой последовательности. Генераторы позволяют определить алгоритм перебора элементов коллекции с помощью единственной функции, поддерживающей собственное состояние.

// пример 1function* 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)// пример 2async 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 Viewproxy.firstname = 'John' // изменено свойство: firstnameproxy.surname = 'Smith' // нет такого свойстваconsole.log(proxy.city) // получено свойство: city Mountain Viewconsole.log('company' in proxy) // truedelete proxy.age // нет такого свойства// proxy + cookieconst 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// forEachlet total2 = 0arr.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 + mapconst 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// mapconst 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}// forEachconst 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 + filterconst 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 quxconsole.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://googlestr = 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) // errorperson.year = 28console.log(person.year) // 1992console.log(Person.type) // человек// console.log(Person.#area) // errorconsole.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 undefinedconsole.log(values.null || 'default null')console.log(values.null ?? 'default null')// default nullconsole.log(values.false || 'default false') // default falseconsole.log(values.false ?? 'default false') // falseconsole.log(values.zero || 'default zero') // default zeroconsole.log(values.zero ?? 'default zero') // 0console.log(values.empty || 'default empty') // default emptyconsole.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)) // veryDeepInsideconsole.log(getValueOld(obj2)) // нет ошибки// новый стильconst getValueNew = obj => obj?.foo?.bar?.baz?.quxconsole.log(getValueNew(obj1)) // veryDeepInsideconsole.log(getValueNew(obj2)) // нет ошибки

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

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

Перевод Использование глобального await в JavaScript

19.10.2020 14:23:26 | Автор: admin


Новая возможность, которая может изменить наш подход к написанию кода

JavaScript очень гибкий и мощный язык, который определяет развитие современного веба. Одна из основных причин главенства JavaScript в сфере веб-разработки заключается в его стермительном развитии и постоянном совершенствовании.

Одним из предложений по улучшению JavaScript является предложение под названием top-level await (await верхнего уровня, глобальный await). Цель данного предложения состоит в превращении ES модулей в некое подобие асинхронных функций. Это позволит модулям получать готовые к использованию ресурсы и блокировать модули, импортирующие их. Модули, которые импортируют ожидаемые ресурсы, смогут запускать выполнение кода только после получения ресурсов и их предварительной подготовки к использованию.

Указанное предложение в настоящее время находится на 3 стадии рассмотрения, поэтому данную возможность пока нельзя использовать в продакшне. Однако можно быть уверенным в том, что в ближайшем будущем она непременно будет реализована.

Не переживайте из-за этого. Продолжайте читать. Я покажу, как можно использовать названную фичу уже сейчас.

Что не так с обычным await?


Если вы попытаетесь использовать ключевое слово await за пределами асинхронной функции, то получите синтаксическую ошибку. Во избежание этого разработчики используют немедленно вызываемые функциональные выражения (Immediately Invoked Function Expression, IIFE).

await Promise.resolve(console.log("")); // Ошибка(async () => {    await Promise.resolve(console.log(""))})();

Указанная проблема и ее решение это лишь вершина айсберга


При работе с ES6 модулями вы, как правило, имеете дело с большим количеством экземпляров, экспортирующих и импортирующих значения. Рассмотрим пример:

// library.jsexport const sqrt = Math.sqrt;export const square = (x) => x * x;export const diagonal = (x, y) => sqrt((square(x) + square(y)));// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// IIFE(async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

В приведенном примере мы экспортируем и импортируем переменные между library.js и middleware.js. Вы можете назвать файлы как угодно.

Функция delay возвращает промис, разрешающийся после задержки. Поскольку данная функция является асинхронной, мы используем ключевое слово await внутри IIFE для ожидания ее завершения. В реальном приложении вместо функции delay будет вызов fetch (запроса на получение данных) или другая асинхронная задача. После разрешения промиса, мы присваиваем значение нашей переменной. Это означает, что до разрешения промиса наша переменная будет иметь значение undefined.

В конце кода мы экспортируем наши переменные для того, чтобы они могли использоваться в другом коде.

Давайте взглянем на код, в котором эти переменные импортируются и используются:

// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // undefinedconsole.log(diagonalOutput); // undefinedconsole.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Если вы запустите этот код, то в первых двух случаях получите undefined, а в третьем и четвертом 169 и 13, соответственно. Почему так происходит?

Это происходит из-за того, что мы пытаемся получить значения переменных, экспортируемых из middleware.js, в main.js до завершения выполнения асинхронной функции. Вы помните, что у нас там промис, ожидающий разрешения?

Для решения данной проблемы, нам нужно каким-то образом сообщить импортирующему модулю о готовности переменных к использованию.

Обходные пути

Существует, как минимум, два способа решить обозначенную проблему.

1. Экспорт промиса для инициализации

Во-первых, можно экспортировать IIFE. Ключевое слово async делает метод асинхронным, такой метод всегда возвращает промис. Вот почему в приведенном ниже примере асинхронное IIFE возвращает промис.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневр или, как еще говорят, костыльexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);})();export { squareOutput, diagonalOutput };

При получении доступа к экспортируемым переменным в main.js можно подождать выполнения IIFE.

// main.jsimport promise, { squareOutput, diagonalOutput } from "./middleware.js";promise.then(() => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Несмотря на то, что данный сниппет решает задачу, он приводит к другим проблемам.

  • При использовании указанного шаблона приходится искать нужный промис
  • Если в другом модуле также используются переменные squareOutput и diagonalOutput, мы должны обеспечить реэкспорт IIFE

Существует и другой способ.

2. Разрешение промиса IIFE с экспортируемыми переменными

В данном случае, вместо экспорта переменных по-отдельности, мы возвращаем их из нашего асинхронного IIFE. Это позволяет файлу main.js просто ждать разрешения промиса и извлекать его значение.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// обходной маневрexport default (async () => {    await delay(1000);    squareOutput = square(13);    diagonalOutput = diagonal(12, 5);    return { squareOutput, diagonalOutput };})();// main.jsimport promise from "./middleware.js";promise.then(({ squareOutput, diagonalOutput }) => {    console.log(squareOutput); // 169    console.log(diagonalOutput); // 169    console.log("From Main");});const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Однако у такого решения также имеются некоторые недостатки.

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

Как глобальный await решает данную проблему?


await верхнего уровня позволяет модульной системе заботиться о разрешении промисов и их взаимодействии между собой.

// middleware.jsimport { square, diagonal } from "./library.js";console.log("From Middleware");let squareOutput;let diagonalOutput;const delay = (ms) => new Promise((resolve) => {    const timer = setTimeout(() => {        resolve(console.log(""));        clearTimeout(timer);    }, ms);});// "глобальный" awaitawait delay(1000);squareOutput = square(13);diagonalOutput = diagonal(12, 5);export { squareOutput, diagonalOutput };// main.jsimport { squareOutput, diagonalOutput } from "./middleware.js";console.log(squareOutput); // 169console.log(diagonalOutput); // 13console.log("From Main");const timer1 = setTimeout(() => {    console.log(squareOutput);    clearTimeout(timer1);}, 2000); // 169const timer2 = setTimeout(() => {    console.log(diagonalOutput);    clearTimeout(timer2);}, 2000); // 13

Ни одна из инструкций в main.js не выполняется до разрешения промисов в middleware.js. Это гораздо более чистое решение по сравнению с обходными путями.

Заметка

Глобальный await работает только с ES модулями. Используемые зависимости должны быть указаны явно. Приведенный ниже пример из репозитория предложения хорошо это демонстирует.

// x.mjsconsole.log("X1");await new Promise(r => setTimeout(r, 1000));console.log("X2");// y.mjsconsole.log("Y");// z.mjsimport "./x.mjs";import "./y.mjs";// X1// Y// X2

Данный сниппет не выведет в консоль X1, X2, Y, как можно ожидать, поскольку x и y отдельные модули, не связанные между собой.

Я настоятельно рекомендую вам изучить рездел часто задаваемые вопросы предложения для лучшего пониманию рассматриваемой фичи.

Реализация


V8

Вы можете протестировать данную возможность уже сейчас.

Для этого перейдите в директорию, в которой размещается Chrome на вашей машине. Убедитесь в том, что все вкладки браузера закрыты. Откройте терминал и введите следующую команду:

chrome.exe --js-flags="--harmony-top-level-await"

Вы таже можете попробовать данную фичу в Node.js. Прочитайте это руководство, чтобы узнать больше.

ES модули

Убедитесь, что добавили тегу script атрибут type со значением module.

<script type="module" src="./index.js"></script>

Обратите внимание, что в отличие от обычных скриптов, ES6 модули следуют политике общего происхождения (одного источника) (SOP) и совместного использования ресурсов (CORS). Поэтому с ними лучше работать на сервере.

Случаи использования


Согласно предложению случаями использования глобального await является следующее:

Динамический путь зависимости

const strings = await import(`/i18n/${navigator.language}`);

Это позволяет модулям использовать значения среды выполнения для вычисления путей зависимостей и может быть полезным для разделения разработка/продакшн код, интернационализации, разделения кода в зависимости от среды выполнения (браузер, Node.js) и т.д.

Инициализация ресурсов

const connection = await dbConnector()

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

Запасной вариант

В приведенном ниже примере показано, как глобальный await может использоваться для загрузки зависимости с реализацией запасного варианта. Если импорт из CDN A провалился, осуществляется импорт из CDN B:

let jQuery;try {  jQuery = await import('https://cdn-a.example.com/jQuery');} catch {  jQuery = await import('https://cdn-b.example.com/jQuery');}

Критика


Rich Harris составил список критических замечаний относительно await верхнего уровня. Он включает в себя следующее:

  • Глобальный await может блокировать выполнение кода
  • Глобальный await может блокировать получение ресурсов
  • Отсутствует поддержка CommonJS модулей

Вот какие ответы на эти замечания даются в FAQ предложения:

  • Поскольку дочерние узлы (модули) имеют возможность выполнения, блокировка кода, в конечном счете, отсутствует
  • Глобальный await используется на стадии выполнения графа модулей. На данном этапе все ресурсы получены и связаны, поэтому риска блокировки получения ресурсов не усматривается
  • await верхнего уровня ограничен ES6 модулями. Поддержка CommonJS модулей, как и обычных скриптов, изначально не планировалась

Я снова настоятельно рекомендую ознакомиться с FAQ предложения.

Надеюсь, мне удалось доступно объяснить суть рассматриваемого предложения. Собираетесь ли использовать эту возможность? Делитесь своим мнением в комментариях.
Подробнее..

Будущее JavaScript классы

22.01.2021 14:05:41 | Автор: admin


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

Сегодня я хочу поговорить с вами о трех предложениях, относящихся к JavaScript-классам, которые находятся на 3 стадии рассмотрения:


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

Вспомним, что такое классы в JavaScript.

По большей части, классы представляют собой так называемый синтаксический сахар (абстракцию или, проще говоря, обертку) для функций-конструкторов. Такие функции используется для реализации паттерна проектирования Конструктор. Данный паттерн, в свою очередь, реализуется (в JavaScript) с помощью модели прототипного наследования (prototypal inheritance). Модель прототипного наследования иногда определяют в качестве самостоятельного паттерна Прототип. Подробнее о паттернах проектирования можно почитать здесь.

Что такое прототип? Это объект, который выступает в роли проекта или схемы (blueprint) для других объектов экземпляров (instances). Конструктор это функция, позволяющая создавать объекты-экземпляры на основе прототипа (класса, суперкласса, абстрактного класса etc.). Процесс передачи свойств и функций от прототипа к экземпляру называется наследованием. Свойства и функции в терминологии классов, обычно, именуются полями и методами, но, де-факто, это одно и тоже.

Как выглядит функция-конструктор?

// обратите внимание на включение строгого режима'use strict'function Counter(initialValue = 0) {  this.count = initialValue  // смотрим на то, что такое this  console.log(this)}

Мы определяем функцию Counter, принимающую параметр initialValue со значением по умолчанию, равным 0. Этот параметр присваивается свойству экземпляра count при инициализации экземпляра. Контекстом this в данном случае является создаваемый (возвращаемый) функцией объект. Для того, чтобы указать JavaScript на вызов не просто функции, но функции-конструктора, необходимо использовать ключевое слово new:

const counter = new Counter() // { count: 0, __proto__: Object }

Как мы видим, функция-конструктор возвращает объект с определенным нами свойством count и прототипом (__proto__) в виде глобального объекта Object, к которому восходят цепочки прототипов почти всех типов (данных) в JavaScript (за исключением объектов без прототипа, создаваемых с помощью Object.create(null)). Поэтому говорят, что в JavaScript все является объектом.

Если вызвать функцию-конструктор без new, то будет выброшено исключение TypeError (ошибка типа), говорящее о том, что свойство 'count' не может быть присвоено undefined:

const counter = Counter() // TypeError: Cannot set property 'count' of undefined// в нестрогом режимеconst counter = Counter() // Window

Это объясняется тем, что значением this внутри функции в строгом режиме является undefined, а в нестрогом глобальный объект Window.

Добавим в функцию-конструктор распределенные (совместно используемые, общие для всех экземпляров) методы по увеличению, уменьшению, сбросу и получению значения счетчика:

Counter.prototype.increment = function () {  this.count += 1  // возвращаем this, чтобы иметь возможность выстраивания цепочки из вызовов методов  return this}Counter.prototype.decrement = function () {  this.count -= 1  return this}Counter.prototype.reset = function () {  this.count = 0  return this}Counter.prototype.getInfo = function () {  console.log(this.count)  return this}

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

Добавление нескольких методов в прототип функции-конструктора можно оптимизировать следующим образом:

;(function () {  this.increment = function () {    this.count += 1    return this  }  this.decrement = function () {    this.count -= 1    return this  }  this.reset = function () {    this.count = 0    return this  }  this.getInfo = function () {    console.log(this.count)    return this  }// привязываем методы к прототипу функции-конструктора// https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/call}.call(Counter.prototype))

Или можно сделать еще проще:

// это современный синтаксис, раньше такой возможности не былоObject.assign(Counter.prototype, {  increment() {    this.count += 1    return this  },  decrement() {    this.count -= 1    return this  },  reset() {    this.count = 0    return this  },  getInfo() {    console.log(this.count)    return this  }})

Воспользуемся нашими методами:

counter  .increment()  .increment()  .getInfo() // 2  .decrement()  .getInfo() // 1  .reset()  .getInfo() // 0

Синтаксис класса является более лаконичным:

class _Counter {  constructor(initialValue = 0) {    this.count = initialValue  }  increment() {    this.count += 1    return this  }  decrement() {    this.count -= 1    return this  }  reset() {    this.count = 0    return this  }  getInfo() {    console.log(this.count)    return this  }}const _counter = new _Counter()_counter  .increment()  .increment()  .getInfo() // 2  .decrement()  .getInfo() // 1  .reset()  .getInfo() // 0

Для демонстрации работы механизма наследования в JavaScript рассмотрим более сложный пример. Создадим класс Person и его подкласс SubPerson.

В классе Person определяются свойства firstName (имя), lastName (фамилия) и age (возраст), а также методы getFullName (получение имени и фамилии), getAge (получение возраста) и saySomething (произнесение фразы).

Подкласс SubPerson наследует все свойства и методы Person, а также определяет новые поля lifestyle (образ жизни), skill (навык) и interest (интерес, хобби), а также новые методы getInfo (получение полного имени посредством вызова родительского-унаследованного метода getFullName и образа жизни), getSkill (получение навыка), getLike (получение хобби) и setLike (определение-установка хобби).

Функция-конструктор:

const log = console.logfunction Person({ firstName, lastName, age }) {  this.firstName = firstName  this.lastName = lastName  this.age = age};(function () {  this.getFullName = function () {    log(`Этого человека зовут ${this.firstName} ${this.lastName}`)    return this  }  this.getAge = function () {    log(`Этому человеку ${this.age} лет`)    return this  }  this.saySomething = function (phrase) {    log(`Этот человек говорит: "${phrase}"`)    return this  }}.call(Person.prototype))const person = new Person({  firstName: 'Иван',  lastName: 'Петров',  age: 30})person.getFullName().getAge().saySomething('Привет!')/*  Этого человека зовут Иван Петров  Этому человеку 30 лет  Этот человек говорит: "Привет!"*/function SubPerson({ lifestyle, skill, ...rest }) {  // привязываем конструктор Person к экземпляру SubPerson применительно к наследуемым свойствам  Person.call(this, rest)  this.lifestyle = lifestyle  this.skill = skill  this.interest = null}// делаем прототип Person прототипом SubPersonSubPerson.prototype = Object.create(Person.prototype)// и добавляем в него новые функцииObject.assign(SubPerson.prototype, {  getInfo() {    this.getFullName()    log(`Он ${this.lifestyle}`)    return this  },  getSkill() {    log(`Этот ${this.lifestyle} умеет ${this.skill}`)    return this  },  getLike() {    log(      `Этот ${this.lifestyle} ${        this.interest ? `любит ${this.interest}` : 'ничего не любит'      }`    )    return this  },  setLike(value) {    this.interest = value    return this  }})const developer = new SubPerson({  firstName: 'Петр',  lastName: 'Иванов',  age: 25,  lifestyle: 'разработчик',  skill: 'писать код на JavaScript'})developer  .getInfo()  .getAge()  .saySomething('Программирование - это круто!')  .getSkill()  .getLike()/*  Этого человека зовут Петр Иванов  Он разработчик  Этому человеку 25 лет  Этот человек говорит: "Программирование - это круто!"  Этот разработчик умеет писать код на JavaScript  Этот разработчик ничего не любит*/developer.setLike('делать оригами').getLike()// Этот разработчик любит делать оригами

Класс:

const log = console.logclass _Person {  constructor({ firstName, lastName, age }) {    this.firstName = firstName    this.lastName = lastName    this.age = age  }  getFullName() {    log(`Этого человека зовут ${this.firstName} ${this.lastName}`)    return this  }  getAge() {    log(`Этому человеку ${this.age} лет`)    return this  }  saySomething(phrase) {    log(`Этот человек говорит: "${phrase}"`)    return this  }}const _person = new Person({  firstName: 'Иван',  lastName: 'Петров',  age: 30})_person.getFullName().getAge().saySomething('Привет!')/*  Этого человека зовут Иван Петров  Этому человеку 30 лет  Этот человек говорит: "Привет!"*/class _SubPerson extends _Person {  constructor({ lifestyle, skill /*, ...rest*/ }) {    // вызов super() почти аналогичен вызову Person.call(this, rest)    // super(rest)    super()    this.lifestyle = lifestyle    this.skill = skill    this.interest = null  }  getInfo() {    // super.getFullName()    this.getFullName()    log(`Он ${this.lifestyle}`)    return this  }  getSkill() {    log(`Этот ${this.lifestyle} умеет ${this.skill}`)    return this  }  get like() {    log(      `Этот ${this.lifestyle} ${        this.interest ? `любит ${this.interest}` : 'ничего не любит'      }`    )  }  set like(value) {    this.interest = value  }}const _developer = new SubPerson({  firstName: 'Петр',  lastName: 'Иванов',  age: 25,  lifestyle: 'разработчик',  skill: 'писать код на JavaScript'})_developer  .getInfo()  .getAge()  .saySomething('Программирование - это круто!')  .getSkill().like/*  Этого человека зовут Петр Иванов  Он разработчик  Этому человеку 25 лет  Этот человек говорит: "Программирование - это круто!"  Этот разработчик умеет писать код на JavaScript  Этот разработчик ничего не любит*/developer.like = 'делать оригами'developer.like// Этот разработчик любит делать оригами

Думаю, тут все понятно. Двигаемся дальше.

Основной проблемой наследования в JavaScript было и остается отсутствие встроенной возможности множественного наследования, т.е. возможности подкласса наследовать свойства и методы нескольких классов одновременно. Разумеется, поскольку в JavaScript возможно все, мы можем создать имитацию множественного наследования, например, с помощью такого миксина:

// https://www.typescriptlang.org/docs/handbook/mixins.htmlfunction applyMixins(derivedCtor, constructors) {  constructors.forEach((baseCtor) => {    Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {      Object.defineProperty(        derivedCtor.prototype,        name,        Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||          Object.create(null)      )    })  })}class A {  sayHi() {    console.log(`${this.name} говорит: "Привет!"`)  }  sameName() {    console.log('Метод класса А')  }}class B {  sayBye() {    console.log(`${this.name} говорит: "Пока!"`)  }  sameName() {    console.log('Метод класса B')  }}class C {  name = 'Иван'}applyMixins(C, [A, B])const c = new C()// вызываем метод, унаследованный от класса Ac.sayHi() // Иван говорит: "Привет!"// вызываем метод, унаследованный от класса Bc.sayBye() // Иван говорит: "Пока!"// одноименный последующий метод перезаписывает предыдущийc.sameName() // Метод класса B

Однако, это не является полноценным решением и представляет собой всего лишь хак для того, чтобы втиснуть JavaScript в рамки объектно-ориентированного программирования.

Перейдем непосредственно к новшествам, предлагаемым обозначенными в начале статьи предложениями.

На сегодняшний день, учитывая стандартизированные возможности, синтаксис класса выглядит следующим образом:

const log = console.logclass C {  constructor() {    this.publicInstanceField = 'Публичное поле экземпляра'    this.#privateInstanceField = 'Приватное поле экземпляра'  }  publicInstanceMethod() {    log('Публичный метод экземпляра')  }  // получаем значение приватного поля экземпляра  getPrivateInstanceField() {    log(this.#privateInstanceField)  }  static publicClassMethod() {    log('Публичный метод класса')  }}const c = new C()console.log(c.publicInstanceField) // Публичное поле экземпляра// при попытке прямого доступа к приватной переменной выбрасывается исключение// console.log(c.#privateInstanceField) // SyntaxError: Private field '#privateInstanceField' must be declared in an enclosing classc.getPrivateInstanceField() // Приватное поле экземпляраc.publicInstanceMethod() // Публичный метод экземляраC.publicClassMethod() // Публичный метод класса

Получается, что мы имеем возможность определять публичные и приватные поля и публичные методы экземпляров, а также публичные методы класса, но не можем определять приватные методы экземпляров, а также публичные и приватные поля класса. Ну, на самом деле возможность определить публичное поле класса все же имеется:

C.publicClassField = 'Публичное поле класса'console.log(C.publicClassField) // Публичное поле класса

Но, согласитесь, что выглядит это не очень хорошо. Складывается впечатление, что мы вернулись к работе с прототипами.

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

publicInstanceField = 'Публичное поле экземпляра'#privateInstanceField = 'Приватное поле экземпляра'

Второе предложение позволяет определять приватные методы экземпляра:

#privateInstanceMethod() {  log('Приватный метод экземпляра')}// вызываем приватный метод экземпляраgetPrivateInstanceMethod() {  this.#privateInstanceMethod()}

И, наконец, третье предложение позволяет определять публичные и приватные (статические) поля, а также приватные (статические) методы класса:

static publicClassField = 'Публичное поле класса'static #privateClassField = 'Приватное поле класса'static #privateClassMethod() {  log('Приватный метод класса')}// получаем значение приватного поле классаstatic getPrivateClassField() {  log(C.#privateClassField)}// вызываем приватный метод классаstatic getPrivateClassMethod() {  C.#privateClassMethod()}

Вот как будет выглядеть (в действительности, уже выглядит) полный комплект:

const log = console.logclass C {  // class field declarations  // https://github.com/tc39/proposal-class-fields  publicInstanceField = 'Публичное поле экземпляра'  #privateInstanceField = 'Приватное поле экземпляра'  publicInstanceMethod() {    log('Публичный метод экземляра')  }  // private methods and getter/setters  // https://github.com/tc39/proposal-private-methods  #privateInstanceMethod() {    log('Приватный метод экземпляра')  }  // получаем значение приватного поля экземпляра  getPrivateInstanceField() {    log(this.#privateInstanceField)  }  // вызываем приватный метод экземпляра  getPrivateInstanceMethod() {    this.#privateInstanceMethod()  }  // static class features  // https://github.com/tc39/proposal-static-class-features  static publicClassField = 'Публичное поле класса'  static #privateClassField = 'Приватное поле класса'  static publicClassMethod() {    log('Публичный метод класса')  }  static #privateClassMethod() {    log('Приватный метод класса')  }  // получаем значение приватного поля класса  static getPrivateClassField() {    log(C.#privateClassField)  }  // вызываем приватный метод класса  static getPrivateClassMethod() {    C.#privateClassMethod()  }  // пытаемся получить публичное и приватное поля класса из экземпляра  getPublicAndPrivateClassFieldsFromInstance() {    log(C.publicClassField)    log(C.#privateClassField)  }  // пытаемся получить публичное и приватное поля экземпляра из класса  static getPublicAndPrivateInstanceFieldsFromClass() {    log(this.publicInstanceField)    log(this.#privateInstanceField)  }}const c = new C()console.log(c.publicInstanceField) // Публичное поле экземпляра// при попытке прямого доступа к значению приватного поля экземпляра выбрасывается исключение// console.log(c.#privateInstanceField) // SyntaxError: Private field '#privateInstanceField' must be declared in an enclosing classc.getPrivateInstanceField() // Приватное поле экземпляраc.publicInstanceMethod() // Публичный метод экземляра// попытка прямого доступа к приватному методу экземпляра также заканчивается ошибкой// c.#privateInstanceMethod() // Errorc.getPrivateInstanceMethod() // Приватный метод экземпляраconsole.log(C.publicClassField) // Публичное поле класса// console.log(C.#privateClassField) // ErrorC.getPrivateClassField() // Приватное поле классаC.publicClassMethod() // Публичный метод класса// C.#privateClassMethod() // ErrorC.getPrivateClassMethod() // Приватный метод классаc.getPublicAndPrivateClassFieldsFromInstance()// Публичное поле класса// Приватное поле класса// публичное и приватное поля экземпляра недоступны из класса,// поскольку на момент доступа к ним экземпляра не существует// C.getPublicAndPrivateInstanceFieldsFromClass()// undefined// TypeError: Cannot read private member #privateInstanceField from an object whose class did not declare it

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

Стоит отметить, что слова private, public и protected в JavaScript являются зарезервированными. При попытке их использования в строгом режиме выбрасывается исключение:

const private = '' // SyntaxError: Unexpected strict mode reserved wordconst public = '' // Errorconst protected = '' // Error

Поэтому надежда на реализацию защищенных полей класса в отдаленной перспективе остается.

Обращаю ваше внимание, что техника инкапсуляции переменных, т.е. их защиты от доступа извне, стара, как сам JavaScript. До стандартизации приватных полей класса для скрытия переменных, обычно, использовались замыкания, а также паттерны проектирования Фабрика и Модуль. Рассмотрим эти паттерны на примере корзины для товаров.

Модуль:

const products = [  {    id: '1',    title: 'Хлеб',    price: 50  },  {    id: '2',    title: 'Масло',    price: 150  },  {    id: '3',    title: 'Молоко',    price: 100  }]const cartModule = (() => {  let cart = []  function getProductCount() {    return cart.length  }  function getTotalPrice() {    return cart.reduce((total, { price }) => (total += price), 0)  }  return {    addProducts(products) {      products.forEach((product) => {        cart.push(product)      })    },    removeProduct(obj) {      for (const key in obj) {        cart = cart.filter((prod) => prod[key] !== obj[key])      }    },    getInfo() {      console.log(        `В корзине ${getProductCount()} товар(а) на ${          getProductCount() > 1 ? 'общую ' : ''        }сумму ${getTotalPrice()} рублей`      )    }  }})()// модуль представляет собой обычный объект с методамиconsole.log(cartModule) // { addProducts: , removeProduct: , getInfo:  }// добавляем товары в корзинуcartModule.addProducts(products)cartModule.getInfo()// В корзине 3 товар(а) на общую сумму 300 рублей// удаляем товар с идентификатором 2cartModule.removeProduct({ id: '2' })cartModule.getInfo()// В корзине 2 товар(а) на общую сумму 150 рублей// пытаемся получить доступ к инкапсулированому полю и методуconsole.log(cartModule.cart) // undefined// cartModule.getProductCount() // TypeError: cartModule.getProductCount is not a function

Фабрика:

function cartFactory() {  let cart = []  function getProductCount() {    return cart.length  }  function getTotalPrice() {    return cart.reduce((total, { price }) => (total += price), 0)  }  return {    addProducts(products) {      products.forEach((product) => {        cart.push(product)      })    },    removeProduct(obj) {      for (const key in obj) {        cart = cart.filter((prod) => prod[key] !== obj[key])      }    },    getInfo() {      console.log(        `В корзине ${getProductCount()} товар(а) на ${          getProductCount() > 1 ? 'общую ' : ''        }сумму ${getTotalPrice()} рублей`      )    }  }}const cart = cartFactory()cart.addProducts(products)cart.getInfo()// В корзине 3 товар(а) на общую сумму 300 рублейcart.removeProduct({ title: 'Молоко' })cart.getInfo()// В корзине 2 товар(а) на сумму 200 рублейconsole.log(cart.cart) // undefined// cart.getProductCount() // TypeError: cart.getProductCount is not a function

Класс:

class Cart {  #cart = []  #getProductCount() {    return this.#cart.length  }  #getTotalPrice() {    return this.#cart.reduce((total, { price }) => (total += price), 0)  }  addProducts(products) {    this.#cart.push(...products)  }  removeProduct(obj) {    for (const key in obj) {      this.#cart = this.#cart.filter((prod) => prod[key] !== obj[key])    }  }  getInfo() {    console.log(      `В корзине ${this.#getProductCount()} товар(а) на ${        this.#getProductCount() > 1 ? 'общую ' : ''      }сумму ${this.#getTotalPrice()} рублей`    )  }}const _cart = new Cart()_cart.addProducts(products)_cart.getInfo()// В корзине 3 товар(а) на общую сумму 300 рублей_cart.removeProduct({ id: '1', price: 100 })_cart.getInfo()// В корзине 1 товар(а) на общую сумму 150 рублейconsole.log(_cart.cart) // undefined// console.log(_cart.#cart) // SyntaxError: Private field '#cart' must be declared in an enclosing class// _cart.getTotalPrice() // TypeError: cart.getTotalPrice is not a function// _cart.#getTotalPrice() // Error

Как мы видим, паттерны Модуль и Фабрика ничем не уступают классу, разве что, синтаксис последнего является немного более кратким, но позволяют полностью отказаться от использования ключевого слова this, основная проблема которого заключается в потере контекста при использовании в стрелочных функциях и обработчиках событий. Это обуславливает необходимость их привязки к экземпляру в конструкторе.

Напоследок, рассмотрим пример создания веб-компонента кнопки с помощью синтаксиса класса (из текста одного из предложений с небольшой модификацией).

Наш компонент расширяет встроенный HTML-элемент кнопки, добавляя в его функционал следующее: при нажатии кнопки левой кнопкой мыши, значение счетчика увеличивается на 1, при нажатии кнопки правой кнопкой мыши, значение счетчика уменьшается на 1. При этом мы можем использовать любое количество кнопок с собственным контекстом и состоянием:

// https://developer.mozilla.org/ru/docs/Web/Web_Componentsclass Counter extends HTMLButtonElement {  #xValue = 0  get #x() {    return this.#xValue  }  set #x(value) {    this.#xValue = value    // привязываем к экземпляру метод рендеринга    // https://developer.mozilla.org/ru/docs/DOM/window.requestAnimationFrame    // https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Function/bind    requestAnimationFrame(this.#render.bind(this))  }  #increment() {    this.#x++  }  #decrement(e) {    // отменяем вызов контекстного меню    e.preventDefault()    this.#x--  }  constructor() {    super()    // привязываем к экземпляру обработчики событий    this.onclick = this.#increment.bind(this)    this.oncontextmenu = this.#decrement.bind(this)  }  // монтирование в терминологии React/Vue или, проще говоря, встраивание элемента в DOM  connectedCallback() {    this.#render()  }  #render() {    // для упрощения будем считать, что 0 - это положительное число    this.textContent = `${this.#x} - ${      this.#x < 0 ? 'отрицательное' : 'положительное'    } ${this.#x & 1 ? 'нечетное' : 'четное'} число`  }}// регистрация веб-компонентаcustomElements.define('btn-counter', Counter, { extends: 'button' })

Результат:



Представляется, что, с одной стороны, классы не получат повсеместного признания в сообществе разработчиков до решения, назовем ее так, проблемы this. Не случайно после продолжительного использования классов (классовых компонентов), команда React отказалась от них в пользу функций (хуков). Похожая тенденция наблюдается в Vue Composition API. С другой стороны, многие причастные к разработке ECMAScript, инженеры из Google, занимающиеся веб-компонентами, а также команда TypeScript активно работают над развитием объектно-ориентированной составляющей JavaScript, поэтому сбрасывать классы со счетов в ближайшие несколько лет точно не стоит.

Весь код, приводимый в статье, находится здесь.

Дополнительно про объектно-ориентированный JavaScript можно почитать здесь.

Статья получилась несколько длиннее, чем я планировал, но, надеюсь, вам было интересно. Благодарю за внимание и хорошего дня.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru