Xiper

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

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

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

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

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

Нами будет рассмотрены различные способы реализации наследования в 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();
}

В действии:

Как видим из примера, объект 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");
    }
}

В действии:

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

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

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

Классический шаблон №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();
}

В действии:

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

Многие 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;
};

В действии:

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

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

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

// объект, который наследуется 
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

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

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

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

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; 
} 

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