Программирование на основе прототипов — это стиль объектно-ориентированного программирования , в котором повторное использование поведения (известное как наследование ) выполняется посредством процесса повторного использования существующих объектов , которые служат прототипами . Эта модель также может быть известна как прототипное , прототипно-ориентированное, бесклассовое или основанное на экземплярах программирование.
Программирование на основе прототипов использует обобщенные объекты процесса, которые затем могут быть клонированы и расширены. Используя фрукт в качестве примера, объект «фрукт» будет представлять свойства и функциональность фруктов в целом. Объект «банан» будет клонирован из объекта «фрукт», и общие свойства, специфичные для бананов, будут добавлены. Каждый отдельный объект «банан» будет клонирован из универсального объекта «банан». Сравните с парадигмой на основе классов , где класс «фрукт» будет расширен классом «банан » .
Первыми языками программирования на основе прототипов были Director, он же Ani (на основе MacLisp ) (1976-1979), и одновременно, но не независимо, ThingLab (на основе Smalltalk ) (1977-1981), соответствующие докторские проекты Кеннета Майкла Кана в Массачусетском технологическом институте и Алана Гамильтона Борнинга в Стэнфорде (но работавшего с Аланом Кеем в Xerox PARC). Борнинг ввел слово «прототип» в своей статье TOPLAS 1981 года. Первым языком программирования на основе прототипов с более чем одним реализатором или пользователем, вероятно, был Yale T Scheme (1981-1984), хотя, как и Director и ThingLab изначально, он просто говорит об объектах без классов. Языком, который сделал название и понятие прототипов популярными, был Self (1985-1995), разработанный Дэвидом Унгаром и Рэндаллом Смитом для исследования тем в области проектирования объектно-ориентированных языков.
С конца 1990-х годов бесклассовая парадигма становится все более популярной. Некоторые современные прототипно-ориентированные языки — это JavaScript (и другие реализации ECMAScript , такие как JScript и ActionScript 1.0 от Flash ), Lua , Cecil , NewtonScript , Io , Ioke, MOO , REBOL и AHK .
С 2010-х годов появилось новое поколение языков с чисто функциональными прототипами, которые сводят ООП к самой сути: Jsonnet — это динамический ленивый чисто функциональный язык со встроенной системой объектов прототипов, использующей наследование mixin ; Nix — это динамический ленивый чисто функциональный язык, который создает эквивалентную объектную систему («расширения» Nix) всего за два коротких определения функций (плюс множество других удобных функций). Оба языка используются для определения больших распределенных конфигураций программного обеспечения (Jsonnet напрямую вдохновлен GCL, языком конфигурации Google, с помощью которого Google определяет все свои развертывания, и имеет схожую семантику, хотя и с динамическим связыванием переменных). С тех пор другие языки, такие как Gerbil Scheme, реализовали чисто функциональные ленивые системы прототипов, основанные на схожих принципах.
Этимологически «прототип» означает «первый отлив» («отлитый» в смысле изготовленный). Прототип — это конкретная вещь, из которой можно создавать другие объекты путем копирования и модификации. Например, Международный Прототип Килограмма — это реальный объект, который действительно существует, из которого можно создавать новые объекты-килограммы путем копирования. Для сравнения, «класс» — это абстрактная вещь, к которой могут принадлежать объекты. Например, все объекты-килограммы находятся в классе KilogramObject, который может быть подклассом MetricObject, и так далее.
Прототипное наследование в JavaScript описано Дугласом Крокфордом как
Вы создаете прототипы объектов, а затем… создаете новые экземпляры. Объекты изменяемы в JavaScript, поэтому мы можем дополнять новые экземпляры, давая им новые поля и методы. Затем они могут выступать в качестве прототипов для еще более новых объектов. Нам не нужны классы, чтобы создавать множество похожих объектов… Объекты наследуются от объектов. Что может быть более объектно-ориентированным, чем это? [1]
Сторонники программирования на основе прототипов утверждают, что оно побуждает программиста сосредоточиться на поведении некоторого набора примеров и только потом беспокоиться о классификации этих объектов в архетипические объекты, которые позже используются аналогично классам . [2] Многие системы на основе прототипов поощряют изменение прототипов во время выполнения , тогда как только очень немногие объектно-ориентированные системы на основе классов (такие как динамическая объектно-ориентированная система, Common Lisp , Dylan , Objective-C , Perl , Python , Ruby или Smalltalk ) позволяют изменять классы во время выполнения программы.
Почти все прототипные системы основаны на интерпретируемых и динамически типизированных языках. Однако системы, основанные на статически типизированных языках, технически осуществимы. Язык Omega, обсуждаемый в Prototype-Based Programming [3], является примером такой системы, хотя, согласно веб-сайту Omega, даже Omega не является исключительно статической, а скорее ее «компилятор может выбрать использование статического связывания, где это возможно, и может повысить эффективность программы».
В языках, основанных на прототипах, нет явных классов. Объекты наследуются напрямую от других объектов через свойство прототипа. Свойство прототипа вызывается prototype
в Self и JavaScript или proto
в Io . Существует два метода создания новых объектов: создание объекта ex nihilo («из ничего») или клонирование существующего объекта. Первый поддерживается через некоторую форму объектного литерала , деклараций, где объекты могут быть определены во время выполнения с помощью специального синтаксиса, такого как {...}
и переданы напрямую переменной. Хотя большинство систем поддерживают различные виды клонирования, создание объекта ex nihilo не так распространено. [4]
В языках, основанных на классах, новый экземпляр создается с помощью функции-конструктора класса , специальной функции, которая резервирует блок памяти для членов объекта (свойств и методов) и возвращает ссылку на этот блок. Необязательный набор аргументов конструктора может быть передан функции и обычно хранится в свойствах. Результирующий экземпляр унаследует все методы и свойства, которые были определены в классе, который действует как своего рода шаблон, из которого могут быть созданы объекты аналогичного типа.
Системы, поддерживающие создание объектов ex nihilo , позволяют создавать новые объекты с нуля без клонирования из существующего прототипа. Такие системы предоставляют специальный синтаксис для указания свойств и поведения новых объектов без ссылки на существующие объекты. Во многих языках прототипов существует корневой объект, часто называемый Object , который устанавливается как прототип по умолчанию для всех других объектов, созданных во время выполнения, и который несет в себе обычно необходимые методы, такие как toString()
функция для возврата описания объекта в виде строки. Одним из полезных аспектов создания объектов ex nihilo является обеспечение того, чтобы имена слотов нового объекта (свойств и методов) не имели конфликтов пространства имен с объектом Object верхнего уровня . (В языке JavaScript это можно сделать, используя нулевой прототип, т. е Object.create(null)
. .)
Клонирование относится к процессу, при котором новый объект создается путем копирования поведения существующего объекта (его прототипа). Затем новый объект несет все качества оригинала. С этого момента новый объект может быть изменен. В некоторых системах результирующий дочерний объект сохраняет явную связь (через делегирование или сходство ) со своим прототипом, и изменения в прототипе вызывают соответствующие изменения, которые становятся очевидными в его клоне. Другие системы, такие как язык программирования Kevo, подобный Forth , не распространяют изменения из прототипа таким образом и вместо этого следуют более конкатенативной модели, где изменения в клонированных объектах автоматически не распространяются на потомков. [2]
// Пример настоящего прототипного стиля наследования в JavaScript.// Создание объекта с использованием литеральной записи объекта {}. const foo = { name : "foo" , one : 1 , two : 2 }; // Другой объект. const bar = { two : "two" , three : 3 }; // Object.setPrototypeOf() — метод, представленный в ECMAScript 2015. // Для простоты представим, что следующая строка // работает независимо от используемого движка: Object.setPrototypeOf ( bar , foo ); // foo теперь является прототипом bar. // Если мы теперь попытаемся получить доступ к свойствам foo из bar, // у нас все получится. bar . one ; // Разрешается в 1. // Свойства дочернего объекта также доступны. bar . three ; // Разрешается в 3. // Собственные свойства затеняют свойства прототипа. bar . two ; // Разрешается в "two". bar . name ; // Не затронуто, разрешается в "foo". foo . name ; // Разрешается в "foo".
Другой пример:
const foo = { один : 1 , два : 2 }; // bar.[[прототип]] = foo const bar = Object.create ( foo ) ; бар . три = 3 ; бар . один ; // 1 бар . два ; // 2 бар . три ; // 3
В прототипных языках, использующих делегирование , среда выполнения языка способна отправлять правильный метод или находить нужную часть данных, просто следуя серии указателей делегирования (от объекта к его прототипу), пока не будет найдено соответствие. Все, что требуется для установления этого совместного поведения между объектами, — это указатель делегирования. В отличие от отношений между классом и экземпляром в объектно-ориентированных языках на основе классов, отношения между прототипом и его ответвлениями не требуют, чтобы дочерний объект имел память или структурное сходство с прототипом за пределами этой связи. Таким образом, дочерний объект может продолжать изменяться и дополняться с течением времени без перестройки структуры его связанного прототипа, как в системах на основе классов. Также важно отметить, что могут быть добавлены или изменены не только данные, но и методы. По этой причине некоторые прототипные языки называют и данные, и методы «слотами» или «членами». [ необходима цитата ]
В конкатенативном прототипировании — подходе, реализованном языком программирования Kevo — нет видимых указателей или ссылок на исходный прототип, из которого клонируется объект. Прототипный (родительский) объект копируется, а не связывается с ним, и нет делегирования. В результате изменения прототипа не будут отражены в клонированных объектах. [5] Кстати, язык программирования Cosmos достигает того же самого с помощью использования постоянных структур данных . [6]
Главное концептуальное отличие в рамках этой договоренности заключается в том, что изменения, внесенные в объект-прототип, не распространяются автоматически на клоны. Это может рассматриваться как преимущество или недостаток. (Однако Kevo предоставляет дополнительные примитивы для публикации изменений в наборах объектов на основе их сходства — так называемые семейные сходства или механизм семейства клонов [5] — а не через таксономическое происхождение, как это типично для модели делегирования.) Иногда также утверждается, что прототипирование на основе делегирования имеет дополнительный недостаток, заключающийся в том, что изменения в дочернем объекте могут повлиять на последующую работу родителя. Однако эта проблема не присуща модели на основе делегирования и не существует в языках на основе делегирования, таких как JavaScript, которые гарантируют, что изменения в дочернем объекте всегда записываются в самом дочернем объекте и никогда в родительских (т. е. значение дочернего объекта затеняет значение родителя, а не изменяет значение родителя).
В упрощенных реализациях конкатенативное прототипирование будет иметь более быстрый поиск членов, чем прототипирование на основе делегирования (потому что нет необходимости следовать цепочке родительских объектов), но, наоборот, будет использовать больше памяти (потому что все слоты копируются, а не существует одного слота, указывающего на родительский объект). Однако более сложные реализации могут избежать этой проблемы, хотя требуются компромиссы между скоростью и памятью. Например, системы с конкатенативным прототипированием могут использовать реализацию копирования при записи , чтобы обеспечить закулисный обмен данными — и такой подход действительно используется Kevo. [7] И наоборот, системы с прототипированием на основе делегирования могут использовать кэширование для ускорения поиска данных.
Сторонники объектных моделей на основе классов, критикующие прототипные системы, часто имеют опасения, схожие с опасениями сторонников статических систем типов для языков программирования относительно динамических систем типов (см. datatype ). Обычно такие опасения касаются корректности , безопасности , предсказуемости , эффективности и незнакомства программиста.
Что касается первых трех пунктов, классы часто рассматриваются как аналоги типов (в большинстве статически типизированных объектно-ориентированных языков они выполняют эту роль) и предлагаются для предоставления договорных гарантий своим экземплярам и пользователям их экземпляров, что они будут вести себя определенным образом.
Что касается эффективности, объявление классов упрощает многие оптимизации компилятора , которые позволяют разрабатывать эффективные методы и поиск переменных экземпляра. Для языка Self много времени разработки было потрачено на разработку, компиляцию и интерпретацию методов для улучшения производительности систем на основе прототипов по сравнению с системами на основе классов.
Распространенной критикой языков на основе прототипов является то, что сообщество разработчиков программного обеспечения незнакомо с ними, несмотря на популярность и проникновение JavaScript на рынок . Однако знания о системах на основе прототипов увеличиваются с распространением фреймворков JavaScript и сложным использованием JavaScript по мере развития Всемирной паутины (Web). [8] [ необходима цитата ] ECMAScript 6 представил классы как синтаксический сахар поверх существующего наследования на основе прототипов JavaScript, предоставляя альтернативный способ создания объектов и управления наследованием. [9]
Kevo реализовал чистую объектную модель на основе конкатенации, в которой новые объекты создавались путем копирования, а пространства имен всех объектов всегда были полностью автономными. … Более того, Kevo имел внутренний механизм
семейства клонов
, который позволял отслеживать "генеалогию" изменений среди групп объектов, так что изменения отдельных объектов могли распространяться на другие объекты при необходимости.