# 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 ); // Свойства и методы можно вызывать непосредственно на значении ``` Поэтому для создания объектов лучше использовать литералы