JavaScript Objects

POJO

(Plain Old JavaScript Objects)

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

Object creation

var calculator;

calculator = {};                  // object literal

calculator = new Object();        // function-constructor

calculator = Object.create();     // meta programming

Property accessors

calculator.lastValue = 5;
calculator.maxNumber = 1502528;
calculator.add = function () {};
calculator.binaryCalculator = {};

Dot notation

calculator['lastValue'] = 5;
calculator['maxNumber'] = 1502528;
calculator['add'] = function () {};
calculator['binaryCalculator'] = {};

Brackets notation

var propertyName = 'lastValue';
calculator[propertyName] = 5;

Dot notation vs brackets notation 

Initializing object

var calculator = {
    lastValue: 5,
    binaryCalculator: {
        cache: 43
    }
};

properties

methods

// new ES6 way
var calculator = {
    add() {}
};

// old ES5 way
var calculator = {
    add: function () {}
};

Getter/Setter

var calculator = {
    _total: 0,

    set total(value) {
        this._total = value;
    },

    get total() {
       return this._total;
    }
};

calculator.total = 14;
calculator.total; // 14

Practical task 1

  • Напишите функцию compareObjects, которая принимает 2 объекта и название числового свойства, по которому нужно выполнить сравнение объектов, и выводит в консоль свойство name того объекта,  у которого значение переданного свойства больше.
  • Создайте два объекта с помощью литерала
  • Вызовите написанную функцию и передайте два созданных объекта и свойство для сравнения

Practical task 2

  • Create object Car with the method `drive` and the property `mileage`
  • Method `drive` accepts distance as a parameter and logs to console `The car has driven ${distance} miles`
  • Property `mileage` has a getter that returns total mileage after several calls of `drive`
  • Make several calls to `drive` and output total mileage

Practical task 3

  • Modify method `drive` to log mileage after `distance` seconds
  • Implement container to collect callbacks to call when `drive` is finished
  • Make several calls to `drive` and output total mileage

Accessing non-existent properties

var calculator = {};

calculator.someNonExistentProperty 
// undefined

calculator.someNonExistentProperty(); 
//throws `TypeError: calculator.someNonExistentProperty is not a function`

Checking if property exists

var f = function() {};

'apply' in f // true

Own properties

Inherited properties

var f = function() {};

f.hasOwnProperty('apply') // false

Valid property names

Late binding

var calculator1 = {
    value: 'I am calculator 1',
    add: function () {
	return this.value;
    },
};

var calculator2 = {
    value: 'I am calculator 2'
};

calculator2.add = calculator1.add;
calculator2.add(); // 'I am calculator 2'

Practical task 4

Create a mixin object that has the `list` method which loops over `props` object property and outputs all existing properties and their respective values, if a property doesn't exist on the current object, it outputs `Property ${name} doesn't exist`

Create two objects with `props` property as an array of strings, where each string denotes a property on an object and other properties listed in the `props` property

Mix in `list` method to these objects and call it

Enumerating object properties

for...in loop

var calculator = {
    lastValue: 5,
    maxNumber: 1502528,
    add: function () {},
    binaryCalculator: {
        add: function () {}
    }
};

for (var property in calculator) {
    console.log(property, calculator[property]);
}

// lastValue 5
// maxNumber 1502528
// add funtion() {}
// binaryCalculator Object {}

Comparing

> var obj1 = {};  // an empty object
> var obj2 = {};  // another empty object
> obj1 === obj2
false
> obj1 === obj1
true

objects

primitives

> var prim1 = 123;
> var prim2 = 123;
> prim1 === prim2
true

Property value shorthands 

const x = 4;
const y = 1;
const obj = { x, y };

The line is equivalent to

const obj = { x: x, y: y };
const obj = { x: 4, y: 1 };
const {x,y} = obj;
console.log(x); // 4
console.log(y); // 1

Work well with destructing

Computed property keys

There are two ways of specifying a key when you set a property:

const propKey = 'foo';
const obj = {
    [propKey]: true,
    ['b'+'ar']: 123
};

Works well with destructing

// fixed name
obj.foo = true;

// expression
obj['b'+'ar'] = 123; 

Practical task 5

  1. Create array of properties
  2. Add all properties in the array to an object using computed property syntax
  3. Loop through the properties and output their name and value

Constructor function

Why are they needed?

// Creating rectangle object
var smallRect = {
    width: 3,
    height: 5,
    area: function() { return this.width*this.height }
};

// Creating another rectangle object
var largeRect = {
    width: 10,
    height: 10,
    area: function() { return this.width*this.height }
};

// Calculating area
smallRect.area(); // 15
largeRect.area(); // 100

Constructor function

Create class\type definition

function Rectangular(width, height) {
  this.width = width;
  this.height = height;
  this.area = function() { return this.width * this.height };
}

Instantiate class instance using `new` keyword

// Creating rectangle object
var smallRect = new Rectangular(3,5);


// Creating another rectangle object
var largeRect = new Rectangular(10,10);

// Calculating area
smallRect.area(); // 15
largeRect.area(); // 100

Public properties

// Defining Rectangular class
function Rectangular(width, height) {
  // public properties
  this.width = width;
  this.height = height;
  this.area = function() { return this.width * this.height };
  this.getWidthValue = function() { return this.width };
}

// Instantiating class instance
var rect = new Rectangular(3,5);

// Calculating area
rect.area(); // 15 = 3 * 5

// Modifying value of the public property `width`
rect.width = 8;

// Calculating area again
rect.area(); // 40 = 8 * 5

// Checking value of the public `width` property
rect.width; // 8
this.getWidthValue(); // 8

Private properties

// Defining Rectangular class
function Rectangular(_width, _height) {
  // private properties
  var width = _width;
  var height = _height;

  // public methods
  this.area = function() { return width * height };
  this.getWidthValue = function() { return width };
}

// Instantiating class instance
var rect = new Rectangular(3,5);

// Calculating area
rect.area() // 15 = 3 * 5

// Modifying value of the public property `width`
rect.width = 8

// Calculating area again
rect.area() // still 15 !!!

// Checking value of the public and private `width` properties
rect.width // 8
this.getWidthValue() // 3

Static properties

// Defining Rectangular class
function Rectangular(width, height) {
  // Incrementing the number of Rectangular instances
  Rectangular.count++;

  // public properties
  this.width = width;
  this.height = height;

  // public methods
  this.area = function() { return this.width * this.height };
  this.getWidthValue = function() { return this.width };
}

// Defining static `count` property
Rectangular.count = 0;

// Defining static `factory` method
Rectangular.factory = function(width, height) {
    return new Rectangular(width, height);
};

// Instantiating class instances
var smallRect = new Rectangular(3,5);
Rectangular.count; // 1

var largeRect = Rectangular.factory(3,5);
Rectangular.count; // 2

smallRect.width; // 3
Rectangular.width // undefined

smallRect.area(); // 15
Rectangular.area(); // throws type error `Rectangular.area is not a function`

Practical task 6

  1. Опишите конструктор объектов (класс) Calculator с двумя методами: add - принимает число и прибавляет его к предыдущему, getCurrentSum - принимает индекс и возвращает результирующее число на шаге указанном в индексе (если индекса нет, возвращает текущую сумму)
  2. Создайте два экземпляра класса Calculator
  3. Добавьте в первый объект числа 3,8,11 и во второй 5,12,17.
  4. Выведите в консоль сумму:
  • всех чисел всех объектов, убедитесь (56)
  • сумму чисел всех объектов на втором шаге (28)
  • для одного объекта сумму после третьего шага и общую результирующую сумму (должна совпадать)

Class/Prototype

Why use prototypes?

function Rectangular(width, height) {
  this.width = width;
  this.height = height;
  this.area = function() { return this.width * this.height };
}


// Creating rectangle object
var smallRect = new Rectangular(3,5);

// Creating another rectangle object
var largeRect = new Rectangular(10,10);

// Calculating area
smallRect.area(); // 15
largeRect.area(); // 100

What's the problem with this code?

Shared object (prototype)

Setting prototype

using Object.setPrototypeOf()

// Defining shared object - prototype
var rectangularPrototype = {
    area: function() {
        return this.width*this.height;
    }
}

// Defining instances
var largeRect = {width: 10, height: 10};
var smallRect = {width: 3, height: 5};

// Setting prototype - inheriting area method
Object.setPrototypeOf(largeRect, rectangularPrototype);
Object.setPrototypeOf(smallRect, rectangularPrototype);

// Calling inherited `area` method
smallRect.area(); // 15
largeRect.area(); // 100

Practical task 7

Modify solution for practical task 4 to share `list` method through prototype

Setting prototype

using constructor function

var rectangularPrototype = {
    area: function() {
        return this.width*this.height;
    }
}

function Rectangular(width, height) {
  this.width = width;
  this.height = height;
}

Rectangular.prototype = rectangularPrototype;

var smallRect = new Rectangular(3,5);
var largeRect = new Rectangular(10,10);

smallRect.area(); // 15
largeRect.area(); // 100

Practical task 8

Create an object `animal` with shared method `run` that outputs '[name] is running', where [name] is placeholder for animal name

 

Create a function constructor for Rabbit that when instantiated accepts the name of the animal and stores it on the instance

 

Set `animal` as a prototype for instances of the Rabbit

 

Instantiate Rabbit instance and call `run` method

Setting prototype

using class inheritance

class Rectangular {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    area: function() {
        return this.width*this.height;
    }
}

var smallRect = new Rectangular(3,5);
var largeRect = new Rectangular(10,10);

smallRect.area(); // 15
largeRect.area(); // 100

Practical task 9

Modify task 8 to use classes instead of a function constructor

Calling parent constructor/methods

class Animal {
    run() {
        console.log(`${this.name} is running`);
    }
}

class Rabbit extends Animal {
    constructor(name) {
        // calling parent constructor
        super();
        this.name = name;
    }

    run() {
        // calling parent run method
        super.run();
    }
}

let r = new Rabbit('Rabbit');

r.run();

Prototype chain

class Figure {}

class Rect extends Figure {}

class Circle extends Rect {}

Prototype chain

class Figure {}
class Rect extends Figure {}
class Circle extends Rect {}

checking inheriinstanceOf

var s = new Square();
s instanceof Rect // ?

s.__proto__ === Rect.prototype // false
new Rect()      new Figure()

s.__proto__.__proto__ === Rect.prototype // true
new Figure()              new Figure()

s instanceof Rect // true

-----------------------------------------------

var c = new Circle();
c instanceof Rect // ?

c.__proto__ === Rect.prototype // false
new Ellipse()      new Figure()

c.__proto__.__proto__ === Rect.prototype // false
new Figure()              new Figure()

c instanceof Rect // false

Accessing properties

var rectangularPrototype = { unit: 'cm' };

function Rectangular() {};
Rectangular.prototype = rectangularPrototype;

var smallRect = new Rectangular();
var largeRect = new Rectangular();
smallRect.unit // 'cm'
largeRect.unit // 'cm'
smallRect.unit = 'mm';
largeRect.unit; // 'cm'

Reading

Writing

Dynamic prototype property change

smallRect.unit; // 'cm'
Rectangular.prototype.unit = 'mm';
smallRect.unit; // 'mm'

Practical task 10

Наследуемся правильно в 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;
PositionedRectangle.prototype = Object.create(Rectangle.prototype);

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

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

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);
}

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

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

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;
    }

    toString() {
        return '(' + this.width + ', ' + this.height + ')';
    }
}


class PositionedRectangle extends Rectangle {
    constructor(x, y, w, h) {
        super(w, h); 
        this.x = x;
        this.y = y;
    }
    toString() {
        return super.toString() + this.x + ', ' + this.y;
    }
}

var pRect = new PositionedRectangle(2, 2, 2, 2);

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

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

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) {
    this.superclass(w, h); // Вызов конструктора надкласса
    Colored.call(this, c); // и заимствование конструктора Colored
}

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

// Заимствовать методы класса 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

Спасибо!

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

Objects

By maximk

Objects

  • 1,174