Skip to content

Instantly share code, notes, and snippets.

@ncer
Last active December 5, 2023 05:45
Show Gist options
  • Save ncer/bcf88c61d1035695b4f88eaeafde6a1b to your computer and use it in GitHub Desktop.
Save ncer/bcf88c61d1035695b4f88eaeafde6a1b to your computer and use it in GitHub Desktop.
JS Шаблоны

JS Шаблоны 1. Литералы и конструкторы.

Содержание

  • Литералы объектов
  • Пользовательские конструкторы
  • Литералы массивов
  • Работа с простыми типами данных

Шаблон - это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто повторяющегося контекста. https://en.wikipedia.org/wiki/Pattern

Стоит различать шаблоны проектирования и шаблоны кодирования

Мы рассмотрим следующие типы шаблонов:

  • шаблоны проектирования
  • шаблоны кодирования
  • антишаблоны

Рекомендуемая литература: Стоян Стефанов. JavaScript. Шаблоны (JavaScript: Patterns)

Литералы объектов

Важно понимать, в каких ситуациях правильно пользоваться лиетаралми, а в каких правильно создавать свои конструкторы

var someObj = {};
var someObj = 'string';
var someObj = 123;

Вся строка в целом называется инструкцией

  • var - это ключевое слово, опредлеяющее переменную
  • someObj - идентификатор, т.е. имя конструкции, которую мы создаем
  • = - оператор присваивания
  • {}, 'string', 123 - литералы
  • {} - литерал, который означает, что создается пустой объект
  • 'string' - строковый литерал

Можно сказать, что литерал - это чистое значение, которое встречается в приложении.

Один из способов создать объект - с помощью литерала:

var user = {}; // создали пустой объект
user.name = 'admin';
user.getName = function () {
  return this.name;
}

Однако для создания объектов лучше использовать литерал, который сразу определяет его структуру (если, конечно, эта структура заранее известна):

var user2 = {
  name: 'admin',
  getname: function () {
    return this.name;
  }
};

Есть и другой способ создания объекта: с помощью конструктора и ключевого слова new.

var user3 = new Object(); // то же самое, что и {}, но на порядок больше кода
user3.name = 'admin';
user3.getName = function () {
  return this.name;
}

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

Но все-таки конструкторы - это системные объекты и некоторые из них тоже стоит использовать, например, конструктор Date(). Тем более, что с помощью литерала мы не сможем создать конструктор Date().

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

Конструктор Object() может принимать параметр и делегировать вызов другому встроенному конструктору, вернув в результате объект другого типа.

var obj = new Object();
console.log( obj.constructor === Object ); // true

var obj = new Object(1); // здесь произойдет перевызов и создасться не пустой объект, а объект типа Number со всеми присущими ему методами
console.log( obj.constructor === Number ); // true
console.log( 'obj.toFixed(3) = ' + obj.toFixed(3) ); // 1.000

var obj = new Object('Hello world');
console.log( obj.constructor === String ); // true

var obj = new Object(true);
console.log( obj.constructor === Boolean ); // true

Из примеров выше понятно, в какой тип данных будет преобразован Object по литералу в параметрах вызова. Однако, если мы передаем в параметры переменную, определенную где-то выше в приложении, то уже становится не понятно, в какой тип будет преобразован Object. Такой код нечитабелен и сложнее будет сопровождаться в будщем: var obj = new Object(a);.

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

Пользовательские конструкторы

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

Во-первых, идентификатор (имя) конструктора всегда пишется с заглавной буквы. И это не соглашение между разработчиками, как многие думают. Это заложено в самом языке. Убедиться в этом можно, если вызвать в консоли объект Window и посмотреть на его свойства и методы: сначала будут идти методы с заглавной буквы - это конструкторы, которые нужно вызывать с ключевым словом new; а затем будут идти методы с прописной буквы - это обычные методы, которые выполняют какую-то функцию.

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

При вызове функции с оператором new происходит следующее:

  1. Создается пустой объект
  2. Пустой объект наследует свойства и методы прототипа функции
  3. Ссылка на этот объект сохраняется в переменной this
  4. В конструкторе добавляются новые свойства и методы в пустой объект.
  5. В конце функции неявно возвращается объект, на который ссылается this
function User(name) {
  this.name = name; // this - контекст
  this.say = function () {
    document.write( 'Hello! My name is ' + this.name );
  }
}

var user = new User('John');

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

Методы конструктора лучше не писать в нем самом, а выносить за передлы конструктора в прототип.

function User2(name) {
  this.name = name;
  // Создавая метод в объекте бессмысленно расходуется память, 
  // т.к. каждый новый объект, созданный этой функцией конструктором, будет содержать в себе копию метода.
  // this.say = function () {
  //   document.write( 'Hello! My name is ' + this.name );
  // }
}
// Хорошей практикой считается добавление методов к прототипу конструктора
User2.prototype.say = function () {
  document.write( 'Hello! My name is ' + this.name );
}

var user = new User2('Иван');

По сути, когда мы пишем оператор new для вызова функции, то контекстом этой функции будет новый объект. Без оператора new контекст функции будет ссылаться на глобальный объект.

function User3(name) {
  this.name = name;
}

var a = new User3('Стив Джобс'); // вызываем функцию через оператор new
console.log( a.name ); // Стив Джобс - контекст функции ссылается на новый объект

var b = User3('Билл Гейтс'); // вызываем функцию без оператора new
console.log( b ); // undefined
console.log( window.name ); // Билл Гейтс - контекст функции ссылается на глобальный объект

В сухом остатке:

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

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

Шаблон принудительного вызова ключевого слова new №1

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

function User4(name) {
  var that = {};
  that.name = name;
  return that;
  
  this.name = name;
}

var a = new User4('Альберт Эйнштейн'); 
console.log( a.name ); // Альберт Эйнштейн

var b = User4('Нильс Бор');
console.log( b ); // Object {name: "Нильс Бор"}
console.log( window.name ); // undefined

Шаблон принудительного вызова ключевого слова new №2

Сохраняется связь с прототипом

function User5(name) {
    // Если контекст не является экземпляром конструктора, а стало быть это экземпляр window
  if ( ! (this instanceof User5) ) {
    return new User5(name); // то возвращаем вызов конструктора с помощью оператора new
  }
  this.name = name;
}

var a = new User5('Бред Питт'); 
console.log( a.name ); // Бред Питт

var b = User5('Джони Депп');
console.log( b ); // User5 {name: "Джони Депп"}
console.log( window.name ); // undefined

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

Литералы массивов

В JS нет массивов в том понимании, в котором мы привыкли в других ЯП. Массив в C# - это непрерывная последовательность байт в оперативной памяти. Мы не можем просто так добавить в этот массив новый элемент, не создав новый массив на большее кол-во записей.

В JS массивы - это обычные объекты и нет разницы между объектом, который был создан фигурными скобками {} и объектом, который был создан квадратными скобками [] с точки зрения организации в оперативной памяти. Объект - это набор значений, где у каждого значения есть свое имя. Массив - это набор значений, где у каждого значения есть свой порядковый номер.

Массивы в JS - это аналог ассоциативных массивов в других языках, например, как dictionary в языке C#, где записи представляют собой пары ключ-значение.

Массивы тоже желательно создавать с помощью литерала.

Формально массивы в JS всегда рассматриваются отдельно, но номинально такого типа данных как Array в JS не существует.

var someArrayA = new Array('Hello! ', 'World', '!');
var someArrayB = ['Hello! ', 'World', '!'];

document.write( typeof someArrayA ); // object
document.write( someArrayA.constructor === Array ); // true
document.write( typeof someArrayB ); // object
document.write( someArrayB.constructor === Array ); // true

Неважно как мы создали массив: с помощью конструктора или литерала. В любом случае он будет принадлежать к типу данных object и для создания объекта используется конструктор Array().

Кроме того, при использовании конструктора, есть подводные камни:

var someArrayC = new Array(); // пустой массив
var someArrayD = new Array('Hello! ', 'World', '!'); // массив на 3 элемента со значениями

Если конструктору массива передать 1 значение, то оно НЕ станет первым элементом массива.

var someArrayE = new Array(10); // пустой массив на 10 элементов
var someArrayF = new Array(3.5); // ошибка - нельзя создать массив на 3.5 элемента
var someArrayD = new Array('Hello!'); // Ошибка - нельзя создать массив на кол-во элементов Hello!

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

Работа с простыми (примитвными) типами данных

Примитивы также лучше определять с помощью литералов.

var someNumA = 100; // простое число, тип number

var someNumB = new Number(100);

Вторая запись: 1) длиннее; 2) конструктор будет тянуть за собой все свои методы, а оно надо? 3) при проверке на тип, будет возвращать object, а не number

Возникает вопрос: если мы создаем примитивы с помощью литералов, а у них тип не object, то как же тогда пользоваться методами типа toString() или toFixed() и т.п.? Ответ простой: вызывать на примитивах методы точно так же, как если бы мы пользовались объектами. Дело в том, что во время вызова метода на примитиве, он временно преобразуется в объект.

var str = 'hello world';
document.write( str.toUpperCase() ); // при вызове метода строка временно преобразуется в объект String
document.write( 'hello'.length ); // Свойства и методы можно вызывать непосредственно на значении

Поэтому для создания объектов лучше использовать литералы

JS Шаблоны 2. Функции

Содержание

  • Терминология
  • Функции обратного вызова
  • Самоопределяемые функции
  • Немедленно вызываемые функции
  • Замыкание
  • Мемоизация
  • Каррирование

Терминология

Функции в JS - это объекты:

  • Могут создаваться динамически во время выполнения
  • Могут присваиваться переменным
  • Могут использоваться в качестве параметров для других функций
  • Могут иметь собственные свойства и методы

Именованная функция-выражение

var add = function add(a, b) {
	return a + b;
}

Функция-выражение или анонимная функция

var add = function (a, b) {
	return a + b;
}

Функция-объявление

function add(a, b) {
	return a + b;
}

Область видимости

Область видимости - область, в которой можно обратиться к переменной или функции по имени

Область видимости в JS:

  • Локальная - функция или объект
  • Глобальная - объект Window (в случае использования браузера)

В JS в отличие от других ЯП, нет блочной области вилимости, которая задается операторными скобками {}

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

Подъем функций (function hoisting) - работает также как и подъем переменных: интерпретатор определяет все функции в области видимости (напр., Window) и делает их методами объекта Window. Исключение составляют функции-выражения (т.е. ф-ии присвоенные переменной var a = function(){}): интерпретатор также определяет такие переменные и делает их свойствами объекта Window, но не он не читает их значения, т.е. не выполняет функцию, которая записана как значение в эту переменную. Надо быть аккуратным с порядком вызова таких функций.

Контекст

Контекст - это ключевое слово this. Контекст - объект на который указывает ключевое слово this в момент вызова функции. Если мы говорим, что контекст функции - глобальный объект, это означает, что когда внутри функции вы обратитесь к ключевому слову this, то обнаружите в нем глобальный объект Window. Если говорим, что контекст - это конкретный объект, то, обращаясь, увидим этот конкретный объект. Иными словами, контекст - это ссылка, которая ведет на какой-то конкретный объект и которая зафиксирована в ключевом слове this внутри самой функции.

В JS контекст может ссылаться на 3 типа объекта:

  • Глобальный
  • Конкретный
  • Новый пустой

Контекст ссылается на глобальный объект

function contextTest1 {
	console.log(this === window); // true
}
contextTest1();

Контекст ссылается на конкретный объект

var obj = {
	contextTest2: function(){
		console.log(this === obj);
	}
}
// При вызове метода, контекстом функции является объект, на котором произошел вызов
obj.contextTest2();

Контекст ссылается на новый пустой объект

function ContextTest3() {
	console.log(this instanceof ContextTest3); // ключевое слово instanceof используется вместе с конструктором (который указывается после ключевого слова), когда хотим проверить создавался ли какой-то объект (здесь this) данным конструктором (здесь ContextTest3)
}
// При вызове функции с ключевым словом new контекстом будет новый пустой объект
new ContextTest3(); // Когда мы указываем перед конструктором ключевое слово new, мы тем самым говорим, что внутри конструктора контекст нужно заменить на новый пустой объект

Callback-функции (или функции обратного вызова)

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

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

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

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

// doOperation принимает на вход в качестве аргумента коллбэк
function doOperation(callback) {
	// Выполнение каких-то операций, в конце которых вызывается коллбэк
	callback();
}

// Коллбэк showAlert, который мы передадим в ф-ию doOperation
function showAlert() {
	alert('Hello from callback');
}

// Функции - это объекты, то есть их можно передавать как аргументы другим функциям.
doOperation(showAlert)

Рекомендации по использованию коллбэков:

  • называть аргумент, в который мы планируем передавать функцию как 'callback' function (callback) {}
  • передавать коллбэк последним аргументом для наглядности function (a, b, callback) {}

Наиболее универсальный способ использования коллбэков - передавать вместе с аргументом callback еще один аргумент для передачи контекста: function (callback, context) {}. И внутри этой функции вызывать на коллбэке метод call(): callback.call(context, args). Если наш коллбэк будет являться методом объекта, то всегда можно явно указать контекст: doOperation(obj.method, obj).

Самоопределяемая функция

После первого запуска функция переопределяет сама себя (свое тело).

var init = function(){
	alert('Инициализация');
	// Какие-то операции инициализации, которые выполняться только один раз в момент первого запуска функции
	init = function(){
		alert('Инициализация уже завершена');
		// Операции, которые будут выполняться все последующие разы, после первого запуска функции
	}
}

Вызов самоопределяемой функции init(); // Инициализация init(); // Инициализация уже завершена

Немедленно вызываемые функции

IIFE (Immediately Invoked Function Expression )

Как правильно написать НВФ и не запутаться: ()(); - первые скобки - сама функция, вторые скобки - передаваемые в функцию параметры (function(){})() - когда интерпретатор дойдет до этой функции, он ее сразу выполнит, а самое главное, что такая функция представляет собой свою локальную область видимости и сама она не создает глобальных переменных и ее имя не появится в глобальном объекте.

2 варианта написания

(function(){
	// code
})(); // Аргументы снаружи
(function(){
	// code
}()); // Аргументы внутри

Данная функция принимает при вызове ссылку на глобальный объект.

(function(global){
	global.alert('Helolo world');
}(this));

Это позволяет обращаться к глобальному объекту без использовния свойства window. Это позволяет использовать эту функцию в средах отличных от браузеров. Кроме того, использование такой конструкции оправдано при написании плагинов или библиотек, где встречается частое обращение к глобальному объекту: при подготовке продакшн-версии (минификация / обфускация) аргумент global может быть сжат минификатором, например, до g, что повлияет на итоговый вес файла, чего нельзя будет добиться при использовании зарезервированного слова window, которое минификатор оставит как есть.

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

var obj = {
	name: (function() {
		var fName = 'Ivan',
				lName = 'Ivanov';
		return fName + ' ' + lname;
	}()),
	sayHello: function() {
		alert('Hello, my name is ' + this.name);
	}
};
obj.sayHello();

В этом примере в свойстве name нам будет доступна уже сконкатенированная строка с именем и фамилией. Однако переменные fName и lName доступны не будут.

Принципы определения функций

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

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

var utils = {
	addListener: null,
	removeListener: null
};

Затем делаются проверки

if ( condition 1 ) {
	utils.addListener = function(){};
	utils.removeListener = function(){};
} else if ( condition 2 ) {
	utils.addListener = function(){};
	utils.removeListener = function(){};
} else {
	utils.addListener = function(){};
	utils.removeListener = function(){};
}

Таким образом, мы избегаем лишних проверок. Проверка в браузере происходит 1 раз и затем нужная функция закрепляется за нужным методом объекта utils.

Функциональное программирование

Замыкания (первая парадигма функционального программирования)

Рассмотрим пример

function init() {
	var str = 'Hello world';
	function displayStr() {
		// вложенная функция имеет доступ к переменной str
		alert(str);	
	}
	displayStr();
}

Благодаря тому, что одна функция в своем теле может содержать другую функцию, есть возможность создавать замыкания.

Для каждой функции, когда она создается, появляется объект, который представляет ее скоуп. Получается для init() создается объект, в котором содержится переменная str, т.к. именно в этом скоупе она находится. Для displayStr() создается другой скоуп, в котором уже нет никаких переменных. Из-за того, что скоуп displayStr() вложен в скоуп init(), есть явная связь между двумя этими скоупами. Также у нас есть глобальный скоуп, который представлен объектом Window. Получается скоуп init() связан с глобальным скоупом. Если мы начнем пользоваться переменной str в функции displayStr(), то интерпретатор начнет искать str в текущем скоупе. Понятно, что в этом скоупе переменной нет, поэтому интерпретатор поднимется выше и возьмет str, опредленную на уровне функции init(). Т.о., когда мы получаем доступ к переменным, находящимся в скоупе выше, мы просто захватываем локальную переменную. Однако, если мы возьмем скоуп init() с его вложенными скоупами и отдадим кому-то другому - это и будет являться замыканием. Замыкание - это функция и переменные, которые ее окружают. Замыкание позволяет сохранить саму функцию и ее окружение.

Посмотрим как выглядит само замыкание:

function makeFunc() {
	var str = 'Hello world';
	function displayStr() {
		alert(str);	
	}
	// функция возвращается, но не запускается
	// Мы возвращаем (но не запускаем) ф-ию и вместе с тем возвращаем как бы снимок скоупа, который был доступен на момент запуска ф-ии, т.е. возвращаем значение переменной str и саму функцию. Но значенние переменной str, понятно, не будет доступно вне функции.
	return displayStr;
}

Замыкание - функция, в которой находятся переменные, объявленные вне тела этой функции. Замыкание - это функция и окружение, в которой была создана функция.

var myFunc = makeFunc(); // Теперь здесь лежит и функция и те переменные, которые необходимы для работы функции - это и есть замыкание
myFunc();

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

Мемоизация (вторая парадигма функционального программирования)

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

function calcFib(x) {
// Проверка наличия результата для параметра x
	if ( ! calcFib.cache[x] ) { // Если результат не найден, то производим вычисления
		if ( x > 1 ) {
			calcFib.cache[x] = calcFib(x - 1) + calcFib(x - 2);
		} else {
			calcFib.cache[x] = x
		}
	}
	return calcFib.cache[x];
}
// Создание сво-ва в ф-ии с пустым объектом
calcFib.cache = {};

for ( var i = 0; i < 40; i++ ) {
	console.log(i + ' = ' + calcFib(i) + '\n');
}

Каррирование (третья парадигма функционального программирования)

Чтобы разобраться что такое каррирование, нужно сначала разобраться с методами apply() и call(), а также дать определение понятию частичное применение.

Методы apply() и call()

Эти методы позволяют запустить функцию с измененным контекстом.

Apply принимает 2 аргумента: 1) контекст и 2) массив аргументов запускаемой функции: fn.apply(context, [a, b]). Call принимает первым аргументом контекст и последующие аргументы функции через запятую: fn.call(context, a, b).

В остальном отличий между этими двумя методами нет.

Где может пригодиться использование apply()? В основном при работе с массивами, когда во второй аргумент apply() можно передать массив. Например, поиск максимального значения в массиве.

var arr = [1, 4, 7, 34, 756, 97, 245, 3, 77];
var maxValue = Math.max.apply(null, arr); // идентичен вызову Math.max(arr[0], arr[1], ..., arr[n]);

Где может быть применен call()? В JS есть понятие Array-like Object - объекты, которые очень похожи на массивы, благодаря тому, что именами свойств у них являются индексы. Хотя массивы в JS являются объектами, есть ряд признаковом отличающих массивы от объектов: например, каждый элемент массива имеет порядковый индекс; массив имеет свойство length, определяющее его размер; массив имеет ряд встроенных методов для работы с массивами, которых нет у объекта (push, slice и т.д.).

Array-like Object выглядит так:

var arrayLikeObject = {
	'0': 'first',
	'1': 'second',
	'2': 'third',
	'3': 'fourth',
	'4': 'fifth',
	'5': 'sixth',
	length: 6
}

Например, чтобы скопировать участок массива, мы можем применить метод slice(). Но у нас объект и такого метода у него нет. Можно, конечно, написать сложную функцию, выполнение которой будет скорее всего завязано на обход в цикле всех свойств объекта с использованием контекста, сво-ва length и числового индексатора [i]. Но все это уже заложено в нашем массивоподобном объекте - порядковые индексы и свойство length - и поэтому гораздо проще все-таки обратиться к методу slice() в прототипе конструктора Array, передав ему в качестве контекста наш объект.

Array.prototype.slice.call(arrayLikeObject, 2, 4);

Частичное применение

Частичное применение - передача в функцию не всех аргументов, а только части.

function add(x, y) {
	var oldX = x, oldY = y;
	if ( typeof oldY === 'undefined' ) { // частичное применение
		return function (newY) {
			return oldX + newY;
		}
	}
	return x + y; // полное применение
}
console.log( typeof add(10) ); // function

Вызывая add(10) мы получаем новую функцию, которая в замыкании хранит значения переменной oldX. После этого запускаем новую функцию и передаем еще одно значение. var r = add(10)(20); // 30

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

Универсальная функция каррирования может выглядеть так (из книги Стояна Стефанова):

function curry(fn) {
	var slice = Array.prototype.slice,
	    oldArgs = slice.call(arguments, 1); // отбрасываем 1ый аргумент
			
	return function () {
		var newArgs = slice.call(arguments),
		    args = oldArgs.concat(newArgs);
		
		return fn.apply(null, args); // вызываем функцию, для которой проводилось каррирование
	}
}

Применение функции curry()

function add(x, y) {
	return x + y;
}

var add10 = curry(add, 10);
var r1 = add10(5); // 15
var r2 = add10(10); // 20

function sum(a, b, c) {
	return a + b + c;
}

var newSum = curry(sum, 1, 2);
var r3 = newSum(5); // 8

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

Содержание

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

JS Шаблоны 4. Шаблоны проектирования

Содержание

  • Singleton
  • Factory
  • Iterator
  • Strategy
  • Facade
  • Proxy
  • Mediator
  • Observer

Существуют паттерны ООП, описанные командой разработчиков, известных как "Банда Четырех" (Gang of Four, GoF). Эти шаблоны актуальны для таких ЯП как C#, C++, Java и прочих строго типизированных языков. Когда же мы говорим о JS, то не все шаблоны из других ЯП могут быть применимы для него. Дело в том, что такие шаблоны решают задачи, связанные с ограниченностью строго типизированных ЯП. Но т.к. JS сам по себе язык с динамической типизацией и в нем нет таких понятий как класс (что-то похожее на класс можно организовать с помощью конструктора), наследование (есть прототипизация) и т.п., то использование шаблонов из строго типизитрованных языков в JS становится просто бессмысленным. Следующие 8 паттернов действительно есть смысл применять в JS.

Singleton

Суть шаблона - обеспечить в приложении наличие только 1 экземпляра определенного класса. Но так как в JS нет классов, то новый объект уже является единственным в приложении. Организовать Singleton можно 2-мя способами: либо использовать литерал объекта (что уже по определению является синглтоном), либо использовать конструктор (который является очень отдаленным примером использования классов в JS).

Singleton с помощью литерала объекта

var obj1 = {
  name: 'test1'
}

var obj2 = {
  name: 'test1'
}

// Сравним адреса ссылок наших объектов
// Строгое сравнение без приведения типов
console.log( obj1 === obj2 ); // false
// Сравнение с приведением типов
console.log( obj1 == obj2 ); // false

https://jsfiddle.net/fo0ddn6v/

Singleton с помощью конструктора

Рассмотрим, как с помощью конструктора можно организовать код, который будет работать по шаблону Singleton.

function Sun() {
  // Проверяем наличие экземпляра созданного ранее.
	// Если св-во instance является объектом,
	// это означает, что конструктор ранее запускался.
	// Поэтому надо просто вернуть существующий экземпляр.
	if ( typeof Sun.instance === 'object' ) {
	  return Sun.instance;
	}
	
	// Если Sun.instance === 'undefined',
	// то определяем логику самого конструктора,
	// т.е. инициализируем его свойствами
	// и т.о. создаем новый экземпляр
	this.color = 'yellow';
	this.isBig = true;
	
	// В св-во instance сохраняем ссылку на контекст,
	// т.е. сохраняем созданный экземпляр для повторного использования
	Sun.instance = this;
	
	// Неявный возврат экземпляра
	// return this;
}

var sun1 = new Sun(); // Получаем ссылку на новый экземпляр
// Sun.instance = null;
var sun2 = new Sun(); // Получаем ссылку на существующий экземпляр
console.log(sun1 === sun2); // true - ссылки одинаковые

Но у этого шаблона есть недостаток: instance является открытым св-вом конструктора и если мы, например, после первичной инициализации экземпляра вызовем Sun.instance = null;, то нарушим работу шаблона. Необходимо инкапсулировать instance.

Factory

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

С помощью этого паттерна можно решить следующие проблемы:

  • Выполнение повторяющихся операций при создании объектов
  • Создание объектов, тип которых неизвестен на этапе компиляции приложения (важно для языков со статической типизацией)

Последний пункт для JS бессмысленнен, поскольку JS - динамический язык программирования и вместо этапа компиляции в нем есть этап интерпретации. Однако для строго типизированных языков (C#, C++, Java) это проблема и решается она с помощью наследования и полиморфизма. В JS нет ни первого ни второго в силу его природы. Поэтому применение шаблона Фабрика в JS используется скорее для того, чтобы собрать воедино ту логику, с помощью которой мы создаем новые объекты.

// Родительский конструктор, выступает в роли
// абстрактного базового типа данных
function Control(){};

// Метод родителя
Control.prototype.render = function(type){
  document.write( 'Control: ' + this.name + '<br> ' + this.control + '<br><br>' );
}

// Фабричный метод
Control.create() = function(type){
  var ctor = type,
	    newControl;
			
	// Проверка наличия конструктора для указанного типа объекта
	if ( typeof Control[ctor] !== 'function' ) {
	  // Выбрасываем исключение, если конструктор не найден
		throw {
		  name: Error,
			message: '' + ctor + ''
		}
	}
	
	// На этом этапе существование конструктора проверено
	// Устанавливаем для конструктора в качестве прототипа объект Control
	// Выполняем данную операцию 1 раз
	if ( typeof Control[ctor].prorotype.render !== 'function' ) {
	  Control[ctor].prorotype = new Control();
	}
	
	// Создаем экземпляр указанного типа
	newControl = new Control[ctor]()
	
	return NewControl;
}

// Специализированные конструкторы (продукты нашей фабрики)
// Выступают в роли производных типов данных
Control.Button = function(){
  this.name = 'Button';
	this.control = '<input type="button" value="testButton" />';
}

Control.TextBox = function(){
  this.name = 'textBox';
	this.control = '<input type="text" />';
}

Control.RadioButton = function(){
  this.name = 'RadioButton';
	this.control = '<input type="radio" /> RadioButton';
}

Использованеи Фабрики:

var btn = Control.create('Button');
var txt = Control.create('TextBox');
var rbtn = Control.create('RadioButton');

btn.render();
txt.render();
rbtn.render();

Iterator

Реализацию шаблона Итератор можно найти практически во всех языках программирования. Например, в C# даже есть отдельное пространство имен - system collections - место, где расположены коллекции. Коллекции по сути являются реализацией шаблона Итератор. Шаблон Итератор нужен для того, чтобы скрыть реализацию объекта, который содержит в себе какую-то совокупность данных (в виде определенной структуры). Иными словами, нам нужно обеспечить доступ к этим данным, не раскрыв способ их хранения.

var collection = (function(){
  // Закрытые данные
  var current = -1, // Указатель на элемент в структуре данных
	    data = [1,2,3,4,5,6,7,8,9,0], // Скрытая структура данных
			count = data.length;
	
	// Открытые данные
	return {
	  // Метод для премещения по структуре данных
		moveNext: function(){
		  if ( current == count - 1 ) {
			  return false;
			} else {
			  current++;
				return true;
			}
		}
		
		// Метод для получения текущего элемента
		getCurrent: fucntion(){
		  return data[current]
		}
		
		// Метод для сброса указателя на начало коллекции
		reset: function() {
		  current = -1;
		}
	}
})();

Использование шаблона Итератор:

while ( collection.moveNext() ) {
  var temp = collection.getCurrent();
	document.write( '<p>' + temp + '</p>' );
}

Strategy

Задача шаблона Стратегия - предоставить изменение алгоритма поведения, но при этом не менять интерфейс объекта.

var validator = {
  types: {}, // Здесь будут храниться стратегии валидации
  messages: [], // Сюда будут записывать сообщения об ошибках, если таковые будут
  config: {}, // Конфиг для указания какие стратегии в каких ситуациях использовать

  // Используется всего один метод, внутри которого и спрятаны стратегии
	// проверяет корректность значений в объекте data в соответствии с настройками указанными в свойстве config
  // возвращает true при наличии ошибок, false - если свойства объекта заполнены правильно.
  validate: function(data) {
    var i,
      msg,
      type,
      invalid,
      checker;

    this.messages = [];

    for (i in data) {
      if (data.hasOwnProperty(i)) {
        type = this.config[i]; // получаем тип проверки для свойства
        checker = this.types[type]; // получаем объект выполняющий проверку

        if (!type) {
          continue;
        }
        if (!checker) {
          throw {
            name: "ValidatorError",
            messgae: "Не найден валидатор " + type
          }
        }

        invalid = checker.validate(data[i]);
        if (invalid) {
          msg = "Не правильное значение для " + i + ", " + checker.message;
          this.messages.push(msg);
        }
      }
    }

    return this.hasErrors();
  },

  hasErrors: function() {
    return this.messages.length !== 0;
  }
};

// объект выполняет проверку наличия значения в свойстве.
validator.types.required = {
  validate: function(value) {
    return value === "";
  },
  message: "Обязательное значение"
};

// объект проверяет значение на соответствие целочисленному типу
validator.types.number = {
  validate: function(value) {
    return !/\d+/.test(value);
  },
  message: "Значение должно быть числом"
};

// проверяет формат email адреса
validator.types.email = {
  validate: function(value) {
    return !/^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/i.test(value);
  },
  message: "Значение должно быть email адресом"
};

Использование шаблона Стратегия

// Правильно заполненный объект
var data1 = {
  firstName: "Ivan",
  lastName: "Ivaonv",
  age: 25,
  email: "ivanov@example.com"
};

// Неправильно заполненный объект
// Надо придумать стратегии валидации
var data2 = {
  firstName: "Ivan",
  lastName: "",
  age: "qwe",
  email: "example"
};

// настройки объекта для проверки
validator.config = {
  firstName: "required", // Стратегия required
  lastName: "required",
  age: "number", // Стратегия number
  email: "email" // Стратегия email
};

var result = validator.validate(data1);
console.log(result); // false - ошибок не найдено

// проверка и вывод ошибок
if (validator.validate(data1)) {
  console.dir(validator.messages);
}

result = validator.validate(data2);
console.log(result); // true - есть ошибки

// проверка и вывод ошибок
if (validator.validate(data2)) {
  console.dir(validator.messages);
}

Facade

Задача шаблона Фасад - предоставить альтернативный интерфейс для объекта. Может быть применен при упрощении интерфейса определенного объекта или для того, чтобы скрыть различия использования определенных функций в разных браузерах. Это довольно распространенный шаблон, который часто используется в библиотеках. Например, в ранних редакциях jQuery метод on() является фасадом для методов addEventListener() и attachEvent(), чтобы обеспечить поддержку событий в IE6-8, которые понимают только последний метод.

// Фасад, который скрывает от пользователя вызов двух других системных методов.
var Events = {
  stop: function(e) {
    e.preventDefault(); // прекратить распространение события по дереву DOM
    e.stopPropagation(); // отменить действие предусмотренное по умолчанию.
  }
}

Proxy

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

Рассмотрим пример с кешированием.

// Реальный объект
var http = {
  makeRequest: function(id, callback) {
    // Имитация запроса на сервер
    setTimeout(function() {
      callback("Данные от сервера " + new Date().getTime());
    }, 3000);
  }
}

// Прокси объект (реализован по шаблону Модуль)
var proxy = (function() {

  // кэш (закрытая часть)
  var cache = {};
  
	// Открытая часть
  return {
    makeRequest: function(id, callback) {
      if (cache[id]) {
        callback(cache[id]);
      } else {
        http.makeRequest(id, function(data) {
          cache[id] = data;
          callback(data);
        });
      }
    }
  }
})();

Интерфейс нашего приложения

<input id="forRequest" type="text" /><span id="loader" style="display:none;">Загрузка...</span>
<br />
<input id="requestBtn1" type="button" value="Запрос" />
<input id="requestBtn2" type="button" value="Запрос (с использование прокси-объекта)" />
<br />
<p id="output"></p>

Добавляем обработчики

function get(id) {
  return document.getElementById(id);
}

function callback(data) {
  loader.style.display = "none";
  output.innerHTML += data + "<br />";
}

get("requestBtn1").addEventListener("click", function() {
  get("loader").style.display = "inline";
  var id = get("forRequest").value;
  // использование напрямую объекта http
  http.makeRequest(id, callback);
});

get("requestBtn2").addEventListener("click", function() {
  get("loader").style.display = "inline";
  var id = get("forRequest").value;
  // использование proxy который обращается к объекту http
  proxy.makeRequest(id, callback);
});

Mediator

Медитатор (Посредник) позволяет уменьшить кол-во связей между объектами. Предположим, нам нужно создать чат-комнату. Каждый пользователь чата - новый объект, который должен знать о существовании других пользователей-объектов. Таких объектов может быть очень много и организовать одновременную связь между всеми может быть ресурсоемкой задачей. Чтобы уменьшить кол-во связей и легко добавлять/убирать пользователей можно использовать шаблон Медиатор, где объекты связаны не непорсдественно друг с дургом, а они связаны с единым медиатором.

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

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

var mediator = {
  // объекты которые объединяет медиатор
  players: {},

  // метод для инициализации всех объектов
  setup: function() {
		// Теперь медиатор знает о Player
    this.players.player1 = new Player("Player 1");
    this.players.player2 = new Player("Player 2");
  },

  // обновление интерфейса, если кто-то из игроков сделал ход.
  updateMediator: function() {
    var score = {
      Player1: this.players.player1.points,
      Player2: this.players.player2.points
    };
    scoreboard.update(score);
  },

  // обработчик действия пользователя
  keypress: function(e) {
    e = e || window.event;

    if (e.keyCode === 49) { // 1
      mediator.players.player2.updatePlayer();
      return;
    }
    if (e.keyCode === 48) { // 0
      mediator.players.player1.updatePlayer();
      return;
    }
  }
}

var scoreboard = {
  // HTML элемент, который должен обновляться.
  element: null,

  // обновляет счет на экране
  update: function(score) {
    var i, msg = "";
    for (i in score) {
      if (score.hasOwnProperty(i)) {
        msg += "<p>" + i + " = " + score[i] + "</p>";
      }
    }
    this.element.innerHTML = msg;
  }
}

// Игрок
function Player(name) {
  this.name = name;
  this.points = 0;
}

// Метод для обновление счета игрока
Player.prototype.updatePlayer = function() {
  this.points++;
  mediator.updateMediator(); // Теперь Player знает о медиаторе
}

mediator.setup();
scoreboard.element = document.getElementById("scoreboard")
window.onkeypress = mediator.keypress;

Observer

Шаблон Observer (или "Издатель-подписчик") - наиболее часто встречающийся шаблон, где есть пользовательский интерфейс. С помощью шаблона Observer можно создать механизм у объекта, который позволит получать сообщения от других объектов об изменениях их состояния, тем самым наблюдая за ними. В JavaScript этот шаблон еще называют "Механизм собственных событий". Например, возьмем кнопку: кнопка - это издатель, потому что, когда мы нажимаем на кнопку, происходит событие. На это событие должен отреагировать обработчик, который является подписчиком.

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

Функция makePublisher(obj) просто перегоняет все свойств из эталонного Издателя (объекта publisher) в тот, объект, который мы подали на вход функции (передали в качестве аргумента).

// объект-издатель - содержит методы для создания подписчиков и оповещения их о изменениях.
var publisher = {

  // коллекция подписчиков
  // все подписчики хранятся в виде массива функций.
  subscribers: {
    // событие defaultEvent на которое пока нет подписчиков
    defaultEvent: []
  },

  // метод для добавления подписчиков fn - функция обработчик, event - имя события, на которое вешается обработчик.
  subscribe: function(fn, event) {

    // если имя события не было указано - рассматриваем вызов метода как подписку на событие по умолчанию.
    event = event || 'defaultEvent';

    // если в коллекции подписчиков еще нет подписчиков на данное событие то добавить свойство заполнив его пустым массивом.
    if (typeof this.subscribers[event] === "undefined") {
      this.subscribers[event] = [];
    }
    // добавление подписчика на событие
    this.subscribers[event].push(fn);
  },

  // метод инициирует событие указанное в первом параметре
  publish: function(args, type) {
    this.visitSubscribers('publish', args, type);
  },

  // метод удаляет указанную функцию подписчика
  unsubscribe: function(fn, type) {
    this.visitSubscribers('unsubscribe', fn, type);
  },

  // вспомогательный метод для работы с подписчиками
  visitSubscribers: function(action, arg, event) {
    var eventType = event || 'defaultEvent',
      subscribers = this.subscribers[eventType],
      i,
      max = subscribers.length;

    for (i = 0; i < max; i++) {
      if (action == 'publish') {
        // запуск событияе - вызов функций-обработчиков всех подписчиков
        subscribers[i](arg);
      } else {
        // удаление определенного подписчика из массива функций-обработчиков подписчиков
        if (subscribers[i] === arg) {
          subscribers.splice(i, 1);
        }
      }
    }
  }
}

// метод для преобразования любого объекта в издателя
function makePublisher(obj) {
  var i;
  for (i in publisher) {
    if (publisher.hasOwnProperty(i) && typeof publisher[i] === "function") {
      obj[i] = publisher[i];
    }
  }

  obj.subscribers = {
    any: []
  };
}

// объект который будет преобразован в издателя.
var button = {
  click: function() {
    // запуск события click c аргументами 123
    this.publish('123', 'click');
  },

  doubleClick: function() {
    // запуск события doubleClick c аргументами abc
    this.publish('abc', 'doubleClick');
  }
}

// превращаем button в издателя
makePublisher(button);

// объект с функциями-обработчиками 
var handlerObject = {
  handler1: function(e) {
    document.write("handler 1 " + e + " <br />");
  },

  handler2: function(e) {
    document.write("handler 2 " + e + " <br />");
  }
}

button.subscribe(handlerObject.handler1, "click");
button.subscribe(handlerObject.handler2, "doubleClick");

button.click();
button.click();
button.click();
button.doubleClick();

button.unsubscribe(handlerObject.handler1, "click");
button.click();

JS Шаблоны 5. Работа с DOM

Содержание

  • Шаблоны и антишаблоны работы с DOM
  • Сценарии, работающие продолжительное время
  • Развертывание готовых сценариев

Рассмотрим, как правильно замерять производительность веб-приложений и какие ошибки часто допускают новички при работе с DOM.

Шаблоны и антишаблоны работы с DOM

Проверка возможностей браузера

Антишаблон - использование имени браузера, чтобы проверить его возможности.

if (navigator.userAgent.indexOf("MSIE") !== -1) {
	document.attachEvent("onclick", function() {
		console.log("clicked");
	});
}

Лучше всегда завязываться на проверку конкретных методов, а не браузеров, которые их поддерживают. В данном случае лучше проверять поддерживет ли браузер метод attachEvent, потому что последние версии IE поддерживают addEventListener. Следующий код будет предпочтительней.

if (document.attachEvent) {
	document.attachEvent("onclick", function() {
		console.log("clicked");
	});
}

Однако, вариант выше может вернуть null или false, что означает, что такой метод существует, но не имеет значения. Тогда более корректный код будет таким:

if (typeof document.attachEvent !== "undefined") {
	document.attachEvent("onclick", function() {
		console.log("clicked");
	});
}

Частые обращения к DOM

Антишаблон

for (var i = 0; i < 1000; i++) {
	// обращаться к DOM на каждой итерации цикла не оптимально.
	document.getElementById("test").innerHTML += i + ", ";
}

В примере выше на каждой итерации цикла происходит поиск элемента #test и добавление в innerHTML какой-то информации. И таких поисков и добавлений - 1000. Это может стать причиной "тормозов" страницы.

При работе с циклами, всегда нужно проверять, работаем ли мы в теле цикла с DOM элементами. Если да, то всегда нужно эту работу оптимизировать.

Предпочтительный вариант. Мы кешируем все итерации в переменную и вне цикла единоразово добавляем данные на страницу. Хоть итераций в цикле и 1000, в DOM происодит всего 1 изменение.

 var i, msg = "";
 for (i = 0; i < 1000; i++) {
 	msg += i + ", ";
 }
 document.getElementById("test").innerHTML += msg;

Можно замерить скорость выполнения кода встроенной функцией браузера console.time().

// АНТИШАБЛОН
console.time("first");
for (var i = 0; i < 1000; i++) {
	document.getElementById("test").innerHTML += i + ", ";
}
console.timeEnd("first");

// Предпочтительней 
console.time("second");
var i, msg = "";
for (i = 0; i < 1000; i++) {
	msg += i + ", ";
}
document.getElementById("test").innerHTML += msg;
console.timeEnd("second");

Работа со стилями

Рассмотрим на примере этого фрагмента разметки:

<div id="test">Hello world</div>

Антишаблон

document.getElementById("test").style.padding = "10px";
document.getElementById("test").style.border = "1px solid black";

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

var e = document.getElementById("test");
e.style.padding = "10px";
e.style.border = "1px solid black";

Еще больший прирост производительности даст использование CSS классов:

var e = document.getElementById("test");
e.className = "test";

Можно также замерить время выполнения:

// АНТИШАБЛОН
console.time("1");
for (var i = 0; i < 1000; i++) {
	document.getElementById("test").style.padding = "10px";
	document.getElementById("test").style.border = "1px solid black";
}
console.timeEnd("1");

// Лучше
console.time("2");
for (var i = 0; i < 1000; i++) {
	var e = document.getElementById("test");
	e.style.padding = "10px";
	e.style.border = "1px solid black";
}
console.timeEnd("2");

// Еще лучше (главное не забыть добавить таблицу стилей)
console.time("3");
for (var i = 0; i < 1000; i++) {
	var e = document.getElementById("test");
	e.className = "test";
}
console.timeEnd("3");

Вывод: при работе со стилями в JS лучше использовать CSS классы, а не инлайн стили.

Получение доступа к элементам DOM

Чтобы получить элемент DOM, можно использовать getElementById, getElementsByName, getElementsByTagName, getElementsByClassName.

var div1 = document.getElementById("test");
div1.innerHTML = "Hello world";

В большинстве случаев функции querySelector работают быстрее чем другие функции для получения элементов из DOM.

var div2 = document.querySelector("#test");
div2.innerHTML = "Hello world";

Создание элементов DOM

Антишаблон

var p, t;

p = document.createElement("p");
t = document.createTextNode("First paragraph");
p.appendChild(t);
document.body.appendChild(p); // плохо

p = document.createElement("p");
t = document.createTextNode("Second paragraph");
p.appendChild(t);
document.body.appendChild(p); // плохо

Код выше плох тем, что дважды выполняется запрос к DOM дереву и дважды перерисовывается страница. Предпочтительнее все добавления производить не в реальный DOM, а в виртуальный. Для этого подойдет Doument Fragment. Doument Fragment - фрагмент документа, в который можно добавлять дочерние элементы. При этом дочерние элементы фрагмента не будут отображаться на странице до тех пор, пока фрагмент не станет дочерним элементом существующего элемента DOM.

var p, t, fragment;

fragment = document.createDocumentFragment();

p = document.createElement("p");
t = document.createTextNode("First paragraph");
p.appendChild(t);
fragment.appendChild(p); // добавление элемента в фрагмент

p = document.createElement("p");
t = document.createTextNode("Second paragraph");
p.appendChild(t);
fragment.appendChild(p); // добавление элемента в фрагмент

document.body.appendChild(fragment); // добавление фрагмента в тело документа.
// все элементы которые есть в фрагменте станут дочерними элементами документа.

Сценарии, работающие продолжительное время

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

Антишаблон

function calcFib(x) {
  if (x > 1) {
    return calcFib(x - 1) + calcFib(x - 2);
  } else {
    return x;
  }
}

for (var i = 0; i < 500; i++) {
  document.write(i + " = " + calcFib(i) + "<br />");
}

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

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

window.onload = function() {
  document.getElementById("startButton").onclick = function() {
    // Создание нового потока
    var worker = new Worker("fibonachiWorker.js");

    // Создание обработчика для получения сообщений от потока
    worker.addEventListener("message", function(e) {
      document.getElementById("output").innerHTML += e.data + "<br />";
    }, false);

    // запуск потока
    worker.postMessage("");
  }
}

fibonachiWorker.js

function calcFib(x) {
  if (x > 1) {
    return calcFib(x - 1) + calcFib(x - 2);
  } else {
    return x;
  }
}

addEventListener("message", function() {

  for (var i = 0; i < 500; i++) {
    postMessage(i + " = " + calcFib(i) + "<br />");
  }

}, false);

Интерфейс воркера

<button id="startButton">Start</button>
<p id="output"></p>

Развертывание готовых сценариев

  • Подключать скрипты перед закрывающим тегом
  • Конкатенировать скрипты в 1 файл для уменьшения запросов к серверу
  • Минифицировать скрипты для уменьшеня их веса.
  • Использовать инструменты профилирования кодаи приложения в целом
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment