ООП в JavaScript
В данной статье мы поговорим об основных особенностях объектно-ориентированного программирования в 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’]); // как к элементу массива через квадратные скобки
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() в контексте нового объекта:
// То же самое, только на грубом псевдокоде: function New (F, args) < /*1*/ var n = ; /*2*/ F.apply(n, args); /*3*/ return n; >
- Создание нового значения (n) и запись значения prototype в proto .
- Вызов нашего метода конструктор через apply .
- Возвращение нового объекта, класса New .
Инкапсуляция через замыкания
Замыкание — это основанный на области видимости механизм, который может создаваться через функцию. Каждая функция создаёт новую область видимости.
В этом цикле десятка выводится на экран десять раз: после последней итерации будет 10, и тогда начнётся выполнение setTimeout .
Анонимная самовызывающаяся функция позволяет начать выполнение функции сразу после ее объявления.
Мы применили принцип замыкания: объявляем функцию, передаем в неё фактическое значение, и она «замыкает» в себе значение переменной 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__ считается небезопасным, и не все браузеры это позволяют.
Создание потомка из базового класса
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 зависит от контекста, т.е. от того, как мы вызываем функцию.
Что такое инкапсуляция в javascript
Инкапсуляция является одним из ключевых понятий объектно-ориентированного программирования и представляет сокрытие состояния объекта от прямого доступа извне. По умолчанию все свойства объектов являются публичными, общедоступными, и мы к ним можем обратиться из любого места программы.
function User(pName, pAge) < this.name = pName; this.age = pAge; this.displayInfo = function()< document.write("Имя: " + this.name + "; возраст: " + this.age); >; >; var tom = new User("Том", 26); tom.name=34; console.log(tom.name);
Но мы можем их скрыть от доступа извне, сделав свойства локальными переменными:
function User (name, age) < this.name = name; var _age = age; this.displayInfo = function()< document.write("Имя: " + this.name + "; возраст: " + _age + "
"); >; this.getAge = function() < return _age; >this.setAge = function(age) < if(typeof age === "number" && age >0 && age <110)< _age = age; >else < console.log("Недопустимое значение"); >> > var tom = new User("Том", 26); console.log(tom._age); // undefined - _age - локальная переменная console.log(tom.getAge()); // 26 tom.setAge(32); console.log(tom.getAge()); // 32 tom.setAge("54"); // Недопустимое значение
В конструкторе User объявляется локальная переменная _age вместо свойства age . Как правило, названия локальных переменных в конструкторах начинаются со знака подчеркивания.
Для того, чтобы работать с возрастом пользователя извне, определяются два метода. Метод getAge() предназначен для получения значения переменной _age. Этот метод еще называется геттер (getter). Второй метод — setAge , который еще называется сеттер (setter), предназначен для установки значения переменной _age.
Плюсом такого подхода является то, что мы имеем больший контроль над доступом к значению _age. Например, мы можем проверить какие-то сопутствующие условия, как в данном случае проверяются тип значение (он должен представлять число), само значение (возраст не может быть меньше 0).