Стоянов Стефан. JavaScript шаблоны

Введение 1

Шаблоны

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

Типы шаблонов:

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

Java Script: концепции

Java Script — объектно-ориентированный язык

В JS существует только пять элементарных типов, не являющихся объектами

  • Числа
  • Строки
  • Логические значения
  • null
  • undefined

Первые три имеют объектно-ориен-е представление в виде оберток.

Даже объявляя переменную в JS мы уже имеем дело с глобальным объектом. Объявленная переменная является свойством и одновременно объектом. Свойством мы знаем почему, а объектом, потому что она имеет собственные свойства или атрибуты которые определяют доступность для изменения, удаления или использования в качестве последовательности в циклах for-in.

Есть две разновидности объектов

  • Собственные объекты языка
  • Объекты окружения

Собственные объекты языка могут быть разделены на встроенные (например, Array, Date) и пользовательские (var o = {};).

Объектами окружения являются такие объекты, как window и все объекты DOM.

В Java Script отсутствуют классы

Прототипы

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

Среда выполнения

Естественной средой обитания программ на Java Script является браузер. Но это не единственная среда.

ECMA Script 5

Ядро языка программирования Java Script (за исключением DOM, BOM и дополнительных объектов окружения) опирается на стандарт ECMAScript, или ES.

Основы 2

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

Любая создаваемая вами глобальная переменная становится свойством глобального объекта.

В JS имеется понятие подразумеваемых глобальных переменных.

function sum(x, y) {
  // антишаблон: подразумеваемая глобальная переменная
  result = x + y;
  return result;
}

Между подразумеваемыми глобальными переменными и глобальными переменными, объявленными явно, существует одно тонкое отличие –возможность использования оператора delete для удаления переменных:

// определение трех глобальных переменных
var global_var = 1;
global_novar = 2; // антишаблон
(function () {
  global_fromfunc = 3; // антишаблон
}());

// попытка удаления
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true

// проверка удаления
typeof global_var; // “number”
typeof global_novar; // “undefined”
typeof global_fromfunc; // “undefined”
Шаблон единственной инструкции var - хорошая практика

Подъем: проблемы с разбросанными переменными

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

// антишаблон
myname = “global”; // глобальная переменная
function func() {
  alert(myname); // “undefined”
  var myname = “local”;
  alert(myname); // “local”
}
func();

Циклы for

При работе с циклами for лучше определять длину массива (или коллекции) заранее, как показано в следующем примере:

for (var i = 0, max = myarray.length; i < max; i++) {
  // выполнить какие-­либо операции над myarray[i]
}

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

Циклы for-in

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

// 1
// цикл for­in
for (var i in man) {
  if (man.hasOwnProperty(i)) { // фильтрация}
}

// 2
// антишаблон:
// цикл for­in без проверки с помощью hasOwnProperty()
for (var i in man) {
  console.log(i, “:”, man[i]);
}
Расширение встроенных прототипов (в том числе нежелательное)

Расширение свойств prototype функций-конструкторов – это довольно мощный способ расширения функциональных возможностей, но иногда он может оказаться слишком мощным.

Шаблон switch

Каждая инструкция case завершается явно с помощью инструкции break;

Избегайте неявного приведения типов

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

Не используйте eval()

Если вы намереваетесь использовать функцию eval() в своем сценарии, вспомните мантру «eval() – это зло». Если программный код генерируется динамически во время выполнения сценария, зачастую есть лучший способ достичь тех же целей без применения функции eval().

Также важно помнить, что передача строк функциям setInterval(), set­Timeout() и конструктору Function() аналогична вызову функции eval().

Преобразование строки в число с помощью parseInt()

Соглашения по оформлению программного кода

  • Отступы ( кто как хочет, клавное чтобы они были )
  • Фигурные скобки ( ставятся всегда )
  • Местоположение открывающей скобки
// Пишем так ->
if (true) {
  alert(“It’s TRUE!”);
}
// А не так ->
if (true) 
{
  alert(“It’s TRUE!”);
}
  • Пробелы ( это мы знаем, отделяем все операторы друг от друга всегда )

Соглашения по именованию

  • Заглавные символы в именах конструкторов
  • Выделение слов (camel case)
  • Все заглавные буквы ( применяется для констант )

Комментарии

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

Документирование API

По примеру как в JSDoc. Тоже самое.

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

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

В заключение

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

  • Снижение количества глобальных переменных, в идеале – не более одной на приложение.
  • Использование единственного объявления var в функциях, что позволяет одним взглядом охватить все переменные и предотвращает появление неожиданностей, вызванных особенностями механизма подъема переменных.
  • Циклы for, циклы for-in, инструкции switch, «eval() – это зло», нежелательность расширения прототипов.
  • Следование соглашениям по оформлению программного кода (последовательное использование пробелов и отступов; использование фигурных скобок и точек с запятой даже там, где они являются необязательными) и соглашениям по именованию (конструкторов, функций и переменных).

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

Литералы и конструкторы 3

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

В JS нет пустых объектов. Даже простейший объект {} уже имеет свойства и методы, унаследованные от Object.prototype.

Объекты JS можно представлять себе как хеш-таблицы, содержащие пары ключ-значение.

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

Синтаксис литералов объектов

--

Создание объектов с помощью конструкторов

В языке Java Script отсутствуют классы, но в JS существуют функции-конструкторы, для вызова которых используется синтаксис, заимствованный из других объектно-ориентированных языков.

Ниже два эквивалентных способа создания двух идентичных объектов:

// первый способ ­­ с помощью литерала
var car = {goes: “far”};

// другой способ ­­ с помощью встроенного конструктора
// внимание: это антишаблон
var car = new Object();
car.goes = “far”;

Преимущество использования литералов перед конструктором:

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

Недостатки конструктора Object

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

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

// Внимание: все эти примеры являются антишаблонами

// пустой объект
var o = new Object();
console.log(o.constructor === Object); // true

// объект­-число
var o = new Object(1);
console.log(o.constructor === Number); // true
console.log(o.toFixed(2)); // “1.00”

// объект­-строка
var o = new Object(“I am a string”);
console.log(o.constructor === String); // true

// обычные объекты не имеют метода substring()
// зато этот метод имеется у объектов ­строк
console.log(typeof o.substring); // “function”

// логический объект
var o = new Object(true);
console.log(o.constructor === Boolean); // true

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

Собственные функции-конструкторы

var adam = new Person(“Adam”);
adam.say(); // “I am Adam”

Этот новый шаблон очень напоминает создание объектов в языке Java с использованием класса Person. Синтаксис очень похож, но в действительности в языке JS отсутствуют классы, и Person – это всего лишь функция.

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

  • Создается пустой объект, наследующий свойства и методы прототипа функции, и ссылка на него сохраняется в переменой this.
  • Добавление новых свойст и методов в объект осуществляется с помощью ссылки this
  • В конце функция неявно возвращает объект, на который ссылается ременная this (если явно не возвращается никакой другой объект)

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

var Person = function (name) {
  // создается пустой объект
  // с использованием литерала
  // var this = {};
  // добавляются свойства и методы
  this.name = name;
  this.say = function () {
    return “I am “ + this.name;
  };
  // return this;
};

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

Значения, возвращаемые конструкторами

При вызове с оператором new функция-конструктор всегда возвращает объект. По умолчанию это объект, на который указывает ссылка this. Если внутри конструктора к нему не добавляются никакие свойства, возвращается «пустой» объект.

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

Шаблоны принудительного использования new

Если вызвать конструктор без оператора new, ссылка this внутри конструктора будет указывать на глобальный объект.

Такое нежелательное поведение устранено в стандарте ECMA Script 5, и в строгом режиме ссылка this больше не указывает на глобальный объект.

Имена конструкторов начинаются с заглавного символа (MyConstructor), а имена «обычных» функций и методов – со строчного символа (myFunction).

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

Подобно литералам объектов, литералы массивов в использовании проще и предпочтительнее.

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

// массив из трех элементов
// внимание: антишаблон
var a = new Array(“itsy”, “bitsy”, “spider”);

// точно такой же массив
var a = [“itsy”, “bitsy”, “spider”];

console.log(typeof a); // “object”, потому что массивы ­ это объекты
console.log(a.constructor === Array); // true

Синтаксис литералов массивов

["Value", "Value", "Value", "Value"]

Странности конструктора Array

Ловушки конструктора Array.

  • Когда конструктору Array() передается единственное число, оно не становится первым элементом массива. Вместо этого число интерпретируется как размер массива.
  • Вызов конструктора с плавающей точкой приведет к ошибке! Потому что такое число не может использоваться как длина массива.
Конструктор Array() может оказаться полезным, например для повторения строк. Следующая инструкция вернет строку, содержащую 255 пробелов var white = new Array(256).join(‘ ‘);

Проверка массивов

// Не совесем то что нам нужно.
console.log(typeof [1, 2]); // “object”

// Вот это уже лучше
Array.isArray([]); // true

// Можно реализовать свою функцию
Array.isArray = function (arg) {
  return Object.prototype.toString.call(arg) === “[object Array]”;
};

JSON

Формат JSON обмена данными, название которого является аббревиатурой от слов Java Script Object Notation (форма записи объектов Java Script).

Единственное, что отличает синтаксис формата JSON от синтаксиса литералов объектов, – это необходимость заключать имена свойств в кавычки.

Обработка данных в формате JSON

// входная строка в формате JSON
var jstr = ‘{“mykey”: “my value”}’;

// антишаб лон
var data = eval(‘(‘ + jstr + ‘)’);

// предпочтительный способ
var data = JSON.parse(jstr);
console.log(data.mykey); // “my value”

Метод JSON.stringify() выполняет операцию, противоположную методу JSON.parse().

Литералы регулярных выражений

Регулярные выражения в Java Script также являются объектами, и у вас есть два способа их создания:

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

Синтаксис литералов регулярных выражений


Объекты-обертки значений простых типов

Объекты-обертки могут быть созданы с помощью встроенных конструкторов Number(), String() и Boolean().

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

// простое число
var n = 100;
console.log(typeof n); // “number”

// объект Number
var nobj = new Number(100);
console.log(typeof nobj); // “object”

Методы могут применяться и к элементарным значениям – при вызове метода элементарное значение временно преобразуется в объект и ведет себя как объект.

// элементарная строка, используемая как объект
var s = “hello”;
console.log(s.toUpperCase()); // “HELLO”

// даже само значение может действовать как объект
“monkey”.slice(3, 6); // “key”

// то же относится и к числам
(22 / 7).toPrecision(3); // “3.14”

Поскольку элементарные значения могут действовать как объекты, нет никаких причин использовать более длинную форму записи с использованием конструктора. Например, нет никакой необходимости писать new String(“hi”);, когда можно просто использовать “hi”

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

// элементарная строка
var greet = “Hello there”;

// значение будет временно преобразовано в объект,
// чтобы предоставить возможность вызвать метод split()
greet.split(‘ ‘)[0]; // “Hello”

// попытка добавить свойство к элементарному значению не вызовет ошибку
greet.smile = true;

// но она не даст положительных результатов
typeof greet.smile; // “undefined”

Объекты Error

Конструкторы объектов ошибок в js:

  • Error()
  • SyntaxError()
  • TypeError()

Свойства объектов ошибок:

  • name - имя функции-конструктора, создающая объект.
  • message - строка, которая передаётся конструктору при создании объекта.

Инструкция throw принимает любые объекты, не обязательно созданные с помощью конструкторов семейства Error.

В заключение

В этой главе обсуждались:

  • Литералы объектов – элегантный способ создания объектов.
  • Функции-конструкторы – встроенные конструкторы (вместо которых проще использовать форму записи в виде литералов) и собственные конструкторы.
  • Способы, гарантирующие, что конструкторы всегда будут вести себя как конструкторы независимо от использования оператора new.
  • Литералы массивов
  • JSON – формат представления данных
  • Литералы регулярных выражений.
  • Встроенные конструкторы, которых следует избегать: String(), Number(), Boolean() и различные конструкторы Error()

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

Функции

Основы

Два свойства функций делающих их особенными:

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

Устранение неоднозначностей в терминологии

// именованная функция-выражение
var add = function add(a, b) { return a + b; };

//функция-выражение, она же анонимная функция
var add = function (a, b) { return a + b; }

Эти два способа идентичны, но есть одно отличие, свойство name объекта функции во втором случае будет равен пустой сроке.

// функция-объявления
function foo() {
  // тело функции
}

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

Объявления и выражения: имена и подъем


Свойство name функции


Подъем функций

// антишаблон
// исключительно в демонстрационных целях

// глобальные функции
function foo() {
  alert('global foo');
}
function bar = {
  alert('global bar')
}

function hoistMe() {
  console.log(typeof foo); // "function"
  console.log(typeof bar); // "undefined"

  foo(); // "local foo"
  var(); // TypeError: bar is not a function

  // функция-объявление:
  // имя 'foo' и его определение "поднимаются" вместе
  function foo() {
    alert('local foo');
  }

  // функция-выражение:
  // "Поднимается" только имя 'bar',
  // без реализации
  var bar = function () {
    alert('local bar');
  }
}
hoistMe();

Функции обратного вызова


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

var findNodes = function(callback) {
  var i = 100000,
      nodes = [],
      found;

  // проверить, является ли объект callback функцией
  if(typeof callback !== "function") {
    callback = false;
  }

  while(i) {
    i -= 1;
    // здесь находится сложная логка выбора узлов...

    // теперь вызвать функцию callback:
    if(callback) {
      callback(found);
    }

    nodes.push(found);
  }

  return nodes;
};

Функции обратного вызова и их область видимости

Представим, что в качестве функции обратного вызова используется метод paint() объекта с именем myapp()

var myapp = {};
myapp.color = "green"
myapp.paint = function (node) {
  node.style.color = this.color;
};

И этот метод передается вот так:

var findNodes = function (callback) {
  //...
  if(typeof callback === "function") {
    callback(found);
  }
  //...
}

Если теперь выполнить вызов findNodes(myapp.paint), он будет действовать не так, как ожидается, потому что свойство this.color не будет определено внутри метода. Ссылка this будет указывать на глобальный объект, потому что findNodes() – это глобальная функция.

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

findNodes(myapp.paint, myapp)

var findNodes = function (callback, callbakc_obj) {
  //...
  if(typeof callback === "function") {
    callback.call(callbakc_obj, found);
  }
  //...
}

Обработчики асинхронных событий

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

Предельное время ожидания


Функции обратного вызова в библиотеках


Возвращение функций


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

Пример:

var scareMe = function () {
  alert(“Boo!”);
  scareMe = function () {
    alert(“Double boo!”);
  };
};

// вызов самоопределяемой функции
scareMe(); // Boo!
scareMe(); // Double boo!

Этот шаблон имеет еще одно название: «отложенное определение функции».

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

// 1 добавление нового свойства
scareMe.property = “properly”;

// 2 присваивание другой переменной
var prank = scareMe;

// 3 использование в качестве метода
var spooky = {
  boo: scareMe
};

// вызов под новым именем
prank(); // “Boo!”
prank(); // “Boo!”
console.log(prank.property); // “properly”

// вызов как метода
spooky.boo(); // “Boo!”
spooky.boo(); // “Boo!”
console.log(spooky.boo.property); // “properly”

// использование самоопределяемой функции
scareMe(); // Double boo!
scareMe(); // Double boo!
console.log(scareMe.property); // undefined

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


Параметры немедленно вызываемых функций

// выведет:
// I met Joe Black on Fri Aug 13 2010 23:26:59 GMT­0800 (PST)

(function (who, when) {
   console.log(“I met “ + who + “ on “ + when);
}(“Joe Black”, new Date()));

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

(function (global) {
  // глобальный объект доступен через аргумент `global`
}(this));
Не рекомендуется передовать слишком много параметров.

Значения, возвращаемые немедленно вызываемыми функциями


Преимущества и особенности использования

Помогают выполнять необходимые операции, не оставляя за собой глобальных переменных.

Немедленная инициализация объектов

Пример:

({
  // свойства, методы...
  //...

  // инициализация
  init: function() {
    // операции по инициализации
  }

}).init();

Этот шаблон обладает теми же еимуществами, что и шаблон немедленно вызываемых функций.

Выделение ветвей, выполняющихся на этапе инициализации.


Свойства функций - шаблон мемоизации

Мемоизация также называется кэширование.
var myFunc = function(param) {
  if(!myFunc.cache[param]) {
    var result = {}
    //...продолжительные операции...
    myFunc.cache[param] = result;
  }

  return myFunc.cache[param];
}

// создание хранилища результатов
myFunc.cache = {}

Объекты с параметрами

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

// плохо
addPerson("Bruce", "Wayne", new Date(), null, null, batman)

//хорошо
var conf = {
  userName: "batman",
  first: "Bruce",
  last: "Wayne"
};
addPerson(conf)

Плюсы объектов с параметрами:

  • Не требуется запоминать количество и порядок следования параметров
  • Можно не указывать необязательные параметры
  • Программный код, в котором используются объекты с параметрами, легко читается и прост в сопровождении.
  • Упрощается возможность добавления и удаления параметров.

Каррирование


Применение функций

В некоторых функциональных языках программирования принято говорить, то функция не вызывается, а применяется. В JS мы можем именять функции с потощью метода Function.prototype.apply()

Пример применения функции:

// определение функции
var sayHi = function(who) {
  return "Hello" + (who ? ", " + who : "") + "!";
}

// вызов функции
sayHi();                     // "Hello!"
sayHi('world');              // "Hello, world!"

// применение функции
sayHi.apply(null, ["hello"]) // "Hello, hello!"

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

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

Каррирование

Слово происходит от имени математика Хаскелла Карри (язык программирования Haskell, также назван в его честь). Каррирование - это процесс преобразования.

Пример:

// каррированая функция add()
// принемает не полный список аргументов

function add(x, y) {
  var oldx = x, oldy = y;
  if(typeof oldy === "undefined") { // частичное применение
    return function(newy) {
      return oldx + newy;
    }
  }
  // полное применение
  return x + y;
}

// проверка
typeof add(5); // "function"
add(3)(4);     // 7

// создать и сохранить новую функцию
var add2000 = add(2000);
add2000(10); // 2010

// более компактная версия
function add(x, y) {
  if(typeof === "undefined") {
    return function (y) {
      return x + y;
    }
  }
  // полное применение
  return x + y;
}
Посмотреть ещё примеры, потому что тема достаточно сложная

Когда использовать каррирование

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

В заключение

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

Шаблоны создания объектов

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

Пространство имен

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

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

Имя для таких объектов обычно прописываются ЗАГЛАВНЫМИ буквами.

Универсальная функция для создания пространства имен