В данной статье мы поговорим об основных особенностях объектно-ориентированного программирования в JavaScript:
- создание объектов,
- функция-конструктор,
- инкапсуляция через замыкания,
- полиморфизм и ключевые слова
call/apply
, - наследование и способы его реализации.
Объекты в JavaScript
Объект в JavaScript — это ассоциативный массив, который содержит в себе наборы пар ключ-значение («хэш», «объект», «ассоциативный массив» означают в JavaScript одно и то же).
Создание объекта в JavaScript:
var obj = new Object(); // вызов функции конструктора
var obj = {}; // при помощи фигурных скобок.
Задание свойств объекта:
obj.name = ‘Victor’; // через .property
obj[‘name’]=‘Victor’; // как элементу массива
Обращение к свойствам:
console.log(obj.name); // через .property
console.log(obj[‘name’]); // как к элементу массива через квадратные скобки
Расширенный вариант:
var obj = {
name : ’Viktor’,
age : 32
};
Constructor и ключевое слово new
«Конструктор — это любая функция, которая используется как конструктор». До появления ECMAScript 6 в JavaScript не было понятия конструктор. Им могла быть любая функция, которая вызывается с помощью ключевого слова new
.
Пример использования конструктора:
var Donkey = function(){ //… }; // создаем объект «ослик»
var iaDonkey = new Donkey();
При вызове new Donkey (), JavaScript делает четыре вещи:
- 1. Создаёт новый объект:
iaDonkey = new Object(); // присваивается новый пустой объект.
- 2. Помещает свойства конструктора объекта Donkey:
aDonkey.constructor == Donkey // true
iaDonkey instanceof Donkey // true
- 3. Устанавливает объект для переноса в Donkey.prototype:
iaDonkey.__proto__ = Donkey.prototype
- 4. Вызывает Donkey() в контексте нового объекта:
var iaDonkey = function(){ this.constructor(); // function Donkey() // … };
// То же самое, только на грубом псевдокоде:
function New (F, args) {
/*1*/ var n = {'__proto__': F.prototype};
/*2*/ F.apply(n, args);
/*3*/ return n;
}
- Создание нового значения (n) и запись значения
prototype
вproto
. - Вызов нашего метода конструктор через
apply
. - Возвращение нового объекта, класса
New
.
Инкапсуляция через замыкания
Замыкание — это основанный на области видимости механизм, который может создаваться через функцию. Каждая функция создаёт новую область видимости.
Рассмотрим два примера.
Пример 1:
for (var i = 0; i < 10; i++) {
setTimeout(function () { console.log(i); }, 0);
}
В этом цикле десятка выводится на экран десять раз: после последней итерации будет 10, и тогда начнётся выполнение setTimeout
.
Пример 2:
for (var i = 0; i < 10; i++) {
(function (m) {
setTimeout(function () { console.log(m); },0);
})(i)
}
Анонимная самовызывающаяся функция позволяет начать выполнение функции сразу после ее объявления.
Мы применили принцип замыкания: объявляем функцию, передаем в неё фактическое значение, и она «замыкает» в себе значение переменной i. m
попытается через замыкания получить значение из ближайшей верхней области видимости. А так как мы передали ее через самовызывающуюся функцию, то она каждый раз будет равна своему значению (значению переменной i
), и мы 10 раз получим от 0 до 9.
Этот пример про замыкания и инкапсуляцию взят из реального проекта:
Есть функция BarChart
, у нее есть публичные методы — построить график и обновить его значения. Есть приватные методы и свойства — что нам нужно выполнять, не показывая это окружающему миру.
Если мы обратимся к BarChart
через new
, то получится, что это функция-конструктор, и мы создадим новый объект этого класса. Но все приватные методы замкнутся в этой переменной, и мы сможем обращаться к ним внутри. Они останутся в области видимости, которую создаст эта функция.
Полиморфизм и ключевые слова call/apply
Применение конструкции apply
:
var obj = { outerWidth: ‘pliers‘ };
function getWidth(){
return this.outerWidth;
}
var a = getWidth();
var b = getWidth.apply(obj);
console.log(a); // текущая ширина браузера, this будет windows
console.log(b); // на экран выведется pliers. outerWidth — это свойство объекта windows, мы, по сути, вызовем windows.outerWidth
Вызов механизма:
Calling func.call(context, a, b...)
эквивалентен записи:
func(a, b...), but this == context.
Оба вызова идентичны, только apply позволяет передавать параметры через массив.
call(context, param1, param2 …)
apply(context, [args])
Четыре варианта вызова и его результаты:
Вызов function: function(args) – this == window
Вызов method: obj.funct(args) – this == obj
Apply: func.apply(obj,args) – this == obj
Constructor: new func(args) – this == new object
Наследование и методы реализации
Модель базового фильтра — стандартный набор параметров, который есть в фильтре любого приложения. Эти параметры необходимы для пагинации, номера страницы и т.п.
Задаём ему метод через прототип. После получения модели сервера вызываем этот метод, и он преобразует некоторые наши данные в нужный формат.
Есть класс-наследник и конкретная страница “RouteHistorical”. Класс наследуется от базового фильтра, но дополнительно имеет свои поля и параметры.
В строке 73 мы передаём в базовый класс через контекст apply
новосозданный объект RouteHistorical
и те же аргументы. Метод инициализирует все поля, и мы получаем новый объект.
Строки 81-82 позволяют нам сделать RouteHistorical
наследником базового фильтра. В строке 81 мы записываем ссылку на класс конструктора базы в свойство prototype
. Метод prototype
перезаписывается полностью, и конструктор теряется. Когда мы создаем новый объект, он не будет знать, к чему обратиться.
В строке 82 мы задаем свойству prototype.constructor
ссылку на саму себя. Свойство класса constructor
всегда ссылается на самого себя.
Прототипы
Свойство prototype
имеет смысл в паре с ключевым словом new
. Когда мы создаем новый объект, то записываем значение prototype
в свойство __proto__
. Оно содержит ссылку на класс, который является родителем для нашего класса.
prototype
нужен только для того, чтобы сказать, что нужно записать в __proto__
при инстанцировании нового объекта.
// unsafe
var filter = {
EnablePagination: true
};
function BaseFilter(size) {
this.PageSize = size;
this.__proto__ = filter;
}
// safe
var filter= {
EnablePagination: true
};
function BaseFilter(size) {
this.PageSize = size;
}
BaseFilter.prototype = filter;
Две записи одинаковы, но обращаться напрямую к __proto__
считается небезопасным, и не все браузеры это позволяют.
Создание потомка из базового класса
Функция extend
:
function extend(Child, Parent) {
var F = function() { }
F.prototype = Parent.prototype //
Child.prototype = new F() // при создании Child в __proto__ запишется наш родитель prototype
Child.prototype.constructor = Child // задаём конструктор, должен ссылаться на самого себя.
Child.superclass = Parent.prototype // чтобы иметь доступ к методам Parent
};
Использование:
function BaseFilterModel(..) { ... }
function RouteHistoricalFilterModel(..) { ... }
instanceof
Позволяет определить, является ли объект экземпляром какого-либо конструктора на основе всей цепочки прототипирования.
instanceof (псевдокод метода):
function isInstanceOf(obj, constructor) {
if (obj.__proto__ === constructor.prototype) {
return true;
}
else if (obj.__proto__ !== null) {
return isInstanceOf(obj.__proto__, constructor)
}
else {
return false
}
};
Итог
1. В JavaScript до ECMAScript 6 не было классов, были только функции конструктора, которые вызываются с помощью ключевого слова new
.
2. Цепочка прототипирования — это основа наследования в JavaScript.
3. Когда мы обращаемся к свойству, то оно ищется в объекте. Если не находится, то в __proto__
, и так далее по всей цепочке. Таким образом в JavaScript реализуется наследование.
4. fn.__proto__
хранит ссылку на fn.prototype
.
5. Оператор new
создает пустой объект с единственным свойством __proto__
, который ссылается на F.prototype
. Конструктор выполняет F
, где контекст this
— ранее созданный объект, устанавливает его свойства и возвращает этот объект.
6. Оператор instanceof
не проверяет, чтобы объект был создан через конструктор ObjectsConstructor
, а принимает решение на основе всей цепочки прототипирования.
7. В JavaScript this
зависит от контекста, т.е. от того, как мы вызываем функцию.
Автор: NIX Solutions