Thursday, November 28, 2019

JavaScript Objects

1. Introduction to Objects

 object ⇔ {prop1, prop2, prop3, ... , propN}, (property ⇔ name:value)

A JavaScript object is a set of properties, each of which has a name and a value.

  • since property names are strings, we say that an object maps string to values. In computer science, a data structure mapping string to values is named hash map, hash table, dictionary or associative array.
  • JavaScript objects are dynamic, as properties can be added and deleted after the object creation.
  • in contrast to primitive value, JavaScript objects are mutable and manipulated by reference.
Object properties

An object property associates a name to a value. A property name may be any string. The property value may be any JavaScript value: a primitive value or an object or the two getter and setter functions.

Object properties attributes

Each property has associated data called attributes:

attribute name attribute meaning attribute values
writable may I set the property's value true|false
enumerable may I iterate with for/in the property's name true|false
configurable may I delete property or modify its attributes true|false
  • JavaScript built-in objects' properties are non-writable, non-enumerable and non-configurable, by default.
  • User-defined objects' properties are writable, enumerable and configurable, by default.
Prototypal Inheritance

In addition to keeping its own properties, a JavaScript object inherits the properties of another object, known as its "prototype", this is called prototypal inheritance.

Terminology for objects

Here are some terms used to distinguish three categories of JavaScript objects:

    native or built-in objects
    A native object is an object or class of objects defined by the ECMAScript specification.
    For example, arrays, functions, dates and regular expressions are native objects
    host objects
    A host object is an object defined by the host environment (such as a web browser) within which the JavaScript interpreter is embedded
    For example HTMLElement objects are host objects. Host objects may also be native objects.
    user-defined objects
    A user-defined object is an object created during execution of JavaScript code
Terminology for object properties
    own property
    An own property is a property defined directly on an object
    inherited property
    An inherited property is a property defined by an object’s prototype object

2. JavaScript Object Creation

Three ways to create objects:

  • with object literals notation
  • with the new keyword
  • with the Object.create() function
Creating objects with literal notation

An object literal is a comma-separated list of properties enclosed within curly braces. Each property is a colon separated 'name:value' pair.
An object literal expression creates a new object each time it is evaluated.

// object with no (own) properties
let emptyObj = {};

// object with two (own) properties
let location = { latitude: '53.349805', longitude: '-6.260310' };

// object with four (own) properties
let actress = {
  firstName: 'Uma',
  middleName: 'Karuna',
  lastName: 'Thurman',
  birth: {place: 'Boston', date: 'April 29, 1970'}
};
Creating objects with new keyword
The new keyword followed by a constructor function invocation creates and initialize a new object. You can use language built-in constructor function or your own defined constructor function.
let object = new Object();  // create an empty object: same as {}
let array  = new Array();   // create an empty array: same as []
let date   = new Date();    // create a Date object with current date
let regexp = new RegExp("\\d"); // create a RegExp object
Creating objects with Object.create() static function

The static function Object.create() creates a new object, using its first argument as the prototype of that object. Object.create() takes an optional second argument for adding properties to the new object.

var object = Object.create(Object.prototype); // create an empty object: like {} or new Object().

var actress = {fullName: 'Uma Thurman'};
var heir = Object.create(actress, {fullName: {value: 'Maya Thurman-Hawke', configurable: false}});

3. Object prototype

The prototype object
  • every JavaScript object references another object (or null) known as its prototype
  • the object inherits the proprieties of its prototype

The object's prototype is set at creation time

  • the way an object is created affects the object's prototype
  • all objects created with literals have as prototype the Object.prototype object
  • objects created with new use the value of the prototype property of the constructor function to set their prototype: objects created with new Object() inherit from Object.prototype; objects created by new Array() uses Array.prototype as their prototype; objects created by new Date() uses Date.prototype as their prototype
  • objects created with Object.create() use the first argument to the function
The [[Prototype]] attribute
  • an object [[Prototype]] attribute is a reference to its prototype object

Accessing the prototype of an object

  • the Object.getPrototypeOf(obj) static function returns the prototype object of the obj argument
  • the Object.setPrototypeOf(obj, prototype) static function sets the prototype object of the obj argument
  • note: accessing an object's [[Prototype]] attribute is equivalent to accessing the object's __proto__ property

Example: getting the prototype of an object

let father = { fullName: 'Kirk Douglas' };
// create an heir object
let son = Object.create(father); 
// get the prototype
console.log(Object.getPrototypeOf(son)===father); // true

Note: the Object's Prototype]] property vs the <functionName>.prototype property

  • do not confuse an object's [[Prototype]] attribute with a <functionName>.prototype property.
  • the <functionName>.prototype property specifies the prototype object to be assigned to all the objects created by the <functionName> when used as a constructor
The prototype chain
  • every prototype object (but not Object.prototype) have a prototype from which it inherits properties
  • for example, Array.prototype, Date.prototype, etc. inherit properties from Object.prototype
  • this means that a date object inherits properties from both Date.prototype and Object.prototype
  • this series of prototype objects is known as a prototype chain

Checking the prototype chain

  • the Object.prototype.isPrototypeOf() method allows to determine whether one object is the prototype of or is part of the prototype chain of another object

Example:

let father = { fullName: 'Kirk Douglas' };
// create an heir object
let son = Object.create(father); 
// check the prototype
console.log(father.isPrototypeOf(son)); // true: son inherits from father
console.log(Object.prototype.isPrototypeOf(son)); // true: son inherits from Object.prototype

4. Querying, Setting and Deleting Object Properties

To obtain the value of a property, use the dot operator or the square brakets.

object.propName // propName is identifier
// or
object[expression] // expression evaluates to string
  • if you use the dot operator, on the right there must be an identifier that gives name to the property.
  • if you use the square brackets, the value within the brackets must an expression that evaluates to a string containing the property name.

Examples

// actress object
const actress = {
    firstName: 'Uma',
    lastName: 'Thurman',
    'Social Security number': 'UT3 BS 3476'
};

// access with identifier
console.log(actress.firstName); // Uma

// access with variable expression
let pName = "Social Security number";
console.log(actress[pName]); // UT3 BS 3476

// accessing a nonexistent property, it yields undefined
console.log(actress.middleName); // undefined 

To create or set a property, use a dot or square brackets on the left-hand side of an assignment expression:

  • when using the square brackets, the value in the brackets must be any expression that evaluates or that can be converted to a string, for example a number is used to access array elements.
// setting properties
let actress = {
    firstName: 'Uma',
    lastName: 'Thurman',
};
actress.middleName = 'Karuna';
actress["TAX ID number"] = '8759825UT';       
console.log(actress); // {firstName: "Uma", lastName: "Thurman", middleName: "Karuna", TAX ID number: "8759825UT"}

To remove a property from an object use the delete operator

  • the delete operator deletes own properties, not inherited properties
  • a delete expression always evaluates to true, whether the property exists or does not exist
  • the delete operator does not remove a property that has a configurable attribute of false
// deleting properties
let actress = {
  firstName: 'Uma',
  middleName: 'Karuna',
  lastName: 'Thurman',
  'Social Security number': 'UT3 BS 3476'
};  
delete actress.middleName; // the actress has no middleName property
delete actress['Social Security number']; // the actress has no Social Security number property either
console.log(actress); // {firstName: "Uma", lastName: "Thurman"}
Objects As Associative Arrays

The following two JavaScript expression have the same value:

 object.property
 object["property"]

The first syntax is similar to accessing a static field of a struct in C or an object in Java, the second syntax is similar to accessing an array indexed by strings. In fact, JavaScript objects are associative arrays.

JavaScript objects are associative arrays, as a consequence object properties must not be defined in advance and a program can create any number of properties in any object at runtime. Since strings are datatype and programs can evaluate strings at runtime, programs which want to define new properties on an object have to use the array notation, not the dot notation.

You can use an object to store (x,y) pairs data from user's input leveraging the array notation.

Property Access and Inheritance

How does the prototype chain affect variable resolution, when querying an object's propriety?

JavaScript objects have a set of own properties and a set of inherited properties: when you query the property x in the object o, the property is searched on that object and, if not found, it is searched on his prototype and if not found on the prototype of the prototype, until the Object.prototype object is reached.

Example: a chain of prototypes

let erectus = {} // erectus inherits object methods from Object.prototype
erectus.erectusBirth = 'erectus born 800,000 years ago'; // and has an own property erectusBirth.

// neanderthalensis inherits properties from erectus and Object.prototype
let neanderthalensis = Object.create(erectus); 
// and has an own property neanderthalensisBirth.
neanderthalensis.neanderthalensisBirth = 'neanderthalensis born 200,000 years ago'; 

// sapiens inherits properties from neanderthalensis, erectus, and Object.prototype
let sapiens = Object.create(neanderthalensis); 
sapiens.sapiensBirth = 'sapiens born 70,000 years ago'; // and has an own property sapiensBirth.

console.log(sapiens.sapiensBirth); // ... "sapiensBirth" is own property
console.log(sapiens.neanderthalensisBirth); // ... "neanderthalensisBirth" property is in sapiens's prototype
console.log(sapiens.erectusBirth); // ... "erectusBirth" property is in prototype's prototype

How does the prototype chain affect variable assignment, when setting an object's propriety?

When you assign a property pName to an object, you creates or reassign an own property to the object. If the object has an inherited property holding the same name pName, the newly created own property hides that inherited property. If the property of the prototype object is read-only, then the assignment is not allowed.

let father = {fullName: 'Kirk Douglas'};
let son = Object.create(father); // it creates a son object
console.log(son.fullName); // "Kirk Douglas", it returns father's property

son.fullName = 'Michael Douglas'; // this hides father's property
console.log(son.fullName); // 'Michael Douglas'

5. Testing Object Properties

A JavaScript object is set of properties. If you want to check whether an object has a property with a given name, you can do this in four ways:

  • with the in operator
  • with the Object.prototype.hasOwnProperty() method
  • with the Object.prototype.propertyIsEnumerable() method
  • by querying the property name

The in operator check whether an object has an own or an inherited property

let obj = { firstname: 'Uma', lastname: 'Thurman' };
"firstname" in obj; // true: obj has an own property "firstname"
"middlename" in obj;  // false: obj doesn't have a property "middlename"
"toString" in obj;  // true: obj has an inherited propery toString

The hasOwnProperty method check whether object has own property with the given name

let obj = { firstname: 'Uma', lastname: 'Thurman' };
obj.hasOwnProperty("firstname"); // true: obj has an own property firstname
obj.hasOwnProperty("middlename");  // false: obj doesn't have a property middlename
obj.hasOwnProperty("toString");  // false: toString is an inherited property

The propertyIsEnumerable method checks whether object has own property and is enumerable

let obj = Object.create({ firstname: 'Uma', lastname: 'Thurman' });
obj.fullname = 'Uma Karuna Thurman';
obj.propertyIsEnumerable("fullname");  // true: obj has an own enumerable property fullname
obj.propertyIsEnumerable("firstname"); // false: firstname is inherited, not own
Object.prototype.propertyIsEnumerable("toString"); // false: not enumerable

The property!==undefined query checks whether it has own or inherited property

  • Note: property!==undefined query is not useful to detect existing property set to undefined, use the in operator instead
let obj = { firstname: 'Uma', lastname: 'Thurman' };
obj.firstname !== undefined; // true: obj has a property 'firstname'
obj.toString !== undefined; // true: obj inherits a toString property
obj.middlename !== undefined; // false: obj doesn't have a property 'middlename'

How to check for existence of null, undefined and falsy values

  • Use the query if(prop != null) instead of if(prop !== undefined) to test for undefined and null at one time.
  • Use the query if(obj.property) to distinguish between truthy and falsy values.
// check whether the object has a property whose value is not null or undefined
if (obj.firstname != null)
    console.log("property", obj.firstname);
 
// check whether the object has a property whose value is not null, undefined, false, "", 0 or NaN
if (obj.firstname)
    console.log("property", obj.firstname);    

6. Enumerating Object Properties

If you want to go through all the properties of an object, iterate though the properties using the for/in loop. If you only want to get a list of the properties of an object, use certain built-in static functions. The for/in loop works with inherited and own properties, the static functions only work with own properties.

Iterating through all the enumerable properties using the for/in loop
  • the for/in loop executes the body of the loop once for each property of the given object:
    • the name of the property is assigned to the loop variable
  • the for/in loop iterates through every enumerable property: both own and inherited properties.
    • the own properties, including methods, are enumerable by default
    • the properties inherited from Object.prototype are not enumerable

Example - The for/in loop iterate through all the object properties

let object = { 
  firstName: 'Kirk', 
  lastName: 'Douglas', 
  job: 'actor'
};
for(propName in object)  // loop through the properties
  console.log(propName); // prints firstName, lastName and job, but not toString which is not enumerable

The for/in loop enumerates own and inherited object properties and object's methods as well. To skip the inherited properties, use the Object.prototype.hasOwnProperty() method.

Example - Using for/in with Object.prototype.hasOwnProperty() method

let father = { 
  firstName: 'Kirk', 
  lastName: 'Douglas', 
  job: 'actor'
};
let son = Object.create(father); // creating son object inheriting from father object
son.firstName = 'Michael'; // updating son's firstName property

for (var key in son) {
  // skip inherited properties
  if(son.hasOwnProperty(key))
    console.log(key); // firstName 
}
Retrieving the names of the properties of an object using certain static functions

Sometimes you may want to get an array of property names for an object and then loop through that array with a for/of loop.

function returns an array of
Object.keys() own property names, only enumerable ones
Object.getOwnPropertyNames() own property names, whether or not they are enumerable
Object.getOwnPropertySymbols() own property names that are Symbols, whether or not they are enumerable
Reflect.ownKeys() own property names, both enumerable and non-enumerable and both string and Symbol

Example - listing own property names

let actress = {
   firstName: 'Uma',
   lastName: 'Thurman',
   middleName: 'Karuna',
   birth: {place: 'Boston', date: 'April 29, 1970'}
};
    
let keys = Object.keys(actress);        // get an array of enumerable own property names
let allKeys = Reflect.ownKeys(actress); // get an array of all the own property names

console.log(keys);    //  ["firstName", "lastName", "middleName", "birth"]
console.log(allKeys); //  ["firstName", "lastName", "middleName", "birth"]

Example - enumerating with Object.keys, Object.values and Object.entries static functions

// iterate over keys with Object.keys and array.forEach 
Object.keys(actress).forEach( key => console.log(key) );
// iterate over keys with for/of and Object.keys
for (const key of Object.keys(actress)) 
  console.log(key);
    
// iterate over values with Object.values and array.forEach
Object.values(actress).forEach( value => console.log(value) );
// iterate over values  with for/of and Object.values
for (const value of Object.values(actress)) 
  console.log(value);

// iterate over entries with Object.entries and array.forEach
Object.entries(actress).forEach( ([key, value]) => console.log(key, value) );
// iterate over entries with for/of and Object.entries
for ([key, value] of Object.entries(actress)) 
  console.log(key, value);

7. Copying object properties with Object.assign()

The Object.assign() static function copies the properties from source objects into a target object

target_obj = Object.assign(target_obj, source_obj1 [, source_obj2 ... [, source_objn] ] );

the Object.assign() function takes two or more objects as arguments

  • the first argument is the target object
    • the target object is modified and returned
  • the second and the other optional arguments are the source objects
    • source objects enumerable own properties, with both string typed names and symbolic typed names, are copied

Example: merging three object - copying the properties of two source objects into a target object

// a target object
let actress = {firstName: 'Uma', lastName: 'Thurman'};
// two source objects
let middleName = {middleName: 'Karuna'};
let SSN = {SSN: 'UT3 BS 3476'};
// merge the three objects
Object.assign(actress, middleName, SSN);
console.log(actress); // {firstName: "Uma", lastName: "Thurman", middleName: "Karuna", SSN: "UT3 BS 3476"}
// clone the actress object 
let clone = Object.assign({}, actress);
console.log(clone); // {firstName: "Uma", lastName: "Thurman", middleName: "Karuna", SSN: "UT3 BS 3476"}

Example: copying whole source objects into property values of a target object

// three source objects
let pub_name = {
  name: 'The Foggy Dew'
};
let pub_address = {
  street: '1, Fownes Street Upper', 
  locality: 'Dublin', 
  county: 'County Dublin', 
  eircode: 'D02 WP21',
  country: 'Ireland'
};
let pub_contacts = {
  phone: [{bar: '01 677 9328'}, {office: '01 677 9659'}],
  email: 'info@thefoggydew.ie'
};
// copy pub_address and pub_contacts source objects into property values of the target empty object
let pub = Object.assign( {}, pub_name, 
  { address: JSON.parse(JSON.stringify(pub_address)) }, 
  { contacts: JSON.parse(JSON.stringify(pub_contacts)) });
// pub only has three properties
console.log('pub', pub);  // {name: "The Foggy Dew", address: {…}, contacts: {…}}
// deep clone the pub object
let clone = Object.assign( {}, JSON.parse(JSON.stringify(pub)) );
console.log('clone', clone); // {name: "The Foggy Dew", address: {…}, contacts: {…}}
// test deep cloning effectiveness
console.log(pub.address === clone.address); // false

the Object.assign() function copies properties invoking property getter and setter methods

  • the source object's getter method and the target object's setter method are invoked
  • the source object's getter and setter methods are not copied

8. Extensions to Object Literal Syntax

Modern JavaScript add new notations to object literals syntax

Shorthand Properties

The shorthand property notation allows to create an object from a set a variables without the need to repeat the variable identifiers twice

let firstName = 'Uma';
let middleName = 'Karuna';
let lastName = 'Thurman';
  
// traditional syntax
var actress = {
    firstName: firstName, 
    middleName: middleName,
    lastName: lastName,
};
console.log(actress); // {firstName: "Uma", middleName: "Karuna", lastName: "Thurman"}
          
// modern javaScript syntax
var actress = {
    firstName,
    middleName,
    lastName,
};
console.log(actress); // {firstName: "Uma", middleName: "Karuna", lastName: "Thurman"}
Computed Property Names

In computed property notation, square brackets encapsulate a JavaScript expression that is evaluated at runtime and the resulting value is used as name for the property

You compute property names when creating an object:

  • if property names are variables containing strings
  • if property names are any expressions that can be evaluated as strings
  • if the property names are only known at run time, for example, because they come from user's input

In ECMAScript 5, you can computes property names only on object instances

// ES5 syntax for computed property names
let actress = {};
let firstName = 'First Name';
actress[firstName] = 'Uma';  // property name is a variable
actress['Last Name'] = 'Thurman'; // property name is a string with space
console.log(actress); // {First Name: "Uma", Last Name: "Thurman"}

ECMAScript 6 introduces computed property names on the object literal syntax

// ES6 syntax to compute property names from variables
let firstName = 'First Name', lastName = 'Last Name';
let actress = {
    [firstName]: 'Uma', 
    [lastName]: 'Thurman' 
};
console.log(actress); // {First Name: "Uma", Last Name: "Thurman"}

// ES6 syntax to compute property names from expressions
const first = 'FIRSTNAME', last = 'LASTNAME';
let actress = {
    [first.toLowerCase()]: 'Uma', 
    [last.toLowerCase()]: 'Thurman' 
};
console.log(actress); // {firstname: "Uma", lastname: "Thurman"}
Symbols as Property Names

Symbol type values are good for creating unique property names:

  • you create a Symbol by calling the Symbol() constructor function, a string may be passed as argument
const symbol = Symbol('label'); // declare a symbol called 'label'

Computed property names allow to use Symbols as property names:

  • use the brackets notation:
let obj = {[symbol]: value}; // declare a property having a symbol as key
obj[symbol] = value; // access the property having symbolic typed key

Example: attach extra properties to a DOM node

const symbol = Symbol('extra'); // create a symbol
const widget = document.querySelector('.mywidget'); // get a DOM node
// add a property whose name does not conflict with existing ones
widget[symbol]: { position: 'absolute', top: 10, left: 20 };
// add more properties without conflict
widget[symbol].width = 400;
widget[symbol].height = 200;

Properties with symbol keys are not enumerated, so they do not show up in for loops

The point of bringing in symbols as property names is providing objects with a safe extension mechanism

  • if you are using an object coming from a source you do not have control over and you want to add new properties to external objects, you can use Symbol as property names to be sure that your properties do not conflict with properties that already exist on the external objects
  • Symbols were not introduced for security reasons: if you are using symbolic names, third party code might use the use Object.getOwnPropertySymbols() function to read the symbols and alter your properties.
Spread Operator

The spread notation in the object literals syntax copy the own enumerable properties from another objects

Example: spread properties to clone object

// an objects with properties to spread
let name = { firstName: 'Uma', lastName: 'Thurman' };
  
// clone the name object into actress object
var actress = {...name};  
console.log(actress);  // {firstName: "Uma", lastName: "Thurman"}

Example: spread properties to merge objects

// two objects with properties to spread
let fullname = { firstName: 'Uma', middleName: 'Karuna', lastName: 'Thurman' };
let birth = { birthDate: 'April 29, 1970', birthPlace: 'Boston' };

// merge the names and birth objects into actress object
var actress = {...fullname, ...birth};
console.log(actress);  // {firstName: "Uma", middleName: "Karuna", lastName: "Thurman", birthDate: "April 29, 1970", birthPlace: "Boston"}

In the code above, the properties of the objects are spread out in the object literal syntax

  • if the first object and the spread objects have properties with the same name, then the first values are overwritten.

Object.assign() vs spread noation [see MDN]

  • they both do shallow-cloning or merging of objects: they do not copy prototypes
  • Object.assign() triggers setters, whereas spread notation doesn't
Shorthand Methods

In defining object methods, traditional javaScript use a function expression, whereas modern JavaScript use a shortcut notation that omits the function keyword.

var actress = {
  firstName: 'Uma',
  lastName: 'Thurman',
  birthdate: 'April 29, 1970',
  // traditional JS notation 
  age: function() {
    let millisecs = ( new Date().getTime()) - (new Date(this.birthdate).getTime() );
    return Math.floor( millisecs / (1000*60*60*24*365) ); 
  }
};
console.log(actress.age()); // 49
  
var actress = {
  firstName: 'Uma',
  lastName: 'Thurman',
  birthdate: 'April 29, 1970',
  // modern JS notation
  age() {
    let millisecs = ( new Date().getTime()) - (new Date(this.birthdate).getTime() );
    return Math.floor( millisecs / (1000*60*60*24*365) ); 
  }
};
console.log(actress.age()); // 49
Property Getters and Setters

Objects properties can be categorized in two kinds: data properties and accessor properties.

  • a data property is a name, a value and a set of attributes
  • an accessor property is a name and a method or a pair of methods, known as getter and a setter

How does JavaScript programs interact with accessor properties?

  • if a program queries an accessor property the getter method is invoked
  • if a program sets an accessor property the setter method is invoked

How does an accessor property happen to be one or two methods?

  • accessor properties do not have a writable attribute
  • if an accessor property only has the getter method the property is read-only
  • if an accessor property only has the setter method the property is write-only
  • if an accessor property has both methods the property is read-write

Notation for accessor properties in object literals

let obj = {
    // a data property handled as private
    _data: value, 
    // a read-write accessor property named propName
    get propName() { return this._data; },
    set propName(value) { this._data = value; }
};

To define accessor properties with object literal syntax:

  • two functions declaration where the function keyword is replaced with get or set
  • the function names are the same as the property name
  • no colon is used, but a comma is still required after the function body

Example: accessor property to generate serial numbers

The object serial generates strictly increasing serial numbers.

  • the data property _number, handled as private, holds the current number. It is only manipulated within getter and setter.
  • the accessor property number implements the serial number and is read and written by the JavaScript program
let serial = {
   // data property
   _number: 0,
   // the getter method returns the current $number and increment it
   get number() { return this._number++; },
   // the setter method sets the _number data property 
   set number(n) {
      if (n >= this._number) 
         this._number = n;
      else
         console.log("number can only be set to values bigger than "+this._number);   
   }
};

// get the number
console.log(serial.number); // 0
console.log(serial.number); // 1
// set the number
serial.number=50;
console.log(serial.number); // 50
// try to set the number but argument is wrong
serial.number=10; // "number can only be set to values bigger than 51"

Example: definition of age property as a read-only accessor property

var actress = {
  // data property
  firstName: 'Uma',
  lastName: 'Thurman',			
  birthDate: "April 29, 1970",
  // age accessor property
  get age() { 
    let millisecs = ( new Date().getTime()) - (new Date(this.birthDate).getTime() );
    return Math.floor( millisecs / (1000*60*60*24*365) ); 
  }
};
// query the accessor property
console.log(actress.age); // 50

No comments:

Post a Comment