- Способы создания пространств имен в JS
- Шаблон модуль
- Повторное использование кода. Классические и современные шаблоны наследования.
В JS нет специальной конструкции для опредления пространства имен. Для того, чтобы избежать конфликтов имен необходимо использовать глобальный объект.
! Антишаблон У нас куча глобальных переменных, которые станут свойствами объекта Window
var count = 1; // глобальная переменная
function Car(){} // глобальная переменная
function Bus(){} // глобальная переменная
var module = {} // глобальная переменная
module.arr = []
Лучшие практики рекомендуют делать 1 глобальную переменную, через которую определять все остальное. Пример такого подхода - $ в библиотеке jQuery - единственный глобальный объект, внутри которого скрыто все остальное.
var App = {}; // одна глобальная переменная
App.count = 1;
App.Car = function(){}
App.Bus = function(){}
App.module = {}
App.module.arr = []
Однако, var App = {};
небезопасный код. Сначала надо убедиться, что такого объекта еще нет. И если его действительно нет, то создать.
Безопасный код будет выглядеть так:
if ( typeof App === 'undefined' ) {
var App = {};
}
или просто
var App = App || {}
Посмотрим, как можно автоматизировать создание неймспесов в большом приложении. Пример из книги Стояна Стефанова:
var App = App || {};
App.define = function (namespace) {
var parts = namescpace.split('.'),
parent = App,
i;
// убрать начальный префикс, если это имя глобальной переменной
if ( parts[0] == 'App' ) {
parts = parts.slice(1);
}
// если в глобальном объекте нет свойства - создать его
for ( i = 0; i < parts.length; i++ ) {
if ( typeof parent[parts[i]] == 'undefined' ) {
parent[parts[i]] = {};
}
parent = parent[parts[i]];
}
return parent;
}
Создадим простарнство имен с помощью нашего метода App.define()
// Вариант с префиксом App
var module1 = App.define('App.utils.ajax');
console.log( module1 == App.utils.ajax ); // true
// Вариант без префикса App, но все равно рабочий
var module2 = App.define('utils.dom');
console.log( module2 == App.utils.dom ); // true
console.log(App);
Рассмотрим варианты создания открытых полей в объектах.
var obj1 = {
name: 'John Doe',
getName: function(){
return this.name;
}
}
console.log(obj1.name); // 'John Doe' - открытый член
console.log(obj1.getName); // 'function' - открытый член
В JS у нас нет специального ключевого слова, которое позволит создать закрытое поле. Чтобы создать закрытое поле в JS необходимо использовать конструктор. Локальные переменные, которые будут определяться внутри конструктора, будут закрытыми и не будут видны вне конструктора (будут ограничены локальной областью видимости конструктора).
function Obj2() {
// закрытый
var name = 'Ivan Ivanov';
// открытый
this.getName = function(){ // засчет того, что свойство name будет храниться в замыкании,
return name; // то даже после того, как функция отработает, оно будет доступно через метод this.getName();
}
}
var obj2 = new Obj2;
console.log(obj2.name); // 'undefined' - закрытый член
console.log(obj2.getName); // 'function' - открытый член
Другой способ инициализации закрытых полей - через литерал и немедленно вызываемую функцию (Immediately Invoked Function Expression):
var obj1;
(function(){
// закрытый член
var name = 'Ivan Ivanov';
// открытый член
obj1 = {
getName = function(){
return name;
}
}
})();
console.log(obj1.name); // 'undefined' - закрытый член
console.log(obj1.getName); // 'function' - открытый член
Более красивый вариант, а кроме того является основной частью шаблона Модуль, который будет рассмотрен далее:
var obj2 = (function(){
// закрытый член
var name = 'Ivan Ivanov';
// открытый член
return {
getName = function(){
return name;
}
}
})();
Шаблон Модуль позволяет структурировать и организовать программный код по мере увеличения его объема.
// Определяем пространство имен (см. метод define() выше),
// чтобы убедиться, что существует свойство calc в объекте utils
// и не затереть ранее кем-то созданный объект utils
App.define('utils.calc');
// Определение модуля
// Инициализируем объект, используя немедленно вызываемую фнкцию
App.utils.calc = (function(){
// закрытые члены
var x, x;
return {
// открытые члены
add: function(){
return x + y;
},
setX: function(val){
x = val;
},
setY: function(val){
y = val;
},
}
})();
Использование нашего модуля:
// Оборачиваем все в немедленно вызываемую ф-ию,
// чтобы не создавать лишних глобальных переменных
(function(){
var calc = App.utils.calc;
calc.setX(2);
calc.setY(3);
console.log(calc.add());
})();
Это другая разновидность шаблона Модуль.
App.utils.calc = (function(){
// закрытые члены
var x, y;
function _add() {
return x + y;
}
function _setX(val) {
x = val;
}
function _setY(val) {
y = val;
}
return {
// открытие доступа к опредленным методам
add: _add,
setX: _setX,
setY: _setY
}
})();
Преимущество данного подхода. Если кто-то возьмет и скажет, что App.utils.calc.setX = null
, то это никак не нарушит внутреннюю работу модуля, т.к. null присвоится открытому свойству setX, но не закрытому методу _setX, в результате чего метод _setX не затрется.
Это очень сложный шаблон, который взят из книги Стояна Стефанова. Больше подробностей можно найти, конечно, в его книге.
Суть шаблона заключается в том, что приложение разбито на массу модулей и этим модули мы подключаем динамически. Это еще больше сводит к минимуму сценарий, когда у нас в коде появятся одноименные модули (переменные), а если даже они и будут существовать, то не факт, что эти модули мы загрузим в приложение.
// Запускаем центральную функцию нашего приложения App
// и говорим, что сейчас будем пользоваться модулями ajax и dom.
// Соответствующая функциональность модулей ajax и dom копируется в объект box.
// Если в этих модулях будут одинаковые переменные, то, конечно, будет конфликт.
App('ajax', 'dom', (box){
box.sendRequest('request');
});
Также мы можем вызвать функцию App передав в нее одну коллбэк-функцию. При этом в переменную box попадут абсолютно все модули, которые есть в приложении.
App(function (box){
var e = box.getElement('div1');
box.addListener(e, 'click');
box.sendRequest('hello world');
alert(box.productName + ' ' + box.version);
});
Рассмотрим из чего состоит шаблон. Шаблон состоит из одного конструктора.
function App() {
// преобразовываем аргументы (псевдомассив arguments) в массив
var args = Array.prototype.slice.call(arguments),
// получаем доступ к последнему элементу массива,
// т.о. последний аргумент, переданный в конструктор, будет считаться коллбэк-функцией
callback = args.pop(),
// имена моделей могут передаться как один массив или через запятую
modules = (args[0] && typeof args[0] == 'string') ? args : args[0],
i;
// проверка, была ли функция вызвана с ключевым словом new
if ( !(this instanceof App) ) {
return new App(modules, callback);
}
// свойства
this.productName = 'Isolated Namesppac Sample',
this.version = '1.0.0.0';
// если в параметр modules передано значение * или моудли не укзаны -
// нужно подключить все доступные модули
if ( !modules || modules === '*' ) {
modules = [];
for (i in App.modules) {
// при использовании цикла for in всегда нужно проверять,
// принадлежит ли свойство проверяемому объекту
if ( App.modules.hasOwnPropety(i) ) {
modules.push(i);
}
}
}
// инициализация всех необходимых модулей
for ( i = 0; i < modules.length; i++ ) {
// каждый модуль представлен функцией, см. код ниже,
// мы вызываем функцию и передаем в нее текущий контекст
App.modules.[modules[i]](this);
}
// Вызываем коллбэк и брсаем в него контекст
// В контексте уже есть ряд библиотечных функций
// А коллбэк - это бизнес-логика, которую разработал программист,
// использующий нашу библиоткеу
callback(this)
}
И свойства modules, в котором должны будут находиться функции.
App.modules = {}
Далее объект App.modules начинаем инициализировать дополнительными свойствами. Каждое свойство представляет собой функцию (то бишь является методом), задача которой - к входящему параметру box, добавить какие-то свойства. Например, если вызвать метод App.modules.dom(obj), то у obj появятся все свойства App.modules.dom(). Изначально box пустой, а все модули используются в момент формирования объета box. Вся магия формирования box проихсодит внутри конструктора App.
//Модуль для работы с DOM
App.modules.dom = function(box){
box.getElement = function ( e ) {}
box.create = function ( e ) {}
box.getStyle = function () {}
box.setStyle = function () {}
};
// Модуль для обработки событий
App.modules.dom = function(box){
box.addListener = function (elem, event) {}
box.removeListener = function (elem, event) {}
};
// Модуль для отправки AJAX запросов
App.modules.dom = function(box){
box.sendRequest = function (data) {}
};
Деляться на 2 категории:
- Классические
- Современные
Классические шаблоны наследования называются так потому, что пытаются подражать принципу наследования таких языков как C++, C#. В этих языках, например, создается базовый класс и производные классы. В производные классы копируются все свойства и методы из базового класса. А также есть возможность сделать приведение производного класса к базовому типу (записать в переменную базового типа).
Но в JS нет такого понятия как типы данных (есть только 7 стандартных типов) и поэтому приведение к базовому (любому другому) типу невозможно и просто бессмысленно. Поэтому польза от наследования в JS - это просто возможность получить доступ к какому-то поведению, которое есть у родителя. Поэтому задача современных шаблонов наследования - просто скопировать поведения родителя.
Классическое наследование:
- Цепочка прототипов
- Заимствование конструкторов
- Заимствование конструктора и установка прототипа (комбинация 2-х предыдущих шаблонов)
- Совместное использование прототипа
- Временный конструктор
Современные шаблоны повторного использования кода:
- Наследование через прототип
- Наследование копированием свойств
- Смешивание
// родительский конструктор
function Parent(name) {
this.name = name || 'Adam';
}
// дополнительное поведение, добавленное в прототип
Parent.prototype.say = function() {
alert('Hello, my name is ' + this.name);
}
// пустой дочерний конструктор
function Child(name) {}
inherit(Child, Parent);
function inherit(C, P) {
// Самый простой способ реализовать наследование -
// создать объект с помощью родительского конструктора
// и присвоить его как прототип для дочернего конструктора.
// В прототипе C будет находиться экземпляр P.
C.prototype = new P();
}
var kid = new Child();
kid.name = 'Ivan';
kid.say(); // Hello, my name is Ivan
// Недостатки шаблона:
// Невозможно передать параметры от дочернего конструктора к родительскому
var kid = new Child('Ivan');
kid.say(); // Hello, my name is Adam - все дело в this
Недостаток этого шаблона: невозможно передать параметры от дочернего конструктора к родительскому.
// родительский конструктор
function Parent(name) {
this.name = name || 'Adam';
}
// дополнительное поведение, добавленное в прототип
Parent.prototype.say = function() {
alert('Hello, my name is ' + this.name);
}
// дочерний конструктор
function Child(name) {
// Если кто-то запустит конструктор Child, то у Child появится контекст,
// который мы передадим как контекст конструктора Parent.
// И мы просто перевызовем этот конструктор и скажем,
// чтобы Parent использовал те же самые аргументы, что и конструктор Child.
// Т.о. структура Child ничем не будет отличаться от структуры Parent.
Parent.apply(this.arguments);
}
var kid = new Child('Ivan');
alert(kid.name); // Ivan
kid.say(); // Ошибка! Прототипы конструкторов не копируются, у Child остался свой протjтип, где нет метода say()
Как видно мы решили проблему с контекстом и теперь параметры от дочернего конструктора к родительскому передаются, но породили новую с невозможностю копирования свойств прототипа от родителя. Чтобы решить это, обычно комбинируют эти 2 подхода.
// родительский конструктор
function Parent(name) {
this.name = name || 'Adam';
}
// дополнительное поведение, добавленное в прототип
Parent.prototype.say = function() {
alert('Hello, my name is ' + this.name);
}
// дочерний конструктор
function Child(name) {
Parent.apply(this.arguments);
}
// Добавляем в прототип Child новый Parent
Child.prototype = new Parent();
var kid = new Child('Ivan');
alert(kid.name); // Ivan
kid.say(); // Hello, my name is Ivan
Однако, этот шаблон также не лишен недостатоков: у нас дважды вызывается конструктор Parent(), что снижает эффективность шаблона.
// родительский конструктор
function Parent(name) {
this.name = name || 'Adam';
}
// дополнительное поведение, добавленное в прототип
Parent.prototype.say = function() {
alert('Hello, my name is ' + this.name);
}
// пустой дочерний конструктор
function Child(name) {}
inherit(Child, Parent);
function inherit(C, P) {
// Вместо того, чтобы повторно вызывать конструктор для опредления прототипа,
// мы просто ссылаемся на уже существующий прототип Parent
C.prototype = P.prototype;
}
var kid = new Child();
kid.name = 'Ivan';
kid.say(); // Hello, my name is Ivan
Недостаток шаблона: если в протоитип Child добавится новый метод, он появится и у Paretn, т.к. прототип общий. Чтобы обеспечить безопасность прототипа Parent мы можем использовать следующий шаблон.
// родительский конструктор
function Parent(name) {
this.name = name || 'Adam';
}
// дополнительное поведение, добавленное в прототип
Parent.prototype.say = function() {
alert('Hello, my name is ' + this.name);
}
// пустой дочерний конструктор
function Child(name) {}
inherit(Child, Parent);
function inherit(C, P) {
var F = function(){}; // новый пустой конструктор F
F.prototype = P.prototype; // Прототипом конструктора F делаем экземпляр родителя
C.prototype = new F(); // Прототипом Child делаем экземпляр конструктора F
// Если Child начнет менять свой прототип, то он будет менять новый объект, а не родителя.
// Если Child начнет вызывать методы, которых не существует в новом объекте, то он будет их искать в прототипе Parent
}
var kid = new Child();
kid.name = 'Ivan';
kid.say(); // Hello, my name is Ivan
Как уже было сказано, суть современного наследования - скопировать методы из одного объекта в другой, но при этом не заморачиваться с конструкторами.
function createObj(obj) {
function F(){};
F.prototype = obj;
return new F();
}
var base = {
name: 'John'
}
var derived = createObj(base);
alert(derived.name); // John
alert(derived.hasOwnProperty('name')); // false - оказывается сво-во name не принадлежит derived, а берется из прототипа
Т.к. это достаточно простой и удобный в применении вариант наследования, то он был сделан частью спецификации ECMA 5. Поэтому, чтобы сегодня воспользоваться таким наследованием, не обязательно создавать свою функцию. Нужно воспользоваться методом create, встроенным в Object.
var base = {
name: 'John'
}
var derived = Object.create(base);
alert(derived.name); // John
// Метод create() принимает 2 параметра:
// 1) объект, сво-ва к-рого необходимо наследовать
// 2) доп. св-ва для нового объекта
var derived2 = Object.create(base, {
age: {value: 30}
});
alert(derived2.name); // John
alert(derived2.age); // 30
alert(derived.hasOwnProperty('name')); // fale - сво-во name берется из прототипа
alert(derived.hasOwnProperty('age')); // true - сво-во age берется из самого объекта
Расширение одного объекта свойствами другого объекта
function extend(parent, child) {
var i;
child = child || {}; // проверка наличия 2-го аргумента
for (i in parent) {
// Если св-во является св-вом объекта parent, то копируем его.
// В итоге получаем объект, к-рый полностью соответствует структуре parent
if ( parent.haOwnProperty(i) ) {
child[i] = parent[i];
}
}
return child;
}
var base = {
id: 0,
name: 'base object'
}
var child = {};
extend(base, child);
// Или без 2-го параметра
// var child = extend(base);
console.log(child.id); // 0
console.log(child.name); // base object
Подобная функция extend() существует во многих библиотеках, например, jQuery.
Рассмотрим пример, когда могут возникнуть проблемы при использовании extend().
function extend(parent, child) {
// Код идентичен предыдущему примеру
}
// Есть отличия в объекте base
var base = {
id: 0,
name: 'base object',
innerArray: [1, 2, 3] // при использовании extend() массив будет передан по ссылке
}
var child = {};
extend(base, child);
child.innerArray.push(4);
condole.log( child.innerArray == base.innerArray ); // true - массив и в child и в base - одна и та же ссылка
condole.log( base.innerArray ); // [1, 2, 3, 4]
condole.log( child.innerArray ); // [1, 2, 3, 4]
Надо учитывать, что в JS есть ссылочные типы данных. Ссылочные типы - это объекты (массивы - тоже объекты). При использовании оператора присовения происходит просто копирование ссылки из одной переменной в другую переменную. Но мы не производим копирование непосредственно данных объекта. Поэтому возникает такая ситуация, как в коде выше, когда мы в extend() просто скопировали ссылку. Чтобы избавиться от этой проблемы, мы можем воспользоваться подходом "глубокое клонирование".
function extendDeep(parent, child) {
var i,
toString = Object.prototype.toString,
aStr = '[object Array]';
child = child || {}; // проверка наличия 2-го аргумента
for (i in parent) {
if ( parent.haOwnProperty(i) ) {
// Если тип родителя == object, мы наткнулись на ссылочный тип данных
if ( typeof parent[i] == 'object' ) {
// Но массивы тоже объекты, поэтому нужно уточнить с чем мы имеем дело
child[i] = ( toString.call(parent[i]) == aStr ) ? [] : {};
// Рекурсивно вызываем extendDeep() для того, чтобы скопировать все свойства объекта или массива
extendDeep( parent[i], child[i] );
} else {
child[i] = parent[i];
}
}
}
}
var base = {
id: 0,
name: 'base object',
innerArray: [1, 2, 3]
}
var child = {};
extend(base, child);
child.innerArray.push(4);
condole.log( child.innerArray == base.innerArray ); // false - массив в child и в base - разные ссылки
condole.log( base.innerArray ); // [1, 2, 3]
condole.log( child.innerArray ); // [1, 2, 3, 4]
Берем несколько объектов с разными св-вами и объединяем эти объекты в один лезуртирующий объект.
function mix() {
var arg, prop, child = {};
// Обходим в цикле все переданные в ф-ию объекты
for (arg = 0; arg < arguments.length; arg++) {
// Перебираем все св-ва каждого объекта
for (prop in arguments[arg]) {
// Проверяем, чтобы св-ва принадлежали именно объекту, а не его прототипу
if (arguments[arg].hasOwnProperty(prop)) {
// Все найденные св-ва складываем в один объект
child[prop] = arguments[arg][prop];
}
}
}
return child;
}
var ingredient1 = { eggs: 2 };
var ingredient2 = { butter: 1, salted: true };
var ingredient3 = { flour: '3 cups' };
var ingredient4 = { sugared: true };
var cake = mix(ingredient1, ingredient2, ingredient3, ingredient4);
console.log(cake);
Привет, у вас ошибка в 4 примере, исправьте 3 строчку.
У вас написано :
var parts = namescpace.split('.'),
Правильно :
var parts = namespace.split('.'),
Одну букву недосмотрели, а так спасибо за крутой материал:)