В 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. Он использует процесс поиска прототипа, чтобы позволить одноранговым объектам делегировать поведение друг другу без строгого объявления иерархии классов или тесно связанной композиции, как в традиционных объектно-ориентированных языках.
Спасибо за прочтение.