Where Marketing, Analytics, and UX meet

JavaScript: Object Inheritance

JavaScript client-side tutorial series image

JavaScript is incredibly powerful but it doesn't follow the rules of a lot of different languages. For example, making objects can be a little bit tricky. Even more tricky is creating new objects based on existing ones, and extending only the new object while leaving the original intact. This post will be about traditional object-oriented inheritance.

This post is a part of a series! Check out the JavaScript Client-Side Tutorial Series

In languages like Java and C#, you can typically create and object, like a Person object, and then create a Mom object that "extends" the Person object, inheriting all of the attributes of the Person object (their properties and methods), giving you the ability to add additional properties and methods. JavaScript, before the ECMA 6 standard, did not have an "extends" feature, but there are some tricks that make it possible to achieve the intended outcome. Let's have a look.

Creating objects

If you've not yet learned how to create objects, then please take a read through my JavaScript: Creating Reusable Objects post. That will explain the code I am about to show you.

For our tutorial example, I am going to use the concept of a bank account. Most bank account types have some similar base functionality: the ability to deposit money and the ability to widthdraw money. So first we are going to create our base Account Object.

(function(){
    
    Account = new function(){
      this.currency = "dollars";
      this.balance = 0.00;
      this.id = 0;
      this.deposit = function( thisAmount ){
        this.balance += thisAmount;
      }
      this.withdraw = function( thisAmount ){
        this.balance -= thisAmount;
      }
    }
   
})();

This creates an object we can create an instance of and immediately make us of, if we desire:

(function(){
    
    Account = new function(){
      this.currency = "dollars";
      this.balance = 0.00;
      this.id = 0;
      this.deposit = function( thisAmount ){
        this.balance += thisAmount;
      }
      this.withdraw = function( thisAmount ){
        this.balance -= thisAmount;
      }
    }
   
})();
  
console.log("-- my generic bank account--")
var myAccount = Object.create( Account );
myAccount.deposit(100);
myAccount.withdraw( 25 );
console.log( myAccount.balance ); // Should return 75

But suppose we want a more complex type of bank account. For example, we might want to create a couple new types of Accounts: a SavingsAccount and a CheckingAccount.  In these two cases, we might want slightly different rules. Let's take a look at the rules for these two more complicated types of Account Objects:

A SavingsAccount should be able to do the following:

  • Deposit money
  • Withdraw money
  • Have an interest rate
  • Add interest to the account on a regular schedule

A CheckingAccount should be able to do the following:

  • Deposit money
  • Withdraw money
  • Require a minimum balance
  • Not let you withdraw below the minimum balance

Extending Objects via object-oriented inheritance

Let's look at how we could take the basic functionality of the Account Object and extend it in the context of two new types of Account Objects:

(function(){
    
    Account = new function(){
      this.currency = "dollars";
      this.balance = 0.00;
      this.id = 0;
      this.deposit = function( thisAmount ){
        this.balance += thisAmount;
      }
      this.withdraw = function( thisAmount ){
        this.balance -= thisAmount;
      }
    }
   
})();
  
  
(function(){

    function calculateInterestPayment( thisInterestFrequency,thisInterestFrequencyLast,thisInterestRate,thisBalance ){
      if( thisInterestFrequency == "Monthly" ){
        if( ( new Date().getMonth() - 1 ) == thisInterestFrequencyLast ){
          console.log( "It's time to add interest!" );
          return {
            balance: thisBalance * ( (new Date().getMonth() - thisInterestFrequencyLast ) + thisInterestRate),
            interestFrequencyLast: new Date().getMonth()
          }
        }else{
          console.log( "It's not time to add interest!" );
          return {
            balance: thisBalance,
            interestFrequencyLast: thisInterestFrequencyLast
          }
        }
      }
    }  
  
    SavingsAccount = Object.create(Account);
    
    SavingsAccount.interestRate = 0.00;
    SavingsAccount.interestFrequency = "Monthly";
    SavingsAccount.interestFrequencyLast = new Date().getMonth();
  
    SavingsAccount.deposit = function( thisAmount ){
      Account.deposit.call( this, thisAmount );
      var results = calculateInterestPayment( this.interestFrequency,
                                              this.interestFrequencyLast,
                                              this.interestRate,
                                              this.balance );
      this.balance = results.balance;
      this.interestFrequencyLast = results.interestFrequencyLast;
      
    }
    
    SavingsAccount.withdraw = function( thisAmount ){
      Account.withdrawl.call( this, thisAmount );
      var results = calculateInterestPayment( this.interestFrequency,
                                              this.interestFrequencyLast,
                                              this.interestRate,
                                              this.balance );
      this.balance = results.balance;
      this.interestFrequencyLast = results.interestFrequencyLast;
    }
})();

(function(){
    CheckingAccount = Object.create(Account);
    CheckingAccount.minimumBalance = 0.00;
    CheckingAccount.withdraw = function( thisAmount ){
      if( (this.balance - thisAmount) < this.minimumBalance ){
          console.log("You cannot withdraw that amount. " + 
                      "You would be lower than your minimum balance.");
        return this.balance;
      }else{
        Account.withdraw.call( this, thisAmount );
        return this.balance;
      }
    }
  
})();
  
console.log("-- your savings account --");
  var savingsAccountForSteve = Object.create( SavingsAccount );
  savingsAccountForSteve.interestRate = .05;
  savingsAccountForSteve.deposit( 200 );
  console.log( savingsAccountForSteve.balance );
  savingsAccountForSteve.deposit( 100 );
  console.log( savingsAccountForSteve.balance );

console.log("-- your checking account --");
  var checkingAccountForSteve = Object.create( CheckingAccount );
  checkingAccountForSteve.minimumBalance = 100;
  checkingAccountForSteve.deposit( 100 );
  console.log( checkingAccountForSteve.balance );
  checkingAccountForSteve.withdraw( 50 );
  console.log( checkingAccountForSteve.balance );

There is a lot going on here so let's take it bit by bit. 

The original Account Object

First, we see the original code that defines our Account Object within a closure. This will immediately create an Account Object that can be used elsewhere. 

(function(){
    
    Account = new function(){
      this.currency = "dollars";
      this.balance = 0.00;
      this.id = 0;
      this.deposit = function( thisAmount ){
        this.balance += thisAmount;
      }
      this.withdraw = function( thisAmount ){
        this.balance -= thisAmount;
      }
    }
   
})();

The new SavingsAccount Object inheriting from the original Account Object

Next we can see we start a new self-executing anonymous function within a new closure. In here we are defining our new SavingsAccount Object:

(function(){

    function calculateInterestPayment( thisInterestFrequency,thisInterestFrequencyLast,thisInterestRate,thisBalance ){
      if( thisInterestFrequency == "Monthly" ){
        if( ( new Date().getMonth() - 1 ) == thisInterestFrequencyLast ){
          console.log( "It's time to add interest!" );
          return {
            balance: thisBalance * ( (new Date().getMonth() - thisInterestFrequencyLast ) + thisInterestRate),
            interestFrequencyLast: new Date().getMonth()
          }
        }else{
          console.log( "It's not time to add interest!" );
          return {
            balance: thisBalance,
            interestFrequencyLast: thisInterestFrequencyLast
          }
        }
      }
    }  
  
    SavingsAccount = Object.create(Account);
    
    SavingsAccount.interestRate = 0.00;
    SavingsAccount.interestFrequency = "Monthly";
    SavingsAccount.interestFrequencyLast = new Date().getMonth();
  
    SavingsAccount.deposit = function( thisAmount ){
      Account.deposit.call( this, thisAmount );
      var results = calculateInterestPayment( this.interestFrequency,
                                              this.interestFrequencyLast,
                                              this.interestRate,
                                              this.balance );
      this.balance = results.balance;
      this.interestFrequencyLast = results.interestFrequencyLast;
      
    }
    
    SavingsAccount.withdraw = function( thisAmount ){
      Account.withdraw.call( this, thisAmount );
      var results = calculateInterestPayment( this.interestFrequency,
                                              this.interestFrequencyLast,
                                              this.interestRate,
                                              this.balance );
      this.balance = results.balance;
      this.interestFrequencyLast = results.interestFrequencyLast;
    }
})();

Notice that it starts off defining a private method for our SavingsAccount Object called calculateInterestPayment. That private method is used within the SavingsAccount's public deposit and withdraw methods. The purpose of this private method is to determine if we need to add an interest payment to the balance. You can see from the way that it works, that each time a deposit or withdrawl happens, it checks to see how long it has been since the last interest payment and then makes it accordingly. To potentially add interest each time the SavingsAccount object runs the deposit or withdraw methods, we have to override those functions. So we redefine them with the same name within the closure, on the new SavingsAccount Object. We do want the traditional Account Object withdraw and deposit functionality to continue to happen, so within our overriding methods, we can call the parent Account Object withdraw or deposit methods using the call method, and first pass in the context of the new SavingsAccount Object by passing in the contextual this keyword. This means, it will first run the original Account Object definition of deposit or withdraw, but affect the current properties of the SavingsAccount Object, like the balance of the SavingsAccount Object instance.

The calculateInterestPayment private method also returns an object. The reason it does this is so that we can return multiple values back to the SavingsAccount Object from that one private method. We can then assign the resulting values to the balance and interestFrequencyLast properties of our SavingsAccount Object instance.

The new CheckingAccount Object inheriting from the original Account Object

Next, we see the definition of our new CheckingAccount Object:

(function(){
    CheckingAccount = Object.create(Account);
    CheckingAccount.minimumBalance = 0.00;
    CheckingAccount.withdraw = function( thisAmount ){
      if( (this.balance - thisAmount) < this.minimumBalance ){
          console.log("You cannot withdraw that amount. " + 
                      "You would be lower than your minimum balance.");
        return this.balance;
      }else{
        Account.withdraw.call( this, thisAmount );
        return this.balance;
      }
    }
  
 })();

The new CheckingAccount Object is created from the original Account Object as well, but we need to add a property to define the minimum balance for a CheckingAccount Object instance. To do that we simply take the new CheckingAccount Object and add a minimumBalance property to it and set the defaul value to 0.00.

We said earlier that we do not want to let people withdraw any amount that reduces the balance below the minimumBalance amount, so we are going to need to override the withdraw method inherited from the original Account Object. In the above code, we override the withdraw method on the CheckingAccount Object and start testing various values. First, we test to see if the current CheckingAccount Object balance property minus the requested withdraw amount creates a new balance value lower than the minimumBalance. If it does, then we send a message to the console saying the amount cannot be withdrawn from the CheckAccount Object and we return the unaffected CheckingAccount Object instance balance. However, if the transaction does not reduce the balance below the minimumBalance then we go ahead and call the original Account Object withdraw method within the context of the CheckingAccount object, so that it reduces the CheckingAccount Object balance property, and we return the value of the updated CheckingAccount balance property.

Testing our new extended objects

Finally, in the code we see a number of examples of using these two new CheckingAccount and SavingsAccount Objects:

console.log("-- your savings account --");
  var savingsAccountForSteve = Object.create( SavingsAccount );
  savingsAccountForSteve.interestRate = .05;
  savingsAccountForSteve.deposit( 200 );
  console.log( savingsAccountForSteve.balance );
  savingsAccountForSteve.deposit( 100 );
  console.log( savingsAccountForSteve.balance );

console.log("-- your checking account --");
  var checkingAccountForSteve = Object.create( CheckingAccount );
  checkingAccountForSteve.minimumBalance = 100;
  checkingAccountForSteve.deposit( 100 );
  console.log( checkingAccountForSteve.balance );
  checkingAccountForSteve.withdraw( 50 );
  console.log( checkingAccountForSteve.balance );

As you can see we can create the new types of Account Objects and make use of their appropriate properties, like interestRate and minimumBalance, while still having access to the original Account Object public deposit and withdraw methods, and in some cases override their behavior.

That does it for creating and extending JavaScript Objects using object-oriented-like inheritance.

In my next post, I will demonstrate how to employ the Singleton pattern to create multiple instances of a shared object that show the same property values no matter where those object instances are in the code.

 

Add new comment

*

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
By submitting this form, you accept the Mollom privacy policy.