Skip to content

Instantly share code, notes, and snippets.

@Sha1fei
Created December 14, 2017 04:11
Show Gist options
  • Save Sha1fei/72f0dc3c0f74d2684bc0fdd6110a907f to your computer and use it in GitHub Desktop.
Save Sha1fei/72f0dc3c0f74d2684bc0fdd6110a907f to your computer and use it in GitHub Desktop.
JS Шаблоны 01. Литералы и конструкторы.

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

Содержание

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

Шаблон - это повторяемая архитектурная конструкция, представляющая собой решение проблемы проектирования в рамках некоторого часто повторяющегося контекста. 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 ); // Свойства и методы можно вызывать непосредственно на значении

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

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