К основному контенту

Объектно-ориентированное программирование в JavaScript, NoPrototype стиль [Object-oriented programming in JavaScript, NoPrototype style]


Большое количество статей присутствует в бескрайнем интернете об объектно-ориентированном программирование на JavaScript. Но мне как C# программисту, привыкшему к его ООП синтаксису и количеству возможностей, которые он дает, не одна из них не подходит. Почему же?

Задача
Что действительно нужно от ООП в JavaScript:
  • Инкапсуляция
  • Наследование
  • Полиморфизм
А теперь, по-порядку.

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

Класс
Единственное, что нам нужно, так это выработать единый стиль написания класса в JavaScript.
Приведем пример класса на C#:
public class Book
    {
        // конструктор
        public Book(string newName)
        {
            name = newName;
        }
        // приватное поле
        private int id;
        // приватное поле
        private string name;
        // публичное поле
        public string Description;
        // публичное свойство
        public string Name
        {
            // getter
            get { return name; }
            // setter
            set { name = value; }
        }
        // приватный метод
        private void incrementId()
        {
            id++;
        }
        // публичный метод
        public void Buy()
        {
            Description += " (куплено)";
        }
    }
А теперь тот же класс только на JavaScript:
var Book = function (newName) {
    var self = this;

    // приватное поле
    var id = null;
    // приватное поле
    var name = null;
    // публичное поле
    self.Description = null;
    // приватный метод
    var incrementId = function () {
        id++;
    };
    // публичное свойство
    self.Name = function (value) {
        if (value === undefined) {
            // getter
            return name;
        } else {
            // setter
            name = value;
        }
    };
    // публичный метод
    self.Buy = function () {
        self.Description += " (куплено)";
    };

    // конструктор
    var constructor = function () {
        id = 0;
        name = newName;
    };
    constructor();
};
Очень важно соблюдать простые правила:
  • this self. Использовать self вместо this. Потому что: this можно подменить, и если скрипт будет сжиматься, то ключевые слова не сжимаются.
  • Соблюдать порядок написания элементов:
  1. Приватные поля
  2. Публичные поля
  3. Приватные методы и приватные свойства. Порядок среди все приватных методов/свойств зависит от использованных внутри других приватных методов/свойств. Например, если метод A() использует метод B() и C(), то порядок написания методов будет: B, C, A.
  4. Публичные методы и публичные свойства. Порядок среди все публичных методов/свойств любой.
  5. Конструктор

Инкапсуляция
В рассмотренном выше примере мы использовали всего 2 модификатора доступа:
  • private. Описание элемента начинается с "var ". Доступ к элементу осуществляется по имени элемента. Например: если мы опишем приватное поле  var name = 'Some book'; то чтобы присвоить новое значение пишем name = 'New book';
  • public. Описание элемента начинается с "self.". Доступ к элементу осуществляется по "self." + имя элемента. Например: описание публичного поля self.Description = null; и его использование self.Description = 'New description';
Приведу таблицу соответствия модификаторов доступа C# и JavaScript:
C#JavaScript
publicpublic
privateprivate
protectedpublic
internalpublic

Наследование
Для реализации наследования и полиморфизма воспользуемся следующим кодом:
function inherit(derivedInstance, baseClass, args) {
                var args = (args === undefined) ? [] : args;
                baseClass.apply(derivedInstance, args);
                var base = {};
                for (var methodName in derivedInstance) (function() {
                    var method = derivedInstance[methodName];
                    if (typeof (method) == "function") {
                        base[methodName] = function() {
                            return method.apply(derivedInstance, arguments);
                        };
                    }
                })();
                return base;
            }

Наследование реализовывается глобальной функцией inherit.
Аргументы функции inherit:
  • derivedInstance - ссылка на объект, который будет наследоваться от класса baseClass
  • baseClass - ссылка на функцию, которая описывает базовый класс, именно от этого базового класса будет осуществляться наследование
  • args - массив аргументов конструктора базового класса, то есть аргументы, необходимые для создания объекта класса baseClass
  • возвращаемое значение - это ссылка на объект, содержащий список базовых методов. Это необходимо для полиморфизма, об этом далее.
Ну и пример (выполнить):
var Car = function () {
    var self = this;

    self.Drive = function () {
        alert("Поехали!");
    };
};

var Toyota = function () {
    var base = inherit(this, Car);
    var self = this;
};

var ToyotaPrado = function () {
    var base = inherit(this, Toyota);
    var self = this;
};

var myPrado = new ToyotaPrado();
myPrado.Drive();


Полиморфизм
Для осуществления полиморфизма воспользуемся так же глобальной функцией inherit.
За неимением в JavaScript чего либо подобного virtual и override, нам опять же необходимо придерживаться правилу:
  • Все виртуальные свойства/методы должны быть публичными, иначе мы никак не сможем переопределить их. Иными словами все публичные свойства/методы класса виртуальны.
Все просто, смотрим пример (выполнить):
var Car = function (motor) {
    var self = this;

    self.Drive = function () {
        alert("Включаем " + motor + " мотор. Поехали!");
    };
};

var Toyota = function (motor) {
    var base = inherit(this, Car, [motor]);
    var self = this;

    self.Drive = function () {
        base.Drive();
        alert("Причем на Toyota.");
    };
};

var ToyotaPrado = function (model) {
    var base = inherit(this, Toyota, ["бензиновый 4,0 л"]);
    var self = this;

    self.Drive = function () {
        base.Drive();
        alert("А теперь наш Prado " + model + " газ в пол!");
    };
};

var myToyota = new Toyota("неизвестный");
var myPrado = new ToyotaPrado("Premium");

alert("Запуск метод: myToyota.Drive()");
myToyota.Drive();

alert("Запуск метод: myPrado.Drive()");
myPrado.Drive();


Один из достоинств NoPrototype стиля - нативная поддержка браузером (если закрыть глаза на метод inherit) или отсутствие зависимости от фреймфорка (JQuery и т.д.), а также реализация инкапсуляции.

Комментарии

  1. как я понимаю, смотря на дату выпуска статий и по отсутствию коментов этим методом не кто не восхетился. а вроде у этого метода есть какойта шарм.

    ОтветитьУдалить
  2. Не так просто наткнуться на эту статью в интернете, где все завалено прототипами. Год назад начал писать на JS, сам перешел из C#, очень не хватало нормального ООП. Частично пришел к описанным в статье методам. А сейчас вот случайно увидел эту публикацию. Автор, большой Вам респект и спасибо за статью! Идея классная, обязательно возьму на вооружение.

    P.S.: картинка тоже очень порадовала :)))

    ОтветитьУдалить
  3. Пользуюсь этим методом, с прототипами что-то не хочется возиться.

    ОтветитьУдалить
  4. Этот комментарий был удален администратором блога.

    ОтветитьУдалить
  5. Вы вводите новичков в JavaScript в большое заблуждение!!!

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

    Во-вторых,
    alert(myPrado instanceof Toyota); //Выдаст false
    alert(myPrado instanceof Car); //Выдаст false

    В общем, не дурите новичков в JavaScript!!!

    Использование же прототипа:

    function Car()
    {
    this._self = 'Car.';
    this.drive = function()
    {
    document.writeln("Вызван Car.drive() from: " + this._self + "
    ");
    }
    }

    function Audi()
    {
    //Car.call(this, name); //Только в случае, если надо передать переменные создателю (constructor) superclass'а. К примеру: Car(year)
    this._self = 'Audi.';
    this.stop = function()
    {
    document.writeln("Audi.stop()
    ");
    }
    }
    Audi.prototype = new Car;

    function Opel()
    {
    this._self = 'Opel.';
    this.stop = function()
    {
    document.writeln("Opel.stop()
    ");
    }
    }
    Opel.prototype = new Car;

    function BMW()
    {
    this._self = 'BMW.';
    this.stop = function()
    {
    document.writeln("BMW.stop()
    ");
    }
    }
    BMW.prototype = new Car;

    var cars = [new Audi(), new Opel(), new BMW()];

    for(key in cars)
    {
    // Убеждаемся что это Car
    if(cars[key] instanceof Car)
    {
    cars[key].drive();
    cars[key].stop();
    }
    }

    ОтветитьУдалить
  6. Наследование - механизм языка, позволяющий описать новый класс на основе уже существующего (родительского, базового) класса (Википедия). NoPrototype подход вполне подходит под описание.
    Да, наследование используя прототипы немного быстрее работает (сравнение приведено в следующей статье), и использует меньше памяти, НО код становится менее читабельным.
    Еще, попробуете реализовать инкапсуляцию наследуя прототипами. Увы не получится.
    В добавок, override (переопределение) метода класса становится очень сложным, и опять же не читабельным, к то мы же нужно иметь ссылку на прототип базового класса.
    В примере выше, не показана перегрузка метода базового класса, а зря. Так же, нет никакой инкапсуляции. Внешний код может легко повредить логику класса.
    Никакого обмана, один из методов реализации ООП.
    Для меня NoPrototype подход имеет больше достоинств, чем недостатков.
    Спасибо за критику!

    ОтветитьУдалить

Отправить комментарий