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

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



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

Сегодня я хочу поговорить с вами о трех предложениях, относящихся к 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 можно почитать здесь.

Статья получилась несколько длиннее, чем я планировал, но, надеюсь, вам было интересно. Благодарю за внимание и хорошего дня.
Источник: habr.com
К списку статей
Опубликовано: 22.01.2021 14:05:41
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Javascript

Программирование

Class

Constructor

Prototype

Oop

Future

Feature

Proposal

Класс

Конструктор

Прототип

Возможность

Предложение

Категории

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

  • Имя: Макс
    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