Объектно ориентированное программирование (ООП)

Объе́ктно-ориенти́рованное программи́рование (ООП) — методология программирования, основанная на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного класса, а классы образуют иерархию наследования.

 

Состоит:

  • Инкапсуляция - Сокрытие данных
  • Наследование - Возможность описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью.

  • Полиморфизм – Это когда подкласс класса может вызвать ту же самую обобщенную унаследованную функцию в своем собственном контексте.

ООП в функциональном стиле

Внутренний и внешний интерфейс

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

Внутренний и внешний интерфейс

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

  • Внутренний интерфейс (private) – это свойства и методы, доступ к которым может быть осуществлен только из других методов объекта, их также называют «приватными» (есть и другие термины, встретим их далее).
  • Внешний интерфейс (public) – это свойства и методы, доступные снаружи объекта, их называют «публичными».

Публичное и приватное свойство,

пример

function CoffeeMachine(power) {
  this.waterAmount = 0; // количество воды в кофеварке

  alert( 'Создана кофеварка мощностью: ' + power + ' ватт' );
}

// создать кофеварку
var coffeeMachine = new CoffeeMachine(100);

// залить воды
coffeeMachine.waterAmount = 200;
  • Локальные переменные, включая параметры конструктора, можно считать приватными свойствами.
  • Свойства, записанные в this, можно считать публичными.

Публичный и приватный методы, константы, доступ к объекту из внутреннего метода

function CoffeeMachine(power) {
  this.waterAmount = 0;
  var WATER_HEAT_CAPACITY = 4200;

  function getBoilTime() {
    return this.waterAmount * WATER_HEAT_CAPACITY * 80 / power;
  }

  function onReady() {
    alert( 'Кофе готово!' );
  }

  this.run = function() {
    setTimeout(onReady, getBoilTime.call(this));
  };

}

// создаю кофеварку, мощностью 100000W чтобы кипятила быстро
var coffeeMachine = new CoffeeMachine(100000);
coffeeMachine.waterAmount = 200;

coffeeMachine.run();

Геттеры и сеттеры

Для управляемого доступа к состоянию объекта используют специальные функции, так называемые «геттеры» и «сеттеры».

function CoffeeMachine(power, capacity) { // capacity - ёмкость кофеварки
  var waterAmount = 0;

  var WATER_HEAT_CAPACITY = 4200;

  function getTimeToBoil() {
    return waterAmount * WATER_HEAT_CAPACITY * 80 / power;
  }

  // "умная" установка свойства
  this.setWaterAmount = function(amount) {
    if (amount < 0) {
      throw new Error("Значение должно быть положительным");
    }
    if (amount > capacity) {
      throw new Error("Нельзя залить воды больше, чем " + capacity);
    }

    waterAmount = amount;
  };

  this.getWaterAmount = function() {
    return waterAmount;
  };

  function onReady() {
    alert( 'Кофе готов!' );
  }

  this.run = function() {
    setTimeout(onReady, getTimeToBoil());
  };

}
var coffeeMachine = new CoffeeMachine(1000, 500);
coffeeMachine.setWaterAmount(450);
alert( coffeeMachine.getWaterAmount() ); // 450

Функциональное наследование

Наследование – это создание новых «классов» на основе существующих.

function Machine() {
  var enabled = false;

  this.enable = function() {
    enabled = true;
  };

  this.disable = function() {
    enabled = false;
  };
}

Базовый класс «машина»

function CoffeeMachine(power) {
  Machine.call(this); // отнаследовать

  var waterAmount = 0;

  this.setWaterAmount = function(amount) {
    waterAmount = amount;
  };

}

var coffeeMachine = new CoffeeMachine(10000);

coffeeMachine.enable();
coffeeMachine.setWaterAmount(100);
coffeeMachine.disable();

Унаследуем от него кофеварку. При этом она получит эти методы автоматически

Перенос свойства в защищённые

function Machine(power) {
  this._power = power; // (1)

  this._enabled = false;

  this.enable = function() {
    this._enabled = true;
  };

  this.disable = function() {
    this._enabled = false;
  };
}

function CoffeeMachine(power) {
  Machine.apply(this, arguments); // (2)

  alert( this._enabled ); // false
  alert( this._power ); // 10000
}

var coffeeMachine = new CoffeeMachine(10000);

Переопределение методов

function Machine(power) {
  this._enabled = false;

  this.enable = function() {
    // используем внешнюю переменную вместо this
    this._enabled = true;
  };

  this.disable = function() {
    this._enabled = false;
  };

}

function CoffeeMachine(power) {
  Machine.apply(this, arguments);

  var waterAmount = 0;

  this.setWaterAmount = function(amount) {
    waterAmount = amount;
  };

  var parentEnable = this.enable;
  this.enable = function() {
      parentEnable.call(this);
      this.run();
    }

  function onReady() {
    alert( 'Кофе готово!' );
  }

  this.run = function() {
    setTimeout(onReady, 1000);
  };

}

var coffeeMachine = new CoffeeMachine(10000);
coffeeMachine.setWaterAmount(50);
coffeeMachine.enable();

ООП в прототипном стиле

Прототип объекта

Свойство __proto__

доступно во всех браузерах, кроме IE10-, а в более старых IE к нему не обратиться, требуются чуть более сложные способы, которые мы рассмотрим позднее.

var animal = {
  eats: true
};
var rabbit = {
  jumps: true,
  //eats: false
};

rabbit.__proto__ = animal;

// в rabbit можно найти оба свойства
alert( rabbit.jumps ); // true
alert( rabbit.eats ); // true

Объект, на который указывает ссылка __proto__, называется «прототипом». В данном случае получилось, что animal является прототипом для rabbit.

Метод hasOwnProperty

var animal = {
  eats: true
};

var rabbit = {
  jumps: true,
  __proto__: animal
};

for (var key in rabbit) {
  // выводит и "eats" и "jumps"
  alert( key + " = " + rabbit[key] );
}
var animal = {
  eats: true
};

var rabbit = {
  jumps: true,
  __proto__: animal
};

for (var key in rabbit) {
 // пропустить "не свои" свойства
  if (!rabbit.hasOwnProperty(key)) continue;

 // выводит только "jumps"
  alert( key + " = " + rabbit[key] );
}

Object.create(null)

var data = {};
data.text = "Привет";
data.age = 35;

// функция, хотя мы её туда не записывали
alert(data.toString); 

Чтобы этого избежать, мы можем исключать свойства, не принадлежащие самому объекту:

var data = {};

// выведет toString только если оно записано в сам объект
alert(data.hasOwnProperty('toString') ? data.toString : undefined);
var data = Object.create(null);
data.text = "Привет";

alert(data.text); // Привет
alert(data.toString); // undefined

Однако, есть путь и проще:

Прототип объекта

Свойство prototype

var animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
  this.__proto__ = animal;
}

var rabbit = new Rabbit("Кроль");

alert( rabbit.eats ); // true, из прототипа

Свойство F.prototype и создание объектов через new

var animal = {
  eats: true
};

function Rabbit(name) {
  this.name = name;
}

Rabbit.prototype = animal;

//  rabbit.__proto__ == animal
var rabbit = new Rabbit("Кроль"); 
  1. Установка Rabbit.prototype = animal буквально говорит интерпретатору следующее: "При создании объекта через new Rabbit запиши ему __proto__ = animal".
  2. Оператора new, оно вообще ничего не делает, его единственное назначение – указывать __proto__ для новых объектов.
function Rabbit() {}

Rabbit.prototype = {
  constructor: Rabbit
};

alert( Rabbit.prototype.constructor == Rabbit ); // true

Свойство constructor

У каждой функции по умолчанию уже есть свойство prototype. Оно содержит объект такого вида:

function Rabbit(name) {
  this.name = name;
  alert( name );
}

var rabbit = new Rabbit("Кроль");

var rabbit2 = new rabbit.constructor("Крольчиха");

Встроенные "классы" в JavaScript

Откуда методы у {} ?

var obj = {};
console.log( obj.toString() ); // "[object Object]" ?
console.log( {}.__proto__.toString ); // function toString

Откуда новый объект obj получает такой __proto__?

  1. Запись obj = {} является краткой формой obj = new Object, где Object – встроенная функция-конструктор для объектов.
  2. При выполнении new Object, создаваемому объекту ставится __proto__ по prototype конструктора, который в данном случае равен встроенному Object.prototype.
  3. В дальнейшем при обращении к obj.toString() – функция будет взята из Object.prototype.

Проверка методов у {} ?

var obj = {};

// метод берётся из прототипа?
alert( obj.toString == Object.prototype.toString ); // true, да

// проверим, правда ли что __proto__ это Object.prototype?
alert( obj.__proto__ == Object.prototype ); // true

// А есть ли __proto__ у Object.prototype?
alert( obj.__proto__.__proto__ ); // null, нет

Встроенные «классы» в JavaScript в схемах

Вызов методов через call и apply из прототипа

function showList() {
  alert( [].join.call(arguments, " - ") );
}

showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша
function showList() {
  alert( Array.prototype.join.call(arguments, " - ") );
}

showList("Вася", "Паша", "Маша"); // Вася - Паша - Маша

Изменение встроенных прототипов

String.prototype.repeat = function(times) {
  return new Array(times + 1).join(this);
};

alert( "ля".repeat(3) ); // ляляля
Object.prototype.each = function(f) {

  for (var prop in this) {

    // пропускать свойства из прототипа
    if (!this.hasOwnProperty(prop)) continue;

    var value = this[prop];
    f.call(value, prop, value);

  }

};

// Теперь все будет в порядке
var obj = {
  name: 'Вася',
  age: 25
};

obj.each(function(prop, val) {
  alert( prop ); // name -> age
});
if (!Object.create) {

  Object.create = function(proto) {
    function F() {}
    F.prototype = proto;
    return new F;
  };

}
Достоинства Недостатки
Методы в прототипе автоматически доступны везде, их вызов прост и красив. Новые свойства, добавленные в прототип из разных мест, могут конфликтовать между собой.
Представьте, что вы подключили две библиотеки, которые добавили одно и то же свойство в прототип, но определили его по-разному. Конфликт неизбежен.

Изменения встроенных прототипов влияют глобально, на все-все скрипты, делать их не очень хорошо с архитектурной точки зрения.

ООП функциональный или прототипный стиль

ООП функциональный или прототипный стиль

function Animal(name) {
  var speed = 0;
  this.name = name;

  this.run = function(val) {
    speed += val;
    alert( this.name + ' бежит, скорость ' + speed );
  };

  this.stop = function() {
    speed = 0;
    alert( this.name + ' стоит' );
  };
};

var animal = new Animal('Зверь');

animal.run(3); // Зверь бежит, скорость 3
animal.run(10); // Зверь бежит, скорость 13
animal.stop(); // Зверь стоит
// конструктор
function Animal(name) {
  this.name = name;
  this.speed = 0;
}

// методы в прототипе
Animal.prototype.run = function(speed) {
  this.speed += speed;
  alert( this.name + ' бежит, скорость ' + this.speed );
};

Animal.prototype.stop = function() {
  this.speed = 0;
  alert( this.name + ' стоит' );
};

var animal = new Animal('Зверь');

alert( animal.speed ); // 0, свойство взято из прототипа
animal.run(5); // Зверь бежит, скорость 5
animal.run(5); // Зверь бежит, скорость 10
animal.stop(); // Зверь стоит
// --------- Класс-Родитель ------------
function Animal(name) {
  this.name = name;
  this.speed = 0;
}

Animal.prototype.jump = function() {
  alert(this.name + " бежит!")
}
// --------- Класс-потомок -----------
// Конструктор потомка
function Rabbit(name) {
  Animal.apply(this, arguments);
}

// Унаследовать
Rabbit.prototype = Object.create(Animal.prototype);

// Желательно и constructor сохранить
Rabbit.prototype.constructor = Rabbit;

// Методы потомка
Rabbit.prototype.jump = function() {
  // Вызов метода родителя внутри своего
  Animal.prototype.jump.apply(this);
  alert( this.name + " подпрыгивает!" );
};

// Готово, можно создавать объекты
var rabbit = new Rabbit('Кроль');
rabbit.jump();

Структура наследования

Достоинства Недостатки
Функциональный стиль записывает в каждый объект и свойства и методы, а прототипный – только свойства. Поэтому прототипный стиль – быстрее и экономнее по памяти. При создании методов через прототип, мы теряем возможность использовать локальные переменные как приватные свойства, у них больше нет общей области видимости с конструктором.

Проверка класса: "instanceof"

function Rabbit() {}

// создаём объект
var rabbit = new Rabbit();

// проверяем -- этот объект создан Rabbit?
alert( rabbit instanceof Rabbit ); // true, верно
var arr = [];
alert( arr instanceof Array ); // true
alert( arr instanceof Object ); // true
// Создаём объект rabbit, как обычно
function Rabbit() {}
var rabbit = new Rabbit();

// изменили prototype...
Rabbit.prototype = {};

// ...instanceof перестал работать!
alert( rabbit instanceof Rabbit ); // false
rabbit.__proto__ == Rabbit.prototype.
arr.__proto__.__proto__ == Object.prototype.

Алгоритм проверки obj instanceof Constructor:

The End

RD Prototype

By Sarhan Azizov

RD Prototype

  • 389