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

Конструктор

Перевод Объектно-ориентированный JavaScript простыми словами

07.10.2020 12:08:05 | Автор: admin


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

В JavaScript существует 4 способа создать объект:

  • Функция-контруктор (constructor function)
  • Класс (class)
  • Связывание объектов (object linking to other object, OLOO)
  • Фабричная функция (factory function)

Какой метод следует использовать? Какой из них является лучшим?

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

Давайте начнем с того, что такое объектно-ориентированное программирование (ООП).

Что такое ООП?


По сути, ООП это способ написания кода, позволяющий создавать объекты с помощью одного объекта. В этом также заключается суть шаблона проектирования Конструктор. Общий объект, обычно, называется планом, проектом или схемой (blueprint), а создаваемые с его помощью объекты экземплярами (instances).

Каждый экземпляр имеет как свойства, наследуемые от родителя, так и собственные свойства. Например, если у нас имеется проект Human (человек), мы можем создавать на его основе экземпляры с разными именами.

Второй аспект ООП состоит в структурировании кода, когда у нас имеется несколько проектов разного уровня. Это называется наследованием (inheritance) или классификацией (созданием подклассов) (subclassing).

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

Перейдем с способам создания объектов.

Способы создания объекта


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

Конструкторами являются функции, в которых используется ключевое слово this.

    function Human(firstName, lastName) {        this.firstName = firstName        this.lastName = lastName    }

this позволяет сохранять и получать доступ к уникальным значениям создаваемого экземпляра. Экземпляры создаются с помощью ключевого слова new.

const chris = new Human('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyierconst zell = new Human('Zell', 'Liew')console.log(zell.firstName) // Zellconsole.log(zell.lastName) // Liew

Класс

Классы являются абстракцией (синтаксическим сахаром) над функциями-конструкторами. Они облегчают задачу создания экземпляров.

    class Human {        constructor(firstName, lastName) {            this.firstName = firstName            this.lastName = lastName        }    }

Обратите внимание, что constructor содержит тот же код, что и функция-конструктор, приведенная выше. Мы должны это делать, чтобы инициализировать this. Мы может опустить constructor, если нам не требуется присваивать начальные значения.

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

Экземпляры также создаются с помощью ключевого слова new.

const chris = new Human('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

Связывание объектов

Данный способ создания объектов был предложен Kyle Simpson. В данном подходе мы определяем проект как обычный объект. Затем с помощью метода (который, как правило, называется init, но это не обязательно, в отличие от constructor в классе) мы инициализируем экземпляр.

const Human = {    init(firstName, lastName) {        this.firstName = firstName        this.lastName = lastName    }}

Для создания экземпляра используется Object.create. После создания экземпляра вызывается init.

const chris = Object.create(Human)chris.init('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

Код можно немного улучшить, если вернуть this в init.

const Human = {  init () {    // ...    return this  }}const chris = Object.create(Human).init('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

Фабричная функция

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

Вот простой пример фабричной функции.

function Human(firstName, lastName) {    return {        firstName,        lastName    }}

Для создания экземпляра нам не требуется ключевое слово this. Мы просто вызываем функцию.

const chris = Human('Chris', 'Coyier')console.log(chris.firstName) // Chrisconsole.log(chris.lastName) // Coyier

Теперь давайте рассмотрим способы добавления свойств и методов.

Определение свойств и методов


Методы это функции, объявленные в качестве свойств объекта.

    const someObject = {        someMethod () { /* ... */ }    }

В ООП существует два способа определения свойств и методов:

  • В экземпляре
  • В прототипе

Определение свойств и методов в конструкторе

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

function Human (firstName, lastName) {  // Определяем свойства  this.firstName = firstName  this.lastname = lastName  // Определяем методы  this.sayHello = function () {    console.log(`Hello, I'm ${firstName}`)  }}const chris = new Human('Chris', 'Coyier')console.log(chris)



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

Для добавления свойства в прототип используют prototype.

function Human (firstName, lastName) {  this.firstName = firstName  this.lastname = lastName}// Определяем метод в прототипеHuman.prototype.sayHello = function () {  console.log(`Hello, I'm ${this.firstName}`)}



Создание нескольких методов может быть утомительным.

// Определение методов в прототипеHuman.prototype.method1 = function () { /*...*/ }Human.prototype.method2 = function () { /*...*/ }Human.prototype.method3 = function () { /*...*/ }

Можно облегчить себе жизнь с помощью Object.assign.

Object.assign(Human.prototype, {  method1 () { /*...*/ },  method2 () { /*...*/ },  method3 () { /*...*/ }})

Определение свойств и методов в классе

Свойства экземпляра можно определить в constructor.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName      this.lastname = lastName      this.sayHello = function () {        console.log(`Hello, I'm ${firstName}`)      }  }}



Свойства прототипа определяются после constructor в виде обычной функции.

class Human (firstName, lastName) {  constructor (firstName, lastName) { /* ... */ }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}



Создание нескольких методов в классе проще, чем в конструкторе. Для этого нам не нужен Object.assign. Мы просто добавляем другие функции.

class Human (firstName, lastName) {  constructor (firstName, lastName) { /* ... */ }  method1 () { /*...*/ }  method2 () { /*...*/ }  method3 () { /*...*/ }}

Определение свойств и методов при связывании объектов

Для определения свойств экземпляра мы добавляем свойство к this.

const Human = {  init (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    this.sayHello = function () {      console.log(`Hello, I'm ${firstName}`)    }    return this  }}const chris = Object.create(Human).init('Chris', 'Coyier')console.log(chris)



Метод прототипа определяется как обычный объект.

const Human = {  init () { /*...*/ },  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}



Определение свойств и методов в фабричных функциях (ФФ)

Свойства и методы могут быть включены в состав возвращаемого объекта.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${firstName}`)    }  }}



При использовании ФФ нельзя определять свойства прототипа. Если вам нужны такие свойства, можно вернуть экземпляр класса, конструктора или связывания объектов (но это не имеет смысла).

// Не делайте этогоfunction createHuman (...args) {  return new Human(...args)}

Где определять свойства и методы


Где следует определять свойства и методы? В экземпляре или в прототипе?

Многие считают, что для этого лучше использовать прототипы.

Однако на самом деле это не имеет особого значения.

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

Например, при использовании классов или связывания объектов, лучше использовать прототипы, поскольку в этом случае код легче писать. В случае ФФ прототипы использовать нельзя. Можно определять только свойства экземпляров.

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

Предварительный вывод


На основе изученного материала можно сделать несколько выводов. Это мое личное мнение.

  • Классы лучше конструкторов, поскольку в них легче определять несколько методов.
  • Связывание объектов кажется странным из-за необходимости использовать Object.create. Я постоянно забывал об этом при изучении данного подхода. Для меня это было достаточной причиной отказаться от его дальнейшего использования.
  • Классы и ФФ использовать проще всего. Проблема состоит в том, что в ФФ нельзя использовать прототипы. Но, как я отметил ранее, это не имеет особого значения.

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

Классы против ФФ Наследование


Прежде чем переходить к сравнению классов и ФФ, необходимо познакомиться с тремя концепциями, лежащими в основе ООП:

  • наследование
  • инкапсуляция
  • this

Начнем с наследования.

Что такое наследование?

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

Это происходит двумя способами:

  • с помощью инициализации экземпляра
  • с помощью цепочки прототипов

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

Понимание создания подклассов

Создание подклассов это когда дочерний проект расширяет родительский.

Рассмотрим это на примере классов.

Создание подклассов с помощью класса

Для расширения родительского класса используется ключевое слово extends.

class Child extends Parent {    // ...}

Например, давайте создадим класс Developer, расширяющий класс Human.

// класс Humanclass Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}

Класс Developer будет расширять Human следующим образом:

class Developer extends Human {  constructor(firstName, lastName) {    super(firstName, lastName)  }    // ...}

Ключевое слово super вызывает constructor класса Human. Если вам это не нужно, super можно опустить.

class Developer extends Human {  // ...}

Допустим, Developer умеет писать код (кто бы мог подумать). Добавим ему соответствующий метод.

class Developer extends Human {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }}

Вот пример экземпляра класса Developer.

const chris = new Developer('Chris', 'Coyier')console.log(chris)



Создание подклассов с помощью ФФ

Для создания подклассов с помощью ФФ необходимо выполнить 4 действия:

  • создать новую ФФ
  • создать экземпляр родительского проекта
  • создать копию этого экземпляра
  • добавить в эту копию свойства и методы

Данный процесс выглядит так.

function Subclass (...args) {  const instance = ParentClass(...args)  return Object.assign({}, instance, {    // Свойства и методы  })}

Создадим подкласс Developer. Вот как выглядит ФФ Human.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${firstName}`)    }  }}

Создаем Developer.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    // Свойства и методы  })}

Добавляем ему метод code.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code (thing) {      console.log(`${this.firstName} coded ${thing}`)    }  })}

Создаем экземпляр Developer.

const chris = Developer('Chris', 'Coyier')console.log(chris)



Перезапись родительского метода

Иногда возникает необходимость перезаписать родительский метод внутри подкласса. Это можно сделать следующим образом:

  • создать метод с тем же именем
  • вызвать родительский метод (опционально)
  • создать новый метод в подклассе

Данный процесс выглядит так.

class Developer extends Human {  sayHello () {    // Вызываем родительский метод    super.sayHello()    // Создаем новый метод    console.log(`I'm a developer.`)  }}const chris = new Developer('Chris', 'Coyier')chris.sayHello()



Тот же процесс с использованием ФФ.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {      sayHello () {        // Вызываем родительский метод        human.sayHello()        // Создаем новый метод        console.log(`I'm a developer.`)      }  })}const chris = new Developer('Chris', 'Coyier')chris.sayHello()



Наследование против композиции

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

Что же такое композиция?

Понимание композиции

По сути, композиция это объединение нескольких вещей в одну. Наиболее распространенным и самым простым способом объединения объектов является использование Object.assign.

const one = { one: 'one' }const two = { two: 'two' }const combined = Object.assign({}, one, two)

Композицию легче всего объяснить на примере. Допустим, у нас имеется два подкласса, Developer и Designer. Дизайнеры умеют разрабатывать дизайн, а разработчики писать код. Оба наследуют от класса Human.

class Human {  constructor(firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}class Designer extends Human {  design (thing) {    console.log(`${this.firstName} designed ${thing}`)  }}class Developer extends Designer {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }}

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

Как нам его создать?

Мы не может одновременно расширить классы Designer и Developer. Это невозможно, поскольку мы не можем решить, какие свойства должны быть первыми. Это называется проблемой ромба (ромбовидным наследованием).



Проблема ромба может быть решена с помощью Object.assign, если мы отдадим одному объекту приоритет над другим. Однако, в JavaScript не поддерживается множественное наследование.

// Не работаетclass DesignerDeveloper extends Developer, Designer {  // ...}

Здесь нам пригодится композиция.

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

Реализация этого подхода приводит к следующему.

const skills = {    code (thing) { /* ... */ },    design (thing) { /* ... */ },    sayHello () { /* ... */ }}

Нам больше не нужен класс Human, ведь мы можем создать три разных класса с помощью указанного объекта.

Вот код для DesignerDeveloper.

class DesignerDeveloper {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    Object.assign(this, {      code: skills.code,      design: skills.design,      sayHello: skills.sayHello    })  }}const chris = new DesignerDeveloper('Chris', 'Coyier')console.log(chris)



Мы можем сделать тоже самое для Designer и Developer.

class Designer {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    Object.assign(this, {      design: skills.design,      sayHello: skills.sayHello    })  }}class Developer {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    Object.assign(this, {      code: skills.code,      sayHello: skills.sayHello    })  }}

Вы заметили, что мы создаем методы в экземпляре? Это лишь один из возможных вариантов. Мы также можем поместить методы в прототип, но я нахожу это лишним (при таком подходе кажется, что мы вернулись к конструкторам).

class DesignerDeveloper {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }}Object.assign(DesignerDeveloper.prototype, {  code: skills.code,  design: skills.design,  sayHello: skills.sayHello})



Используйте тот подход, который считаете самым подходящим. Результат будет одинаковым.

Композиция с помощью ФФ

Композиция с помощью ФФ заключается в добавлении распределенных методов в возвращаемый объект.

function DesignerDeveloper (firstName, lastName) {  return {    firstName,    lastName,    code: skills.code,    design: skills.design,    sayHello: skills.sayHello  }}



Наследование и композиция

Никто не говорил, что мы не можем использовать наследование и композицию одновременно.

Возвращаясь к примеру с Designer, Developer и DesignerDeveloper, нельзя не отметить, что они также являются людьми. Поэтому они могут расширять класс Human.

Вот пример наследование и композиции с использованием синтаксиса классов.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}class DesignerDeveloper extends Human {}Object.assign(DesignerDeveloper.prototype, {  code: skills.code,  design: skills.design})



А вот тоже самое с использованием ФФ.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${this.firstName}`)    }  }}function DesignerDeveloper (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code: skills.code,    design: skills.design  })}



Подклассы в реальном мире

Несмотря на то, что многие эксперты утверждают, что композиция по сравнению с подклассами является более гибкой (и поэтому более полезной), подклассы нельзя сбрасывать со счетов. Многие вещи, с которыми мы имеем дело, основаны на этой стратегии.

Например: событие click является MouseEvent (событием мыши). MouseEvent это подкласс UIEvent (событие пользовательского интерфейса), который, в свою очередь, является подклассом Event (событие).



Другой пример: HTML Elements (элементы) являются подклассами Nodes (узлов). Поэтому они могут использовать все свойства и методы узлов.



Предварительный вывод относительно наследования

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

Продолжим сравнение.

Классы против ФФ Инкапсуляция


По сути, инкапсуляция это скрытие одной вещи внутри другой, из-за чего внутренняя сущность становится недоступной снаружи.

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

Простая инкапсуляция

Простейшей формой инкапсуляции является блок кода.

{  // Переменные, объявленные здесь, будут иметь блочную область видимости}

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

const food = 'Hamburger'{  console.log(food)}



Но не наоборот.

{  const food = 'Hamburger'}console.log(food)



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

Инкапсуляция с помощью функции

Функциональная область видимости похожа на блочную. Переменные, объявленные в функции, доступны только внутри нее. Это относится ко всем переменным, даже объявленным с помощью var.

function sayFood () {  const food = 'Hamburger'}sayFood()console.log(food)



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

const food = 'Hamburger'function sayFood () {  console.log(food)}sayFood()



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

function sayFood () {  return 'Hamburger'}console.log(sayFood())



Замыкание

Замыкание это продвинутая форма инкапсуляции. Это просто функция внутри другой функции.

// Пример замыканияfunction outsideFunction () {  function insideFunction () { /* ... */ }}


Переменные, объявленные в outsideFunction, могут использоваться в insideFunction.

function outsideFunction () {  const food = 'Hamburger'  console.log('Called outside')  return function insideFunction () {    console.log('Called inside')    console.log(food)  }}// Вызываем outsideFunction, которая возвращает insideFunction// Сохраняем insideFunction в переменной "fn"const fn = outsideFunction()



Инкапсуляция и ООП

При создании объектов мы хотим, чтобы одни свойства были открытыми (публичными), а другие закрытыми (частными или приватными).

Рассмотрим пример. Скажем, у нас имеется проект Car. При создании нового экземпляра мы добавляем ему свойство fuel (топливо) со значением 50.

class Car {  constructor () {    this.fuel = 50  }}


Пользователи могут использовать это свойство для определения количества оставшегося топлива.

const car = new Car()console.log(car.fuel) // 50


Пользователи также могут самостоятельно устанавливать количество топлива.

const car = new Car()car.fuel = 3000console.log(car.fuel) // 3000

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

Существует два способа это сделать:

  • использование частных свойств по соглашению
  • использование настоящих частных полей

Частные свойства по соглашению

В JavaScript частные переменные и свойства, обычно, обозначаются с помощью нижнего подчеркивания.

class Car {  constructor () {    // Отмечаем свойство "fuel" как частное, которое не должно использоваться за пределами класса    this._fuel = 50  }}

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

class Car {  constructor () {    this._fuel = 50  }  getFuel () {    return this._fuel  }  setFuel (value) {    this._fuel = value    // Определяем вместимость бака    if (value > 100) this._fuel = 100  }}

Для определения и установки количества топлива пользователи должны использовать методы getFuel и setFuel, соответственно.

const car = new Car()console.log(car.getFuel()) // 50car.setFuel(3000)console.log(car.getFuel()) // 100

Но переменная "_fuel" в действительности не является частной. Она доступна извне.

const car = new Car()console.log(car.getFuel()) // 50car._fuel = 3000console.log(car.getFuel()) // 3000

Для ограничения доступа к переменным следует использовать настоящие частные поля.

По-настоящему частные поля

Поля это термин, объединяющий переменные, свойства и методы.

Частные поля классов

Классы позволяют создавать частные переменные с помощью префикса "#".

class Car {  constructor () {    this.#fuel = 50  }}

К сожалению, данный префикс нельзя использовать в конструкторе.



Частные переменные должны определяться вне конструктора.

class Car {  // Определяем частную переменную  #fuel  constructor () {    // Используем ее    this.#fuel = 50  }}

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

class Car {  #fuel = 50}

Теперь переменная "#fuel" доступна только внутри класса. Попытка получить к ней доступ за пределами класса приведет к возникновению ошибки.

const car = new Car()console.log(car.#fuel)



Для управления переменной нам нужны соответствующие методы.

class Car {  #fuel = 50  getFuel () {    return this.#fuel  }  setFuel (value) {    this.#fuel = value    if (value > 100) this.#fuel = 100  }}const car = new Car()console.log(car.getFuel()) // 50car.setFuel(3000)console.log(car.getFuel()) // 100

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

class Car {  #fuel = 50  get fuel () {    return this.#fuel  }  set fuel (value) {    this.#fuel = value    if (value > 100) this.#fuel = 100  }}const car = new Car()console.log(car.fuel) // 50car.fuel = 3000console.log(car.fuel) // 100

Частные поля ФФ

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

function Car () {  const fuel = 50}const car = new Car()console.log(car.fuel) // undefinedconsole.log(fuel) // Error: "fuel" is not defined

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

function Car () {  const fuel = 50  return {    get fuel () {      return fuel    },    set fuel (value) {      fuel = value      if (value > 100) fuel = 100    }  }}const car = new Car()console.log(car.fuel) // 50car.fuel = 3000console.log(car.fuel) // 100

Вот так. Легко и просто!

Предварительный вывод относительно инкапсуляции

Инкапсуляция с помощью ФФ проще и легче для восприятия. Она основана на области видимости, которая является важной частью JavaScript.

Инкапсуляция с помощью классов предполагает использование префикса "#", что может быть несколько утомительным.

Классы против ФФ this


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

Однако, на самом деле концепция this не так уж и сложна. Всего существует 6 контекстов, в которых может использоваться this. Если вы разбираетесь в этих контекстах, у вас не должно возникать проблем с this.

Названными контекстами являются:

  • глобальный контекст
  • контекст создаваемого объекта
  • контекст свойства или метода объекта
  • простая функция
  • стрелочная функция
  • контекст обработчика событий

Но вернемся к статье. Давайте рассмотрим особенности использования this в классах и ФФ.

Использование this в классах

При использовании в классе this указывает на создаваемый экземпляр (контекст свойства/метода). Вот почему экземпляр инициализируется в constructor.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastName = lastName    console.log(this)  }}const chris = new Human('Chris', 'Coyier')



Использование this в функциях-конструкторах

При использовании this внутри функции и new для создания экземпляра, this будет указывать на экземпляр.

function Human (firstName, lastName) {  this.firstName = firstName  this.lastName = lastName  console.log(this)}const chris = new Human('Chris', 'Coyier')



В отличии от ФК в ФФ this указывает на window (в контексте модуля this вообще имеет значение undefined).

// Для создания экземпляра не используется ключевое слово "new"function Human (firstName, lastName) {  this.firstName = firstName  this.lastName = lastName  console.log(this)}const chris = Human('Chris', 'Coyier')



Таким образом, в ФФ не следует использовать this. В этом состоит одно из основных отличий между ФФ и ФК.

Использование this в ФФ

Для того, чтобы иметь возможность использовать this в ФФ, необходимо создать контекст свойства/метода.

function Human (firstName, lastName) {  return {    firstName,    lastName,    sayThis () {      console.log(this)    }  }}const chris = Human('Chris', 'Coyier')chris.sayThis()



Несмотря на то, что мы можем использовать this в ФФ, нам это не нужно. Мы можем создать переменную, указывающую на экземпляр. Такая переменная может использоваться вместо this.

function Human (firstName, lastName) {  const human = {    firstName,    lastName,    sayHello() {      console.log(`Hi, I'm ${human.firstName}`)    }  }  return human}const chris = Human('Chris', 'Coyier')chris.sayHello()

human.firstName является более точным, нежели this.firstName, поскольку human явно указывает на экземпляр.

На самом деле нам даже не нужно писать human.firstName. Мы можем ограничиться firstName, поскольку данная переменная имеет лексическую область видимости (это когда значение переменной берется из внешнего окружения).

function Human (firstName, lastName) {  const human = {    firstName,    lastName,    sayHello() {      console.log(`Hi, I'm ${firstName}`)    }  }  return human}const chris = Human('Chris', 'Coyier')chris.sayHello()



Рассмотрим более сложный пример.

Сложный пример


Условия таковы: у нас имеется проект Human со свойствами firstName и lastName и методом sayHello.

Также у нас имеется проект Developer, наследующий от Human. Разработчики умеют писать код, поэтому у них должен быть метод code. Кроме того, они должны заявлять о своей принадлежности к касте разработчиков, поэтому нам необходимо перезаписать метод sayHello.

Реализуем указанную логику с помощью классов и ФФ.

Классы

Создаем проект Human.

class Human {  constructor (firstName, lastName) {    this.firstName = firstName    this.lastname = lastName  }  sayHello () {    console.log(`Hello, I'm ${this.firstName}`)  }}

Создаем проект Developer с методом code.

class Developer extends Human {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }}

Перезаписываем метод sayHello.

class Developer extends Human {  code (thing) {    console.log(`${this.firstName} coded ${thing}`)  }  sayHello () {    super.sayHello()    console.log(`I'm a developer`)  }}

ФФ (с использованием this)

Создаем проект Human.

function Human () {  return {    firstName,    lastName,    sayHello () {      console.log(`Hello, I'm ${this.firstName}`)    }  }}

Создаем проект Developer с методом code.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code (thing) {      console.log(`${this.firstName} coded ${thing}`)    }  })}

Перезаписываем метод sayHello.

function Developer (firstName, lastName) {  const human = Human(firstName, lastName)  return Object.assign({}, human, {    code (thing) {      console.log(`${this.firstName} coded ${thing}`)    },    sayHello () {      human.sayHello()      console.log('I\'m a developer')    }  })}

ФФ (без this)

Поскольку firstName за счет лексической области видимости доступна напрямую мы можем опустить this.

function Human (firstName, lastName) {  return {    // ...    sayHello () {      console.log(`Hello, I'm ${firstName}`)    }  }}function Developer (firstName, lastName) {  // ...  return Object.assign({}, human, {    code (thing) {      console.log(`${firstName} coded ${thing}`)    },    sayHello () { /* ... */ }  })}

Предварительный вывод относительно this

Простыми словами, классы требуют использования this, а ФФ нет. В данном случае я предпочитаю использовать ФФ, поскольку:

  • контекст this может меняться
  • код, написанный с помощью ФФ, является более коротким и чистым (в том числе, благодаря автоматической инкапсуляции переменных)

Классы против ФФ Обработчики событий


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

Поскольку обработчики событий изменяют контекст this, работа с ними в классах может быть проблематичной. В тоже время в ФФ таких проблем не возникает.

Однако изменение контекста this не имеет значения, если мы знаем, как с этим справиться. Рассмотрим простой пример.

Создание счетчика

Для создания счетчика воспользуемся полученными знаниями, включая частные переменные.

Наш счетчик будет содержать две вещи:

  • сам счетчик
  • кнопку для увеличения его значения



Вот как может выглядеть разметка:

<div class="counter">  <p>Count: <span>0</span></p>  <button>Increase Count</button></div>

Создание счетчика с помощью класса

Для облегчения задачи попросим пользователя найти и передать разметку счетчика классу Counter:

class Counter {  constructor (counter) {    // ...  }}// Использованиеconst counter = new Counter(document.querySelector('.counter'))

В классе необходимо получить 2 элемента:

  • <span>, содержащий значение счетчика нам нужно обновлять это значение при увеличении счетчика
  • <button> нам нужно добавить обработчик событий, вызываемых данным элементом

class Counter {  constructor (counter) {    this.countElement = counter.querySelector('span')    this.buttonElement = counter.querySelector('button')  }}

Далее мы инициализируем переменную count текстовым содержимым countElement. Указанная переменная должна быть частной.

class Counter {  #count  constructor (counter) {    // ...    this.#count = parseInt(countElement.textContent)  }}

При нажатии кнопки значение счетчика должно увеличиваться на 1. Реализуем это с помощью метода increaseCount.

class Counter {  #count  constructor (counter) { /* ... */ }  increaseCount () {    this.#count = this.#count + 1  }}

Теперь нам необходимо обновить DOM. Реализуем это с помощью метода updateCount, вызываемого внутри increaseCount:

class Counter {  #count  constructor (counter) { /* ... */ }  increaseCount () {    this.#count = this.#count + 1    this.updateCount()  }  updateCount () {    this.countElement.textContent = this.#count  }}

Осталось добавить обработчик событий.

Добавление обработчика событий

Добавим обработчик к this.buttonElement. К сожалению, мы не можем использовать increaseCount в качестве функции обратного вызова. Это приведет к ошибке.

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', this.increaseCount)  }  // Методы}



Исключение выбрасывается, потому что this указывает на buttonElement (контекст обработчика событий). В этом можно убедиться, если вывести значение this в консоль.



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

  • с помощью bind
  • с помощью стрелочной фукнции

Большинство использует первый способ (однако второй проще).

Добавление обработчика событий с помощью bind

bind возвращает новую функцию. В качестве первого аргумента ему передается объект, на который будет указывать this (к которому this будет привязан).

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', this.increaseCount.bind(this))  }  // ...}

Это работает, но выглядит это не очень хорошо. К тому же bind это продвинутая функция, с которой сложно иметь дело новичкам.

Стрелочные функции

Стрелочные функции, помимо прочего, не имеют собственного this. Они заимствуют его из лексического (внешнего) окружения. Поэтому код счетчика может быть переписан следующим образом:

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', () => {      this.increaseCount()    })  }  // Методы}

Есть еще более простой способ. Мы можем создать increaseCount в виде стрелочной функции. В этом случае this будет указывать на экземпляр.

class Counter {  // ...  constructor (counter) {    // ...    this.buttonElement.addEventListener('click', this.increaseCount)  }  increaseCount = () => {    this.#count = this.#count + 1    this.updateCounter()  }  // ...}

Код

Вот полный код примера:



Создание счетчика с помощью ФФ

Начало аналогичное мы просим пользователя найти и передать разметку счетчика:

function Counter (counter) {  // ...}const counter = Counter(document.querySelector('.counter'))

Получаем необходимые элементы, которые по умолчанию будут частными:

function Counter (counter) {  const countElement = counter.querySelector('span')  const buttonElement = counter.querySelector('button')}

Инициализируем переменную count:

function Counter (counter) {  const countElement = counter.querySelector('span')  const buttonElement = counter.querySelector('button')  let count = parseInt(countElement.textContext)}

Значение счетчика будет увеличиваться с помощью метода increaseCount. Вы можете использовать обычную функцию, но я предпочитаю другой подход:

function Counter (counter) {  // ...  const counter = {    increaseCount () {      count = count + 1    }  }}

DOM будет обновляться с помощью метода updateCount, который вызывается внутри increaseCount:

function Counter (counter) {  // ...  const counter = {    increaseCount () {      count = count + 1      counter.updateCount()    },    updateCount () {      increaseCount()    }  }}

Обратите внимание, что вместо this.updateCount мы используем counter.updateCount.

Добавление обрабочика событий

Мы можем добавить обработчик событий к buttonElement, используя counter.increaseCount в качестве колбэка.

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

function Counter (counterElement) {  // Переменные  // Методы  const counter = { /* ... */ }  // Обработчики событий  buttonElement.addEventListener('click', counter.increaseCount)}

Первая особенность this

Вы можете использовать this в ФФ, но только в контексте метода.

В следующем примере при вызове counter.increaseCount будет вызван counter.updateCount, поскольку this указывает на counter:

function Counter (counterElement) {  // Переменные  // Методы  const counter = {    increaseCount() {      count = count + 1      this.updateCount()    }  }  // Обработчики событий  buttonElement.addEventListener('click', counter.increaseCount)}

Однако обработчик событий работать не будет, потому что значение this изменилось. Данная проблема может быть решена с помощью bind, но не с помощью стрелочных функций.

Вторая особенность this

При использовании синтаксиса ФФ, мы не можем создавать методы в виде стрелочных функций, потому что методы создаются в контексте функции, т.е. this будет указывать на window:

function Counter (counterElement) {  // ...  const counter = {    // Не делайте так    // Не работает, поскольку this указывает на window    increaseCount: () => {      count = count + 1      this.updateCount()    }  }  // ...}

Поэтому при использовании ФФ я настоятельно рекомендую избегать использования this.

Код




Вердикт относительно обработчиков событий

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

При использовании ФФ рекомендую вообще обходиться без this.

Заключение


Итак, в данной статье мы рассмотрели четыре способа создания объектов в JavaScript:

  • Функции-конструкторы
  • Классы
  • Связывание объектов
  • Фабричные функции

Во-первых, мы пришли к выводу, что наиболее оптимальными способами создания объектов являются классы и ФФ.

Во-вторых, мы увидели, что подклассы легче создавать с помощью классов. Однако, в случае композиции лучше использовать ФФ.

В-третьих, мы резюмировали, что, когда речь идет об инкапсуляции, ФФ имеют преимущество перед классами, поскольку последние требуют использования специального префикса "#", а ФФ делают переменные частными автоматически.

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

На этом у меня все. Надеюсь, статья вам понравилась. Благодарю за внимание.
Подробнее..

Будущее 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 можно почитать здесь.

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

Хорошо забытое новое Falcon Acoustics возрождает DIY-акустику и продаёт колонки-конструктор за 150 000 рублей

24.09.2020 20:09:56 | Автор: admin
Неизвестно, что послужило толчком к появлению старого-нового формата, но один из крупнейших производителей динамических громкоговорителей в Европе решил порадовать покупателей акустическими системами для самостоятельной сборки. По словам производителя, это сделано для сокращения стоимости устройства. Известно, в западной Европе, тем более в Великобритании, акустические системы высокой верности стоят невменяемых денег (мы не говорим об аудиофильских изысках, которые дороги везде), речь о добротной HI-FI-акустике, но произведенной в Западной Европе. Британская компания решила исключить самую затратную часть себестоимости сборку, и делегировала этот процесс меломанам.



Тем самым уменьшилась стоимость полочников, которые произвели экономные британцы. Со среднестатистических 3 3,5 до очень привлекательных по европейским меркам 1,5 тысяч. Известно, что любое китайское изделие будет дешевле, и не факт, что сильно хуже, но не мало привередливых европейских меломанов подозрительно относятся к технике из поднебесной и предпочитают переплачивать за своё родное. Под катом подробнее о том, что за акустика получилась у производителя динамиков.


DIY-акустика как предмет роскоши


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



Могу предположить, что ещё одна внушительная часть стоимости это как раз корпус из березовой фанеры, изготовление и сырье, для которого стоят совсем не дёшево в Европе. Можно долго спорить о влиянии или не влиянии пород древесины на верность воспроизведения, но факт в том, что деревянные АС (при правильном расчете) при прочих равных звучат определенно лучше (вернее), чем корпуса из ЛДСП и аналогичных материалов.

К слову, корпуса сделаны в Италии. И это я не про качество, а про стоимость, при соблюдении технологических норм и надлежащем контроле качества нет никакой разницы, кто собирал, европейцы, китайцы или жители центральной Африки. Но, видимо, британцы решили не экспериментировать с азиатской сборкой, что, наверняка, отразилось в стоимости. Толщина стенок корпуса достаточно внушительная 9 и 12 мм. Фанера богато декорируется на выбор натуральным ореховым или махагоновым шпоном.

Я не эксперт в ценообразовании на столярные работы, но рискну предположить, что факт итальянского происхождения фанерных корпусов прибавил к стоимости каких-нибудь ощутимых 15 % (особенно, учитывая маркетинговые манипуляции на этом факте).

Реинкарнация прототипа 1974 года


Вышеназванные составляющие уже позволяют отнести устройство не столько к бытовым утилитарным предметам, сколько к предметам роскоши, по факту, геморроя с фанерой и прочими танцами с аудиофильскими бубнами. Помимо прочего акустике подарили культовые динамики, как утверждает производитель, попарно подобранные Falcon B110 и T27. Именно эти драйверы стояли в прототипах студийных мониторов Falcon LS3/5a 1974 года.



Пара представляет собой пятидюймовый СЧ/НЧ-излучатель с бекстреновым диффузором и 9-мм майларовый твитер. Частотный диапазон устройств составляет от 38 Гц до 20 кГц с чувствительностью 86 дБ. Параметры в целом совпадающие с характеристиками студийных устройств.



Совокупная масса одной колонки около 12,5 кг, основная часть этой массы сосредоточена в толстостенном корпусе и достаточно мощном постоянном магните СЧ/НЧ динамика. В фильтре нет ничего сверхвыдающегося. Как меня заверили знакомые инженеры, вполне надежная и заурядная конструкция, почти стандартная для большинства двухполосных мониторов. Электролитические конденсаторы с запасом, годная катушка.



Колонки за 30 минут


По утверждениям производителя, сборка осуществляется за 30 минут, никакой пайки, все соединения при помощи стандартных разъемов и клемм. По сути, нужно соединить в единую цепь фильтр, клеммы и динамики, установить в корпуса демпфирующий материал и закрепить динамики в корпусе при помощи шурупов. Таким образом можно говорить о максимально простом варианте самостоятельной сборки, которую может провести даже ребёнок.



Можно также утверждать, что у АС не так много общего с классической аудиоэлектроникой для самостоятельной сборки, распространённой в послевоенные годы. То, что некогда было популярным, требовало много усилий, более продолжительное время, навыки пайки и, в идеале, чтобы всё было правильным, руководить процессом должен инженер.

В качестве заключения


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



Реклама
Мы продаем электронику, в нашем каталоге много разного, в том числе акустические системы, наушники и многое другое.
Подробнее..

Маяк, ночник и конструктор для вашего ребенка (50 деталей, 3D печать)

16.03.2021 02:15:34 | Автор: admin
image

Добрый день, читатели.

Сегодня я хочу поделиться с вами своей разработкой, точнее не только своей, а сделанной совместно с моей 7-ми летней дочерью.

Предыстория
Как-то в преддверии осенних школьных каникул 2020, за обедом, предложил дочери Придумай, чтобы ты хотела, чтобы мы вместе с тобой разработали на этих каникулах. Например это может быть робот или какой-то механизм, или , но главное, чтобы это был конструктор и ты могла сама его собрать!.

На что дочь ответила Я хочу ночник в виде маяка, и он должен проецировать звезды на потолок.

Для меня это было неожиданно, носказано сделанои на наш взгляд получилосьвполне достойно! :) Теперь мы хотим поделиться с вами всем этим. Задальнейшей историей прошу в статью

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

Цель ясна и надо приступать к реализации


Задача 1 выбрать образец
Перед каникулами, по вечерам, мы начали готовиться к предстоящей работе. Несколько вечеров просидели в google в поисках того единственного маяка, который мил сердцу и который хотелось бы взять за основу.
Среди огромного разнообразия Юле очень понравился этот маяк (ссылка на исходный сайт увы утеряна):

image

Задача 2 тест
Теперь перед нами встал вопрос хватит ли мощности у небольшого светодиода, для создания тени на потолке, с расстояния 2 метра? Для дочери это было самое увлекательное, одно только предложение поставить эксперимент её так вдохновило, что даже когда мы уже все узнали, она просила еще что-то проверить :)
Собственно с этим как раз и возникли нюансы. Классические светодиоды, коих огромное количество в любых магазинах и у меня в закромах, имеют направленный луч в 30 и чтобы тень создавалась, нужно опустить светодиод практически к основанию маяка. Но ведь у маяка источник света вверху! Благо у меня нашлась еще одна куча 3 миллиметровых светодиодов с углом 120 и теплым белым свечением это оказался самый подходящий светодиод.

Далее была поездка в Кронштадт, ведь нельзя проникнуться идеей создания маяка, если ты не увидел и не прочувствовал его. А в Кронштадте их аж 17! Поездка была отличная, мы погуляли по дамбе, но в самом городе замерзли, с трудом нашли место где купить чего-то теплого и на обратной дороге на КАД пробили колесо. Зато все точно запомнилось!

image

Задача 3 проектирование
Ну вот пришли каникулы и мы начали разработку. Я сидел в Solidworks и создавал детали, Юля сидела рядом и консультировала :) Выбирали оптимальную высоту, ширину основания и прочее. Процесс был очень увлекательный. Итого вышло 50 деталей.

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

Процесс сборки первых деталей
image
image
image
image


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

Схема проще не бывает
image

Всё это, за исключением пайки, ребенок может собрать сам. Что касается электронной части, то я решил припаять только огрызки, а дальше дочь сама соединила элементы между собой на скрутку и маяк ожил на её глазах! Все модули размещаются в тамбуре модуль заряда приклеен на двухсторонний скотч на полу (есть отверстие для разъема micro-usb в задней стенке), а переключатель к крыше.


Макеты STL на thingiverse.com

Собственно мы и подошли к концу рассказа. Будем рады, если вы захотите повторить наш проект, а может даже раскрасите или сделаете экстерьер и выложите фотографии в комментариях.

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

Железяка 1 робот из обычного металлического конструктора

12.04.2021 22:14:50 | Автор: admin

Небольшой дисклеймер. Эта статья нашлась в моих старых "запасах", и она написана несколько лет назад. Но на фоне новости, что компания, которая производит "донора" для робота решила выпустить набор на Arduino, решил показать, что можно сделать похожее самостоятельно. Благо информация до сих пор не устарела, и никто вам не мешает заменить микроконтроллер на что-то другое.



Собрать робота из отечественного металлического конструктора, который многие из моего поколения помнят еще с уроков школьного труда в детстве. Возможно ли? Такой вызов я себе поставил, заодно решив посмотреть как возможность данного предприятия, так и для прощупывания почвы создания хоть и не очень сильного, но конкурента импортных наборов, занимающих на российском рынке монопольную долю. Предупрежу, что далее будет много фотографий и текста и что я покажу только как создать управляемую тележку с микроконтроллером, которую вы самостоятельно сможете дооснастить любыми датчиками.


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

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

Также из электронно-китайских запасов были взяты мотор-редукторы с колесами и дисками с прорезями для энкодера 2 щт, самый дешевый драйвер двигателя 1 шт, блютус-адаптер HC-06 1 шт, энкодеры 2 шт, макетная плата 17x10 1 шт, провода для макетной платы, перемычки, а также плата Arduino Leonardo.

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

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

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

Собираем основание тележки с мотор-редукторами и энкодерами.

Основание тележки мы собираем из двух симметричных половинок. Я буду использовать терминологию для деталей из официальной инструкции набора. Сначала прикрепляем к большой панели пластик 50x20 двумя винтами. Это будет изолирующее основание для энкодеров. Вы можете их не ставить для управления по блютус или базовых задач по робототехнике они не обязательны.

К боковой стороне прикручиваете две детали планку сегментную 2 и планку с тремя отверстиями.

Далее устанавливаете скобу I и уголок I как показано на фото. Энкодеры прикручиваете винтами с гайками М3 (у меня под рукой были только M3x15). Скобы чуть отгибаете. Наклон будет нужен для размещения мотор-редуктора в правильном положении.

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

Фиксируем мотор уголком I и зеркально собираем вторую половину основания и скрепляем их между собой.

Колеса

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

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

Для сборки одной оси нам понадобятся шпилька L=75M4, колесо большое, уголок I, диск большой и винты с гайками. Главная задача при сборке ось должна быть закреплена неподвижно и законтрена вторыми гайками, а колесо, наоборот, должно вращаться свободно с небольшим люфтом.

Электроника

Переходим к электронике. Так как у нас экспериментальный робот, то все соединения мы будем делать на макетной плате, а также использовать для управления плату Arduino Leonardo.

Из Панели, 4-х Уголков I, пластика 50x20, Скобы большой и 6 стоек под плату 10x3 (можно использовать три стойки и три винта M3x5) сооружаем каркас для установки макетной платы и платы Arduino.

Далее закрепляем на двусторонний скотч макетную плату 10x17 со сдвигом, так как нам надо будет разместить драйвер двигателя. Если у вас драйвер двигателя с прямыми ножками подключения, вы можете не делать такой сдвиг главное, чтобы после установки платы Ардуино у вас было место для установки остальных плат. Прикручиваем плату Arduino (я использовал опять же стойки, так как у меня их был избыток, а гаек M3 недостаток.

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

После решил защитить провода и зафиксировать блютус модуль от разбалтывания, поэтому собрал вот такую защиту для передней части нашего робота (у нас передние колеса ведущие). Для этого использовали детали: Скоба II 2 шт, Планка с 10 отв. 2 шт., Пластик 75x100.

Программное обеспечение

На деле это самая простая часть в Интернет полно примеров, как заставить такую тележку бегать управляя ей со смартфона или другим способом. Для этого используется передача через Serial порт команды в виде буквы и ее последующая обработка. Используя данные производителя платы драйвера двигателей по разводке и таблицу истинности легко задать скетч для Arduino. Конечно, легко на словах я потратил 4 часа, пытаясь найти проблему работы скетча. Как оказалось для Arduino Leonardo (что кстати написано в документации на сайте) надо использовать Serial1, вместо простого Serial. Поэтому если у вас плата, отличная от Leonardo, используйте нужную подпрограмму для вашей платы (для UNO, к примеру замените все Serial1 на Serial в тексте). Сам код можно найти по ссылке.

Посмотрев код видно, что мы используем стандартную раскладку WASD для движения и T для остановки.

После того как зальете программу в плату, проверьте что моторы подключены правильно и крутятся в нужную сторону, открыв терминал в Arduino IDE и передавая символы команд. Также можете поэкспериментировать со значением PWM.

Теперь приложение для Android. Тут тоже все достаточно просто: мы воспользуемся конструктором приложений MIT AppInventor 2 и создадим вот такую программу. В ней мы используем события TouchUp и TouchDown к элементам управления, передавая символ движения при его касании и остановки при отпускании.

Вы можете установить на смартфон специальное приложение для отладки или сформировать пакет для установки на Android смартфон, подключить к тележке батарею (я использовал внешний аккумулятор для смартфона), подключить bluetooth устройство (HC-06 в моем случае) и наслаждаться тем, что вы сами создали радиоуправляемую тележку.

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


Подробнее..

Категории

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

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