Pull to refresh
0

ООП в JavaScript

Reading time 5 min
Views 53K


В данной статье мы поговорим об основных особенностях объектно-ориентированного программирования в 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. 1. Создаёт новый объект:
    iaDonkey = new Object(); // присваивается новый пустой объект.
  2. 2. Помещает свойства конструктора объекта Donkey:
    aDonkey.constructor == Donkey // true
    iaDonkey instanceof Donkey // true
  3. 3. Устанавливает объект для переноса в Donkey.prototype:
    iaDonkey.__proto__ = Donkey.prototype
  4. 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; 
    }

  1. Создание нового значения (n) и запись значения prototype в proto.
  2. Вызов нашего метода конструктор через apply.
  3. Возвращение нового объекта, класса 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 зависит от контекста, т.е. от того, как мы вызываем функцию.
Tags:
Hubs:
+2
Comments 50
Comments Comments 50

Articles

Information

Website
www.nixsolutions.com
Registered
Founded
1994
Employees
1,001–5,000 employees