Русский
Русский
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 исходный контекст, измененный обработчиком событий.

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

Короткий путь к Искусственному интеллекту?

07.07.2020 06:22:58 | Автор: admin
Давайте признаемся: мы как-то буксуем. Разработки в сфере ИИ, при всех значительных затратах, не дают ожидаемого выхлопа. Конечно, кое-чего получается, но дело идет медленно. Медленнее, чем хотелось бы. Может, задача не решается потому, что решается не та задача?

Сейчас у нас есть много алгоритмов, выполняющих те или иные (отдельные) когнитивные функции. Одни обыгрывают нас в игры, другие водят машины, третьи Не мне вам рассказывать. Мы создали программы компьютерного зрения, которые различают дорожные знаки лучше, чем мы сами. Программы, которые рисуют и пишут музыку. Алгоритмы ставят медицинские диагнозы. Алгоритмы могут заткнуть нас за пояс в распознавании котиков, но конкретно этот, который для котиков, ни в чем ином, кроме распознавания котиков. А мы-то хотим такую программу, которая решала любые задачи! Нам нужен сильный или универсальный ИИ, но без собственного сознания, чтоб не смог отказаться решать поставленную задачу, верно? Где нам его взять?



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

Наш мозг. Представьте его себе. Два кило (по максимуму) податливого розовато-серого вещества. Сто миллиардов (тоже возьмем по максимуму) нейронов, каждый из которых готов отрастить до десяти тысяч динамических связей синапсов, которые могут то появляться, то исчезать. Плюс несколько типов сигналов между ними, да еще и глия сюрприз подкинула тоже что-то проводит, помогает и способствует. (Для справки: нейроглия или просто глия совокупность вспомогательных клеток нервной ткани. Составляет около 40% объёма ЦНС. Количество глиальных клеток в среднем в 10-50 раз больше, чем нейронов). Дендриты недавно удивили оказывается, они выполняют куда больше функций, чем считалось ранее (1). Мозг очень сложная штука. Если не верите спросите у Константина Анохина. Он подтвердит.

Человек все делает с помощью мозга. Собственно, мы это и есть он. Отсюда совершенно неудивительным является представление человека о том, что мозг = интеллект и еще более неудивительна идея скопировать устройство мозга и вуаля! получить искомое. Но мозг это не интеллект. Мозг это носитель. Железо. А Интеллект это алгоритм, софт. Попытки повторить софт через копирование железа это провальная идея. Это культ карго (2). Вы же знаете, что такое культ карго?

Аборигены островов Меланезии (увидев во время WWII, как самолеты привозят оружие, продовольствие, медикаменты и многое другое), соорудили из соломы копии самолетов и будку диспетчера, но никак не помогли себе в получении товаров, потому что не имели никакого представления о том, что скрывается за внешним видом самолетов. Так и мы, разобрав до винтиков калькулятор, не найдем внутри ни одной цифры. И, тем более, никакого намека на какие-либо операции с числами.

Пару лет назад Андрей Константинов в одном из номеров журнала Кот Шрёдингера (12 за 2017 г.), в своей колонке Где у робота душа, написал: Со времён Лейбница мы так и не нашли в мозге ничего, кроме частей, толкающих одна другую. Конечно не нашли! И не найдем. По компьютерному железу мы пытаемся восстановить программу, а это невозможно. В качестве подтверждающего аргумента приведу длинную цитату (3):

нейробиологи, вооружившись методами, обычно применяемыми для изучения живых нейроструктур, попытались использовать эти методы, чтобы понять, как функционирует простейшая микропроцессорная система. Мозгом стал MOS 6502 один из популярнейших микропроцессоров всех времён и народов: 8-битный чип, использованный во множестве ранних персональных компьютеров и игровых приставок, в том числе Apple, Commodore, Atari. Естественно, что мы знаем об этом чипе всё ведь он создан человеком! Но исследователи сделали вид, что не знают ничего и попытались понять его работу, изучая теми же методами, которыми изучают живой мозг.

Химически была удалена крышка, под оптическим микроскопом изучена схема с точностью до отдельного транзистора, создана цифровая модель (тут я немного упрощаю, но суть верна), причём модель настолько точная, что на ней оказалось возможно запускать старые игры (Space Invaders, Donkey Kong, Pitfall). А дальше чип (точнее, его модель) был подвергнут тысячам измерений одновременно: во время исполнения игр измерены напряжения на каждом проводке и определено состояние каждого транзистора. Это породило поток данных в полтора гигабайта в секунду который уже и анализировался. Строились графики всплесков от отдельных транзисторов, выявлялись ритмы, отыскивались элементы схемы, отключение которых делало её неработоспособной, находились взаимные зависимости элементов и блоков и т. п.

Насколько сложной была эта система по сравнению с живыми? Процессор 6502, конечно, и рядом не стоит с головным мозгом даже мыши. Но он приближается по сложности к червю Caenorhabditis elegans ломовой лошадке биологов: этот червь изучен вдоль и поперёк и уже предпринимаются попытки смоделировать его полностью в цифровом виде () Таким образом, задача анализа системы на чипе 6502 не является чрезмерным упрощением. И результаты имеют право быть экстраполированы на системы in vivo.

Вот только исследователи потерпели поражение! Нет, какие-то результаты, конечно, получены были. Анализируя чип, удалось выделить функциональные блоки, набросать схему их вероятных взаимосвязей, получить некоторые интересные подсказки насчёт того, как, вероятно, работает микропроцессор в целом. Однако понимания в том смысле, в каком его требует нейробиология (в данном случае: быть способным исправить любую поломку), достигнуто не было".

В какой-то момент появились исследователи, которые стали говорить примерно то же самое что надо изучать алгоритмы, что нужно понять, какую функцию выполняет интеллект. К примеру, Демис Хассабис (DeepMind), готовясь к выступлению на Singularity-саммите в Сан-Франциско (2010 г.), сказал следующее: В отличие от других выступлений на саммите по теме AGI, мой доклад будет другим, так как я интересуюсь системным уровнем нейронауки алгоритмами мозга а не деталями, как они реализуются мозговой тканью в виде спайков нейронов и синапсов или специфической нейрохимией и т. д. Я интересуюсь, какими алгоритмами мозг пользуется для решения проблем, и которые нам нужно найти, чтобы добраться до AGI.

Однако, спустя 10 (!!!!!) лет, все идет по-прежнему: ученые исследуют мозг и пытаются из внешних проявлений физиологической активности и его внутреннего устройства вычислить, как происходит интересующий процесс. Сколько задач столько процессов. Люди все разные. Мозги у всех немного, но отличаются. Некая усредненная картина, конечно, имеется, однако Представьте себе, что в любой произвольный момент времени мозг решает массу, в том числе и подсознательных задач, отслеживает и контролирует внутреннее состояние организма, воспринимает и интерпретирует сигналы внешней среды (и это мы не говорим о многочисленных петлях обратной связи). Сможем ли мы уверенно выявить, надежно идентифицировать и четко отделить эти активности одну от другой? Возможно ли это в принципе? Честно говоря, сомневаюсь. Не говоря уже о воспроизводимости этих процессов на небиологических носителях

Просмотрим на ситуацию иначе. Что такое задача вообще? Это затруднительная ситуация, с которой сталкивается, и которую пытается разрешить человек. Как показали в середине прошлого века американские математики Герберт Саймон и Аллен Ньюэлл, любая задача в общем виде может быть описана как переход из состояния Система с проблемой в состояние Система без проблемы. Они разработали компьютерную программу, назвав её General Problem Solver (Универсальный решатель задач), но дальше решения задач специфического вида они не продвинулись, поэтому универсальность именно их алгоритма осталась под вопросом. Но формула Система с проблемой --> Система без проблемы оказалась абсолютно верна!



Преобразование Системы это процесс ее перевода из исходного состояния с проблемой в желаемое состояние без проблемы (4). В процессе преобразования, (т. е. решения задачи) проблемная система становится беспроблемной (ну или менее проблемной), улучшается, избавляется от своих недостатков и выживает, т. е. продолжает использоваться. Ой, погодите, что это мы сейчас сказали? Избавление от недостатков? Выживание? Хм Что-то знакомое. Где-то мы это Ах, ну да. Эволюция! Чем меньше недостатков тем больше шансы выжить!

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

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

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

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

Кто-то мне сейчас наверняка возразит: задачи, которые решает человек, связаны с миллионами самых разных систем природными, общественными, производственными, техническими Материальными и абстрактными, находящимися на разных уровнях иерархии. И развиваются-де они каждая по-своему, а дарвиновская эволюция это про живую природу. Зайчики, цветочки, рыбки, птички Но исследования показывают, что законы эволюции универсальны. Доказательства долго искать не надо они все перед глазами. Имеющие их да увидят. Что ни возьми от спички до Боинга, от танка до контрабаса везде (5) мы видим наследственность, изменчивость и отбор! А все многообразие эволюционных изменений (кажущаяся сложность которых связана с тем, что все системы очень разные по своей природе и находятся на разных уровнях иерархии) можно выразить единственным циклом. Вы же помните, да? Система с проблемой --> Система без проблемы.

Что такое Система с проблемой? Это Система (материальная и абстрактная, социальная, производственная и техническая, научная и любая объект, идея, гипотеза всё, что угодно), в которой обнаружены какие-то недостатки, влияющие (внимание!) на наше желание и возможность её использования. Система недостаточно хороша. Система недостаточно эффективна. У неё низкое соотношение польза / затраты. Мы хотим, можем и готовы от нее отказаться, и часто отказываемся. Но нам нужна другая (выполняющая нужную нам полезную функцию), но уже без проблем более эффективная, без недостатков (или с меньшим их количеством). Ну, вы видели эту картинку выше Конечно, одной стрелочки между двумя крайними состояниями (исходным и желаемым) нам мало. Нам нужен тот самый оператор, преобразователь, верно? Попробуем его найти? Вы же согласитесь, что в случае успеха мы получим описание (хотя бы, для начала и упрощенное) так нужного нам универсального алгоритма?



Исходная точка Система с проблемой. Мы начинаем задумываться о том, чтоб отказаться от её использования. Момент, который мы называем (или ощущаем) Надо что-то делать!
Причина, угрожающая выживанию системы низкая идеальность, выражающаяся в пониженной величине отношения полезных функций системы к функциям затратным (вредным).

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

В связи с тем, что представленная выше Схема описывает процесс развития, улучшения или, если хотите, эволюции любых систем (в чем легко убедиться, подставив вместо слова Система любое иное по желанию от Абажур до Якорь), я думаю, ее смело можно и даже нужно! назвать Универсальной Схемой Эволюции. И обратите внимание она абсолютно алгоритмична, т. е. полностью подпадает под определение алгоритма: Алгоритм точное предписание о выполнении в определённом порядке некоторой системы операций, ведущих к решению всех задач данного типа. значит может быть реализована в виде компьютерной программы).

В представленном виде Универсальная Схема Эволюции:
естественная законы эволюции выявлены в системах самого разного типа, и их действие проверено в технике, производстве, обществе, природе и мышлении;
объективная законы эволюции не зависят от мнения исследователя и/или пользователя;
логичная и непротиворечивая законы эволюции вытекают один из другого;
полная набор законов эволюции достаточен для описания любой системы;
жесткая законы эволюции нельзя переставлять и
замкнутая законы эволюции образуют цикл: система, пройдя один цикл изменений, тут же начинает новый.

Что у нас получается в итоге: Эволюция системы (представленная в виде Универсальной Схемы) это путь её улучшения, избавления от недостатков. Иными словами, это алгоритм решения задачи. А решение задачи это именно то, чем занимается интеллект. Упрощаем и получаем: Универсальная Схема = описание функции интеллекта.

Конструктивная критика приветствуется. :)



1. Дендриты важнее для мозга, чем ранее считалось chrdk.ru/news/dendrity-vazhnee-chem-schitalos
2. ru.wikipedia.org/wiki/Карго_культ
3. Е. Золотов. Пойми меня! Как неживое помогает разбираться в живом www.computerra.ru/161756/6502
4. Chapter 6. Problem Solving. Artificial Intelligence. A Knowledge-Based Approach by Morris W.Firebaugh University of Wisconsin Parkside PWS-Kent Publishing Company Boston 1988, p. 172.
5. Дарвиновская эволюция в мире техносферы. Мир вещей, создаваемый человеком, развивается по тем же законам, что и живая природа. www.ng.ru/science/2017-01-11/14_6899_evolution.html
Подробнее..

Альтернативный способ заполнения спиральной матрицы

31.05.2021 18:12:13 | Автор: admin

В процессе изучения основ алгоритмизации и программирования в качестве студента еще в середине 2000х мне попалась довольно известная всем задача по заполнению спиральной матрицы. Суть состоит в том, чтобы начиная с позиции [1, 1], продвигаясь по часовой стрелке, заполнить квадратную матрицу заданной величины числами в возрастающем порядке. На ее решение было потрачено около двух часов.

Собственно алгоритм заполнения был тривиален, циклы, в общей сложности состоящие из N2 итерации, предполагали прохождение по всем элементам массива в требуемом порядке, при этом увеличивая значение итератора на 1 и заполняя им текущий элемент матрицы. Маршрут начинался с элемента [1, 1], далее продвигается по горизонтали до правого верхнего элемента [1, N], после вниз до нижнего правого угла [N, N], затем до левого нижнего угла [N, 1] и завершал первый круг на столбец ниже отправной точки [2, 1]. В дальнейшем, такое же круговое движение происходило уже в следующем внутреннем круге, и так далее до центра матрицы.

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

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

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

Спустя эти самые 18 лет, перебирая наиболее интересные задачи и пути их решения для обучения уже следующего поколения представителей неординарной профессии программист, меня заинтересовала одна статья на ресурсе Хабр (http://personeltest.ru/aways/habr.com/ru/post/261773), описывающая процесс создания формулы для вычисления количества дней в заданном месяце без использования каких-либо условных операторов, циклов и заранее подготовленных данных.

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

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

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

Итак, задача: разработать алгоритм для вычисления значения элемента спиральной матрицы на основании его координат [i, j] и размера самой матрицы, пользуясь только простыми арифметическими вычислениями. Исключение составляет модуль - абсолютное значение числа.

Условие: никаких условных (прошу прощения за тавтологию) переходов или заготовленных данных в виде массивов, словарей и т.д.

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

Очевидно, такую непростую задачу необходимо разделить на несколько этапов, или, если угодно, выделить основные элементы.

1) Найти закономерность между координатами ячейки и ее порядковым номером в первом внешнем кольце, и вывести соответствующую форму.

2) Найти связь между, опять же, координатами ячейки и номером кольца, в котором она находится. Составить формулу.

3) Связать между собой два алгоритма для выведения общей формулы вычисления значения элемента массива.

Входные данные: N размер квадратной матрицы (массива).

1 этап. Заполнение внешнего кольца. Для начала попытаемся вывести формулу вычисления значения ячейки для внешнего кольца массива, в котором отсчет начинается с ячейки [1, 1] и двигается по часовой стрелке поворачивая на углах [1, N], [N, N]. Заканчивается на строку ниже начальной точки, т.е. [2, 1].

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

Для наглядного изучения процесса напишем соответствующий скрипт, объявив в нем массив размером 5x5 (назовем его a), элементы которого будем перебирать традиционным способом двумя вложенными циклами от 1 до N. На данном этапе будем работать только с внешним кольцом.

#include <iostream>using namespace std;int main(){    int N = 5;          // задаем размер матрицы    int a[N][N];        // и инициализируем ее    for (int ik = 0; ik < N; ik++)        for (int jk = 0; jk < N; jk++)            a[ik][jk] = 0;          // заполнив для удобства нулями                                          for (int ik = 0; ik < N; ik++){     //назовем его "Основной цикл"        for (int jk = 0; jk < N; jk++){            if (!(ik == 0 || ik == N - 1 || jk == 0 || jk == N - 1))                 continue;      // Временное условие для фильтрации элементов внесшего "кольца"            int i = ik + 1;     // Номера строк и столбцов приводим в удобный            int j = jk + 1;     // в математическом плане вид (от 1 до N)              //  ... здесь будем вставлять основной код вычислений        }       }        for (int ik = 0; ik < N; ik++){          //Блок "Вывод массива"        for (int jk = 0; jk < N; jk++){           printf( "%02d ", a[ik][jk]);// дополняем число ведущими нулями        }        cout << endl;    }      return 0;}

Очевидно, что, по крайней мере, до правого нижнего угла внешнего кольца суммарное значение координат (i + j) увеличивается ровно на 1 с каждым шагом. Однако первый элемент в таком случае равняется 2, E1,2 = 3 и т.д. Поэтому необходимо уменьшить значение суммы (i + j) на один. В результате введем переменную Xs = i + j - 1, которая часто будет использоваться в дальнейших вычислениях. Пишем код:

int Xs = (i + j  1);a[ik][jk] = Xs;

В результате запуска первого скрипта получаем массив:

Как видим заполнение от начала до правого нижнего угла произведено корректно. Однако далее у нас происходит уменьшение шага, вместо увеличения.

Очевидно a[ik][jk] = Xsнуждается в дополнении, при котором все его значения до [5, 5] останутся неизменными, но после этой точки начнут увеличиваться на 1.

Но для начала постараемся привести в норму вторую часть кольца, которая заполнялась бы с ячейки (i = 5, j = 4) начиная со значения 10. В данном случае это легко сделать, лишь вычитая от общего количества элементов первого кольца увеличенного на два (равняется периметру первого кольца N * 4 - 4 = 16 плюс 2) значение Xs.

То есть a1,2 = 4N 4 + 2 Xs = 4N Xs - 2.

int Xs = i + j - 1;     a[ik][jk] = 4 * N - Xs - 2;

Однако теперь у нас правильно заполнилась только левая и нижняя стороны кольца, а противоположенные элементы нет.

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

1. ai,j = Xs = i + j - 1; действует от [1, 1] до [N, N].

2. ai,j = 4*N 2 - X; действует от [N, N] до [2, 1].

Самым простым решением было бы использование условного перехода, однако это не соответствует нашей начальной установке. Здесь необходимо дополнительно отметить, что из всех стандартных кусочно-заданных функций, как отмечено выше, мы будем использовать только модуль (y = |x|) и формулы собственной разработки.

В этой связи, необходимо привести наше уравнение в вид:

a_1, _2= F _1(switcher) * Xs + F _2 (switcher) * (4 * N Xs - 2);

Здесь функция F1 принимает значение 1 при i, j между [1, 1] [N, N] , в остальных случаях 0. В свою очередь, F2 , наоборот, принимает значение 1 когда ячейка находится в диапазоне [N, N - 1] [2, 1], и 0 между [1, 1] [N, N].

В качестве аргумента функции используется переменная switcher, вычисляемая на основе значении i и j, и опять же, принимающая различные значения в вышеуказанных диапазонах.

Неплохим вариантом выглядит идея получения значений -1, 0 и/или 1 от манипуляций с координатами элемента. Тогда F1 и F2 содержали бы простые арифметические операции.

Итак, мы уже использовали сложение i и j, но оно всегда дает положительное число. Почему бы сейчас не попробовать вычитание?

Заменим в нашем предыдущем листинге значение a[ik][jk].

int Xs = i + j - 1;     a[ik][jk] = j  i;

Наиболее легким вариантом видится целочисленное деление на самого себя, но в таком случае мы столкнемся с ошибкой деления на 0, поскольку а[1][1] и а[N][N] уже содержат 0. В данном случае можно прибавить ко всем значениям N и осуществить целочисленное деление на N. Введенную переменную назовем switcher.

int switcher =  (j - i + N) / N;a[ik][jk] =  switcher; // временно подставим в ячейки switcher

Теперь осталось немногое для завершения данного этапа, а именно создать функции F1 и F2. F1 должна возвращать такое же значение, какое ей передали в качестве аргумента, т.е. F1 (switcher) = switcher. В таком случае F1 (switcher) * Xs работает только для диапазона от [1, 1] до [N, N], в остальных случаях равняется нулю. Вторая часть уравнения, должна действовать наоборот. В таком случае она должна возвращать значение по модулю switcher 1, т.е. F2.(switcher) = |switcher 1|.

Итак, пишем:

a[ik][jk] =  switcher * Xs + abs(switcher - 1) * (4 * N - 2 - Xs);

Проверяем:

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

2 этап. Альтернативная система координат.

Нам удалось заполнить внешнее первое кольцо требуемыми данными. Однако, что произойдет, если мы снимем фильтр, и попытаемся вычислить данные для остальных элементов массива? Для этого необходимо удалить участок кода if (!(ik == 0 || ik == N - 1 || jk == 0 || jk == N - 1)) continue;

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

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

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

Мы же, перенесем начало координат в центр нашей матрицы следующим образом:

a[ik][jk] =  abs(N / 2 + 1 -  i);

Поскольку нам требуется только расстояние ячейки от центра, мы берем только абсолютные значения.

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

Введем две новые переменные Ic, Jc (c обозначает center).

Ic = abs(i -  N / 2  - 1);Jc = abs(j -  N / 2  - 1);

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

Опять же, самый простой способ что-то узнать попробовать вывести суммы номеров строк и столбцов.

a[ik][jk] =  Ic + Jc;

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

a[ik][jk] =  Ic - Jc;

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

a[ik][jk] =  abs(Ic  Jc) + Ic + Jc;

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

Ring = N / 2 -  (abs(Ic  Jc) + Ic + Jc) / 2; a[ik][jk] =  Ring;

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

N = 6

При четном размере центральный квадрат из четырех элементов должен считаться центром матрицы. Возникает вопрос: как это реализовать? Здесь к нам опять на помощь приходит целочисленное деление и кусочно-заданная функция.

Но для этого вернемся немного назад, к вычислению Ic и Jc. Попробуем запустить наш скрипт при N = 6, и посмотрим значения Ic = |i - N / 2 - 1|.

В данный момент требуется привести массив в симметричную форму. Легче всего это сделать, увеличив значение на 1 всех строк после 3-й (то есть вторую половину).

Ic = abs(i - N / 2  - 1) + (i - 1) / (N /2) * ((N-1) % 2);

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

Тоже самое проделываем и Jc.

Jc = abs(j - N / 2  - 1) + (j - 1)/(N /2) * ((N-1) % 2);

В итоге получаем симметричную матрицу уже по вертикали. Теперь проверяем значение Ring для матрицы с размером 6.

 a[ik][jk] =  Ring;

Все работает нормально. Второй этап завершен.

3 этап. Соединение. На данном этапе мы объединим полученные данные и выведем искомую матрицу (через формулу вычисления значения заданной ячейки).

Но для начала, вернемся к первому этапу и выведем на экран:

a[ik][jk] =  switcher * Xs + abs(switcher - 1) * (4 * N - 2 - Xs);

Дальнейший порядок действий представляется следующим образом: привести значения элементов внутренних колец к спиральному виду (т.е. заполнить их начинания с верхнего левого угла по спирали возрастающими значениями от 1), далее вычислить коэффициент прироста, которой обеспечит нормальный переход от одного кольца к нижестоящему (в нашем примере от ячейки [1,2] к ячейке [2,2], при этом меняя значение с 16 на 17)

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

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

Xs = (i Ring) + (j Ring) 1.

Теперь попробуем вывести

a[ik][jk] =  switcher * Xs + abs(switcher - 1) * (4 * N - 2 - Xs);

Как вы могли заметить верхние и правые элементы внутренних колец пришли в требуемое значение. Однако нижние и левые стороны приняли гораздо большие значения, чем ожидалось. Это связано с тем, что выражение 4 * N - 2 - Xs во второй части функции вычисляет значения исходя из размера внешнего кольца, которое нужно уменьшить, заменив на N 2 * Ring. То есть формула будет работать в соответствии с размером текущего кольца.

Итак:

a[ik][jk] =  switcher * (Xs) + abs(switcher - 1) * (4 * (N - 2 * Ring) - 2 - Xs); 

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

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

Coef = N2 (N 2Ring)2

Воспользовавшись правилами разложения квадратов разницы элементов ((ab)2=a22ab+b2), можно сократить до 4Ring(N - Ring).

Теперь этот коэффициент нужно добавить к нашей основной формуле.

a[ik][jk] =  Coef + switcher * (Xs) + abs(switcher - 1) * (4 * (N - 2 * Ring) - 2 - Xs);

Требуемый результат!

Полный код участка вычисления значений представлены следующим образом:

int switcher =  (j - i + N) / N;int Ic = abs(i - N / 2  - 1) + (i - 1)/(N /2) * ((N-1) % 2);int Jc = abs(j - N / 2  - 1) + (j - 1)/(N /2) * ((N-1) % 2);int Ring = N / 2 - (abs(Ic - Jc) + Ic + Jc) / 2;int Xs = i - Ring + j - Ring - 1;    int Coef =  4 * Ring * (N - Ring);a[ik][jk] =  Coef + switcher * Xs + abs(switcher - 1) * (4 * (N - 2 * Ring) - 2 - Xs);

Можно конечно попробовать развернуть единую формулу, заменив дополнительные переменные выражениями, основанными только на i, j и N, и попытаться сократить ее математическими методами. Но поверьте мне, я пытался, получилось нечто такое невообразимое (на половину страницы), что я решил оставить все как есть.

(PS. Не работает только при N = 1, так как возникает ошибка деления на ноль. Но как говорится, чуть-чуть не считается).

Подробнее..

Категории

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

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