Skip to content

Instantly share code, notes, and snippets.

@GoodDayTodayOkey
Created December 14, 2017 04:12
Show Gist options
  • Save GoodDayTodayOkey/d266f725982d42be548ef68d3764b7d2 to your computer and use it in GitHub Desktop.
Save GoodDayTodayOkey/d266f725982d42be548ef68d3764b7d2 to your computer and use it in GitHub Desktop.
JS Шаблоны. Шаблоны создания объектов и повторное использование кода

JS Шаблоны. Шаблоны создания объектов и повторное использование кода

Содержание

  1. Способы создания пространств имен в JS
  2. Шаблон модуль
  3. Повторное использование кода. Классические и современные шаблоны наследования.

Пространства имен в 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);

Открытые (public) и закрытые (private) поля

Рассмотрим варианты создания открытых полей в объектах.

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 не затрется.

Шаблон "Изолированный неймспейс" (Isolated namespace)

Это очень сложный шаблон, который взят из книги Стояна Стефанова. Больше подробностей можно найти, конечно, в его книге.

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

// Запускаем центральную функцию нашего приложения 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.

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

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);
@Viktor727
Copy link

Привет, у вас ошибка в 4 примере, исправьте 3 строчку.
У вас написано :
var parts = namescpace.split('.'),
Правильно :
var parts = namespace.split('.'),

Одну букву недосмотрели, а так спасибо за крутой материал:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment