ООП в JavaScript

  • Классы и/или конструкторы
  • Прототипы
  • Наследование свойств
  • Надклассы и подклассы
  • Эмуляция классического наследования
  • Расширение без наследования (*extend)
  • Define Class
  • Классы в ES6
  • Композиция

Объект – это структура, которая содержит данные, а также может содержать методы для работы с этими данными. Объект группирует связанные значения и методы в единый
удобный набор, который, как правило, облегчает процесс программирования.

Класс -  определитель структуры объекта. Задает поля, которые содержатся в объекте

Как создать обьект?

Лиетрал

var rect = {
    width: 5, 
    height: 10
};

Самый простой и быстрый способ

Фабрика(Factory)

function RectangleFactory(w, h) {
    return {
        width: w,
        height: h
    }
}

var rectangle = RectangleFactory(5, 10);

Конструктор

var rect = new Object({width: 5, height: 10});

var arr = new Array(10);

var date = new Date();


function Rectangle(w, h) {
    this.width = w;
    this.height = h;
}

var rectangle = new Rectangle(5, 10);

Что делает new?

  • создает новый пустой объект без каких-либо свойств
  • вызывает функцию, передавая ей только что созданный объект в виде значения ключевого слова this
  • !устанавливает ссылку на прототип(На свойство prototype функции конструктора)

Function после new = ConstructorFunction

Главная задача конструктора - инициализация вновь созданного объекта – установке всех его
свойств, которые необходимо инициализировать до того, как объект сможет использоваться

Добавление новых свойств к объекту, на который
ссылается ключевое слово this

ConstructorFunction Riddle

const rect = new Rectangle(10, 5)

function Rectangle(w, h) {
    this.width = w;
    this.height = h;
}
const rect = Rectangle.call({}, 10, 5)

function Rectangle(w, h) {
    this.width = w;
    this.height = h;
    return this;
}
// Определяем конструктор.
// Инициализация объекта с помощью "this"

function Rectangle(w, h) {
    this.width = w;
    this.height = h;
}


// Вызываем конструктор для создания двух объектов Person. 
var rect1 = new Rectangle(2, 4); // rect1 = { width:2, height:4 };
var rect2 = new Rectangle(8.5, 11); // rect2 = { width:8.5, height:11 };
  • ES5 - Нет классов, только ФК
  • Обычно ФК ничего не возвращает, а лишь инициализирует полученный обьект
function Rectangle(w, h) {
    this.width = w;
    this.height = h;
    return 25;
}


const rect = new Rectangle(10, 5);
// {width: 10, height: 5}
function Rectangle(w, h) {
    this.width = w;
    this.height = h;
    return {z: 13};
}


const rect = new Rectangle(10, 5);
function Rectangle(w, h) {
    this.width = w;
    this.height = h;
    return () => {};
}


const rect = new Rectangle(10, 5);
// {z: 13}
// () => {}

ConstructorFunction Riddle

Рекомендации к ФК

  • Выбирайте понятные имена ФК
  • Согласно JS Code Guidelines ФК пишутся с большой буквы.
  • Вызов ФК только с new
  • В современном front-end development flow довольно слабо распространены

И еще один метод...

...но для начала рассмотрим прототипы

Prototype

Попробуем вычислить площадь прямоугольника

function computeAreaOfRectangle(r) { return r.width * r.height; }

computeAreaOfRectangle(new Rectangle(4, 5));
// Создать объект Rectangle
var r = new Rectangle(8.5, 11);

// Добавить к объекту метод
r.area = function() { return this.width * this.height; }

// Теперь рассчитать площадь, вызвав метод объекта
var a = r.area();

совсем не обьектоно-ориентировано, но очень функционально

добавлять новый метод? каждый раз?

function Rectangle(w, h) {
    this.width = w;
    this.height = h;
    this.area = function( ) { 
        return this.width * this.height; 
    }
}

var r = new Rectangle(8.5, 11);
var a = r.area();

Уже лучше, но еще не совсем оптимально.

Нужно место для хранения свойств и методов общих для всех экземпляров класса. (обектов созданых одной ФК).

 

И оно есть....

const circle = { radius: 25 }

Prototype Riddle

Object.getPrototypeOf(circle);
const date = new Date();
Object.getPrototypeOf(date);
const regExp = /[a-z]/g
Object.getPrototypeOf(regExp);
const myFunc = function(r) {
   return { radius: r };
}
const myArrow = r => ({ radius: r });
Object.getPrototypeOf(myFunc);
Object.getPrototypeOf(myArrow);

don't use .__proto__ in production

Prototype

const circle = {radius: 25};

function Circle(r) {
    this.radius = r;
}


Object.getPrototypeOf(circle) === Object.prototype // true
Object.getPrototypeOf(Circle) === Function.prototype // true

circle.prototype // undefined

Circle.prototype // {constructor: Circle}
Circle.prototype.constructor === Circle // true

  • все объекты в JavaScript содержат внутреннюю ссылку на объект, известный как прототип
  • "Все" функции имею свойство .prototype
  • изначально прототип это обьект с одним свойством сonstructor 
  • значение constructor - ссылка на саму ФК
  • !свойсва прототипа наследеются
// Функцияконструктор инициализирует те свойства, которые могут
// иметь уникальные значения для каждого отдельного экземпляра.
function Rectangle(w, h) {
    this.width = w;
    this.height = h;
}

// Прототип объекта содержит методы и другие свойства, которые должны
// совместно использоваться всеми экземплярами этого класса.
Rectangle.prototype.area = function() { return this.width * this.height; }

var r = new Rectangle(8, 1);
var a = r.area(); // 80

Constructor

Prototype

Instances

Rectangle()

              prototype

constructor

area

new Rectangle(1, 2)

new Rectangle(3, 4)

Inherits

Inherits

  • Свойства не копируются из объектапрото
    типа в новый объект
  • Использование объектов-прототипов может в значительной степени уменьшить объем памяти
  • Объект наследует свойства, даже если они были добавлены в прототип после создания объекта
  • Как результат автоматическое наследование, как процесс поиска значения свойсва

Наследование свойств

Унаследованные свойства ничем не отличаются от обычных свойств объекта.
Они поддаются перечислению в цикле for/in и могут проверяться с помощью оператора in. Отличить их можно только с помощью метода Object.hasOwnProperty():

var r = new Rectangle(2, 3);
r.hasOwnProperty("width"); // true: width – непосредственное свойство "r"
r.hasOwnProperty("area"); // false: area – унаследованное свойство "r"
"area" in r; // true: area – свойство объекта "r"

Чтение и запись унаследованных свойств

var Circle = function (x, y, r) {
    this.x = x;
    this.y = y;
    this.r = r
}

Circle.prototype.pi = 3.14;
Circle.prototype.area = function() {return this.pi * Math.pow(this.r, 2)};

var c = new Circle(2, 3, 1);

// c  ->> {r:1, x:2, y:3}
// Object.getPrototypeOf(c) -->
    // {pi: 3.14, area: function() {return this.pi * Math.pow(this.r, 2)}}


c.x  // 2
c.pi // 3.14

c.x = 4 // c --> {r:1, x:4, y:3}
c.pi = 4 // c --> {r:1, x:4, y:3, pi:4}, Object.getPrototypeOf(c).pi - 3.14
function Circle(r) {
    this.radius = r;
}

Circle.prototype.pi = 3.14;
Circle.prototype.are = () => this.pi*(this.r**2);
Circle
   .prototype
        .pi
        .area
   .__proto__
const smallCircle = new Circle(10);
smallCircle
    .radius // 10
    .__proto__
const bigCircle = new Circle(1000);
bigCircle
    .radius // 1000
    .__proto__

internal hidden link

internal hidden link

instance

instance

Function.prototype

Наследование свойств происходит только при чтении значений свойств,
но не при их записи.

Встроенные классы также имеют объекты-прототипы, как результат могут быть расширены

Наследуемся правильно в JavaScript

Создадим класс PositionedRectangle более специфический чем Rectangle и наследующий его свойства и методы.

// Этот код нам знаком...
function Rectangle(w, h) {
    this.width = w;
    this.height = h;
}

Rectangle.prototype.area = function( ) { return this.width * this.height; }

// Далее идет определение подкласса
function PositionedRectangle(x, y, w, h) {

    // Вызываем конструктор надкласса
    // _super()
    Rectangle.call(this, w, h);

    // Далее сохраняются координаты верхнего левого угла прямоугольника
    this.x = x;
    this.y = y;
}

Если мы будем использовать объект-прототип по умолчанию,который создается при определении конструктора PositionedRectangle(),
был бы создан подкласс класса Object.

// Чтобы создать подкласс класса Rectangle, необходимо явно создать объектпрототип.
PositionedRectangle.prototype = new Rectangle();

// Мы создали объектпрототип с целью наследования, но мы не собираемся
// наследовать свойства width и height, которыми обладают все объекты
// класса Rectangle, поэтому удалим их из прототипа.
delete PositionedRectangle.prototype.width;
delete PositionedRectangle.prototype.height;

// Переопределяем конструктор
PositionedRectangle.prototype.constructor = PositionedRectangle;

// можно приступать к добавлению методов экземпляров.
PositionedRectangle.prototype.contains = function(x,y) {
    return (x > this.x && x < this.x + this.width &&
                y > this.y && y < this.y + this.height);
}
var r = new PositionedRectangle(2,2,2,2);

console.log(r.contains(3,3)); // true // Вызывается метод экземпляра
console.log(r.area( )); // 4 // Вызывается унаследованный метод экземпляра

// Работа с полями экземпляра класса:
console.log(r.x + ", " + r.y + ", " + r.width + ", " + r.height); // "2, 2, 2, 2"


// Наш объект может рассматриваться как экземпляр всех 3 классов
console.log(
    r instanceof PositionedRectangle &&
    r instanceof Rectangle &&
    r instanceof Object
); //true

Создание и работа с экземпляром

Сложности итд...

  • необходимость вызова конструктора надкласса из конструктора подкласса, причем

    конструктор надкласса приходится вызывать как метод вновь созданного объекта.

  • потребоность явно создать объект-прототип как экземпляр надкласса

  • после чего - явно изменить свойство constructor объекта-прототипа

  • есть желание удалить любые свойства, которые

    создаются конструктором надкласса в объекте-прототипе

Решения

// Вместо использования метода call() или apply()
// для вызова конструктора надкласса как метода данного объекта

Rectangle.call(this, w, h);

// можно упростить синтаксис конструктора, добавив
// свойство superclass в объектпрототип подкласса:

PositionedRectangle.prototype.superclass = Rectangle;


function PositionedRectangle(x, y, w, h) {
    this.superclass(w,h);
    this.x = x;
    this.y = y;
}

Упрощение функции конструктора, отказ от call()

!!! Не работает для многоуровневого наследования

Установка прототипа

PositionedRectangle.prototype = new Rectangle();

delete PositionedRectangle.prototype.width;
delete PositionedRectangle.prototype.height;
PositionedRectangle.prototype = Rectangle.prototype;
Object.setPrototypeOf(PositionedRectangle.prototype, Rectangle.prototype);

Object.create() создаёт новый объект с указанными объектом прототипа и свойствами.

PositionedRectangle.prototype = Object.create(Rectangle.prototype);

Object.create()

Object.create(proto[, propertiesObject])

proto - Объект, который станет прототипом вновь созданного объекта.

 

propertiesObject - Необязательный параметр. Если указан и не равен undefined, должен быть объектом.

все вместе

function Rectangle(w, h) {
    this.width = w;
    this.height = h;
}

Rectangle.prototype.area = function() { return this.width * this.height; }


function PositionedRectangle(x, y, w, h) {
    Rectangle.call(this, w, h);

    this.x = x;
    this.y = y;
}

PositionedRectangle.prototype = Object.create(Rectangle.prototype);
PositionedRectangle.prototype.constructor = PositionedRectangle;

PositionedRectangle.prototype.pertimeter = function() {return 2*(this.width + this.height)}

установка constructor

function Rectangle(w, h) {
    this.width = w;
    this.height = h;
}

Rectangle.prototype.copy = function() { return new this.constructor(this.width, this.height)}


function PositionedRectangle(x, y, w, h) {
    Rectangle.call(this, w, h);

    this.x = x;
    this.y = y;
}

PositionedRectangle.prototype = Object.create(Rectangle.prototype);

const posRect = new PositionedRectangle(1,2,3,4);

const samePosRect = posRect.copy();

Вызов переопределенных методов

PositionedRectangle.prototype.toString = function() {
    return "(" + this.x + "," + this.y + ") " + // поля этого класса
    Rectangle.prototype.toString.apply(this); // вызов надкласса по цепочке
}
PositionedRectangle.prototype.toString = function( ) {
    return "(" + this.x + "," + this.y + ") " + // поля этого класса
    this.superclass.prototype.toString.apply(this);
}

Java-Style Classes

в JavaScript

Свойства экземпляра

Каждый объект имеет собственные копии свойств экземпляра

var Rectangle = function (w, h) {
    this.width = w;
    this.height= h;
}

var rect = new Rectangle(5, 10);


r.width //свойство экземпляра

свойства экземпляра в JavaScript – свойства, которые создаются и/или инициализируются функцией конструктором

Методы экземпляра

Схожи со свойствами, но являются функцией. Может содержаться в прототипе а не в эеземпляре класа

var Rectangle = function (w, h) {
    this.width = w;
    this.height= h;
};

Rectangle.prototype.area = function() {return this.width * this.height};

var rect = new Rectangle(5, 10);

r.area//метод экземпляра

Методы экземпляра ссылаются на объект, или экземпляр, с которым они работают, при помощи ключевого слова this.

Свойства класса

Свойство связанное с самим классом, а не с каждым экземпляром этого класса

var Rectangle = function (w, h) {
    this.width = w;
    this.height= h;
};

Rectangle.prototype.area = function() {return this.width * this.height};

Rectangle.UNIT = new Rectangle(1,1);

r.UNIT //свойство экземпляра

Методы класса

метод, связанный с классом, а не с экземпляром класса; он вызывается через сам класс

var Rectangle = function (w, h) {
    this.width = w;
    this.height= h;
};

Rectangle.prototype.area = function() {return this.width * this.height};

Rectangle.max= function(r1, r2) {
    if (r1.area() > r2.area()) {
        return r1;
    }

    return r2;
};

Rectangle.max// метод класса

this ссылается на саму функцию конструктор

Надклассы и подклассы

defineClass()

// superclass  - Function
// constructor - Function
// methods     - Object of Functions

function defineClass(superclass, constructor, methods) {
        
    constructor.prototype = Object.create(superclass.prototype);
    constructor.prototype.constructor = constructor;
    constructor.prototype.superClass = superclass;

   
    for(var m in methods) {
        constructor.prototype[m] = methods[m];
    }
    
    
    return constructor;

}

PositionedRectangle = defineClass(
    Rectangle, 
    function(x, y, w, h){
        this.superClass(w,h);
        this.x=x, 
        this.y=y
    },
    {perimeter:function(){return 2*(x+y)}}
)

Расширение без наследования

Заимствование методов одного класса для использования в другом

function borrowMethods(borrowFrom, addTo) {
    var from = borrowFrom.prototype; // прототип источник
    var to = addTo.prototype; // прототип приемник

    for(m in from) { // Цикл по всем свойствам прототипа-источника

        if (typeof from[m] != "function") continue; // Игнорировать не функции
        to[m] = from[m]; // Заимствовать метод
    }
}

Классы-смеси, или просто смеси (Mixin) -

классы, разрабатываемые специально с целью заимствования, обычно ничего особо полезного не

делающие, зато реализующих методы, которые могут быть заимствованы другими классами. 

function GenericToString() {}
GenericToString.prototype.toString = function( ) {
    //реализация универсального метода toString
}


function GenericEquals() {}
GenericEquals.prototype.equals = function(compareTo) {
    // реализация универсального метода equals
    // для сравнения 2х объектов
}


// Заимствование некоторых методов
borrowMethods(GenericEquals, Rectangle);
borrowMethods(GenericToString, Rectangle);
// Эта смесь содержит метод, зависящий от конструктора. Оба они,
// и конструктор, и метод должны быть заимствованы.

function Colored(c) { this.color = c; }
Colored.prototype.getColor = function() { return this.color; }

// Определение конструктора нового класса
function ColoredRectangle(w, h, c) {
    Rectangle.call(this, w, h); // Вызов конструктора надкласса
    Colored.call(this, c); // и заимствование конструктора Colored
}

// Настройка объектапрототипа на наследование методов от Rectangle
ColoredRectangle.prototype = Object.create(Rectangle.prototype);
ColoredRectangle.prototype.constructor = ColoredRectangle;

// Заимствовать методы класса Colored в новый класс
borrowMethods(Colored, ColoredRectangle);

"Множественное наследование"

Реализации на уровне библиотек

// jQuery
// jQuery.extend( [deep ], target, object1 [, objectN ] )
// Returns target

var object1 = {
    apple: 0,
    banana: { weight: 52, price: 100 },
    cherry: 97
};

var object2 = {
    banana: { price: 200 },
    durian: 100
};

$.extend(object1, object2)  // Merge object2 into object1

object1 //{"apple":0,"banana":{"price":200},"cherry":97,"durian":100}

_.assign, _.merge from lodash, angular.extend

Classes ES6

Базовый класс

class Rectangle {
    constructor(w, h) { //class constructor
        this.width = w;
        this.height = h;
    }

    area() { //class method
    	return this.width * this.height;
    }
}

var rect = new Rectangle(5, 10);

rect.width // 10
rect.area() // 50
rect.hasOwnProperty("area") //false
const iForgotNew = Rectangle(10, 15);
Uncaught TypeError: Class constructor Rectangle cannot be invoked without 'new'

Добавления метода класса

class Rectangle {
    constructor(w, h) { //class constructor
        this.width = w;
        this.height = h;
    }

    static max(r1, r2) { // Rectangle.max
        if (r1.area() > r2.area()) {
            return r1;
        }

        return r2;
    }
}

Добавления свойства класса

class Rectangle {
    constructor(w, h) { //class constructor
        this.width = w;
        this.height = h;
    }

    static defaultRect() { // Rectangle.defaultRect
        return new Rectangle(1, 1);
    }
}
Rectangle.defaultRect = new Rectangle(1,1);
Object.defineProperty(Rectangle, defaultRect, {value: new Rectangle(1,1)});
static get ZERO() {
        return new Point(0, 0);
    }

Наследование классов

class Rectangle {
    constructor(w, h) { //class constructor
        this.width = w;
        this.height = h;
    }

    static defaultRect() {
        return new Rect(1,1);
    }

    toString() {
        return `Dimensions are ${this.width} x ${this.height}`;
    }
}


class PositionedRectangle extends Rectangle {
    constructor(x, y, w, h) {
        super(w, h); 
        this.x = x;
        this.y = y;
    }
    toString() {
        return `${super.toString()}, located on ${this.x}, ${this.y}`;
    }
}

var pRect = new PositionedRectangle(2, 2, 2, 2);
PositionedRectangle.defaultRect(); // {width: 1, height: 1}

mixin

class Rectangle {
    constructor(w, h) { //class constructor
        this.width = w;
        this.height = h;
    }
}

function colorMixin(Base) {
    return class extends Base {
        setColor (color) {
            this.color = color;
        }
    }
}



class coloredRectange extends colorMixin(Rectangle) {}

Поддержка браузерами

Transpile for others

Composition

Спасибо!

Остались вопросы?

OOP in Javascript

By Viktor Shevchenko

OOP in Javascript

  • 2,388