Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Sha1fei/eaaccd3336ddd6d436faa1bf906f6119 to your computer and use it in GitHub Desktop.
Save Sha1fei/eaaccd3336ddd6d436faa1bf906f6119 to your computer and use it in GitHub Desktop.
JS Advanced 1. Конструкторы и прототипы

JS Advanced 1. Конструкторы и прототипы

Содержание

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

Создание пользовательских конструкторов, ключевое слово this

Конструктор - это функция, которая создает пустой объект и определяет его свойства и методы. Т.е. конструктор наполняет пустой объект данными. Конструкторы - эквивалент типов данных (классов) в ООП языках программирования (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

Конструктор Object и его методы

В других ЯП, таких как 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, о которых обязательно нужно знать.

Метод toString().

С помощью него мы можем превратить объект в строковое представление. Мы можем заместить системный метод пользовательским, присвоив любую функцию прототипу конструктора Rectangle3 (Rectangle3.prototype.toString = function(){}). Если вызвать rect4.toString() , то интерпретатор начнет поиск этого метода в цепочке прототипов, и найдя первый встретившийся метод toString(), выполнит его. Поскольку первым встретившимся методом toString() будет разработанный нами пользовательский метод в прототипе конструктора, то именно он и выполнится. До системного метода toString() в Object интерпретатор уже не дойдет. Т.о. мы можем замещать любые системные методы.

Если вывести в документ rect3 и rect4.toString(), то результат будет одинаковым: вывод функциональности метода toString() Т.о. с помощью пользовательского метода toString() мы реализовали возможность превращать объект в строковое значение.

Метод valueOf()

...

Метод hasOwnProperty()

...

Техники ООП: инкапсуляция, наследование, полиморфизм

Инкапсуляция

Инкапсуляция - сокрытие реализации и данных объекта

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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment