Tuesday, 18 August 2020

ES6 Modules (Export and Import) in Javascript

 To make objects,functions,classes or variables available to the outside world it's as simple as 

exporting them and then importing them where needed in other files.

1.Import default exports

import carTires from './autoParts/tires.js';

2.Import default export and additional functions

import carTires, { ShineTires } from './autoParts/tires.js';

3.Import a default export plus all the other exports in a file.

import carTires, * as tireOptions from './autoParts/tires.js';

Note : This is known as namespace import.

If you'r importing a default export along with named or namespace imports, be aware that the default export will have to be declared first.

4.A single export from a module : named import

import { apples } from './plants/fruits.js';

5.Multiple exports from a single module :

import { carrots, potatoes, onions } from './plants/veggies.js';

6.Alias an import with a more convenient name :

import { mySuperCaliFragilisticExpialidociusObject as mySuperObject } from './maryPoppins.js';

7.Rename multiple exports during import :

import { ladyMaryCrawley as ladyMary, ladyEdithCrawley as ladyEdith, ladyCoraCrawley as ladyGrantham } from './downtonAbbeyFamily/ladies.js';

Just like renaming one export,you can rename multiple exports on import.

8.Dynamic imports or import() operator :

Dynamically importing ES modules with import() operator.

with import() operator we can load modules dynamically .

ex : const myModule='./myModules.js';

     import(myModule)

        .then(x => x.someMethod());

1. we can load some modules on demand.

2. we can load some modules depending on whether a condition is true.

The 'import' keyword may be called as a function to dynamically import a module.when used this way,it returns a promise.

ex: dynamic imports with promise syntax

import ('./waysToTravel.js')

  .then((vehicles) => {

    // do something with planes, trains and automobiles

  });


ex: dynamic imports with async/await syntax

let vehicles = await import('./waysToTravel.js');

Exports :

1.Default Export

ex :

export const myNumbers = [1, 2, 3, 4];

const animals = ['Panda', 'Bear', 'Eagle'];


export default function myLogger() {

  console.log(myNumbers, pets);

}


Note: Each Javascript file can only have one default export.

All other export in that same file will just have the 'export' keyword in front of them.


2.Individual named exports


  ex : 

     export const myPets = [ "dog", "cat", "guinea pig", "gold fish"];

const mySecretPets = [ "dragon", "griffin", "Loch Ness monster", "Big Foot"];

export function nameMyPets() {

        console.log(myPets, mySecretPets)

      }


   named import syntax: 


  import { myPets, nameMyPets } from './pets.js';

  

3.Multiple named export in a single line


export { cakes, cookies, makeDessert, makeTea };


4.Exporting with aliases


ex : export { myNumbers, myLogger as Logger, Alligator }


Javascript Functions

 A function is a block of organized, reusable code that is used to perform a single,related action.

Argument :

An argument is a value that we pass to the function when we invoke it.

Parameter :

A parameter is a variable that we list as part of a function definition.

The Arguments Object :

ex :

  function printAll{

     for(let i=0;i<arguments.length;i++){

  console.log(arguments[i]);

}

  }


 printAll(1,2,3,4,5); // 1 2 3 4 5

 printAll(10,20);     // 10 20


you can invoke a function with an indefinite number of arguments.

The arguments object allows us to represent them as an array-like object. 


Function scope :

A function scope is created for every function.

function iHaveScope(){

  // local function scope

  function iHaveNestedScope(){

    // nested local function scope

  }

}


Block Scope :

when we say block,we are referring to the code placed between curly braces.

The lifetime of variable within the curly brace {} of an if statement, a while, or a for loop,

or any set of curly braces other than a function.


ex : 

   let message='Hello';

   if(message ==='Hello'){

     let message = 'Inside if block';

console.log(message);   // Inside if block

   }

   

   console.log(message); // Hello

   



Note: variables declared with the var keyword or within function declaration

'DO NOT' have block scope.

There are three ways to declare variable in JS :

1. var width=100;

2. let height = 200;

3. const key = 'abc123';

Note : 'var' variable are 'function scope'.what does this mean?

It means they are only available inside the function they're created in,

or if not created inside a function, they are 'globally scoped'.

what is the block?

A block is a set of opening and closing curly brackets.

Note:

var is function scope.

let and const are block scope.

Function scope is within the function.

Block scope is within curly brackets.


Immediately Invoked Function Expression(IIFE):

The IIFE pattern let us group our code and have it work in isolation,

independent of any other code.


Function Expression :

Define a function and assign it to a variable.

Immediately Invoked 

Invoking the function right away where it's defined.


ex : (function () {

        console.log("Hello"); // Hello

     })();


Closures :

=========


let greeting = ( function () {

  let message='Hello';

  let getMessage= function(){

     return message;

  };

  

  return {

    getMessage : getMessage,

  };

})();  


console.log(greeting.getMessage()); // Hello


ex :

function setupCounter ( val ){

  return function counter(){

    return val++;

  }

}


let counter1 = setupCounter(0);

console.log(counter1());  // 0

console.log(counter1());  // 1

let counter2 = setupCounter(10);

console.log(counter2());  // 10

console.log(counter2());  // 11


Note : Each of them have their own environment and variables,

and anytime we call either counter1 or counter2, the variable that

was initially set up in the context is incremented.


Improving Readability with Arrow Functions :


Arrow Functions are introduced in ES6.

Simpler way to write a function expression.

--> shorter syntax

--> 'this' derives its value from enclosing lexical scope.

Arrow Functions are not really a substitute for regular functions.

Arrow functions have several side effects

1.Behavior of 'this' keyword.

2.No arguments object


Note : Arrow functions don't have their own implicit arguments object.

ex : 

   

   let greetings = () => {

       return 'Hello world!';

    };

   let message=greetings();

   console.log(message);   // Hello world!

   

ex :

    let greetings = () => 'Hello world!';

let message = greetings();

console.log(message);  // Hello world!


ex :

  let greetings = name => 'Hello '+name;

  let message = greetings('John');

  console.log(message);    // Hello John

  

Note : if you have only one input parameter ,

the parenthesis around it is optional.


ex :

  let sum =(num1,num2) => num1+num2;

  let output =sum(10,7);

  console.log(output);  // 17


Behavior of 'this' keyword :


In Javascript,'this' always refers to the owner of the function we are executing.


Note : In standard function, 'this' refers to the global window object

otherwise 'this' refer to the object that a function is a method of.


Note : Arrow functions do not have their own 'this' value.

The value of 'this' is always inherited from the enclosing scope.


ex :

  let message = {

    name:'john',

regularFunction : function(){

   console.log('Hello ' + this.name); 

},

arrowFunction : () => console.log('Hi '+ this.name)

  };

  

  message.regularFunction();   // Hello John

  message.arrowFunction();     //  Hi


 This is because when we log our message.regularFunction, our 'this' keyword refers to the message object that holds a function. 

 So when we access this.name, we get John. Let's print out the value of this inside the regularFunction. 

 As you can see, its value is the message object. Next, when we call message.arrowFunction, 

 it looks for the name variable in the global context,the Window object, and cannot find it. Therefore, only Hi gets printed to the console.  

Function Context and Built-in Functions :

Note : Anytime you invoke a function with a new keyword, javascript implicitly creates an empty object within the function before returning it.

ex : constructor Function


function SayHi(){

   console.log('Hi');

   console.log(this);

}

let greeting = new SayHi();

// Hi

// SayHi {} 


console.log(greeting) // SayHi {}


Note :

'this' in a normal function refers the 'global' object.

if it's in strict mode,'this' will be undefined.

And if the function is used with new operator, a new empty object will be assigned to 'this'.

Changing default context with call/apply/bind.

call Method :


ex : 

     let person1 = {name: 'John',age:22};

let person2= {name:'Mary',age:26};

let sayHi = function(){

    console.log('Hi, ' + this.name);

}

sayHi.call(person1) // Hi, John

sayHi.call(person2) // Hi, Mary

 

You can also pass additional arguments to the call method.


ex :

     let person1={name:'John',age:22};

     let sayHi = function(message){

    console.log(message+', '+ this.name);

}

     sayHi.call(person1,'Hi'); // Hi, John


apply Method :


ex : 


    function introduction(name,profession){

       console.log("My name is "+ name +" and I am a "+ profession+" .");

     }

introduction("John","student");                     // My name is John and I am a student .

introduction.apply(undefined,["Mary","lawyer"]);    // My name is Mary and I am a lawyer .

introduction.call(undefined,"James","artist");      // My name is James and I am a artist .


we are not really changing the function context of 'this' value here,

so we just pass 'undefined' as the first argument.  

 

Note :

call accepts an argument list,while apply accepts a single array of arguments.


apply vs call :


apply :

1.array input with similar elements.

call:

1.individual arguments of varying type.


with call and apply, we call an existing function and change the function context that is the value of the 'this' object.

 

Bind Method :

To make a copy of a function and then change the value of 'this'.

ex :

  let person1 = { name:'Mary',

                  getName : function(){

    return this.name;

  }

  };

  let person2 = {name: 'John'};

  let getNameCopy = person1.getName.bind(person2);

  console.log( getNameCopy());  // John


When we call bind on the person1.getName function and pass the person2 object 

as an argument, a new function is created and returned and assigned to the getNameCopy variable.

Additionally, the value of 'this' in the new function is set to the Person2 object.


Note : we are no longer changing the context of an existing function the way we 

did in call and apply,but instead create a new copy of function and then change its context to another object.


Built-in functions :

1. eval

Eval, as its name suggests, accepts a string as input, a valued set, and returns a value.


ex : 

 let x = 1;

 let y = 2;

 console.log(eval('x+y+1')); // 4

 

 ex :

  let x=1;

  let x=2;

  let s='abc';

  console.log(eval('x+y+s')); // 3abc

  

2.ParseInt

The function parseInt parses a string and returns an integer.

Optionally, you can also specify an additional base or radix argument to return an integer of that base.


The 'ParseInt' function converts its first argument to a string,parses that string,then returns an integer or NAN.


console.log(ParseInt('F',16));  // 15

console.log(ParseInt('15',10));  // 15

console.log(ParseInt('Hi',10));  // NaN


3.ParseFloat


ParseFloat works just like parseInt,but it returns a floating point number.


console.log(ParseFloat('3.99'))  // 3.99

console.log(ParseFloat('3.99e-1)) // 39.9

console.log(ParseFloat(''));      // NaN


4.Escape 

Escape returns the hexadecimal encoding of an argument in the isolated one character set.


console.log(escape('text')); // text

console.log(escape(' '));    // %20

console.log(escape('abc&%')); // abc%26%25


5.Unescape


Unescape does the opposite of escape, it returns the RC string for a given input value.



console.log(unescape('text')); // text

console.log(unescape('%20'));  //

console.log(unescape('abc%26%25)); // abc&%


Note :

call and apply methods change the context to another object.

bind method create a copy of a function and then change the function context.


Default and Rest Parameters and Spread Operator :


Default Parameters :


Default parameters allow you to give a default value to a parameter in a function.


ex : 

    function sayHi(name = 'World'){

  console.log('Hello ' + name);

}

 

sayHi();       // Hello world

sayHi('John'); // Hello John


ex : 

    function sayHi(message,name='World'){

console.log(message+name);

}

    

    sayHi('Hello');     // Hello World

    sayHi('Hi','John'); // Hi John


Note : The default parameter in function definition should always come after the regular parameters.

So in our sayHi function here, message cannot come after the name parameter.


Rest Parameters :


With rest parameters,you can define a function to store multiple arguments in a single array.

This is especially useful when you're invoking a function with multiple arguments.


ex : 

   let sayHi = function greet(...names){

     names.forEach(name => console.log('Hi ' + name));

 

   }

   

   sayHi('Mary','John','James');

   // Hi Mary

   // Hi John

   // Hi James

so the rest parameter really allows us to send any number of arguments to a function and then collect them in an array.


ex 2 :

        let sayHi = function greet(message,...names){

    console.log(message + 'everyone!');

names.forEach(name => console.log('Hi ' + name));

}

sayHi('Welcome','Mary','John','James');

// Welcome everyone!

// Hi Mary

// Hi John

// Hi James


Note : Just like the default parameters, rest parameters should appear after any regular parameters in your function definition.

rest parameter always stores the rest or remaining arguments as an array.


Spread Operator :


The spread operator is the opposite of how a rest parameter works.

it allows a function to take an array as an argument and then spread out 

its elements so that they can be assigned to individual parameters.


ex :

    function greet(name1,name2){

console.log('Hello '+name1+' and '+ name2);

}

    let names=['John','Mary'];

    greet(...names); // Hello John and Mary

ex : 

    function display(char1,char2,char3,char4){

  console.log(char1,char2,char3,char4);   


    let letters ='abcd';

    display(...letters); // abcd


so the spread operator can be used with any iterable like 

string,array or list.


ex :

    function display(char1,char2,char3,char4,...others){

  console.log(char1,char2,char3,char4);

      console.log(others);   


    let letters ='abcdefg';

    display(...letters); 

// abcd

//  ["e","f","g"]


Note : The rest parameter collects individual arguments and stores them in an array,

The spread operator takes an array as argument and spreads it into individual parameters.


Saturday, 15 August 2020

Generators in javascript

 Normal functions vs Generator functions :


functions in javascript "run until return/end".

Generator Functions " run until yield/return/end".


Generator functions :

======================


A generator is a function that can stop midway and then continue from where it stopped.


Generator functions once called,returns the 'Generator Object',which holds the 

entire 'Generator Iterable' that can be iterated using next() method or for..of loop.


" Every next() call on the generator executes every line of code until the next yield it encounters and suspends its execution temporarily".


Syntactically generators are identified with a *, either 'function* X' or 'function *X' -- both mean the same thing.




Note :

Once created, calling the generator function returns the Generator Object. 

This generator object needs to be assigned to a variable to keep track of the subsequent next() methods called on itself.

If the generator is not assigned to a variable then it will always yield only till first yield expression on every next().


The generator functions are normally built using yield expressions. 

Each yield inside the generator function is a stopping point before the next execution cycle starts. 

Each execution cycle is triggered by means of next() method on the generator.

On each next() call, the yield expression returns its value in the form of an object containing the following parameters.


{ value: 10, done: false } // assuming that 10 is the value of yield


Value — is everything that is written on the right side of the yield keyword, it can be a function call, 

object or practically anything. For empty yields this value is undefined.


Done — indicates the status of the generator, whether it can be executed further or not. 

When done returns true, it means that the function has finished its run.


1.Assign the generator to a variable.



ex : function* generatorFunction(i){

   yield i;

   yield i+1;   

}


let generator = generatorFunction(5);

console.log(generator.next()); //{value: 5, done: false}

console.log(generator.next()); //{value: 6, done: false}


//Accessing generatorFunction directly


console.log(generatorFunction(5).next()); //{value: 5, done: false}

console.log(generatorFunction(5).next()); //{value: 5, done: false}


the generator function accessed directly without a wrapper always executes only until the first yield.

Hence , by definition you need to assign the Generator to a variable to properly iterate over it.


Lifecycle of a Generator Function 


Each time a yield encountered the generator function returns an object containing the value of the 

encountered yield and the done status.


Similarly, when a return is encountered, we get the return value and also done status as true.

Whenever, done status is returned as true,is essentially means that the generator function has 

completed its run, and no further yield is possible.


Note: Everything after the first 'return' is ignored,including other 'yield' expressions.




2.Assigning Yield to a Variables


ex : 

function *generatorFunction(){

  const x=yield 1;

  console.log(x);

  const y=yield 2;

  console.log(y);

}


let generator=generatorFunction();


console.log(generator.next());

//{value: 1, done: false}

console.log(generator.next());

// {value: 1, done: false}, x undefined

console.log(generator.next());

//{value: undefined, done: true}, y undefined


Note : 

Starting from second next(), the previous yield is replaced with arguments passed in the next function. 

Since, we do not pass anything here in the next method, its assumed that the entire ‘previous-yield expression’ as undefined.


3.Passing Arguments to the next() method :


ex : 

  function *generatorFunction(i){

     console.log(i);

const j = 5 * (yield (i * 10));

console.log(j);

const k = yield (2 * j / 4);

console.log(k);

return (i + j + k);

  }

  

  var generator1 = generatorFunction(10);

   // i is 10

  console.log( generator1.next(20));

  // {value: 100, done: false}

  console.log( generator1.next(10));

  // j is 50

  // {value: 25, done: false}

  console.log( generator1.next(5));

  // k is 5

  // {value: 65, done: true}

  

4. Passing Yield as an Argument of a function


 ex : 

  function *generatorFunction(){

    yield;      // pause here and returned undefined

foo( yield "I am useless"); // pause and wait for next() to finish. 

  } 

  

  function foo(x){

    console.log(" Just printing argument passed ", x);

  }

  

  let generator3 = generatorFunction();

  console.log( generator3.next());

  // {value: undefined, done: false}

  console.log( generator3.next());

  // {value: "I am useless", done: false}

  console.log( generator3.next());

  // Just printing argument passed  undefined

  // {value: undefined, done: true}

  

5. Yield with a Function Call


Apart from returning values yield can also call functions and return the value or print the same.

 

 function *fetchUser(){

   const user = yield getData();

   console.log(user) // undefined

 }

 

 function getData(){

   return { name:"Jim Cooper", dob:'1991'};

 }

  

 let fetchGen = fetchUser();

 console.log(fetchGen.next());

// { value: {name: "Jim Cooper", dob: "1991"} , done: false}

  console.log(fetchGen.next());

// undefined

//{value: undefined, done: true}


6. Yield with Promises


Yield with promises follows the same approach as the function call,

instead of returning a value from the function, it returns a promise 

which can be evaluated further for success or failure.


ex :


function * fetchUser(action){

  const user = yield apiCall();

}


function apiCall(){

  return new Promise(resolve => {

    setTimeout(() => {

  resolve ( { name:'Jim Cooper', dob:'1991'});

},2000);

  })

}


let fetchgen2 = fetchUser();


// prints user data after 2 seconds

console.log( fetchgen2.next().value.then( n => console.log(n)));

// Promise

// { name: "Jim Cooper", dob:"1991"}


The apicall returns the promises as the yield value, when resolved after 2 seconds prints the value we need.


7. Yield*

Yield* when used inside a generator function delegates another generator function.


function* g1(){

 yield 2;

 yield 3;

 yield 4;

}


function* g2(){

 yield 1;

 yield* g1();

 yield 5;


}


let iterator = g2();


console.log( iterator.next()); // {value: 1, done: false}

console.log( iterator.next()); // {value: 2, done: false}

console.log( iterator.next()); // {value: 3, done: false}

console.log( iterator.next()); // {value: 4, done: false}

console.log( iterator.next()); // {value: 5, done: false}

console.log( iterator.next()); // {value: undefined, done: true}


8. Yield* with Return

 Yield* with a return behaves a bit differently than the normal yield*.

 When yield* is used with a return statement it evaluates to that value,

 meaning the entire yield* function() becomes equal to the value returned 

 from the associated generator function.

 

 

ex : 


  function* genFuncChild(){

    yield 1;

yield 2;

return 'foo';

yield 3;     // note that this is ignored

  }

  

  function* genFuncMain(){

   const result = yield* genFuncChild();

   console.log(result);

   yield 'the end';

  }

 

 let generator = genFunction();

 console.log(generator.next());  // yield 1

 console.log(generator.next());  // yield 2

 console.log(generator.next());  //  returns 'foo' and yields 'the end'

 console.log(generator.next());  // finishes run

 

9. Yield* with a Built-in iterable Object


yield* can also iterate over iterable objects like Array,String and Map.


function* genFunc(){

 yield* [1,2];

 yield* 'HI';

 yield* arguments;


}


let generator = genFunc(5,6);


console.log(generator.next());  // prints 1

console.log(generator.next());  // prints 2

console.log(generator.next());  // prints H

console.log(generator.next());  // prints I

console.log(generator.next());  // prints 5

console.log(generator.next());  // prints 6


yield* iterates over every possible iterable object that is 

passed as its expression.


10.yield with for..of


Every Iterator/generator can be iterated over a for..of loop.

similar to our next() method which is called explicitly , for..of loop 

internally moves on to the next iteration based on the 'yield keyword'.

And it iterates only till the 'last yield' and doesn't process the return 

statement like next() method.


ex : 

   function* genFuncChild(){

     yield 1;

yield 2;

return 'foo';

yield 3;  // note that this is ignored

   }

   

   function* genFuncMain(){

   const result = yield* genFuncChild();

   console.log(result);

   yield 'the end';

  }

 

 let generatorforof=genFuncMain();

 

 // using for..of to print the generator

 for(let i of generatorforof){

   console.log(i);

 }

//result 

//  1

//  2

//  the end


The final return in not printed, because for..of loop iterates only till the last yield.

So,it comes under best practice to avoid return statements inside a generator function as 

it would affect the reusability of the function when iterated over a for..of.


Sunday, 2 August 2020

Objects, Prototypes, and Classes in JavaScript

Object Literals :
==================
let person ={
  firstName:'Jim',
  lastName:'Cooper'
};

dynamic nature of javascript :
ex:

let person ={
  firstName:'Jim',
  lastName:'Cooper'
};
person.age=29;
person.isAdult=function(){ return this.age>=18;}

console.log(person.isAdult()); // true

ex 1:

let person ={
  firstName:'Jim',
  lastName:'Cooper',
  age:29,
  isAdult:function() {return this.age>=18;}
};


console.log(person.isAdult()); // true

ex 3:

function registerUser(fName,lName){
  let person ={
   firstName:fName,
   secondName:lName
  };
  console.log(person); // {firstName: "Jim", secondName: "Cooper"}
}

registerUser('Jim','Cooper');

ex : shorthand syntax


function registerUser(firstName,secondName){
  let person ={
   firstName,
   secondName
  };
  console.log(person); // {firstName: "Jim", secondName: "Cooper"}
}

registerUser('Jim','Cooper');

Object Literal Method Declaration :
=====================================

let person ={
   firstName:'Jim',
   secondName:'Cooper',
   age:18,
   isAdult() {return this.age>=18;}
};

person.isAdult(); // true

Inspecting Object Properties with Object.keys() and for...in :
==============================================================
Object.keys() accepts an object as argument and returns an array of all its(own) enumerable properties.

ex :

let person ={
   firstName:'Jim',
   secondName:'Cooper',
   age:18,
   isAdult() {return this.age>=18;}
};

Object.keys(person); // [firstName,secondName,age,isAdult]

for (let propertyName in person){
   console.log(propertyName);  // [firstName,secondName,age,isAdult]
}

so 'Object.keys' and 'for in' basically accomplish the same thing,
they give access to each of the property and method names on an object.

Object Equality and the Object.is() Function :
==============================================
Javascript Equality Operators

1.==
should be avoided. Useful only in rare cases.

Note : Not type-safe
ex :
"42" ==42 //true
0==false  // true
null==undefined // true
""==0      // true
[1,2] == "1,2"  // true


2.===
Most common.should be used in almost all cases.
Note : 
1.Type-Safe
2.Convenient/Concise
3.NaN not equal to NaN
 ex : NaN === NaN //false
4.+0 equals -0
  ex : -0 === +0 //true

3.Object.is()
Less common.Like === except for a few mathmatical differences.
   ex:  Object.is(person1,person2)
Note :
1.Type-Safe
2.Verbose   
3.NaN equals NaN
  ex : Object.is(NaN,NaN) // true
4.+0 does not equal -0  
  ex: Object.is(-0,+0) //false


Note : so for primitive types like strings, javascript compares their values,
but with objects,it compares their memory addresses.

Object Assign and Immutability :
=================================
Object.assign() method allows you to copy or merge the properties 
from one object to another object.

ex : 

let person1={
  firstName:'Jim',
  lastName:'Cooper',
  age: 29
};

let person2={};

Object.assign(person2,person1);

Object.is(person1,person2); //false
person1===person2           //false

Equality operator returns false because even though the properties
are the same between the two objects,they are not the same object in memory.

ex 2:
let person1={
  firstName:'Jim',
  lastName:'Cooper',
  age: 29
};

let healthStats = {
   height : 68,
   weight : 150
};

Object.assign(person1,healthStats);
console.log(person1); //{firstName: "Jim", lastName: "Cooper", age: 29, height: 68, weight: 150}

Note : Object.assign() merges the properties into that first object,
so it mutates it, and then it also returns the mutated object.

To avoid mutating the objects you're merging,it's helpful to pass in the empty object as the first parameter
to prevent any of the component objects from changing .

ex :

let person1={
  firstName:'Jim',
  lastName:'Cooper',
  age: 29
};

let healthStats = {
   height : 68,
   weight : 150
};

function mergeHealthStats(person,healthStats){
   return Object.assign({},person,healthStats);
}

let mergedPerson = mergeHealthStats(person1,healthStats);
console.log(mergedPerson); //{firstName: "Jim", lastName: "Cooper", age: 29, height: 68, weight: 150}

Object.assign method used with an empty object as the first parameter 
to avoid mutating the other objects.

Using Constructor Functions to Create Objects :
===============================================
'new' creates a new object,sets the context of the 'this' keyword to that new object.

ex: 
function Person(firstName,lastName){
 this.firstName = firstName;
 this.lastName = lastName;
}
let person=new Person('Jim','Cooper');
console.log(person); //{firstName: "Jim", lastName: "Cooper"}

using Object.create() :
=========================
let person2 = Object.create (
   Object.prototype,
   {
    firstName:{value:'Jim',enumerable:true,writable:true,configurable:true},
lastName:{value:'Cooper',enumerable:true,writable:true,configurable:true},
age:{value: 29,enumerable:true,writable:true,configurable:true}
   }
);

console.log(person2); // {firstName: "Jim", lastName: "Cooper", age: 29}

Object.create(proto,props) has two arguments.
The first argument i prototype for new object.
the second argument is the property descriptors.

Javascript Object Properties :
==============================

Using Bracket Notation to Access javascript properties.

ex : 

let person ={
  firstName:'Jim',
  lastName:'Cooper',
  age:29
};

console.log(person['lastName']); //Cooper

Note :
1.work with invalid Identifiers
2.Work with Variables

Square bracket notation allows access to properties
containing special characters and selection of properties using variables.

ex : const variable ='name';
     const obj = {name:'value'};
console.log(obj[variable]); // value

Modifying properties with property Descriptor :
===============================================
The Object.getOwnPropertyDescriptor() method returns a property descriptor
for an own property of a given object.

ex : 
let person ={
  firstName:'Jim',
  lastName:'Cooper',
  age:29
};

console.log(Object.getOwnPropertyDescriptor(person,"firstName"));
//{value: "Jim", writable: true, enumerable: true, configurable: true}

Object.defineProperty

syntax : Object.defineProperty(object,propertyName,descriptor);

By default a property defined with 'Object.defineProperty' isn't writable,
enumerable and configurable.


Note : if you make nested object read only,you're just preventing that pointer from being 
changed,so you can't point the name property to a new object.

ex : let person ={
       name:{
    firstName:'Jim',
secondName:'Cooper'
   },
  age:29 
 };
 
 Object.defineProperty(person,'name',{writable:false});
 person.name.firstName='Kris';
 console.log(person.name); // {firstName: "Kris", secondName: "Cooper"}
 
 'name' here is just a reference and javascript only prevents us from changing this 
 reference and will not stop us changing the properties of the object that this reference 
 points to.
 
 To make the object itself non-writable we can use Object's freeze method.
 
Note : Object.freeze works shallowly at the 'object' level,whereas
'Object.defineProperty' works at the property level.
Shallowly means,that if a property contains another object type,this object is  
affected by 'Object.freeze'. 

// Prevents any changes to an object
Object.freeze(object);

ex: 
  let person ={
       name:{
    firstName:'Jim',
secondName:'Cooper'
   },
  age:29 
 }; 
 
 Object.freeze(person.name);
 person.name.firstName='Kris';
 console.log(person.name); // {firstName: "Jim", secondName: "Cooper"}
 
 Using Enumerable Attribute :
 ============================
 By default, properties on an object are enumerable ,
 meaning we can enumerate over them with for in loops and list them with object.keys.
 
 ex:
 
 let person ={       
firstName:'Jim',
    secondName:'Cooper',    
age:29 
 }; 
 
 for(let propertyName in person){
   console.log(propertyName + ': '+ person[propertyName]);
 }

Output :
// firstName: Jim
// secondName: Cooper
// age: 29 

 ex :
  let person ={       
firstName:'Jim',
    secondName:'Cooper',    
age:29 
 }; 
 
 Object.defineProperty(person,'firstName',{enumerable:false});
 
 for(let propertyName in person){
   console.log(propertyName + ': '+ person[propertyName]);
 }
 
 output :
 // secondName: Cooper
 // age: 29
 
 Object.keys(person); // ["secondName", "age"]
 
 console.log(JSON.stringify(person)); // {"secondName":"Cooper","age":29}
 
 The 'enumerable' descriptor,when set to false on a property will hide it
 when looping over all properties using for...in loop.it also hides the property
 from the list of keys as obtained by Object.keys() and will not show up during 
 JSON serialization with JSON.stringify() as well.
 
Using Configurable Attribute :

Once 'configurable' property is set to false, it locks down that property and 
prevents from modifying 'enumerable' and even configurable.It also prevents from
deleting that property.

ex:  

let person ={       
firstName:'Jim',
    secondName:'Cooper',    
age:29 
 };

 Object.defineProperty(person,'firstName',{configurable:false });
 
 Object.defineProperty(person,'firstName',{enumerable:false }); // Cannot redefine property : firstName
 
 delete person.firstName; // cannot delete property 'firstName' 
 
 Object.defineProperty(person,'firstName',{configurable:true }); // Cannot redefine property : firstName

 So making a property non-configurable makes it so you can't change
 the configurable or enumerable attributes, and you can't delete the 
 property.
 
 Creating Property Getters and Setters :
 
 To create getters and setters,you have to use 'defineProperty'.
 
 ex: let person={
       name:{
     firstName:'Jim',
secondName:'Cooper'
   },
   age : 29
   };
   
   Object.defineProperty(person,'fullName',{get:function(){ return this.name.firstName + ' '+this.name.secondName;}});
   console.log(person.fullName); // Jim Cooper

ex 2 :
   
let person={
       name:{
     firstName:'Jim',
secondName:'Cooper'
   },
   age : 29
   };
   
   Object.defineProperty(person,'fullName',
   {get:function(){ 
      return this.name.firstName + ' '+this.name.secondName;
     },
    set:function(value){
  let nameParts=value.split(' ');
  this.name.firstName=nameParts[0];
  this.name.secondName=nameParts[1];
}
    });
person.fullName='Fred Jones';
   console.log(person.fullName); // Fred Jones

Javascript Prototypes and Interfaces :
======================================

Prototype:

A prototype is an object that exists on
every function in javascript.So every function in 
javascript has a prototype property.

ex : 
  let myFunction = function(){}
  console.log(myFunction.prototype); // {}
  
Objects do not have a prototype property.

ex : 
 let person={
   firstName:'Jim',
   lastName:'Cooper',
   age:29
 };  

console.log(person.prototype); // undefined 
   
ex : 
let person={
   firstName:'Jim',
   lastName:'Cooper',
   age:29
 }; 
 console.log(person.__proto__); // Object {}
 
 Object's prototype and a function prototype are used differently.
 
 Function's Prototype :
 A function's prototype is the object 'instance' that will become the prototype
 for all objects created using this function as a constructor.
 
 Object's Prototype :
 An Object's prototype is the object 'instance' from which the object is inherited.
 
 Note : A prototype,whether it's a functions prototype or an object's
 prototype, is actually an instance of an object in memory.
 
 
 ex :
 
 function Person(firstName,lastName){
  this.firstName=firstName;
  this.lastName=lastName;
 }
 
 console.log(Person.prototype); // Person {}
 let objJim=new Person('Jim','Cooper');
 console.log(objJim.__proto__); // Person {}
 console.log(Person.prototype === objJim.__proto__); // true
 
 ex :
 
 function Person(firstName,lastName){
   this.firstName=firstName;
   this.lastName=lastName;
 }
 
 Person.prototype.age=29;
 console.log(Person.prototype); // Person{ age: 19 }
 
 let jim = new Person('Jim','Cooper');
 let sofia = new Person('Sofia','Cooper');
 sofia.__proto__.age=19;

console.log(jim.__proto__); // Person { age: 19   } 
console.log(sofia.__proto__); // Person { age: 19 }

console.log(Person.prototype===jim.__proto__);

Note : The object instance that is the function's prototype 
becomes the prototype for all objects created from that prototype.

Instance vs Prototype Properties :

'hasOwnProperty' returns a boolean value indicating whether the object
on which you are calling it has a property with the name of the argument.

ex : 
function Person(firstName,lastName){
  this.firstName=firstName;
  this.lastName=lastName;
}

Person.prototype.age=29;
let jim=new Person('Jim','Cooper');
let sofia=new Person('Sofia','Cooper');

jim.age=18;

console.log(jim.hasOwnProperty('age')); // true
console.log(jim.age);  // 18

age value comes from Prototype

console.log(sofia.age); // 29



Note : hasOwnProperty does not look at the prototype chain of the object.

Changing a Function Prototype :

Change the function's prototype to point to a completely different object.

ex : function Person(firstName,secondName){
       this.firstName=firstName;
   this.secondName=secondName;
     } 
 
Person.prototype.age=29;
var jim=new Person('Jim','Cooper');
var sofia = new Person('Sofia','Cooper');

Person.prototype ={age:18};

console.log(Person.prototype); Object { age:18}
console.log(jim.age); // 29
console.log(sofia.age); // 29

let kris = new Person('Kris','Cooper');
console.log(kris.age);  // 18

Multiple level of Inheritance :

By default, all objects in javascript inherit from object, and object has no prototype.

ex : function Person(firstName,lastName){
       this.firstName=firstName;
   this.lastName=lastName;
     }
Person.prototype.age=24;
let jim=new Person('Jim','Cooper');
console.log(jim.__proto__);          // Person { age : 24 }
console.log(jim.__proto__.__proto__); // Object { }
console.log(jim.__proto__.__proto__.__proto__); // null
 
Creating Your Own Prototypal Inheritance Chains :

 ex :
   function Person(firstName,lastName,age){
     this.firstName=firstName;
this.lastName=lastName;
this.age=age;
Object.defineProperty(this,'fullName',{
   get:function(){
     return this.firstName+' '+this.lastName
   },
   enumerable:true
});
   } 

 function Student(firstName,lastName,age){
   Person.call(this,firstName,lastName,age);
   this._enrolledCourses=[];
   this.enroll=function(courseId){
     this._enrolledCourses.push(courseId);
   };
   
   this.getCourses = function(){
      return this.fullName+"'s enrolled Courses are : "+
         this._enrolledCourses.join(', '); 
   };
 
 }

Student.prototype=Object.create(Person.prototype);
Student.prototype.constructor = Student; 

let jim=new Student('Jim','Cooper',29);

console.log(jim); // Student {firstName: "Jim", lastName: "Cooper", age: 29,_enrolledCourses: Array(0), enroll: ƒ, getCourses: ƒ}
console.log(jim.__proto__); // Student {}
console.log(jim.__proto__.__proto__); // Person {}

jim.enroll('CS205');
jim.enroll('MA101');
jim.enroll('PS101');
console.log(jim.getCourses()); // Jim Cooper's enrolled Courses are : CS205, MA101, PS101

Javascript Classes :
======================
classes were introduced in ECMAScript version 6.
classes just offer a little cleaner syntax to accomplish it.

class Person{
 constructor(firstName,lastName,age){
  this.firstName=firstName;
  this.lastName=lastName;
  this.age=age;
  }
  // getter and setters
 get fullName(){
   return this.firstName+' '+this.lastName;
 }
 set fullName(fullName){
   var nameParts=fullName.split(' ');
   this.firstName=nameParts[0];
   this.lastName=nameParts[1];
 } 
 // adding functions
 isAdult() {
   return this.age >=18;
 }
}

let jim = new Person('Jim','Cooper',29);
console.log(jim); // {firstName: "Jim", lastName: "Cooper", age: 29}

console.log(jim.fullName); // Jim Cooper

jim.fullName='Fred Jones';
console.log(jim.fullName); // Fred Jones

console.log(jim.isAdult()); // true

Note : classes have prototypes,just like functions.
getter and setters live on the prototype,whereas other
properties live on the object instances.

console.log(jim); // {firstName: "Jim", lastName: "Cooper", age: 29}

Object.defineProperty(Person.prototype,'fullName',{enumerable:true}); 

console.log(jim); //{age: 29,firstName: "Jim",fullName: "Jim Cooper",lastName: "Cooper"}

class Student extends Person {
 constructor(firstName,lastName,age){
   super(firstName,lastName,age);
   this._enrolledCourses=[];
 }
 // static functions
 static fromPerson(person){
   return new Student(person.firstName,person.lastName,person.age);
 }
 
 enroll(courseId){
   this._enrolledCourses.push(courseId);
 }
 
 getCourses(){
   return this.fullName + "'s enrolled courses are : "+
          this._enrolledCourses.join(', ');
 }


let jim=new Student('Jim','Cooper',29);
jim.enroll('CS101');
console.log(jim.getCourses()); // jim Cooper's enrolled courses are : CS101


let jimStudent=Student.fromPerson(jim);

console.log(jimStudent); // Student { firstName:Jim lastName:Cooper age:29 _enrolledCourses: }