JavaScript — шаблоны наследования.

После выхода окончательной версии спецификации ECMA Script 2015 (ES2015 ) сообщество получило возможность двигаться в направлении ее реализации в движках JavaScript .

ES2015 предлагает для существующих функций множество новых полезных возможностей и более чистый синтаксис. Например, ключевое слово class , а также улучшенный синтаксис для JavaScript prototype .

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

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

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

Наследование прототипов в JavaScript предполагает, что один объект наследуется от другого объекта, вместо того, чтобы одна спецификация наследовалась от другой. Даже ключевое слово нового класса является некорректным, потому что подразумевает спецификацию. Но на самом деле один объект наследуется от другого. Синтаксис в более ранних версиях JavaScript был слишком сложным, и ему трудно было следовать. Поэтому, как только разработчики принимают наследование от объекта к объекту, возникает вторая задача. Она состоит в том, чтобы улучшить синтаксис JavaScript prototype наследования — ввести классы ES2015 .

ES2015 классы в JavaScript

Данная спецификация обеспечивает более четкий синтаксис для определения структур классов, создания функций конструктора, расширения классов, вызова конструктора и функций в супер классе, а также предоставляет статические функции. Также ES2015 улучшает синтаксис для создания стиля ES5 получателя / установщика дескриптора свойств, что позволяет разработчикам использовать эти малоизвестные возможности спецификации.

Определения классов

JavaScript не содержит классов. Даже классы ES2015 это не совсем классы в традиционном смысле этого слова. А всего лишь «вычищенный » синтаксис для создания наследования прототипов между объектами. Но поскольку ES2015 использует термин «класс » для объектов, созданных с помощью функции конструктора (функция-конструктор является конечным результатом ключевого слова class ), в этой статье мы будем использовать термин «класс «, чтобы описать не только классы ES2015 , но и ES5 .

В версии ES5 и более ранних функции конструктора определяли «классы » следующим образом:

function MyClass() { } var myClass = new MyClass();

В ES2015 был введен новый синтаксис, с использованием ключевого слова class :

class MyClass { constructor() { } } var myClass = new MyClass();

Функция конструктора осталась той же, что определена в ES5 . В обернутом блоке ключевого слова class определяются свойства для JavaScript function prototype . Синтаксис ключевого слова new для установки нового экземпляра класса остался неизменным.

С введением ключевого слова class появляется объект функции, который используется ES5 . Рассмотрим следующий выходной результат среды Node.js REPL . Во-первых, мы определяем новый класс, а затем оператор TypeOf перечисляет типы объекта класса:

> class MyClass { constructor() {} } class MyClass { constructor() {} } > typeof MyClass "function" >

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

Что такое конструкторы в JavaScript?

Конструктор — это функция, которая выполняется, когда используется оператор new для создания нового экземпляра класса. В функцию конструктора могут быть переданы аргументы для инициализации свойств объекта и выполнения других задач.

В ES5 функция конструктора выглядит следующим образом:

function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }

Аналог функции конструктора с синтаксисом ES2015 выглядит следующим образом:

// имя функции конструктора ES5 - // это имя класса ES2015 class Person { // обратите внимание, что здесь нет ключевого слова "function" // также используется слово "constructor", а не "Person" constructor(firstName, lastName) { // этот код представляет новый созданный и // инициализированный объект this.firstName = firstName; this.lastName = lastName; } }

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

Чтобы установить объект с тем же синтаксисом, код должен быть тот же:

var person = new Person("Bob", "Smith"); // выводит "Bob" console.log(person.firstName); // выводит "Smith" console.log(person.lastName);

Нажмите здесь, чтобы загрузить код

Расширение классов

До ES2015 большинство разработчиков не понимали, как реализовать наследования между объектами и использовать JavaScript prototype . Пообщавшись с разработчиками на C ++ , Java или C # , вы поймете, с какой легкостью они настраивают наследование одного класса от другого, а затем создают экземпляр объекта из подкласса. Попросите JavaScript разработчика продемонстрировать, как происходит наследование между двумя объектами, и в ответ вы увидите пустой взгляд.

Настройка наследования прототипов является непростым делом, а понятие наследования прототипа является неизвестным для большинства JavaScript разработчиков. Вот несколько примеров кода с комментариями, которые поясняют процесс настройки наследования:

// вызывается с оператором "new", // создается новый объект Person function Person(firstName, lastName) { // оператор "new" устанавливает связь // от "this" к новому объекту this.firstName = firstName; this.lastName = lastName; } // это свойство, связывающее функцию, // конфигурируется для объекта прототипа Person, // и наследуется Student Person.prototype.getFullName = function() { return this.firstName + " " + this.lastName; }; // Когда функция конструктора Student // вызывается с оператором "new", // создается новый объект Student function Student(studentId, firstName, lastName) { // оператор "new" устанавливает связь от "this" к // новому объекту, новый объект затем передается в // функцию конструктора Person через использование вызова, // таким образом могут быть установлены свойства имени и фамилии this._super.call(this, firstName, lastName); this.studentId = studentId; } // Student наследуются от нового объекта, // который наследуется от родительского Student.prototype = Object.create(Person.prototype); // устанавливаем свойства конструктора обратно для // функции конструктора Student Student.prototype.constructor = Student; // "_super" НЕ является частью ES5, его конвенция, определенная // разработчиком, устанавливает // "_super" для функции конструктора Person Student.prototype._super = Person; // это будет существовать в прототипе объекта студента Student.prototype.getStudentInfo = function() { return this.studentId + " " + this.lastName + ", " + this.firstName; }; // устанавливаем новый объект Student var student = new Student(1, "Bob", "Smith"); // вызываем функцию в выводе родительского // прототипа "Bob Smith" console.log(student.getFullName()); // вызываем функцию в выводе родительского // прототипа "1 Smith, Bob" console.log(student.getStudentInfo());

Нажмите здесь, чтобы загрузить код

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

Чтобы решить эту проблему, в новом синтаксисе структуры классов в ES2015 было введено ключевое слово extends . В следующем коде продемонстрировано то же наследование, что и в первом примере кода, но с использованием синтаксиса ES2015 для JavaScript object prototype :

"use strict"; class Person { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } getFullName() { return this.firstName + " " + this.lastName; } } class Student extends Person { constructor(studentId, firstName, lastName) { super(firstName, lastName); this.studentId = studentId; } getStudentInfo() { return this.studentId + " " + this.lastName + ", " + this.firstName; } } var student = new Student(1, "Bob", "Smith"); console.log(student.getFullName()); console.log(student.getStudentInfo());

Нажмите здесь, чтобы загрузить код

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

Другой способ изучить, как это работает — рассмотреть код наследования ES5 , сгенерированный TypeScript . TypeScript – это препроцессорный язык, который оптимизирует JavaScript через строгую типизацию и транспиллинг кода ES2015 в код ES5 . Транспилинг — это процесс компиляции исходного кода одного языка программирования в исходный код другого языка.

Функция _extends в JavaScript

Для поддержки наследования классов ES2015 TypeScript транспилирует функционал ключевого слова extends в функцию с именем __extends , которая запускает код, необходимый для настройки наследования. Вот код функции __extends :

var __extends = (this && this.__extends) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); };

Приведенный выше код немного труден, поэтому ниже приводится его расширенная, задокументированная версия. Чтобы понять назначение каждой строки кода, прочтите комментарии, добавленные в исходный код JavaScript prototype . Функция __extends работает с любой парой родительских и дочерних объектов:

// объявляем переменную, чтобы связать функцию extends var __extends; if (this && this.__extends) { // функция extends уже определена в контексте // этого кода, поэтому используйте существующую функцию __extends __extends = this.__extends; } else {

Остальное содержимое блока — это реализация функции __extends . Она использует как шаблон примеси и JavaScript prototype наследование, чтобы построить взаимосвязь наследования между родительским и дочерним объектами. Шаблон примеси копирует свойства из одного объекта в другой. Приведенный ниже код обрабатывается через функцию __extends :

// функция extends еще не определена в текущем контексте; // поэтому определяем ее __extends = function (child, parent) { // шаблон примеси для копирования свойств функции родительского конструктора // в качестве статических свойств для свойств функции дочернего конструктора // в функции конструктора часто называют статическим свойством for (var parentPropertyName in parent) { // только скопированные свойства отдельно определяются для родителя if (parent.hasOwnProperty(parentPropertyName)) { // для простейших типов этот код копирует значения, // для типов объектов этот код копирует только связи child = parent; } } // функция конструктора для объекта, который установил дочерний объект, // наследуемый из этой функции, // является уникальной внутри контекста каждого вызова extend function __() { this.constructor = child; } if (parent === null) { // объект, установленный с помощью дочерней функции конструктора, // наследуется от объекта, который в свою очередь не наследуется ни от чего, // даже не от встроенного JavaScript Object child.prototype = Object.create(parent); } else { // назначаем свойства прототипа родительской функции конструктора // свойствам прототипа функции конструктора, определенной выше __.prototype = parent.prototype; // создаем объект, от которого наследуются все дочерние экземпляры, // и назначаем его свойству прототипа дочерней функции // конструктора child.prototype = new __(); } };

Следующие две строки кода сбивают с толку многих разработчиков:

// назначаем свойство прототипа родительской функции конструктора // свойству прототипа функции конструктора, определенной выше __.prototype = parent.prototype; // создаем объект, от которого наследуются все дочерние экземпляры // и назначаем его свойству прототипа дочерней // функции конструктора child.prototype = new __();

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

// Этот код не даст нужного результата child.prototype = parent.prototype;

Разработчики ошибочно полагают, что дочерний объект теперь будет наследоваться от объекта прототипа родительской функции конструктора. Но на самом деле объекты, созданные с помощью родительской функции конструктора, а также объекты, созданные с помощью дочерней функции конструктора, наследуются от точно такого же JavaScript object prototype . Это нежелательно, так как свойство прототипа дочерней функции конструктора не может быть изменено без одновременного изменения свойства прототипа родительской функции конструктора. Поэтому все изменения, внесенные в дочерний объект, будут также применены к родителю. Это некорректное наследование:

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

Таким образом, целью данного кода является установить следующую структуру наследования:

Prototype = parent.prototype; child.prototype = new __();


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

И в конце закрывающая фигурная скобка относится к изначальному блоку if :

Нажмите здесь, чтобы загрузить код

Синтаксис ES2015 для расширения классов гораздо более прост для понимания JavaScript prototype . Он содержит два новых ключевых слов: extends и super . Ключевое слово extends устанавливает отношения наследования прототипа между родительскими и дочерними классами. Ключевое слово super вызывает конструктор для класса родителя (он же суперкласс ). Вызов функции super требуется, даже если родительский объект не содержит конфигурацию.

Нет ничего более постоянного, чем временное.
Народная мудрость

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

В JavaScript каждый объект может иметь асоциацию с другим объектом - так называемый «прототип» (prototype). В случае, если поиск некоторого свойства (или метода - это одно и то же) в исходном объекте заканчивается неудачно, интерпретатор пытается найти одноименное свойство (метод) в его прототипе, затем - в прототипе прототипа и т. д. К примеру, если мы затребовали обращение к obj.prop (или, что абсолютно то же самое, obj["prop"]), JavaScript начнета искать свойство prop в самом объекте obj , затем - в прототипе obj , прототипе прототипа obj , и так до конца.

Секреты прототипов

В Интернете масса литературы, описывающей, что такое prototype , и в каком контексте его обычно используют. Однако львиная доля статей страдает одним большим недостатком: там не разъясняется детально, как именно работают прототипы, когда их можно применять, а когда - нельзя .

Продемонстрируем «классическое» применение прототипов для реализации наследования в JavaScript.

Листинг 1

//** //** Базовый "класс" Car (Машина). //** function Car() { document.writeln("Вызван конструктор Car()."); } // Определяем новый метод "класса" Car. Car.prototype.drive = function() { document.writeln("Вызван Car.drive()"); } //** //** Производный "класс" Zaporojets (Запорожец - тоже Машина). //** function Zaporojets() { document.writeln("Вызван конструктор Zaporojets()."); } // Говорим, что прототип Car - "класс" Zaporojets. Zaporojets.prototype = new Car(); // Определяем новый метод "класса" Zaporojets. Zaporojets.prototype.crack = function() { document.writeln("Вызван Zaporojets.crack()"); } //** //** Основная программа. //** document.writeln("Программа запущена."); // Создаем объект производного "класса" Zaporojets. // (*) вызывается функция базового объекта var other = new Zaporojets(); vehicle.crack();

Запустив данный пример, можно заметить, что с точки зрения "обычного" ООП результат выглядит несколько необычно:

Листинг 2

Вызван конструктор Car(). Программа запущена. Вызван конструктор Zaporojets(). Вызван Car.drive() Вызван конструктор Zaporojets(). Вызван Zaporojets.crack()

В объектно-ориентированных языках с поддержкой классов (C++, Java, PHP, Perl, Python и т. д.) конструкторы базовых классов обычно вызываются непосредственно внутри конструкторов производных. В JavaScript, как было уже сказано в предыдущей набле , классов нет, есть только объекты. Здесь мы видим совершенно другую картину: конструктор Car запустился даже до вывода сообщения "Программа запущена"! Кроме того, при повторном создании объекта Zaporojets конструктор Car вызван не был, а значит, один и тот же объект Car «разделяется» многими объектами Zaporojets ! С точки зрения идеологии наследования это совершенно неправильно.

К сожалению, невозможно задать прототип для некоторого объекта, не создав предварительно объект базового класса. Если вы хотите присвоить Zaporojets.prototype новое значение, вы просто обязаны использовать оператор new Car() . Иными словами, создание подобъекта базового «класса» производится в JavaScript не в конструкторе производного (как во всех остальных объектно-ориентированных языках), а гораздо раньше, еще на этапе конструирования «класса-потомка», и при том однократно.

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

Вывод: в JavaScript «стандартное» наследование реализуется совсем не так, как в других, «класс-ориентированных» языках программирования. Понятие «конструктора» в нем - не то же самое, что конструктор в C++, Java или даже Perl.

Чем не являются прототипы?

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

Листинг 3

Var obj = { // Зато у него есть прототип... prototype: { // ...в котором данное свойство определяется... prop: 101 } // ...так что в итоге интерпрететор должен считать его. } // Проверим? alert("Значение свойства: " + obj.prop); // What a...

Увы и ах: данный пример не работает , выдавая: "Значение свойства: undefined". А следовательно, присваивание свойству prototype произвольного объекта нового значения ничего нам не дает!

Модифицируем теперь код программы:

Листинг 4

Var obj = { // В самом объекте свойства prop нет. } // Пробуем обратиться к прототипу по-другому. obj.constructor.prototype.prop = 101; // Проверим? alert("Значение свойства: " + obj.prop); // Он в этом-то объекте свойства быть не должно... var newObj = {}; // пустой хэш alert("Пустота: " + newObj.prop); // А это еще откуда?!

Результат "Значение свойства: 101" говорит нам, что программа заработала. Однако какой ценой? Свойство prop теперь появилось вообще в любом объекте, создаваемом когда-либо в программе, а не только в obj ! Убедиться в этом позволяет второй вызов alert() , гордо сообщающий, что «пустота», оказывается, является числом 101. («Просветлей сам - просветлятся все существа в мире.»)

Какие выводы можно сделать из примера?

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

    Новый объект в JavaScript может быть создан только одним способом: применением оператора new:

    Листинг 5

    Var vehicle = new Car(); // создание нового объекта var hash = {}; // сокращенная запись для new Object() var array = ; // сокращенная запись для new Array()

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

    Листинг 6

    Var vehicle = new window.Car(); // можно и так... var vehicle = new self.Car(); // в браузере self==window

    или даже такому:

    Листинг 7

    Он также функционально не отличается от следующего примера:

    Листинг 8

    // Создание объекта стандартным способом. self.Car = function() { alert("Car") } var vehicle = new self.Car();

    Ну что, понравилось? Начали улавливать закономерности? Вот еще примеры:

    Листинг 9

    // Создаем "класс" на лету. var clazz = function() { alert("Динамическая!") } var obj = new clazz(); // А можно и без промежуточной переменной. var obj = new (function() { alert("Wow!") })();

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

    Так вот, после создания объекта интерпретатор присваивает его свойству constructor значение, равное величине, стоящей справа от оператора new . Таким образом, vehicle.constructor == self.Car , а obj.constructor в последнем примере вообще ссылается на функцию, не имеющую отдельного имени в глобальной области видимости (анонимную). Это настолько важно, что я приведу еще один поясняющий пример:

    Листинг 10

    // Создаем "класс" на лету. var clazz = function() { alert("Динамическая!") } var obj = new clazz(); alert(obj.constructor == clazz); // выводит true!

    Но позвольте, ведь справа от new не может стоять совсем уж все, что угодно. К примеру, там недопустимо число или строка... Следующий пример также не работает:

    Листинг 11

    Var clazz = {}; // clazz.constructor == self.Object var obj = new clazz(); // не работает!

    Что же можно использовать с оператором new ? Ответ прост: только функции (точнее, объекты, конструктор которых равен self.Function). А если еще точнее - разрешено использовать стандартные объекты JavaScript self.Array , self.String и т. д.

    Оказывается, что свойство prototype со специальным назначением есть только у таких объектов, которые могут быть использованы в правой части new ! Например, допустимы обращения к Function.prototype , String.prototype или Array.prototype .

    Теперь вы понимаете, почему JavaScript не рассматривает элемент obj.prototype произвольного хэша obj как специальный, но обращается к obj.constructor.prototype ? Ведь специальное назначение prototype имеет только для встроенного объекта, коим всегда является ссылка obj.constructor .

    Итак, вывод: прототипы объектов доступны по цепочке obj.constructor.prototype.constructor.prototype... , а не obj.prototype.prototype , как можно понять из многих руководств по JavaScript в Интернете. Конструктором объекта может быть только объект встроенного класса (обычно это Function).

    Заставляем конструкторы базовых классов работать

    Данная набла имеет циклический характер, и сейчас, хорошо понимая, как работают прототипы и конструкторы, мы снова возвращаемся к самому первому примеру. Речь пойдет о создании базового и производных объектов в стиле «класс-ориентированного» программирования.

    Итак, перед нами стоят следующие задачи:

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

    Если программировать на «чистом» JavaScript, данные две задачи выливаются в довольно громоздкий код. Чтобы каждый раз его не писать, я предлагаю вам использовать совсем небольшую библиотечку, обеспечивающую удобное применение рассматриваемых подходов. С ее использованием создание производных классов выглядит весьма просто:

    Листинг 12

    // Базовый "класс". Car = newClass(null, { constructor: function() { document.writeln("Вызван конструктор Car()."); }, drive: function() { document.writeln("Вызван Car.drive()"); } }); // Производный "класс". Zaporojets = newClass(Car, { constructor: function() { document.writeln("Вызван конструктор Zaporojets()."); this.constructor.prototype.constructor.call(this); }, crack: function() { document.writeln("Вызван Zaporojets.crack()"); }, drive: function() { document.writeln("Вызван Zaporojets.drive()"); return this.constructor.prototype.drive.call(this); } }); document.writeln("Программа запущена."); // Создаем объект производного "класса". var vehicle = new Zaporojets(); vehicle.drive(); // вызывается функция базового объекта // Создаем еще один объект того же класса. var vehicle = new Zaporojets(); vehicle.crack(); // функция производного объекта

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

    Листинг 13

    Программа запущена. Вызван конструктор Zaporojets(). Вызван конструктор Car(). Вызван Zaporojets.drive() Вызван Car.drive() Вызван конструктор Zaporojets(). Вызван конструктор Car(). Вызван Zaporojets.crack()

    Как видите, все работает так, как и ожидает программист на «класс-ориентированном» языке: конструктор Car() вызывается вместе с конструктором Zaporojets() . Однако запускать конструктор базового класса в конструкторе производного нужно явно (заодно приведено, как вызывать метод drive из базового объекта, если он был переопределен в производном):

    Листинг 14

    // Вызов конструктора базового объекта. this.constructor.prototype.constructor.call(this); // Вызов переопределенного метода базового объекта. this.constructor.prototype.drive.call(this); // У стандартного метода call() можно указывать // дополнительные аргументы (после this), которые // будут переданы функции-члену объекта.

    Библиотека Oop.js состоит из определения одной-единственной функции newClass . Она невелика, однако детальный разбор механизма ее работы, возможно, займет у вас немало времени (по крайней мере, я потратил не один час на эксперименты в разных браузерах). Могу сказать, что информации данной наблы должно быть вполне достаточно.

    Листинг 15

    // // Create proper-derivable "class". // // Version: 1.2 // function newClass(parent, prop) { // Dynamically create class constructor. var clazz = function() { // Stupid JS need exactly one "operator new" calling for parent // constructor just after class definition. if (clazz.preparing) return delete(clazz.preparing); // Call custom constructor. if (clazz.constr) { this.constructor = clazz; // we need it! clazz.constr.apply(this, arguments); } } clazz.prototype = {}; // no prototype by default if (parent) { parent.preparing = true; clazz.prototype = new parent; clazz.prototype.constructor = parent; clazz.constr = parent; // BY DEFAULT - parent constructor } if (prop) { var cname = "constructor"; for (var k in prop) { if (k != cname) clazz.prototype[k] = prop[k]; } if (prop && prop != Object) clazz.constr = prop; } return clazz; }

    (prototypal ). В любом случае, так как речь про объектно-ориентированное программирование, наследование - это про создание множества объектов, обладающих общими свойствами , на основе уже существующих .

    Рассмотрим пример на некотором вымышленном языке программирования удивительно похожем на JavaScript:

    // Простейший способ создать объект var greeterInstance = { person: null, greeting: function() { return "Hello " + this.person; } }; greeterInstance.person = "Alice"; // Некоторый код, который использует greeterInstance ... // А здесь нам нужен аналогичный объект, но для Bob"а. var greeterInstance2 = { person: null, greeting: function() { return "Hello " + this.person; } }; greeterInstance2.person = "Bob"; // ...

    Упс, похоже на copy/paste! Такой код будет трудно поддерживать, так как изменения придется вносить сразу во все места, где создается instance. Попробуем улучшить:

    Function createGreeter(person) { return { person: person, greeting: function() { return "Hello " + this.person; } }; } var aliceGreeter = createGreeter("Alice"); // Некоторый код, который использует aliceGreeter ... var bobGreeter = createGreeter("Bob"); // ...

    Отлично, теперь мы можем создавать множество похожих объектов , используя createGreeter() , и мы избавились от дублирования кода. Это уже наследование? Нет, так как никто ни от кого ничего не наследует. Это способ повторного использования кода - да.

    Function createGreeter(person) { return { person: person, greeting: function() { return "Hello " + this.person; } }; } function createGateKeeper(person) { var keeper = { opened: false, open: function() { this.opened = true; console.log(this.greeting()); } }; var greeter = createGreeter(person); for (var k in keeper) { greeter[k] = keeper[k]; } return greeter; } var gateKeeper = createGateKeeper("Alice"); gateKeeper.open();

    А вот это уже больше похоже на наследование, так как с помощью createGateKeeper() мы можем создавать множество похожих объектов , каждый из которых основывается на объектах типа Greeter .

    Наследование на основе классов

    Рассмотрим аналогию из реального мира. Класс можно воспринимать, как чертеж, по которому создаются изделия на заводе (объекты). Чертеж изделия!= самому изделию. Это лишь информация о том, как построить изделие (создать объект). В основанном на классах наследовании, одни чертежи наследуют общие свойства от других чертежей. А затем, объекты, созданные на основе таких чертежей, обладают свойствами, определенными как в первых, так и во вторых. Рассмотрим пример на Python 3:

    Class Greeter: def __init__(self, person): self.person = person def greeting(self): return "Hello " + self.person aliceGreeter = Greeter("Alice") # Во многих языках (Java, C++, C#, etc) принято писать aliceGreeter = new Greeter(...). # Обратите внимание на ключевое слово new. Такую реализацию наследования # принято называть "классической". print(aliceGreeter.greeting()) # "Чертеж" для gate keeper"ов построен на основе чертежа для greeter"ов. # Т.е. наследует все его свойства. А также добавляет собственные. class GateKeeper(Greeter): def __init__(self, person): super().__init__(person) self.opened = False def open(self): self.opened = True print(self.greeting()) gateKeeper = GateKeeper("Alice") gateKeeper.open()

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

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

    Прототипное наследование можно реализовать минимум двумя разными способами:

    • копированием всех свойств основного объекта в создаваемый на этапе его построения
    • делегированием обращений к свойтвам, не заданным в создаваемом объекте, базовому объекту

    Оба способа имеют свои плюсы и минусы. Рассмотрим реализацию первого способа :

    Function createBaseObject() { return { foo: "bar", method1: function() { return 42; } }; } var base = createBaseObject(); function createChildObject() { var child = { baz: 9001, method2: function() { return 43; } }; for (var k in base) { child[k] = base[k]; } return child; }

    • Так как каждый child объект содержит копию свойств base требуется дополнительная память.
    • Дополнительное время на копирование свойств base в child при создании.
    • Скорость обращения к свойствам child"ов не страдает за счет делегирования (см. ниже).
    • Изменение base объекта после создания child"ов не влияет на уже созданные объекты (это может быть как плюсом, так и минусом).

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

    Function createBaseObject() { return { foo: "bar", method1: function() { return 42; } }; } var base = createBaseObject(); function createChildObject() { return { baz: 9001, method2: function() { return 43; }, __get__: function(prop) { assert !this.hasOwnProperty(prop) return base; // Делегирование обращения базовому объекту } }; }

    Предположим, что "магический" метод __get__ переопределяет поведение при обращении к свойтвам, которые не заданы у самого объекта. Т.е. внутри __get__ вызов this.hasOwnProperty(prop) всегда возращает false .

    • Скорость такого кода должна быть ниже (если не рассматривать различные оптимизации под капотом языка) за счет дополнительного уровня косвенности, вводимого методом __get__ .
    • Создание объектов происходит быстрее.
    • Требуется меньше дополнительной памяти.
    Прототипное наследование в JavaScript

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

    Для ссылки на базовый объект при обращении к свойствам, не заданным у текущего объекта, используется свойство [] . Т.е., для того, чтобы унаследовать один объект от другого, нужно каким-либо способом задать для наследника [] равным ссылке на базовый объект. Простейший (но не стандартизованный до ES6 и не самый быстрый при этом) способ - это использовать свойство __proto__ :

    Var base = { foo: "bar" }; var child = { baz: 42, __proto__: base }; console.log(child.baz); console.log(child.foo); // Делегирование обращения базовому объекту

    До ES6 было как минимум два "законных" способа сделать это. Первый и не самый прямолинейный - это использование ключевого слова new . Поговорим о нем чуть позже. Второй же - изобретенная Дугласом Крокфордом функция Object.create() (ссылка), которая в итоге была добавлена в сам язык.

    Код из примера выше можно переписать следующим образом:

    Var base = { foo: "bar" }; var child = Object.create(base); // создает новый объект с заданным прототипом child.baz = 42; console.log(child.baz); console.log(child.foo);

    Обернув две строки создание child в функцияю createChild() мы создадим удобную реализацию прототипного наследования от base .

    Запутывающая всех конструкция new

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

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

    Var base = { greeting: function() { return "Hello " + this.person; } }; function Greeter(person) { this.person = person; } Greeter.prototype = base; var greeter = new Greeter("Alice"); console.log(greeter.greeting()); // prints "Hello Alice" console.log(greeter.__proto__ === base); // prints "true"

    Функции наподобие Greeter в JavaScript называются конструкторами (а иногда не совсем корректно - классами). При вызове new Greeter() создается новый объект, this внутри конструктора ссылается на этот объект. А в качестве прототипа этого объекта задается объект Greeter.prototype . Таким образом, вводится дополнительный уровень косвенности.

    This indirection was intended to make the language seem more familiar to classically trained programmers, but failed to do that, as we can see from the very low opinion Java programmers have of JavaScript. JavaScript’s constructor pattern did not appeal to the classical crowd. It also obscured JavaScript’s true prototypal nature. As a result, there are very few programmers who know how to use the language effectively. (с) Дуглас Крокфорд

    Теперь, обладая этими знаниями, мы можем без труда понять первоначальную реализацию Object.create() :

    Object.create = function(o) { function F() {} F.prototype = o; return new F(); };

    В результате вызова Object.create() будет создан новый пустой объект (new F()), прототипом которого будет объект o . И достигается это за счет описанной выше особенности JavaScript.

    • Перевод

    Примечание переводчика: Тема наследования в JavaScript является одной из самых тяжелых для новичков. С добавлением нового синтаксиса с ключевым словом class, понимание наследования явно не стало проще, хотя кардинально нового ничего не появилось. В данной статье не затрагиваются нюансы реализации прототипного наследования в JavaScript, поэтому если у читателя возникли вопросы, то рекомендую прочитать следующие статьи: Основы и заблуждения насчет JavaScript и Понимание ООП в JavaScript [Часть 1]

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

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

    JavaScript использует прототипное наследование Это означает, что в JavaScript объекты наследуются от других объектов. Простые объекты в JavaScript, созданные с использованием {} фигурных скобок, имеют только один прототип: Object.prototype . Object.prototype , в свою очередь тоже объект, и все свойства и методы Object.prototype доступны для всех объектов.

    Массивы, созданные с помощью квадратных скобок, имеют несколько прототипов, в том числе Object.prototype и Array.prototype . Это означает, что все свойства и методы Object.prototype и Array.prototype доступны для всех массивов. Одноименные свойства и методы, например .valueOf и .ToString , вызываются из ближайшего прототипа, в этом случае из Array.prototype .

    Определения прототипа и создание объектовСпособ 1: Шаблон конструктор JavaScript имеет особый тип функции называемых конструкторами, которые действуют так же, как и конструкторы в других языках. Функции-конструкторы вызываются только с помощью ключевого слова new и связывают создаваемый объект с контекстом функции-конструктора через ключевое слово this . Типичный конструктор может выглядеть следующим образом:
    function Animal(type){ this.type = type; } Animal.isAnimal = function(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; }; function Dog(name, breed){ Animal.call(this, "dog"); this.name = name; this.breed = breed; } Object.setPrototypeOf(Dog.prototype, Animal.prototype); Dog.prototype.bark = function(){ console.log("ruff, ruff"); }; Dog.prototype.print = function(){ console.log("The dog " + this.name + " is a " + this.breed); }; Dog.isDog = function(obj){ return Animal.isAnimal(obj, "dog"); };
    Использование этого конструктора выглядит также как и создание объекта в других языках:
    var sparkie = new Dog("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true
    bark и print методы прототипа, которые применяются для всех объектов созданных с помощью конструктора Dog . Свойства name и breed инициализируются в конструкторе. Это общепринятая практика, когда все методы определяются в прототипе, а свойства инициализируются конструктором.Способ 2: Определение класса в ES2015 (ES6) Ключевое слово class было зарезервировано в JavaScript с самого начала и вот наконец-то пришло время его использовать. Определения классов в JavaScript схоже с другими языками.
    class Animal { constructor(type){ this.type = type; } static isAnimal(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; } } class Dog extends Animal { constructor(name, breed){ super("dog"); this.name = name; this.breed = breed; } bark(){ console.log("ruff, ruff"); } print(){ console.log("The dog " + this.name + " is a " + this.breed); } static isDog(obj){ return Animal.isAnimal(obj, "dog"); } }
    Многие люди считают этот синтаксис удобным, потому что он объединяет в одном блоке конструктор и объявление статичных и прототипных методов. Использование точно такое же, как и в предыдущем способе.
    var sparkie = new Dog("Sparkie", "Border Collie"); Способ 3: Явное объявление прототипа, Object.create, фабричный метод Этот способ показывает, что на самом деле новый синтаксис с ключевым словом class использует прототипное наследование. Также этот способ позволяет создать новый объект без использования оператора new .
    var Animal = { create(type){ var animal = Object.create(Animal.prototype); animal.type = type; return animal; }, isAnimal(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; }, prototype: {} }; var Dog = { create(name, breed){ var proto = Object.assign(Animal.create("dog"), Dog.prototype); var dog = Object.create(proto); dog.name = name; dog.breed = breed; return dog; }, isDog(obj){ return Animal.isAnimal(obj, "dog"); }, prototype: { bark(){ console.log("ruff, ruff"); }, print(){ console.log("The dog " + this.name + " is a " + this.breed); } } };
    Этот синтаксис удобен, потому что прототип объявляется явно. Понятно что определено в прототипе, а что определено в самом объекте. Метод Object.create удобен, потому что он позволяет создать объект от указанного прототипа. Проверка с помощью .isPrototypeOf по-прежнему работает в обоих случаях. Использование разнообразно, но не чрезмерно:
    var sparkie = Dog.create("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true Способ 4: Object.create, фабрика верхнего уровня, отложенный прототип Этот способ является небольшим изменение способа 3, где сам класс является фабрикой, в отличии от случая когда класс является объектом с фабричным методом. Похоже, на пример конструктора (способ 1), но использует фабричный метод и Object.create .
    function Animal(type){ var animal = Object.create(Animal.prototype); animal.type = type; return animal; } Animal.isAnimal = function(obj, type){ if(!Animal.prototype.isPrototypeOf(obj)){ return false; } return type ? obj.type === type: true; }; Animal.prototype = {}; function Dog(name, breed){ var proto = Object.assign(Animal("dog"), Dog.prototype); var dog = Object.create(proto); dog.name = name; dog.breed = breed; return dog; } Dog.isDog = function(obj){ return Animal.isAnimal(obj, "dog"); }; Dog.prototype = { bark(){ console.log("ruff, ruff"); }, print(){ console.log("The dog " + this.name + " is a " + this.breed); } };
    Этот способ интересен тем, что похож на первой способ, но не требует ключевого слова new и работает с оператором instanceOf . Использование такое же, как и в первом способе, но без использования ключевого слова new :
    var sparkie = Dog("Sparkie", "Border Collie"); sparkie.name; // "Sparkie" sparkie.breed; // "Border Collie" sparkie.bark(); // console: "ruff, ruff" sparkie.print(); // console: "The dog Sparkie is a Border Collie" Dog.isDog(sparkie); // true СравнениеСпособ 1 против Способа 4 Существует довольно мало причин, для того чтобы использовать Способ 1 вместо Способа 4. Способ 1 требует либо использование ключевого слова new , либо добавление следующей проверки в конструкторе:
    if(!(this instanceof Foo)){ return new Foo(a, b, c); }
    В этом случае проще использовать Object.create с фабричным методом. Вы также не можете использовать функции Function#call или Function#apply с функциями-конструкторами, потому что они переопределяют контекст ключевого слова this . Проверка выше, может решить и эту проблему, но если вам нужно работать с неизвестным заранее количеством аргументов, вы должны использовать фабричный метод.Способ 2 против Способа 3 Те же рассуждения о конструкторах и операторе new , что были упомянуты выше, применимы и в этом случае. Проверка с помощью instanceof необходима, если используется новый синтаксис class без использования оператора new или используются Function#call или Function#apply .Мое мнение Программист должен стремиться к ясности своего кода. Синтаксис Способа 3 очень четко показывает, что именно происходит на самом деле. Он также позволяет легко использовать множественное наследование и стековое наследования. Так как оператор new нарушает принцип открытости/закрытости из-за несовместимости с apply или call , его следует избегать. Ключевое слово class скрывает прототипный характер наследования в JavaScript за маской системы классов.
    «Простое лучше мудреного», и использование классов, потому что оно считается более «изощренным» является просто ненужной, технической головомойкой.
    Использование Object.create является более выразительным и ясным, чем использование связки new и this . Кроме того, прототип хранится в объекте, который может быть вне контекста самой фабрики, и таким образом может быть более легко изменен и расширен добавлением методов . Прям как классы в ES6.
    Ключевое слово class , возможно будет наиболее пагубной чертой в JavaScript. Я испытываю огромное уважение к блестящим и очень трудолюбивым людям, которые были вовлечены в процесс написания стандарта, но даже блестящие люди иногда делают неправильные вещи. - Eric Elliott
    Добавление чего-то ненужного и возможно пагубного, противоречащего самой природе языка является необдуманным и ошибочным.
    Если вы решите использовать class , я искренне надеюсь, что мне никогда не придется работать с вашим кодом. На мой взгляд, разработчики должны избегать использования конструкторов, class и new , и использовать методы, которые более естественны парадигме и архитектуре языка. ГлоссарийObject.assign(a, b) копирует все перечислимые (enumerable) свойства объекта b в объект a , а затем возвращает объект a
    Object.create(proto) создает новый объект от указанного прототипа proto
    Object.setPrototypeOf(obj, proto) меняет внутреннее свойство [] объекта obj на proto

    Теги: Добавить метки

    JavaScript – это язык, основанный на прототипах. Это значит, что свойства и методы объектов можно повторно использовать посредством общих объектов, которые можно клонировать и расширять. Это называется наследованием прототипов и отличается от наследования классов. Среди популярных объектно-ориентированных языков программирования JavaScript относительно уникален, поскольку другие известные языки (PHP, Python и Java) являются языками на основе классов, которые в качестве макетов для объектов используют классы вместо прототипов.

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

    Прототипы в JavaScript

    Создайте новый массив:

    Помните, что создать его можно также с помощью конструктора массива: let y = new Array().

    Если посмотреть на [] нового массива y, вы увидите, что он имеет больше свойств и методов, чем объект x. Он унаследовал все это от Array.prototype.

    y.__proto__;

    Вы увидите свойство constructor в прототипе, для которого задано значение Array(). Свойство constructor возвращает функцию-конструктор объекта, которая является механизмом для построения объектов из функций.

    Теперь можно объединить два прототипа, так как в этом случае цепочка прототипов будет длиннее. Он выглядит так: y-> Array -> Object.

    y.__proto__.__proto__;
    {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, …}

    Эта цепочка теперь относится к Object.prototype. Можно проверить внутренний [] на свойство prototype функции конструктора, чтобы увидеть, что они ссылаются на одно и то же.

    y.__proto__ === Array.prototype; // true
    y.__proto__.__proto__ === Object.prototype; // true

    Также для этого можно использовать свойство isPrototypeOf():

    Array.prototype.isPrototypeOf(y); // true
    Object.prototype.isPrototypeOf(Array); // true

    Можно использовать оператор instanceof, чтобы проверить, появляется ли свойство prototype конструктора в пределах цепочки прототипов объекта.

    y instanceof Array; // true

    Итак, все объекты JavaScript имеют скрытое внутреннее свойство [] (которое можно определить с помощью __proto__ в некоторых браузерах). Объекты могут быть расширены и наследуют свойства и методы от [] их конструктора.

    Прототипы складываются в цепочки, и каждый дополнительный объект наследует все по этой цепочке. Цепочка заканчивается на Object.prototype.

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

    Функции-конструкторы – это функции, которые используются для построения новых объектов. Оператор new используется для создания новых экземпляров на основе функции конструктора. Вы уже знаете некоторые встроенные конструкторы JavaScript (new Array() и new Date(), например); вы также можете создавать собственные пользовательские шаблоны для построения объектов.

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

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

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

    // Initialize a constructor function for a new Hero
    function Hero(name, level) {
    this.name = name;
    this.level = level;
    }

    Теперь у вас есть функция-конструктор Hero с двумя параметрами: name и level. Поскольку у каждого персонажа будет имя и уровень, для них имеет смысл наследовать эти свойства. Ключевое слово this будет ссылаться на новый созданный экземпляр; this.name в параметре name гарантирует, что новый объект будет иметь свойство name.

    Создайте новый экземпляр с помощью new.

    let hero1 = new Hero("Bjorn", 1);

    Если запросить в консоли hero1, вы увидите новый объект с правильно установленными свойствами:

    Hero {name: "Bjorn", level: 1}

    Теперь, если запросить [] объекта hero1, вы увидите constructor Hero().

    Object.getPrototypeOf(hero1);
    constructor: ƒ Hero(name, level)

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

    Мы можем добавить помощью prototype. Создайте метод greet().

    // Add greet method to the Hero prototype


    }

    Поскольку greet() – это prototype в Hero, а hero1 является экземпляром Hero, метод будет доступен и для hero1:

    hero1.greet();
    "Bjorn says hello."

    Если вы проверите [] в Hero, вы увидите доступную опцию greet().

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

    С помощью метода call() скопируйте свойства одного конструктора в другой. Создайте конструкторы Warrior и Healer.

    ...
    // Initialize Warrior constructor

    // Chain constructor with call

    // Add a new property
    this.weapon = weapon;
    }
    // Initialize Healer constructor

    Hero.call(this, name, level);
    this.spell = spell;
    }

    Оба новых конструктора теперь обладают свойствами Hero и несколькими уникальными свойствами. Добавьте метод attack() в Warrior и метод heal() в Healer.

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

    Tags: