- Конструкторы
- Создание пользовательских конструкторов, ключевое слово this
- Прототипы
- Конструктор Object и его методы
- Техники ООП: инкапсуляция, наследование, полиморфизм
В ходе урока рассматриваются:
- Основные конструкторы -- Object(), Array(), Function(), Date(), String().
- Принцип работы конструкторов, назначение ключевого слова this в конструкторе.
- Создание пользовательских конструкторов.
- Что такое прототип, использование прототипов и добавление свойств и методов в прототип.
- Работа с конструктором Object
- Объектно-ориентированные техники в языке JavaScript. Реализация наследования в JavaScript.
Конструктор - это специальная функция, задача которой заполнить пустой объект свойствами и методами. Иными словами, конструктор - это функция, которая конфигурирует объект для дальнейшего использования.
Объект - это составной тип данных, он объединяет множество значений в единый модуль и позволяет сохранять и извлекать значения по их именам. Самым простым способом создания объекта является использование конструктора Object().
- new - ключевое слово, которое создает новый пустой объект
- Object() - функция, в которую передается новый пустой объект; она конфигурирует объект и добавляет в него стандартные свойства и методы ; настроенный готовый объект возвращается в переменную, которой он присвоен
var point = new Object();
console.log( 'point' + point );
В JS мы можем создать строковое значение 2мя способами:
- используя литерал
- используя конструктор
var simpleStr = 'My String'; // переменная со строковым значением (литерал)
var objectStr = new String('some String object'); // объект типа String (конструктор)
function newLine(str) {
console.log( str + '\n');
}
newLine( simpleStr ); // My String
newLine( objectStr ); // some String object
Добавлять новые свойства и методы можно только к объектам, созданным через оператор new
objectStr.customProperty = 123;
console.log( objectStr.customProperty ); // 123
Добавить свойство к переменной невозможно
simpleStr.customProperty = 123; // В момент выполнения этой инструкции создается временный объект String(), а после ее завершения он удаляется из памяти
// new String(simpleStr).customProperty = 123; // вот что происходит под капотом
console.log( simpleStr.customProperty ); // undefined - на этой строке новая инструкция simpleStr не знает о добавленном свойстве, потому что нет того объекта, к которому добавлялось это свойство
// console.log( String(simpleStr).customProperty ); // Более того, в новой инструкции на этой строке снова создается временный объект, у которого заведомо нет свойства customProperty
Т.о. переменную, в отличие от объекта, невозможено расширять. Каждый раз, когда мы обращаемся к примитиву через переменную, под капотом JS временно превращает его значение в объект для того, чтобы можно было взаимодействовать с определенными нативными методами, характерными для этого примитива (например, toString() для строк). Но этот объект не сохраняется в памяти, поэтому и невозможно обратиться ни к одному его кастомному свойству/методу, если такое было добавлено.
Конструктор Function() позволяет динамически создавать и компилировать анонимные функции. Он принимает неограниченное кол-во параметров, последний параметр всегда является телом создаваемой функции. Параметры, которые передаются в начале списка аргументов Function(), являются входными параметрами для генерируемой функции.
var func = new Function('x', 'y', 'return x + y');
console.log( func(10, 20) ); // 30
Использовать этот конструктор для создания функций не рекомендуется, т.к. он снижает производительность, а также, являясь экивалентом функции eval(), он открывает потенциальную брешь в безопасности: код, который попадет в тело функции, обязательно выполнится и если этот код функция получает извне от пользователя, то передав вредоносный код, можно получить нежелательные последствия.
Конструктор - это функция, которая создает пустой объект и определяет его свойства и методы. Т.е. конструктор наполняет пустой объект данными. Конструкторы - эквивалент типов данных (классов) в ООП языках программирования (C++, C#, Java).
В других языках программирования можно создавать свои типы данных (классы), однако в JS такой возможности нет, можно оперировать лишь теми типами данных, которые заложены в язык: примитивами (Строка, Число, Булев тип), тривиальными типами null и undefined, сложными типами (Объект, Массив) и специальным типом Функция.
Ключевое слово this - это то, что отличает конструктор от обычной функции. Конструктор использует в своем теле ключевое слово this, чтобы в объект добавить какие-то свойства. Ключевое слово this в момент запуска функции всегда ссылается на пустой объект, который был создан с помощью оператора new.
Как мы отличаем, что объекты относятся к какому-то типу данных (классу)? Если 2 объекта имеют одинаковый набор свойств и методов, значит эти объекты относятся к одному типу (классу).
Функция-конструктор для создания объектов Point
function Point(x, y) { // Например, Point в других ЯП был бы отдельным типом данных Point (наравне с Object, Array etc.)
this.x = x; // Значение x будет записано в свойство x объекта this
this.y = y;
}
Создание 3х экземпляров класса Point В большинстве ЯП мы бы сказали, что эти переменные типа Point. Но в случае JS эти переменные типа Object, в которых есть по 2 свойства. Поскольку набор свойств этих объектов одинаков, можно условно говорить о том, что они принадлежат к одному типу (классу).
var leftTop = new Point(0, 0);
var topPoint = new Point(15, 30);
var rightBottom = new Point(30, 30);
Свойства и методы экзмепляра и свойства и методы конструктора
function Point(x, y) {
// Свойства экземпляра
this.x = x;
this.y = y;
// Метод экземпляра
this.print = function () {
console.log( this.x + ', ' + this.y );
}
// Свойства и методы экзмепляра создаются для каждого объекта, созданного с помощью конструктора, и принадлежат каждому конкретному объекту. Для этого требуется больше производительности и места в памяти.
}
Свойство функции-конструктора (аналог статического свойства в других ЯП)
Point.maxPointCount = 100;
Метод функции-конструктора (аналог статического метода в других ЯП)
Point.getOrigin = function () {
return new Point(0, 0) // Использовать this недопустимо
}
Свойства и методы конструктора создаются единожды и являются принадлежащими не конкретному объекту, а всем объектам, т.е. являются общими. Т.е. запись в память происходит один раз, что гораздо производительнее.
Создание экземпляров и работы с их свойствами и методами
var p1 = new Point(100, 200);
p1.x = 300;
p1.y = 400;
p1.print(); // '300, 400'
var p2 = new Point(100 ,200);
p2.print(); // '100, 200'
Работа со свойствами и методами конструктора
Point.maxPointCount = 10;
Point.getOrigin().print(); // '0, 0'
function Rectangle(w, h) {
// Хранения свойств в экземпляре объекта оправдано, т.к. значения этих свойств от объекта к объекту меняются
this.width = w;
this.height = h;
// Хранение метода, который не содержит индивидуальных данных и не будет меняться от объекта к объекту, неоправдано, т.к. будет тратиться место в памяти для его создания и хранения для каждого объекта в отдельности. Этот код корректен с т.з. синтаксиса, но его можно оптимизировать с помощтью записи метода не в экземпляр, а в прототип объекта.
this.getArea = function () {
return this.width * this.height;
}
}
В большинстве ЯП функции - это синтаксические единицы, позволяющие выделить блок кода для повторного использования. В JS функция - это кроме прочего и тип данных. По сути, создавая функцию, мы создаем объект (сущность), в котором хранится поведение и ряд свойств.
Как только мы определяем функцию, вместе с этой функцией появляется связанный с ней объект Prototype. Вместе с созданием функции и ее прототипа, у них появляются ссылки: каждая функция содержит в себе скрытое системное свойство prototype, а каждый прототп содежрит в себе скрытое системное свойство constructor. Свойство prototype связывает функцию-конструктор с прототипом, а свойство constructor наоборот связывает прототип с функцией-конструктором.
Когда мы создаем новый объект (экзмепляр), с помощью функции-конструктора (через ключевое слово new), то помимо всех действий, описанных выше (ключевое слово new создает пустой объект, функция-конструктор заполняет пустой объект свойствами), ключевое слово new также создает в этом объекте скрытую системную ссылку proto, которая связывает этот объект с его прототипом.
function Rectangle2(w, h) {
// Хранения свойств в экземпляре объекта оправдано, т.к. значения этих свойств от объекта к объекту меняются
this.width = w;
this.height = h;
}
Метод прототипа Rectangle будет доступен каждому экземпляру, но хранится будет в прототипе, а не в экземпляре. Это сэкономит нам ресурсы при последующем создании новых объектов с помощью конструктора Rectangle. Что происходит в коде ниже: мы берем функцию конструктор Rectangle() и с помощью ее свойства prototype мы получаем доступ к прототипу функции, т.е. к пустому объекту. Далее мы говорим этому пустому объекту, что в нем должен появиться метод getArea().
Rectangle2.prototype.getArea = function () {
return this.width * this.height; // this всегда ссылается на тот объект, на котором был вызван метод (а не на прототип, как можно сперва подумать)
}
Штампуем объекты с помощью конструктора
var a = new Rectangle2();
var b = new Rectangle2();
var rect = new Rectangle2(100, 50);
Когда мы обращаемся к объекту rect и вызываем на нем метод getArea()
, интерпретатор идет в объект rect и пытается найти в нем метод getArea()
. Но в объекте rect этого метода нет. Тогда интерпретатор автоматически берет ссылку __proto__
этого объекта, по этой ссылке поднимается к прототипу и ищет метод getArea()
уже в этом прототипе. Если метод getArea()
найден, метод запустится. Если нет, то в случае наличия прототипов у прототипа, поиск будет продолжаться вверх по иерархии прототитпов.
rect.getArea();
Свойство прототипа будет доступно всем экземплярам
Rectangle2.prototype.name = 'Rectangle';
var rect1 = new Rectangle2(100, 50);
var rect2 = new Rectangle2(30, 150);
В этом случае мы добавляем свойство name к объекту, а не прототипу. В прототипе не произойдет никаких изменений. При вызове rect1.name интерпретатор найдет свойство name в объекте и не станет подниматься дальше к прототипу. rect1.name = 'first rectangle';
В следующем примере разберем, как узнать, с помощью какого конструктора был создан объект. Создаем несколько объектов, используя различные нативные конструкторы
var MyArray = new Array(10);
var MyDate = new Date();
var MyString = new String('My string');
var MyObj = new Object();
var MyFunc = new Function('x', 'y', 'return x + y');
Создаем пользовательский конструктор
var MyCtor = function(x, y) {
this.x = x;
this.y = y;
}
Создаем объект с помощью пользовательского конструктора
var MyCtorObject = new MyCtor(12, 3);
Функция для вывода содержимого свойства constructor аргумента
function showCtor( obj, name ) {
console.log( 'Конструктор объекта ' + name + ' - это ' + obj.constructor ); // свойство constructor укажет, с каким конструктором связан объект
}
showCtor(MyArray, 'MyArray');
Конструктор объекта MyArray - это function Array() { [native code] } (это нативная функция и она для нас закрыта)
showCtor(MyDate, 'MyDate');
Конструктор объекта MyDate - это function Date() { [native code] }
showCtor(MyString, 'MyString');
Конструктор объекта MyString - это function String() { [native code] }
showCtor(MyObj, 'MyObj');
Конструктор объекта MyObj - это function Object() { [native code] }
showCtor(MyFunc, 'MyFunc');
Конструктор объекта MyFunc - это function Function() { [native code] }
showCtor(MyCtorObject, 'MyCtorObject');
Конструктор объекта MyCtorObject - это function MyCtor(x, y){this.x = x; this.y = y;}
Также с помощью свойства constructor можно создавать объекты того же типа, что и существующий конструктор. Вызываем конструктор, с помощью которого был создан объект MyDate:
var someNewObject = new MyDate.constructor();
console.log( someNewObject ); // Выведет дату в формате UTC
В других ЯП, таких как Java или C#, любой класс (пользовательский или системный) наследуется от базового типа - от типа Object. И это означает, что все типы данных, которые есть в ЯП имеют общую базовую функциональность, которая по наследству перешла от класса Object. В JS происходит тоже самое. Хоть мы и не можем сказать, что у нас есть полноценное наследование и что у нас есть типы данных (классы), которые мы создаем, тем не менее, когда мы создаем любой объект (или функцию-конструктор), автоматически у этого объекта появляется ряд базовых методов, которые можно вызвать на любом объекте. Все объекты, которые создаются в JS наследуются от Object. Давайте посмотрим, какие методы в себе содержит Object и для чего они используются.
Чтобы убедиться в том, что у нас есть наследование, рассмотрим пример.
function Rectangle3(w, h) {
this.width = w;
this.height = h;
}
Rectangle3.prototype.getArea = function () {
return this.width * this.height;
}
// Переопределение метода toString из Object
Rectangle3.prototype.toString = function () {
return 'Прямоугольник W: ' + this.width + ' , H: ' + this.height;
}
var rect3 = new Rectangle3(100, 200);
var rect4 = new Rectangle3(300, 400);
document.write( rect3 ); // Прямоугольник W: 100 , H: 200
document.write( rect4.toString() ); // Прямоугольник W: 300 , H: 400
Если рассмотреть вышеприведенный код в отладчике, то мы увидим, что у объекта rect3 есть свойства width и height, а также системное свойство-ссылка proto. Перейдя по ссылке proto мы увидим свойства и методы конструктора Rectangle3, который является прототипом для rect3, а именно: 2 пользовательских метода getArea() и toString(), системное свойство constructor, а также снова увидим ссылку proto. Перейдя по этой ссылке, мы попадем в Object, который является прототипом конструктора Rectangle3. В Object мы увидим ряд системных методов, таких как hasOwnProperty, valueOf, toString и т.д.
По сути мы не используем наследование, мы используем прототипы. Если в других ЯП наследование подразумевает копирование какой-то функциональности из родительского класса, то в случае с JS мы просто указываем, что один объект связан с другим объектом посредством прототипов, т.е. у одного объекта есть прототип в виде другого объекта. Object является прототипом для всех объектов, которые существуют в JS.
Т.о., если на объекте rect3 мы вызовем свойство width, интерпретатор обратиться к объету rect3 и найдет это свойство. Если мы вызовем метод getArea(), то интерпретатор обратится к объекту rect3, не найдет этот метод, по ссылке proto перейдет к прототипу объекта rect3 и найдет метод в нем. Если мы вызовем метод hasOwnProperty, интерпретатор двинется по цепочке прототипов и, не найдя этот метод в объекте и его прототипе (конструкторе), по ссылке proto обратиться к прототипу прототипа объекта rect3 (т.е. к Object) и, найдя там метод hasOwnProperty, вернет результат (ту функциональность, которая заложена в этом методе).
Итак, методы Object, о которых обязательно нужно знать.
С помощью него мы можем превратить объект в строковое представление. Мы можем заместить системный метод пользовательским, присвоив любую функцию прототипу конструктора Rectangle3 (Rectangle3.prototype.toString = function(){}). Если вызвать rect4.toString() , то интерпретатор начнет поиск этого метода в цепочке прототипов, и найдя первый встретившийся метод toString(), выполнит его. Поскольку первым встретившимся методом toString() будет разработанный нами пользовательский метод в прототипе конструктора, то именно он и выполнится. До системного метода toString() в Object интерпретатор уже не дойдет. Т.о. мы можем замещать любые системные методы.
Если вывести в документ rect3 и rect4.toString(), то результат будет одинаковым: вывод функциональности метода toString() Т.о. с помощью пользовательского метода toString() мы реализовали возможность превращать объект в строковое значение.
...
...
Инкапсуляция - сокрытие реализации и данных объекта
var MyClass = function () {
// Закрытый метод - о нем знает только конструктор MyClass
var privateMethod = function () {
document.write( 'MyClass Закрытый метод' );
};
// Открытый метод
this.publicMethod = function () {
document.write( 'MyClass Открытый метод' );
privateMethod(); // обращение к закрытому методу возможно только внутри открытого метода конструктора
}
}
var myClassObj = new MyClass();
myClassObj.publicMethod(); // Выведет сначала 'MyClass Открытый метод', а ниже 'MyClass Закрытый метод'
// myClassObj.privateMethod(); // ошибка - нельзя обратиться к закрытому методу напрямую
Наследование - механизм ООП позволяющий передать поведение и свойства от одного объекта другому объекту. Т.е. от родительского класса к классу-наследнику. Но т.к. в JS нет понятия классов, то и нет классического наследования. Наследование в JS происходит через подмену прототипов.
Базовая функция-конструктор, которая создает объекты типа Человек
function Human (name) {
this.name = name;
this.talk = function () {
document.write( 'Hello! My name is ' + this.name );
}
}
Производная функция-конструктор (производный класс)
function Student(name) {
this.name = name;
this.school = 'Cool school'
}
function Worker(name) {
this.name = name;
this.speciality = 'Frontend Developer'
}
Создадим экземпляр родителя
var human = new Human('name');
Наследуем Student и Worker от Human, установив функцию, как прототип. Теперь конструкторы Student и Worker будут наследовать не прототип Object, а прототип Human.
Student.prototype = human;
Worker.prototype = human;
var Alex = new Student('Alexandr');
var Andrew = new Worker('Andrew');
В конструкторах Student и Worker нет метода talk(), но зато он есть в прототипе Human
Alex.talk(); // 'Hello! My name is Alexandr'
document.write( 'Alex.school is ' + Alex.school ); // Alex.school is Cool school
Andrew.talk(); // Hello! My name is Andrew
document.write( 'Andrew.speciality is ' + Andrew.speciality ); // Andrew.speciality is Frontend Developer
Это не единственный способ реализации наследования. Множество других способов описано в книге Стояна Стефанова "Javascript Шаблоны"
function Worker2(name) {
this.name = name;
this.speciality = 'Backend Developer';
// Переопредление метода из базовой функции-конструктора (override)
this.talk = function () {
document.write( 'Привет! Меня зовут ' + this.name );
}
}
Worker2.prototype = human;
var Billy = new Worker2('Billy');
Billy.talk(); // Привет! Меня зовут Billy
Если стоит задача в проекте использовать ООП, то вместо написания больших объемов кода, который требуется для реализации ООП в JS, можно воспользоваться готовыми фреймворками. Одним из таких фреймворков, заточенных под применение техник ООП является Mootools.
instanceof
- оператор проверки типа объекта, позволяет проверить, с помощью какого конструктора был создан объект. Это аналог опреатора is
в C#.
var x = new Array(10);
if ( x instanceof Array ) { // true
document.write( 'x is array' );
}
typeof
- оператор для пролучения типа объекта в виде строкового значения. Это аналог GetType
в C#.
var test = 'hello';
document.write( typeof test ); // string
var test2 = 123;
document.write( typeof test2 ); // number
var test3 = true;
document.write( typeof test3 ); // boolean
var test4 = new Array();
document.write( typeof test4 ); // object
var test5 = new Date();
document.write( typeof test5 ); // object
document.write( typeof test5.toString ); // здесь результат function, т.к. мы обращаемся к методу прототипа test5, а в этом методе записана функция
Где и как применяют оператор typeof. С помощью оператора typeof можно получить строку, указывающую тип операнда. Оператор typeof может быть удобен, чтобы отличить объект от базового типа.
function printMessage(message) {
if ( typeof message != 'string' ) {
console.log( 'Параметр должен быть строкового типа!' );
} else {
document.write(message);
}
}
printMessage( Array() ); // 'Параметр должен быть строкового типа!'
printMessage( 'Hello world!' ); // 'Hello world!'
- Дэвид Флэнаган. JavaScript. Подробное руководство (JavaScript: The Definitive Guide)
- Николас Закас. JavaScript. Оптимизация производительности (High Performance: JavaScript)
- Стоян Стефанов. JavaScript. Шаблоны (JavaScript: Patterns)