Реализация «классов» в JavaScript

в 1:18, , рубрики: AtomJS, class, javascript, MooTools, Песочница, метки: , , ,

Привет всем. JavaScript это весьма гибкий язык, но так получилось что классов в этом языке нет. Да, в ECMAScript 6 появятся классы, но еще не скоро наступят те времена, когда большинство пользователей будут использовать браузер с этой версией языка. А пока программисты на JavaScript используют различные фреймворки (MooTools, AtomJS и другие) для создания «классов». Прочитав эту статью вы узнаете: как устроены выше упомянутые фреймворки, и как самим на чистом JavaScript можно реализовать классы несколькими способами.

Начнем с простого. Это то, что придет на ум новичку в JavaScript сразу-же, как перед ним встанет задача написать «класс»:

function myClass(){
    this.message = "Hello!";
    this.alert = function(){
        alert(this.message);
    };
}

Конструкция вполне работоспособна, но этого мало. Вот как можно ее дополнить:

1. Контекст this

Переменная this является контекстной, и она не всегда является тем что нужно, поэтому сделаем следующее:

function myClass(){
    var _this = this;      

    _this.message = "Hello!";
    _this.alert = function(){
        alert(_this.message);
    };
}

2. Конструктор

function myClass(){
    var _this = this;

    function _constructor(message){
       _this.message = (message) ? message : "";
    };

    _this.alert = function(){
        alert(_this.message);
    };

    _constructor.apply(this,arguments);
}

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

3. Приватные переменные, геттеры и сеттеры

Приватные переменные можно объявлять так: или классически объявлять каждую переменную по-отдельности (var a,b,c,d…), или объявляем один объект и все переменные записываем в него. Я предпочитаю второй способ.
Про геттеры и сеттеры можно почитать в этой замечательной статье. Создание геттеров и сеттеров можно сократить способом, который показан в следующем примере:

function myClass(){
    var _this = this, _pv = {}, _gt = {}, _st = {};

    function _constructor(message){
        _this.message = (message) ? message : "";
        _pv.test = "private var for demo";
    };

    _this.alert = function(){
        alert(_this.message);
    };

    _gt.test = function(){
        return "getter returns: "+_pv.test;
    };
 
    _st.test = function(val){
        _pv.test = val;
    };
      
    for(var i in _gt) if(_gt.hasOwnProperty(i)) _this.__defineGetter__(i,_gt[i]);
    for(var i in _st) if(_st.hasOwnProperty(i)) _this.__defineSetter__(i,_st[i]);
    _constructor.apply(this,arguments);
}

4. Самовызывающийся конструктор

Это позволит создавать экземпляр класса без оператора new. Про самовызывающийся конструктор можно прочитать тут.
Для класса с произвольными аргументами конструктора код будет выглядеть так:

function myClass(){
    if(!(this instanceof myClass)) return new myClass({"@arguments":arguments});
    var _this = this, _pv = {}, _gt = {}, _st = {};

    function _constructor(message){
       _this.message = (message) ? message : "";
       _pv.test = "private var for demo";
    };

    _this.alert = function(){
        alert(_this.message);
    };

    _gt.test = function(){
        return "getter returns: "+_pv.test;
    };
 
    _st.test = function(val){
        _pv.test = val;
    };
      
    for(var i in _gt) if(_gt.hasOwnProperty(i)) _this.__defineGetter__(i,_gt[i]);
    for(var i in _st) if(_st.hasOwnProperty(i)) _this.__defineSetter__(i,_st[i]);
    _constructor.apply(this, ( arguments.hasOwnProperty("@arguments") ) ? arguments["@arguments"] : arguments );
}

Для определенных аргументов делаем следующие изменения:

function myClass(message){
    if(!(this instanceof myClass)) return new myClass(message);
    ...
    _constructor.apply(this,arguments); // или _constructor.call(this,message); или вовсе _constructor(message);
}

5. Параметры свойств

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

6. Скорость

И так, мы получили вполне хорошую реализацию классов, но есть одно но: это подойдет только тогда, когда вы будите пользоваться экземпляром, а не постоянно конструировать объект. То есть такой объект как jQuery таким способом собирать не стоит — это медленно. Метод сборки таких объектов я тоже сейчас объясню.

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

function $(selector){
    var elements = document.querySelectorAll(selector);
    elements.__proto__ = $.fn;
    return elements;
}
  
$.fn = {
    html : function(html){
        if(!html) return this[0].innerHTML;
        else for(var i in this) this[i].innerHTML = html;
    }
};

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

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

function myClass(message){
    var obj = { message : (message) ? message : "", test : "private var for demo" }; // от приватных переменных придется избавиться 
    obj.__proto__ = myClass.methods;
    return obj;
}

(function(){ // ну или сплошным объектом как в примере с jQuery, но тогда без геттеров и сеттеров
    var _this = myClass.methods, _gt = {}, _st = {};

    _this.alert = function(){
        alert(this.message);
    };

    _gt.test = function(){
        return "getter returns: "+this.test;
    };
 
    _st.test = function(val){
        this.test = val;
    };
      
    for(var i in _gt) if(_gt.hasOwnProperty(i)) _this.__defineGetter__(i,_gt[i]);
    for(var i in _st) if(_st.hasOwnProperty(i)) _this.__defineSetter__(i,_st[i]);
    // для наследования: _this.__proto__ = myAnotherClass.methods;
})();

Для наследования хочу отметить одну вещь:

var a = {}, b = { num : 123 };
a.__proto__ = b; // a.num == b.num == 123;
a.num = 321; // a.num == 321; a.__proto__.num == b.num == 123;

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

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

Автор: dangreen

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js