Javascript. Шаблоны проектирования. Повторное использование программного кода.

Автор: Валерий Съестов Дата публикации: 10.03.2014

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

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

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

Нами будет рассмотрены различные способы реализации наследования в JavaScript и такие понятия, как классическое наследование и не классическое. Под классическим наследованием подразумевается ситуация, что бы объекты создаваемые функцией-конструктором Child(), приобретали свойства, присущие другому конструктору Parent().

Приведем пример реализации конструкторов Parent() и Child():

function Parent(name) {
    this.name = name || "Adam";
}

Parent.prototype.say = function () {
    return this.name;
};

function Child(name) {}

inherit(Child, Parent);

Здесь у нас имеются родительский и дочерний конструкторы, метод say(), добавленый в прототип родительского конструктора, и вызов функции inherit(), которая устанавливает зависимость наследования.

Классический шаблон №1: Шаблон по умолчанию

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

Делается єто следующим образом:

function inherit(C, P) {
    C.prototype = new P();
}

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

var child = new Child();
child.say(); // "Adam"

Таким образом, экземпляр объекта child, будет иметь свойства и методы добавленные через прототип объекта Parent().

Достоинства и недостатки

Достоинствами этого шаблона есть то, что объект Child() получает все свойства и методы объекта Parent(), и простота реализации.

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

Пример:

var button = document.getElementById("button");

button.addEventListener("click", function () {
    test();
}, false);


function inherit(C, P) {
    C.prototype = new P();
}

/**
 * Phone
 */
function Phone(name, size) {
    this.name = name || "Phone";
    this.size = size || {
        w: 50,
        h: 50
    };
}

Phone.prototype.getName = function () {
    alert(this.name);
};

Phone.prototype.getSize = function () {
    var size = this.size;
    alert("width: " +size.w + "; height: " + size.h);
};

/**
 * Nokia
 */
function Nokia(name, size) {
    if (name) {
        this.name = name;
    }
    if (size) {
        this.size = size;
    }
};

function test() {
    
    inherit(Nokia, Phone);

    var nokia_1 = new Nokia(),
        nokia_2 = new Nokia("Nokia 6600", {w: 100, h: 200});

    nokia_1.getName();
    nokia_1.getSize();
    nokia_2.getName();
    nokia_2.getSize();
    
}

В действии:
http://jsfiddle.net/valsie/G8uWG/4/

Как видим из примера, объект Child() унаследовал методы и свойства объекта Parent().

Классический шаблон №2: Заимствование конструктора

В этом шаблоне исключается недостаток классического шаблона №1, пердача параметров через дочерний объект к родительскому. В этом шаблоне выполняется связывание дочернего объекта со ссылкой this.

function Child(a, b, c, d) {
    Parent.apply(this, arguments);
}

При таком подходе будут наследоваться только свойства добавленные внутри конструктора, а свойства прототипа - не наследуются.

Рассмотрим следующий пример:

var button = document.getElementById("button");

button.addEventListener("click", function () {
    test();
}, false);


/**
 * Phone
 */
function Phone(name, size) {
    this.name = name || "Phone";
    this.size = size || {
        w: 50,
        h: 50
    };
    
    this.showNameAndSize = function () {
        var size = this.size;
        alert(name + " - width: " + size.w + ", height: " + size.h);
    };
}

Phone.prototype.getName = function () {
    alert(this.name);
};

Phone.prototype.getSize = function () {
    var size = this.size;
    alert("width: " +size.w + "; height: " + size.h);
};

/**
 * Nokia
 */
function Nokia(name, size) {
    Phone.apply(this, arguments);
};

function test() {
    
    var nokia_1 = new Nokia(),
        nokia_2 = new Nokia("Nokia 6600", {w: 100, h: 200});

    if (nokia_1.hasOwnProperty("getName")) {
        nokia_1.getName();
    } else {
        console.log("getName is undefined");
    }
    
    if (nokia_1.hasOwnProperty("getSize")) {
        nokia_1.getSize();
    } else {
        console.log("getSize is undefined");
    }
    
    if (nokia_1.hasOwnProperty("showNameAndSize")) {
        nokia_1.showNameAndSize();
    } else {
        console.log("showNameAndSize is undefined");
    }
    
    if (nokia_2.hasOwnProperty("getName")) {
        nokia_2.getName();
    } else {
        console.log("getName is undefined");
    }
    
    if (nokia_2.hasOwnProperty("getSize")) {
        nokia_2.getSize();
    } else {
        console.log("getSize is undefined");
    }
    
    if (nokia_2.hasOwnProperty("showNameAndSize")) {
        nokia_2.showNameAndSize();
    } else {
        console.log("showNameAndSize is undefined");
    }
}

В действии:
http://jsfiddle.net/valsie/nnhQ2/1/

Достоинства и недостатки

Недостаток этого шаблона заключается в том, что он не обеспечивает наследования свойств прототипа.

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

Классический шаблон №3: Заимствование и установка прототипа

Этот шаблон направлен на совершенствование предыдущего, а именно добавить возможность наследовать функции прототипа

function Child(a, b, c, d) {
    Parent.apply(this, arguments);
}

Child.prototype = new Parent();

Преимущество - дочерний объект получается копии собственных членов родителя и ссылку на функции прототипа.

Недостатки - необходимость дважды вызывать родительский конструктор, что снижает эффективность, так как некоторые свойства наследуются дважды.

Классический шаблон №4: Совместное использование прототипа

function inherit(C, P) {
    C.prototype = P.prototype;
}

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

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

Классический шаблон №5: Временный конструктор

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

function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
}

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

Для расширения возможностей фукнции inherit, можно добавить сохранение ссылка на класс родителя.

function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.superClass = P.prototype;
}

Последнее что можно сделать, что бы эта функция была приближенная к идеалу, это исключить постоянное создание пустой функции F при обращении к функции inherit.

var inherit = (function () {
    var F = function () {};
    return function (C, P) {
        F.prototype = P.prototype;
        C.prototype = new F();
        C.superClass = P.prototype;
    };
}());

Пример:

var button = document.getElementById("button");

button.addEventListener("click", function () {
    test();
}, false);


var inherit = (function () {
    var F = function () {};
    return function (C, P) {
        F.prototype = P.prototype;
        C.prototype = new F();
        C.superClass = P.prototype;
    };
}());


/**
 * Phone
 */
function Phone() {}

Phone.prototype = {
    
    init: function (name, size) {
        
        this.name = name || "Phone";
        this.size = size || {
            w: 50,
            h: 50
        };
    },
    
    getName: function () {
        alert(this.name);
    },
    
    getSize: function () {
        var size = this.size;
        alert("width: " +size.w + "; height: " + size.h);
    }
};

/**
 * Nokia
 */
function Nokia() {
    this.name = null;
    this.size = null;
};

function test() {
    
    inherit(Nokia, Phone);
    
    var nokia_1 = new Nokia(),
        nokia_2 = new Nokia();

    nokia_1.init();
    nokia_2.init("Nokia 6600", {w: 100, h: 200});
        
    nokia_1.getName();
    nokia_1.getSize();
    nokia_2.getName();
    nokia_2.getSize();
}

В действии:
http://jsfiddle.net/valsie/LVH5F/

Шаблон имитации класса

Многие JavaScript библиотеки пытаются эмитировать классы, но все реализации имеют схожие черты:

  • Присутствует метод который считается конструктором, и вызывается автоматически
  • Присутствует наследование одних классов другими
  • Присутствует доступ к родительскому классу (суперклассу) из дочернего.

Рассмотрим следующую конструкцию описания класса:

var Phone = jClass(null, {
    init: function (variable) {
        console.log("constructor");
        this.name = variable;
    },
    getName: function () {
        return this.name;
    }
});

Эта функция выполняет объявление класса и принимает два параметра, первый - класс родитель, второй - свойства и методы создаваемого класса. В данном примере, в качестве функции конструктора, была выбрана функция init.

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

Расширим этот класс и создадим новый:

var Nokia = jClass(Phone, {
    init: function (variable) {
        console.log("Nokia constructor");
    },
    getName: function () {
        var name = Nokia.superClass.getName.call(this);
        return "I am " + name;
    }
});

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

Перейдем к реализации функции jClass:

var jClass = function (Parent, properties) {
    var Child, F, i;

    // 1. Новый конструктор
    Child = function () {
        if (Child.superClass && child.superClass.hasOwnProperty("init")) {
            Child.superClass.init.apply(this, arguments);
        }

        if (Child.prototype.hasOwnProperty("init")) {
            Child.prototype.init.apply(this, arguments);
        }
    }

    // 2. Наследование

    Parent = Parent || Object;
    F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.superClass = Parent.prototype;
    Child.prototype.constructor = Child;

    // 3. Добавить реализацию методов

    for (i in properties) {
        if (properties.hasOwnProperty(i)) {
            Child.prototype[i] = properties[i];
        }
    }

    return Child;
};

В действии:
http://jsfiddle.net/valsie/pK58m/6/

Наследование через прототип

Этот шаблон не имеет никакого отношения к классами, здесь одни объекты наследуют другие.

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


// объект, который наследуется 
var parent = { 
    name: "Papa" 
}; 
// новый объект 
var child = object(parent); 
// проверка 
alert(child.name); // "Papa" 

Реализация функции object имеет следующий вид:

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

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

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

Однако нужно учитываться, что в последнем случае унаследованы будут не только свойства прототипа конструктора, но и “собственные” свойства объекта:

// родительский конструктор 
function PersonO { 
    // "собственное" свойство 
    this.name = "Adam"; 
} 
// свойство, добавляемое в прототип 
Person.prototype.getName = function () { 
    return this.name; 
}; 
// создать новый объект типа Person 
var papa = new PersonO; 
// наследник 
var kid = object(papa); 
// убедиться, что было унаследовано не только 
// свойство прототипа, но и собственное свойство 
kid.getName(); // "Adam" 

Дополнения в стандарте ECMAScript5

В стандарте ECMAScript 5 шаблон наследования черзе протип стал официальной частью языка. Этот шаблон реализован в виде метода Object.create(). Другими словами, вам не потребуется создавать собственную функцию, похожую на object(); она уже будет встроена в язык: var child = Object.create(parent);

Метод Object.create() принимает дополнительный параметр - объект. Свойства этого объекта будут добавлены во вновь созданный дочерний объект как собственные свойства. Это позволяет создавать дочерние объекты и определять отноешния наследования единственным вызовом метода.

Например:

var child = Object.create(parent, {
    age : {
        value: 2
    }
});

child.hasOwnPorperty("age"); // true

Наследование через прототип в действии:
http://jsfiddle.net/valsie/ZrLA7/

Наследование копированием свойств

Рассмотрим другой способ наследования - наследование копированием свойств. В этом шаблоне один объект получает доступ к функциональности другого объекта за счет простого копирования свойств. Ниже пример реализации:

function extend(parent, child) { 
    var i; 
    child - child || {}; 
    for (i in parent) { 
        if (parent.hasOwnProperty(i)) { 
            child[i] = parent[i]; 
        } 
    }
    return child; 
} 

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

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

function extendDeep(parent, child) { 
    var i, 
          toStr = Object.prototype.toString, 
          astr = "[object Array]"; 

    child = child || {}; 
    for (i in parent) { 
        if (parent.hasOwnProperty(i)) { 
            if (typeof parent[i] === "object") { 
                child[i] = (toStr.call(parent[i]) === astr) ? [] : {}; 
                extendDeep(parent[i], child[i]); 
            } else { 
                child[i] = parent[i]; 
            } 
        }
    } 
    return child; 
} 

Теперь, с помощью этой функции можно выполнить полное копирование свойств объекта.