Знакомство с прототипным объектно-ориентированным программированием

JavaScript, интерфейс программирования для Интернета самого нижнего уровня, распространен повсеместно. В связи с тем, что Интернет все в большей мере становится неотъемлемой частью нашей повседневной жизни, так же все более значимым становится и JavaScript. JavaScript — это язык, возможности которого зачастую недооцениваются; некоторые считают его «игрушечным» или называют «детским языком Java™». Одной из наиболее обсуждаемых особенностей этого языка является прототипная объектная система. Хотя нельзя отрицать, что JavaScript имеет свои недостатки, прототипная объектная система не относится к их числу. Узнайте из этой статьи о невероятной мощи, простоте и элегантности прототипного объектно-ориентированного программирования на языке JavaScript.

Делон Ньюмен, внештатный разработчик, вештатный сотрудник

Photo of Delon NewmanДелон Ньюмен (Delon Newman) занимается программированием на досуге с 1997 г. Он начал с C и C++, а затем перешел на HTML, Perl и JavaScript. Делон работает в области ИТ с 1999 года в качестве технического специалиста службы поддержки пользователей, художника компьютерной графики, Web-дизайнера, системного администратора, программиста, аналитика и разработчика программного обеспечения. За время работы в своей собственной компании, в том числе и в качестве консультанта, Делон изучил много языков и сред, включая Ruby, Python, Java, C#, PHP, Smalltalk, Lisp, Haskell, Erlang, Scala и Clojure. Связаться с Делоном можно по электронной почте delon.newman@gmail.com.



13.12.2012

Мир объектов

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

История

Обычно первым объектно-ориентированным языком называют язык моделирования Simula. Затем появились Smalltalk, C++, Java и C#. В то время в большинстве объектно-ориентированных языков объекты определялись с помощью классов. Впоследствии разработчики Smalltalk-подобного языка программирования Self создали альтернативный, более простой метод определения объектов, названный объектно-ориентированным программированием на основе прототипов, или прототипным объектно-ориентированным программированием.

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

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

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

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

Узнайте из этой статьи о преимуществах прототипного объектно-ориентированного программирования и объектных шаблонах языка JavaScript.


Что такое прототип? Классы и прототипы

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

Классическим примером является использование класса Point и его потомка Point3D для определения двухмерных и трехмерных точек соответственно. Листинг 1 показывает, как могли бы выглядеть указанные классы в коде Java.

Листинг 1. Класс Point в Java
class Point {
    private int x;
    private int y;

    static Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    int getX() {
        return this.x;
    }

    int getY() {
        return this.y;
    }

    void setX(int val) {
        this.x = val;
    }

    void setY(int val) {
        this.y = val;
    }
}

Point p1 = new Point(0, 0);
p1.getX() // => 0;
p1.getY() // => 0;

// The Point3D class 'extends' Point, inheriting its behavior
class Point3D extends Point {
    private int z;

    static Point3D(int x, int y, int z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    int getZ() {
        return Z;
    }

    void setZ(int val) {
        this.z = val;
    }
}

Point3D p2 = Point3D(0, 0, 0);
p2.getX() // => 0
p2.getY() // => 0
p2.getZ() // => 0

В отличие от способа определения объектов с использованием классов, прототипные объектные системы поддерживают более прямой метод создания объектов. Например, в JavaScript объект представляет собой простой список свойств. Каждый объект содержит специальную ссылку на другой родительский объект или прототип— объект, поведение которого наследуется. Пример с классом Point может быть воспроизведен в JavaScript, как показано в листинге 2.

Листинг 2. Класс Point в JavaScript
var point = {
    x : 0,
    y : 0
};

point.x // => 0
point.y // => 0

// создается новый объект с point в качестве прототипа, наследующий поведение 
point3D = Object.create(point);
point3D.z = 0;

point3D.x // => 0
point3D.y // => 0
point3D.z // => 0

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

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

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

Возможность прямого определения объектов также обеспечивает невероятную эффективность и простоту создания объектов и манипулирования ими. Например, в листинге 2 вы можете легко объявить свой объект point с помощью одной строки: var point = { x: 0, y: 0 };. Написав лишь одну строку, вы получили законченный рабочий объект, который наследует поведение объекта JavaScript Object.prototype, например, метод toString. Для расширения поведения своего объекта вам необходимо лишь объявить еще один объект с point в качестве его прототипа. В противоположность этому даже в самом лаконичном классическом объектно-ориентированном языке вам пришлось бы сначала определить класс, а затем создать его экземпляр, прежде чем вы получили бы доступный для манипуляций объект. Для наследования вам пришлось бы определить еще один класс, расширяющий первый.

Прототипный шаблон концептуально проще. Наше человеческое мышление часто оперирует прототипами. Например, Стив Йегге (Steve Yegge) в своем блоге «The Universal Design Pattern» («Универсальный шаблон проектирования») (см. раздел Ресурсы), приводит пример с игроком в американский футбол (скажем, Эммиттом Смитом), который со своей скоростью, ловкостью и силой становится прототипом для всех новых игроков в Национальной футбольной лиге (NFL). Затем, когда появляется новый одаренный игрок задней линии, LT, комментаторы говорят:

"У LT ноги, как у Эммитта".
"Он может прорываться за линию совсем как Эммитт".
"Да он пробегает милю ровно за пять минут!".

Комментаторы моделируют новый объект — LT — на основе прототипного объекта, Эммитта Смита. В JavaScript подобная модель выглядела бы примерно так, как показано в листинге 3.

Листинг 3. Модель JavaScript
var emmitt = {
    // ... здесь определяются свойства
};

var lt = Object.create(emmitt);
// ... добавьте остальные свойства непосредственно в lt

Вы можете сопоставить приведенный пример со случаем классического моделирования, где вы могли бы определить класс RunningBack (игрок задней линии), наследующий атрибуты из класса FootballPlayer (игрок в американский футбол). «LT» и «emmitt» были бы экземплярами класса RunningBack. Эти классы могли бы выглядеть в коде Java так, как показано в листинге 4.

Листинг 4. Три класса Java
class FootballPlayer {
    private string name;
    private string team;

    static void FootballPlayer() { }

    string getName() {
        return this.name;
    }

    string getTeam() {
        return this.team;
    }

    void setName(string val) {
        this.name = val;
    }

    void setTeam(string val) {
        this.team = val;
    }
}

class RunningBack extends FootballPlayer {
    private bool offensiveTeam = true;

    bool isOffesiveTeam() {
        return this.offensiveTeam;
    }
}

RunningBack emmitt = new RunningBack();
RunningBack lt   = new RunningBack();

Классическая модель сопряжена со значительно бóльшими концептуальными «накладными расходами», но при этом не предоставляет возможности детального контроля над экземплярами классов emmitt и lt— той возможности, которую вы получаете при использовании прототипной модели. (Справедливости ради, следует отметить, что класс FootballPlayer не является на 100 % необходимым; он используется здесь для сравнения с последующим примером.) Порой подобные «накладные расходы» могут оказаться полезными, но зачастую они являются просто обузой.

Эмулировать классическое моделирование с помощью прототипной объектной системы достаточно легко. (Следует признать, что возможно и обратное, хотя иногда это может быть непросто.) Например, вы можете создать объект footballPlayer с другим объектом runningBack, который наследует атрибуты из footballPlayer в качестве своего прототипа. В JavaScript эти объекты выглядели бы так, как показано в листинге 5.

Листинг 5. Моделирование в JavaScript
var footballPlayer = {
    name : "";
    team : "";
};

var runningBack = Object.create(footballPlayer);
runningBack.offensiveTeam = true;

Вы также могли бы создать еще один объект lineBacker, наследующий атрибуты из footballPlayer, как показано в листинге 6.

Листинг 6. Наследование объектов
var lineBacker = Object.create(footballPlayer);
lineBacker.defensiveTeam = true;

Как показано в листинге 7, вы можете добавить поведение в объекты lineBacker и runningBack путем добавления в объект footballPlayer.

Листинг 7. Добавление поведения
footballPlayer.run = function () { this.running = true };
lineBacker.run();
lineBacker.running; // => true

В этом примере вы работаете с footballPlayer как с классом. Вы также можете создать объекты для Эммитта и LT, как показано в листинге 8.

Листинг 8. Создание объектов
var emmitt = Object.create(runningBack);
emmitt.superbowlRings = 3;

var lt = Object.create(emmitt);
lt.mileRun = '5min';

Поскольку объект lt наследует атрибуты из объекта emmitt, вы даже можете работать с emmitt как с классом, как показано в листинге 9.

Листинг 9. Наследование и классы
emmitt.height = "6ft";
lt.height // => "6ft";

Если бы вы попытались воспроизвести приведенные примеры на языке, который характеризуется применением статических классических объектов (например, в коде Java), вам бы пришлось использовать шаблон декоратора, требующий еще больших концептуальных «накладных расходов», и вам все равно не удалось бы реализовать наследование непосредственно из объекта emmitt в качестве экземпляра. Напротив, шаблон свойств, используемый в языках, основанных на прототипах (например, в JavaScript), позволяет вам намного свободнее декорировать свои объекты.


JavaScript – это не Java

JavaScript и некоторые из его возможностей, например, прототипные объекты, явились жертвами грубых первоначальных просчетов и неудачных маркетинговых решений. Например, автор JavaScript Брендан Эйч (Brendan Eich) обсуждал в своем блоге необходимость создания нового языка: «Диктат со стороны высшего руководства, занимающегося управлением ходом разработки, заключался в том, что этот язык должен был «выглядеть, как Java». Это вычеркивало из списка Perl, Python и Tcl вместе со Scheme». Поэтому JavaScript выглядит как Java и имеет название, связанное с Java, что вводит в заблуждение любого человека, не знакомого с одним или с обоими из этих языков. Несмотря на то, что JavaScript на первый взгляд выглядит как язык Java, при более глубоком рассмотрении оказывается, что он не имеет с Java ничего общего, что приводит к обманутым ожиданиям. Цитата из блога Брендана Эйча:

Я не горжусь, но счастлив оттого, что выбрал в качестве основных ингредиентов первоклассные функции в стиле Scheme и прототипы (хотя и своеобразные) в стиле Self. Влияние Java, особенно в отношении ошибок, связанных с проблемой 2000 года, а также в отношении разграничения примитивов и объектов (например, string и String), было пагубным

С неоправданными ожиданиями трудно смириться. Если вы ожидаете увидеть статический язык корпоративного типа в стиле Java, а получаете язык, который имеет синтаксис, похожий на код Java, но похожий по поведению скорее на Scheme и Self, ваше удивление совершенно оправданно. Если вам нравятся динамические языки, это будет приятным сюрпризом; если же нет или такие языки вам просто незнакомы, то перспектива программирования в JavaScript может оказаться неприятной.

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


Шаблоны объектов JavaScript

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

Листинг 10. Объявление объекта
function Point(x, y) {
    this.x = x;
    this.y = y;
}

Затем вы можете создавать данный объект с использованием ключевого слова new подобно тому, как это делается в коде Java (см. листинг 11).

Листинг 11. Создание объекта
var p = new Point(3, 4);
p.x // => 3
p.y // => 4

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

Листинг 12. Добавление метода
Point.prototype.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y));
};

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

Листинг 13. Псевдоклассическая модель наследования
function Point3D(x, y, z) {
    this.x = x;
    this.y = y;
    this.z = z;
}

Point3D.prototype = new Point(); // наследование из Point

Point3D.prototype.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};

Несмотря на то, что этот способ, безусловно, является допустимым для определения объектов в JavaScript (и иногда даже может быть наилучшим), он представляется немного неуклюжим. Использование этого метода добавляет ненужные «помехи» в ваш код по сравнению с использованием прототипной модели и определением объектов исключительно в таком стиле. Напомним, что вы определяете свой объект с использованием литерала объекта, как показано в листинге 14.

Листинг 14. Определение объекта
var point = {
    x: 1,
    y: 2,
    r: function () {
        return Math.sqrt((this.x * this.x) + (this.y * this.y));
    }
};

Затем, как показано в листинге 15, вы реализуете наследование с использованием Object.create.

Листинг 15. Наследование с использованием Object.create
var point3D = Object.create(point);
point3D.z = 3;
point3D.r = function() {
    return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z));
};

Такой метод создания объектов в JavaScript выглядит естественным и подчеркивает преимущества прототипных объектов этого языка. Однако общим недостатком прототипной и псевдоклассической моделей является то, что они не обеспечивают конфиденциальности элементов. Иногда конфиденциальность не имеет значения, но порой она важна. В листинге 16 показан шаблон, который позволяет создавать объекты с частными элементами. В своей книге JavaScript: The Good Parts («JavaScript: способности») Дуглас Крокфорд (Douglas Crockford) называет такой шаблон моделью функционального наследования.

Листинг 16. Модель функционального наследования
var point = function(spec) {
    var that = {};

    that.getTimesSet = function() {
        return timesSet;
    };

    that.getX = function() {
        return spec.x;
    };

    that.setX = function(val) {
        spec.x = val;
    };

    that.getY = function() {
        return spec.y;
    };

    that.setY = function(val) {
        spec.y = val;
    };

    return that;
};

var point3D = function(spec) {
    var that = point(spec);

    that.getZ = function() {
        return spec.z;
    };

    that.setZ = function(val) {
        spec.z = val;
    };

    return that;
};

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

Листинг 17. Создание экземпляров
var p = point({ x: 3, y: 4 });
p.getX();  // => 3
p.setX(5);

var p2 = point3D({ x: 1, y: 4, z: 2 });
p.getZ();  // => 2
p.setZ(3);

Заключение

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

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

Ресурсы

Научиться

Получить продукты и технологии

  • Язык программирования Self: загрузите пакет Self, который включает язык программирования, коллекцию объектов, определенных на языке Self, а также среду программирования для написания программ Self, созданную на языке Self.
  • Внесите инновации в свой следующий проект разработки с помощью пробного программного обеспечения IBM.

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


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

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура
ArticleID=851819
ArticleTitle=Знакомство с прототипным объектно-ориентированным программированием
publish-date=12132012