Object Theory

In this lesson we will look at:

  • Objects
  • Iteration
  • Custom Getters & Setters
  • Object Methods
  • Object Oriented Programming
  • Constructor functions (for class emulation) 
  • Inheritance
  • Composition (via Design patterns)

The Humble Object

  • Objects are key/pair value stores
  • They are similar to 'dictionaries' in other programming languages
  • They are meant to hold associative information, such as configuration information
  • They are often used to model real life things
  • They are often analogous to database 'records'
  • They are large data constructs and are therefore passed by reference
let me = { name: 'James' }; // object expression

someFn({ color: 'red' }); // object literal passed to a function

Accession & Mutation

let friend = {
    name: 'amy'
};

console.log(friend.name);

Get with dot notation:

console.log(friend['name']);
let prop = 'name';
console.log(friend[prop]);

Get with square brackets notation:

Set with dot notation:


friend.name = 'sophie';

Set with square brackets notation:


friend['name'] = 'sophie';

Custom [G,S]etters

const person = {
    firstName: 'James',
    lastName: 'Sherry',
    get fullName(){
        return `${this.firstName} ${this.lastName}`;
    },
};

console.log(person.fullName); // James Sherry

person.fullName = 'Fred Durst';
console.log(person.fullName); // Fred Durst

Name collision:

const rapper = {
    _name: 'snoop',
    get name(){
        return this._name + ' dogg';
    }
};

The in operator

// Arrays
const trees = ['redwood', 'bay', 'cedar', 'oak', 'maple'];
0 in trees        // returns true
3 in trees        // returns true
6 in trees        // returns false
'bay' in trees    // returns false (you must specify the 
                  // index number, not the value at that index)
'length' in trees // returns true (length is an Array property)
Symbol.iterator in trees // returns true (arrays are iterable, works only in ES2015+)

// Predefined objects
'PI' in Math          // returns true

// Custom objects
const mycar = {make: 'Honda', model: 'Accord', year: 1998};
'make' in mycar  // returns true
'model' in mycar // returns true

key in object => true or false

  • object on right hand side of expression cannot be a primitive
  • It does search the prototype chain
  • it searches for the presence of keys, so even if the value is undefined it will return true

Iteration

for...in loop

const obj = {a: 1, b: 2, c: 3};
    
for (const key in obj) {
    if (obj.hasOwnProperty(key)) { // stops it searching the prototype chain
      console.log('obj.' + key, '=', obj[key]);
    }
}
  • prop is the key
  • obj is the object [family member]
  • obj[prop] gets the value

delete keyword

// Creates a new object, myobj, with two properties, a and b.
const myobj = {};
myobj.a = 5;
myobj.b = 12;

// Removes the 'a' property, leaving myobj with only the b property.
delete myobj.a;
console.log ('a' in myobj); // "false"

Removes configurable properties (see later)

pen

Defining Individual Properties more throughly

A property can be:

  • enumerable

  • writable

  • configurable

They can be set with:

Object.defineProperty(obj, propName, setting);

Object.defineProperties(obj, { propName1: setting1, propName2: setting2 });

and Read with: Object.getOwnPropertyDescriptor( obj, 'c' ); 

See this article for more

Object Static Methods

Iteration cont...

const obj = {a: 1, b: 2, c: 3};
console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]]

Object.entries(<object>);

Iteration

Object.keys(<object>);

Object.keys() returns an array whose elements are strings corresponding to the enumerable properties found directly upon object.

const tutor1 = {
    n: 'james',
    a: 39,
    get age(){return this.a*2},
    get name(){return this.n+' dogg'}
};

const arr = ['dog', 'cat', 'fish'];

Object.keys(tutor1); // ["n", "a", "age", "name"]
Object.keys(arr); // [0, 1, 2]

Iteration cont...

const obj = {a: 1, b: 2, c: 3};
console.log(Object.getOwnPropertyNames(obj)); // ["a", "b", "c"]

Object.getOwnPropertyNames(<object>);

Unlike Object.keys(), this shows non-enumerable properties too...

Iteration cont...

const obj = {a: 1, b: 2, c: 3};
console.log(Object.values(obj)); // [1, 2, 3]

Object.values(<object>);

Iteration cont...

const formdata = new FormData(myForm);
const data = Object.fromEntries(formdata);
console.log('data', data);

Object.fromEntries(<various>);

Assigning Properties

// Copying
const obj = { a: 1 };
const copy = Object.assign({}, obj);
obj.a = 2;
console.log(obj); // { a: 2 }
console.log(copy); // { a: 1 }

// Merging ( First object is mutated )
const obj1 = { a: 1 };
const obj2 = { b: 2 };
Object.assign(obj1, obj2);
console.log(obj1); // { a: 1, b: 2 }

// INSTEAD
const newObj = Object.assign({}, obj1, obj2);
console.log(obj1); // { a: 1 }
console.log(newObj); // { a: 1, b: 2 }

Object.assign(<targetObj>, <object2>[, <object3>, etc.]);

Used for shallow copying  and merging

Preventing change

Object.freeze(<object>);

(test: Object.isFrozen(<object>);)

Object.seal(<object>);

(test: Object.isSealed(<object>);)

Object.preventExtension(<object>);

(test: Object.isExtensible(<object>);)

(Works on all object family members!!)

WARNING: These methods are SHALLOW. For true immutability, either recurse through the object or try immutable.js constructs

Can't add new properties:

Can't add new properties, delete props or change prop definitions:

Can't add new properties, delete props or change prop definitions or property values:

Creating one based on another

const obj = { a: 1 };
const obj2 = Object.create(obj);

console.log(obj2); // { }
console.log(obj2.__proto__); // { a: 1 }
console.log(obj2.a); //1

Object.create(<object1>);

More on prototypes next...

Prototype System

The Prototype Chain

  • Every object in JavaScript is a prototype based on another.
  • Most objects are based off the object in Object
  • If you create an object you can see this in the object accessor _ _proto_ _ (Don't access directly!!)
  • This is how JavaScript stores common code
  • The sequence of these nested objects is called the prototype chain
  • When you reference a method or property on an object, the system checks the object you're in, then its prototype, then the prototype of that object, etc. etc. going up the prototype chain
  • Every function gets a prototype object, which __proto__ is the accessor for when that function is used as an object constructor

Don't extend Native Prototypes

  • It is bad practice to add your own methods to the prototype of a system entity.
    • i.e. dont do:
Array.prototype.myCoolNewFunction = function(){/*....*/};
  • Instead, create a new structure of your own that extends array (See inheritance next...)
function SuperArray(){}
SuperArray.prototype = Object.create(Array.prototype);
SuperArray.prototype.constructor = SuperArray;

SuperArray.prototype.myCoolNewFunction = function(){/*..*/};

Inheritance

A way to re-use common code

Prototypal

    const Snake = {};

    Snake.sound = 'Hiss';
    Snake.speak = function () {
        return this.sound + '!!';
    };

    const Constrictor = Object.create(Snake);
    Constrictor.constrict = function () {
        return 'The ' + this.name.toLowerCase() +' squeezes you to death!';
    };

    const Venomous = Object.create(Snake);
    Venomous.invenomate = function () {
        return 'The ' + this.name.toLowerCase() +' bites you and the venom kills you!';
    };


    const python = Object.create(Constrictor);
    python.name = 'Python';

    const cobra = Object.create(Venomous);
    cobra.name = 'Cobra';

The way it was supposed to be

Pseudo-Classical Inheritance

What are classes? And why...

  • In other programming languages you can't just create objects
  • This is because they wanted to make sure that all objects were consistent.
  • In other languages you can describe objects as instances
  • To do this the created a common mould that re-used common features and programatically oversaw the construction of instances - these moulds built into the language (in the same way, say, if statements are) and are called classes which can themselves inherit (extends) from other classes. (super class -> sub class)
  • An object is an instance of a class
  • Javascript doesn't have classes natively, so we used functions - constructor functions!

Constructors

Pseudo-Classical

(function() {

function Snake(name) { // super class
    let sound = 'Hiss'; // private variable
    this.name = name; // public property (this is the object created, of type 'Snake')
    this.getSound = function() { // privileged method (goes on each object, can see private vars)
      return sound;
    };
  }
  
  Snake.getAgeInSnakeYears = function(age){ // Static method (useful methods to be used with that type/class of thing, e.g. Array.from() or Array.isArray())
    return age * 13;
  };

  Snake.prototype.speak = function() { // shared public method (aka 'instance method') - all objects produced have access
    console.log(`${this.name } says ${this.getSound()}`);
  };


// SUB-CLASS INHERITANCE
function Constrictor(name, coils) { // sub class
  // Snake.call(this, ...arguments);
  Snake.call(this, name);
  this.coils = coils;
}
// Constrictor.prototype = new Snake();
Constrictor.prototype = Object.create(Snake.prototype); // Better way
Constrictor.prototype.constructor = Constrictor;

Constrictor.prototype.constrict = function() {
  console.log('The ' + this.name.toLowerCase() + ' squeezes you to death!');
};



function Venomous(name) { // sub class
  // Snake.call(this, ...arguments);
  Snake.call(this, name);
}
// Constrictor.prototype = new Snake();
Venomous.prototype = Object.create(Snake.prototype); // Better way
Venomous.prototype.constructor = Venomous;

  Venomous.prototype.invenomate = function() {
    return 'The ' + this.name.toLowerCase() + ' bites you and the venom kills you!';
  };

const python = new Constrictor('python', 8);
console.log('python', python);
  
const cobra = new Venomous('cobra', 8);
console.log('cobra', cobra);


  console.log('From the base class both can \'speak\' using a shared method and shared private variable');
  python.speak();
  cobra.speak();

  console.log('They have access to their sub-class methods: \n');
//   python.constrict();

//   cobra.invenomate();

  console.log('...but not to each others: \n');
  try {
    console.log(python.name + ' invenomating: \n');
    python.invenomate();
  } catch (e) {
    console.log('A python cannot invenomate\n', e);
  }

  try {
    console.log(cobra.name + ' constricting: \n');
    cobra.constrict();
  } catch (err) {
    console.log('A cobra cannot constrict\n', err);
  }

}());

Because new thoughts are hard for old programmers

Problems with Inheritance

  • It can make things trickier
  • Unlike other class-based programming languages, you can only inherit from one class
  • If you end up adding properties to the base (super) class for one sub class then all sub classes get those properties, whether they need them or not
  • Basically, don't inherit more than one level, and preferably:

 

“Favor object composition over class inheritance.” ~ The Gang of Four, Design Patterns: Elements of Reusable Object Oriented Software

Solution: Factory Patterns

Programatically assemble your objects like a factory process, using if statements to decide what goes in an object...