В JavaScript нет абстрактных схем объектов, называемых «классами», как в традиционных объектно-ориентированных языках. Однако механизм [[prototype]] JavaScript, который мы обсудим в этой статье, обеспечивает делегирование поведения для моделирования объектов в приложении, таких как эмуляция наследования, полиморфизма, создания экземпляров и т. Д. Делегирование поведения имеет более гибкие и динамические функции, чем функции объектного моделирования на основе классов в традиционных объектно-ориентированных языках.

Традиционный объектно-ориентированный подход к объектному моделированию

Многие языки предоставляют синтаксис, который позволяет проектировать программное обеспечение, ориентированное на классы, то есть мы проектируем родительские и дочерние классы как схемы для объектов (или экземпляров). В традиционном объектно-ориентированном языке, таком как Java, объекты создаются с помощью механизма копирования поведения из классов (как шаблонов) в экземпляры. Когда классы наследуются, это копирование также происходит из родительских классов в дочерние классы. Для полиморфизма методы на разных уровнях отношения наследования выглядят как ссылочные ссылки от дочерних классов к родительским (например, super), но на самом деле это механизм копирования в действии от родительских классов к дочерним. Итак, общий сценарий объектного моделирования в большинстве традиционных объектно-ориентированных языков выглядит следующим образом: мы определяем родительский класс с общим поведением (методами), анализируя проблемную область. Затем мы можем определить дочерние классы, унаследованные от родительского класса, с помощью механизма копирования ниже. Каждый дочерний класс может дополнительно добавить специализированное поведение (переопределить) для обработки определенных требований, относящихся только к нему самому. На следующем рисунке показан традиционный объектно-ориентированный подход к моделированию и проектированию:

Механизм прототипа JavaScript

Механизм прототипа JavaScript - это внутренняя ссылка (свойство с именем [[Prototype]]), которая существует для одного объекта и ссылается на другой объект. Когда для объекта запрашивается ссылка на свойство / метод, а такое свойство / метод не существует, связь internal[[Prototype]], в свою очередь, запрашивает у механизма JavaScript поиск свойства / метода связанного объекта. Это связано с тем, что операция по умолчанию [[Get]] следует по ссылке [[Prototype]] объекта для достижения запрошенного свойства, если она не может найти запрошенное свойство на объекте напрямую.

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

Поскольку Object.create(..) создает новый объект и связывает внутреннее [[Prototype]] этого нового объекта с указанным вами объектом, мы можем создать [[Prototype]] связь между двумя объектами, используя следующий код.

const obj1 = {
	a: 5
};
// create an object linked to obj1
const obj2 = Object.create( obj1 );
myObject.a; // 5

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

Все функции в JavaScript по умолчанию получают свойство с именем prototype, которое указывает на произвольный объект, например на объект User.prototype в приведенном ниже примере. И когда функция вызывается с new, создается объект, который получает внутреннюю [[prototype]] ссылку на объект, на который указывает prototype функции. По умолчанию User.prototype получает общедоступное неперечислимое свойство с именем constructor, которое является ссылкой на саму функцию, в данном случае User. Однако объект user1 или user2, созданный вызовом new User(), не означает, что он был создан User конструктором (просто побочный эффект вызова new), даже user1.constructor === User равно true. user1 не имеет свойства constructor, указывающего на User, вместо этого user1.constructor указывает на User через [[prototype]]mechanism, как описано ранее.

function User(name) {
	this.name = name;
}
User.prototype.myName = function() {
    return this.name;
}
const user1 = new User("a");
const user2 = new User("b"); 
Object.getPrototypeOf(user1) === User.prototype; //true
User.prototype.constructor === User; // true
user1.constructor === User; // true
user1.myName(); // a
user2.myName(); // b

В традиционных объектно-ориентированных языках процесс создания экземпляра (или наследования) класса использует механизм копирования определенного поведения из этого класса в физический объект для создания каждого нового экземпляра. Однако, как мы видим в JavaScript, мы не создаем несколько экземпляров класса. Вместо этого мы создаем несколько объектов (например, const user1 =), которые [[Prototype]] ссылаются на общий объект.

Попытка эмулировать класс-ориентированный дизайн с помощью JavaScript, как в традиционных объектно-ориентированных языках, таких как классы, конструкторы, new, операции копирования, наследование и полиморфизм, часто приводит к большой путанице в том, как на самом деле работает JavaScript. Названные с заглавной буквы функции, вызываемые с new для создания объекта JavaScript, не означают, что функция является конструктором, а ее имя - это имя класса. Функции - это просто обычные функции, а не конструкторы, и нет операций копирования из классов в экземпляры при использовании new в функциях. Есть только объекты, [[prototype]] связанные с другими объектами, которым можно делегировать доступ к свойствам и функциям.

Sonew User() - это неявный способ создания нового объекта, связанного с другим объектом, и есть другой способ, Object.create(..), - это явный способ создания нового объекта, связанного с другим объектом.

Прототипное наследование

В JavaScript нет настоящего «классического наследования», поскольку наследование подразумевает операцию копирования, а JavaScript не использует операцию копирования, а вместо этого использует механизм прототипа для создания связей между двумя объектами, что позволяет одному объекту делегировать доступ к свойствам и функциям другому объекту.

Основываясь на механизме прототипа, мы можем сказать, что user1 может иметь [[prototype]] ссылку на User.prototype для делегирования свойств и функций. И мы также можем сформировать концепцию наследования родительско-дочернего «класса», такую ​​как связь fromEmployee.prototype «объект» с User.prototype «объектом», чтобы напоминать наследование классов в традиционных объектно-ориентированных языках.

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

function User(name) {
    this.name = name;
}

User.prototype.getName = function() {
    return this.name;
};

function Employee(name, position) {
    User.call(this, name);
    this.position = position;
}
// Make a new Employee.prototype linked to User.prototype
// Employee.prototype.constructor is gone
Employee.prototype = Object.create(User.prototype);
Employee.prototype.myPosition = function() {
    return this.position;
};

const john= new Employee( "John", "Developer" );

john.myName(); // "John"
john.myPosition(); // "Developer"

Классы ES6

С введением ES6 class и super, которые обеспечивают синтаксический сахар для устранения уродства различных prototype связанных вызовов, замеченных ранее, и для имитации традиционных концепций и синтаксиса, связанных с объектно-ориентированными классами; поскольку это не настоящие классы, и они все еще основаны на механизме [[prototype]] , который мы обсуждали ранее. У нас, как у разработчиков, есть выбор: как спроектировать объектную модель на основе синтаксического сахара ES6, или спроектировать ее на основе делегирования поведения или более гибких миксинов.

class User {
    constructor(name) {
        this.name = name;
    }
    getName() {
        return this.name;
    }
}
class Employee extends User {
    constructor(name, position) {
        super(name);
        this.position = position;
    }
    getName() {
        return super.getName();
    }
    getPosition() {
        return this.position;
    }
}
const john = new Employee(“John”, “Developer”);
john.getName(); // "John"
john.getPosition(); // "Developer"

Делегирование поведения

Delegation Behavior Delegation использует механизм прототипов JavaScript (связывание объектов с другими объектами ), который позволяет некоторым объектам делегировать общие служебные объекты для ссылок на свойства или методы, если они не обнаружены в самом объекте. Это сильно отличается от традиционного объектно-ориентированного шаблона проектирования. Здесь нет родительских и дочерних классов, наследования, полиморфизма и т. Д. Мы можем просматривать объекты в JavaScript как горизонтально одноранговые объекты, и при необходимости они могут быть связаны бок о бок с делегированием. В отличие от традиционных объектно-ориентированных языков, здесь нет сложных уровней наследования, которые необходимо учитывать, и механизма копирования.

const User = {
    init(name) {
        this.name = name;
    },
    getName() {
        return this.name;
    }
};
const Employee = Object.create(User);
Employee.build = function(name, position) {
    this.init(name);
    this.position = position;
};
Employee.outputDetails = function() {
    console.log(this.getName() + " is a " + this.position);
};
const john = Object.create(Employee);
john.build("John", "Developer"); 
const mary = Object.create(Employee);
mary.build("Mary", "Manager");
john.outputDetails(); // John is a Developer
mary.outputDetails(); // Mary is a Manager

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

Сначала мы должны загрузить jQuery в инструмент разработчика нашего браузера (например, Chrome).

let script = document.createElement("script");
script.setAttribute('src', 'https://code.jquery.com/jquery-    latest.min.js');
script.addEventListener('load', function() {
        let script = document.createElement('script');
        document.body.appendChild(script);
    }, false);
document.body.appendChild(script);

Затем мы объявляем переменную Widget, указывающую на объект, с помощью init() и insert() утилит для других объектов, чтобы связать их и делегировать поведение.

const Widget = {
    init(width,height){
        this.width = width || 50;
        this.height = height || 50;
        this.$elem = null;
    },
    insert($where){
        if (this.$elem) {
            this.$elem.css({
                width: this.width + "px",
                height: this.height + "px"
            }).appendTo($where);
        }
    }
};

Определяемый здесь Button объект может быть независимым от объекта previousWidget. Теперь они просто одноранговые объекты.

const Button = {
    setup(width,height,label){
        // delegated call
        this.init(width, height);
        this.label = label || "Default";
        this.$elem = $("<button>").text(this.label);
    },
    build($where) {
        // delegated call
        this.insert($where);
        this.$elem.click(this.onClick.bind(this));
    },
    onClick(evt) {
        console.log("Button '" + this.label + "' clicked!");
    }
};

Теперь мы можем установить прототип (т.е. внутреннее свойство [[Prototype]]) Button на новый прототип, которым является Widget.

Object.setPrototypeOf(Button, Widget);

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

$(document).ready(function(){
    const $body = $(document.body);
    const btn1 = Object.create(Button);
    btn1.setup(120, 30, "A");
    const btn2 = Object.create(Button);
    btn2.setup(150, 50, "B");
    btn1.build($body);
    btn2.build($body);
} );

Выводы

В JavaScript механизм [[Prototype]] связывает объекты с другими объектами. Не существует абстрактных механизмов, подобных «классам», как бы мы ни пытались убедить себя в обратном.

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

Спасибо за прочтение.