Today, we are going to take a look at how to work with simple (and not so simple) javascript objects. The javascript object is a very misunderstood beast - quite often people don't even know that javascript has objects. And of the people who do know about them, many think that it is a stripped down and weak object system, and so never bother to actually use it when writing any javascript. But actually the opposite of that is true - the javascript object model is extremely flexible, and allows you as the programmer to do things that can't be done without extreme contortions in languages like C++ and C#. Today, we will only scratch the surface of that flexibility, but it should give you ideas about how to use javascript objects in your own web applications.
The example that we will be working through today is a Color object for javascript. It will be able to work with RGB and HSV values, as well as hex strings - for example, you will be able to give it an HSV value, and the object will be able to tell you the RGB values as well as the hex string representation. As an added bonus, it can return the complement of any color.
So lets get straight down to it! First off, lets write a very simple RGB holder object, and take a look at how that works:
function Color(r,g,b)
{
this.Red = r;
this.Green = g;
this.Blue = b;
}
myColor = new Color(10,20,30);
//Prints out: "Red: 10"
alert("Red: " + myColor.Red);
This is a simple object definition and instantiation. Don't be weirded
out by the fact that we used the word function to do the definition -
in javascript, functions and objects are in the end equivalent. While
this might not make a whole lot of sense now, it should become clearer
as to why that is when we delve into more complex objects. In this
object we made 3 public fields (Red, Green, and Blue), and set them
equal to the values passed in when the object is "constructed". When the
command new Color(10,20,30); was run, the contents of the Color
function were executed - which makes Color, in the terms of regular
object oriented languages, both the object definition and the
constructor.
Lets take this example up a notch:
function Color(r,g,b)
{
this.Red = r;
this.Green = g;
this.Blue = b;
this.HexString = function()
{
var rStr = this.Red.toString(16);
if (rStr.length == 1)
rStr = '0' + rStr;
var gStr = this.Green.toString(16);
if (gStr.length == 1)
gStr = '0' + gStr;
var bStr = this.Blue.toString(16);
if (bStr.length == 1)
bStr = '0' + bStr;
return ('#' + rStr + gStr + bStr).toUpperCase();
}
}
myColor = new Color(10,20,30);
//Prints out: "#0A141E"
alert(myColor.HexString());
Here, we have added a public function to the color object. Since
functions are first class in javascript, you can assign them just like
variables, which is why the syntax this.HexString = function() makes
sense. So now we have a color object which you can declare with RGB
values, and get the hex string representation back out.
So far, everything we have added to this object has been a public property. But public properties don't always cut it in the world of objects, so how do we declare private variables in a javascript object?
function Color(r,g,b)
{
var red = r;
var green = g;
var blue = b;
this.Red = function()
{ return red; }
this.Green = function()
{ return green; }
this.Blue = function()
{ return blue; }
this.HexString = function()
{
var rStr = red.toString(16);
if (rStr.length == 1)
rStr = '0' + rStr;
var gStr = green.toString(16);
if (gStr.length == 1)
gStr = '0' + gStr;
var bStr = blue.toString(16);
if (bStr.length == 1)
bStr = '0' + bStr;
return ('#' + rStr + gStr + bStr).toUpperCase();
}
}
Now we have three private member variables: red, green and blue. How
does this work, and why does this even make sense? Well, when the
Color function is run to construct the Color object, the variables
red, green, and blue got created. But instead of those variables
disappearing when the function completes, like they would with a normal
function call, here javascript stores the local environment at the end
of the function. That environment is actually a large part of what makes
up an object. Then, whenever a function in the object is run, it is run
within that environment, meaning is has access to those private
variables.
Now, in that last code sample, we actually lost a little functionality - we made the red, green and blue values read-only (by making them private, and only creating public accessors and no setters). But thats ok, because we are about to add that functionality and more back:
function Color(r,g,b)
{
//Stored as values between 0 and 1
var red = 0;
var green = 0;
var blue = 0;
//Stored as values between 0 and 360
var hue = 0;
//Strored as values between 0 and 1
var saturation = 0;
var value = 0;
this.SetRGB(r,g,b);
this.Red = function()
{ return Math.round(red*255); }
this.Green = function()
{ return Math.round(green*255); }
this.Blue = function()
{ return Math.round(blue*255); }
this.SetRGB = function(newR, newG, newB)
{
red = r/255.0;
green = g/255.0;
blue = b/255.0;
calculateHSV();
}
this.HexString = function()
{
var rStr = this.Red().toString(16);
if (rStr.length == 1)
rStr = '0' + rStr;
var gStr = this.Green().toString(16);
if (gStr.length == 1)
gStr = '0' + gStr;
var bStr = this.Blue().toString(16);
if (bStr.length == 1)
bStr = '0' + bStr;
return ('#' + rStr + gStr + bStr).toUpperCase();
}
function calculateHSV()
{
var max = Math.max(Math.max(red, green), blue);
var min = Math.min(Math.min(red, green), blue);
value = max;
saturation = 0;
if(max != 0)
saturation = 1 - min/max;
hue = 0;
if(min == max)
return;
var delta = (max - min);
if (red == max)
hue = (green - blue) / delta;
else if (green == max)
hue = 2 + ((blue - red) / delta);
else
hue = 4 + ((red - green) / delta);
hue = hue * 60;
if(hue < 0)
hue += 360;
}
}
Ok, now we have started to get complicated. Lets go through the changes
from the previous example, and explain them all. First off, we are now
storing HSV values as private variables as well. We have also changed
how we are storing the RGB values - instead of integer values from 0 to
255, we are now storing them as floating point values from 0 to 1 (this
makes it easier for the conversion to and from HSV). As you may have
noticed, the RGB accessor functions still return the values in the range
from 0 to 255, since no one actually wants to use RGB values from 0 to
1. There is also now a SetRGB function. This function takes in RGB
values in the range 0 to 255, converts them to the 0 to 1 range, and
then calls calculateHSV.
The function calculateHSV is our first private function. It is private
for the same reasons that the private variables are private - the
function exists in the environment scope of the object, and is not
accessible outside that scope. I'm not going to get into the details of
the code to convert RGB to HSV - it is a well known process, and if you
would like details,
Wikipedia has a good
section on the formula.
The other interesting change here is that we are no longer directly
setting the RGB values in the constructor. We actually call the function
SetRGB, so we don't have to duplicate that code. The reason this is of
note is because it shows that you can do a lot more than just declare
and set things in the constructor. You can write and call any code you
want - even functions that are declared in the object itself.
Now we are going to add a couple more functions to the Color object,
but nothing terribly exciting:
function Color(r,g,b)
{
//Stored as values between 0 and 1
var red = 0;
var green = 0;
var blue = 0;
//Stored as values between 0 and 360
var hue = 0;
//Strored as values between 0 and 1
var saturation = 0;
var value = 0;
this.SetRGB(r,g,b);
this.SetRGB = function(r, g, b)
{
red = r/255.0;
green = g/255.0;
blue = b/255.0;
calculateHSV();
}
this.Red = function()
{ return Math.round(red*255); }
this.Green = function()
{ return Math.round(green*255); }
this.Blue = function()
{ return Math.round(blue*255); }
this.SetHSV = function(h, s, v)
{
hue = h;
saturation = s;
value = v;
calculateRGB();
}
this.Hue = function()
{ return hue; }
this.Saturation = function()
{ return saturation; }
this.Value = function()
{ return value; }
this.SetHexString = function(hexString)
{
if(hexString == null || typeof(hexString) != "string")
{
this.SetRGB(0,0,0);
return;
}
if (hexString.substr(0, 1) == '#')
hexString = hexString.substr(1);
if(hexString.length != 6)
{
this.SetRGB(0,0,0);
return;
}
var r = parseInt(hexString.substr(0, 2), 16);
var g = parseInt(hexString.substr(2, 2), 16);
var b = parseInt(hexString.substr(4, 2), 16);
if (isNaN(r) || isNaN(g) || isNaN(b))
{
this.SetRGB(0,0,0);
return;
}
this.SetRGB(r,g,b);
}
this.HexString = function()
{
var rStr = this.Red().toString(16);
if (rStr.length == 1)
rStr = '0' + rStr;
var gStr = this.Green().toString(16);
if (gStr.length == 1)
gStr = '0' + gStr;
var bStr = this.Blue().toString(16);
if (bStr.length == 1)
bStr = '0' + bStr;
return ('#' + rStr + gStr + bStr).toUpperCase();
}
this.Complement = function()
{
var newHue = (hue >= 180) ? hue - 180 : hue + 180;
var newVal = (value * (saturation - 1) + 1);
var newSat = (value*saturation) / newVal;
var newColor = new Color();
newColor.SetHSV(newHue, newSat, newVal);
return newColor;
}
function calculateHSV()
{
var max = Math.max(Math.max(red, green), blue);
var min = Math.min(Math.min(red, green), blue);
value = max;
saturation = 0;
if(max != 0)
saturation = 1 - min/max;
hue = 0;
if(min == max)
return;
var delta = (max - min);
if (red == max)
hue = (green - blue) / delta;
else if (green == max)
hue = 2 + ((blue - red) / delta);
else
hue = 4 + ((red - green) / delta);
hue = hue * 60;
if(hue < 0)
hue += 360;
}
function calculateRGB()
{
red = value;
green = value;
blue = value;
if(value == 0 || saturation == 0)
return;
var tHue = (hue / 60);
var i = Math.floor(tHue);
var f = tHue - i;
var p = value * (1 - saturation);
var q = value * (1 - saturation * f);
var t = value * (1 - saturation * (1 - f));
switch(i)
{
case 0:
red = value; green = t; blue = p;
break;
case 1:
red = q; green = value; blue = p;
break;
case 2:
red = p; green = value; blue = t;
break;
case 3:
red = p; green = q; blue = value;
break;
case 4:
red = t; green = p; blue = value;
break;
default:
red = value; green = p; blue = q;
break;
}
}
}
Here we have added functions for getting and setting
Hue/Saturation/Value numbers, as well as a function for setting the
color using a hex string. We have also added a function called
Complement, which, when called on a color, will return the complement
to that color. The ability to set HSV values required a new private
function: calculateRGB. This function calculates RGB values from HSV
values. The contents of the calculateRGB and Complement functions
are not anything special - they are from the formulas on the HSV
Wikipedia page that I
referred to earlier.
So that color object has pretty much all the functionality we want (for now, at least). But it really isn't wrapped that nice. To create one, I am required to give it RGB values at the moment. What if I want to create one straight from a hex string? Or HSV values? You can't have multiple constructors in javascript - in fact the concept doesn't even really make much sense, when you consider how the object is being created. So instead, we are going to create, essentially, a wrapper object. Take a close look at what is happening in the following code:
var Colors = new function()
{
this.ColorFromHSV = function(hue, sat, val)
{
var color = new Color();
color.SetHSV(hue,sat,val);
return color;
}
this.ColorFromRGB = function(r, g, b)
{
var color = new Color();
color.SetRGB(r,g,b);
return color;
}
this.ColorFromHex = function(hexStr)
{
var color = new Color();
color.SetHexString(hexStr);
return color;
}
}
();
At first glance, it doesn't look that weird. But in looking closer, you
might have noticed that we are saying new function(), and thats not
the name of an object we have defined. Also, what is up with the ();
on the very last line?
Here it is again, with all the inner content stripped out, and some extra parentheses to help visually differentiate things:
var Colors = new (function(){ }) ();
We are essentially defining an object on the fly here. The code
(function(){ }) defines a new object type without ever giving it a
name (i.e., its anonymous), and we instantiate it on the same line. In
the end, it has elements reminiscent of both a Singleton and a Static in
regular object oriented languages, but it is not quite either.
So why are we doing this? If you look up at the code again, you'll
notice we defined three functions - ColorFromHSV, ColorFromRGB, and
ColorFromHex. Each of those functions take appropriate arguments, and
then return a color object of the color given in the arguments. So the
code:
var myColor = Colors.ColorFromHex("#FFFFFF");
Will create a color object whose color is (in this case) white. So this gives us some nice "static" functions that we can use to create color objects in any way we want - without having to rely only on the color object's constructor.
But now we have this color object sitting out there that people can
still call new Color() on, and we don't want that. We only want people
to use these new functions on this Colors variable. But there is
actually an easy way to deal with this - we stick the entire Color
object definition into the anonymous object definition. That way, the
anonymous object still knows how to create a Color object, and so can
return them with the ColorFromRGB calls and the like - but the
definition won't exist in the scope of the rest of the code. Once we do
this, calling new Color() anywhere but inside the anonymous object
definition will cause a javascript error saying that "Color is
undefined."
So here is what the final code looks like:
var Colors = new function()
{
this.ColorFromHSV = function(hue, sat, val)
{
var color = new Color();
color.SetHSV(hue,sat,val);
return color;
}
this.ColorFromRGB = function(r, g, b)
{
var color = new Color();
color.SetRGB(r,g,b);
return color;
}
this.ColorFromHex = function(hexStr)
{
var color = new Color();
color.SetHexString(hexStr);
return color;
}
function Color()
{
//Stored as values between 0 and 1
var red = 0;
var green = 0;
var blue = 0;
//Stored as values between 0 and 360
var hue = 0;
//Strored as values between 0 and 1
var saturation = 0;
var value = 0;
this.SetRGB = function(r, g, b)
{
red = r/255.0;
green = g/255.0;
blue = b/255.0;
calculateHSV();
}
this.Red = function()
{ return Math.round(red*255); }
this.Green = function()
{ return Math.round(green*255); }
this.Blue = function()
{ return Math.round(blue*255); }
this.SetHSV = function(h, s, v)
{
hue = h;
saturation = s;
value = v;
calculateRGB();
}
this.Hue = function()
{ return hue; }
this.Saturation = function()
{ return saturation; }
this.Value = function()
{ return value; }
this.SetHexString = function(hexString)
{
if(hexString == null || typeof(hexString) != "string")
{
this.SetRGB(0,0,0);
return;
}
if (hexString.substr(0, 1) == '#')
hexString = hexString.substr(1);
if(hexString.length != 6)
{
this.SetRGB(0,0,0);
return;
}
var r = parseInt(hexString.substr(0, 2), 16);
var g = parseInt(hexString.substr(2, 2), 16);
var b = parseInt(hexString.substr(4, 2), 16);
if (isNaN(r) || isNaN(g) || isNaN(b))
{
this.SetRGB(0,0,0);
return;
}
this.SetRGB(r,g,b);
}
this.HexString = function()
{
var rStr = this.Red().toString(16);
if (rStr.length == 1)
rStr = '0' + rStr;
var gStr = this.Green().toString(16);
if (gStr.length == 1)
gStr = '0' + gStr;
var bStr = this.Blue().toString(16);
if (bStr.length == 1)
bStr = '0' + bStr;
return ('#' + rStr + gStr + bStr).toUpperCase();
}
this.Complement = function()
{
var newHue = (hue >= 180) ? hue - 180 : hue + 180;
var newVal = (value * (saturation - 1) + 1);
var newSat = (value*saturation) / newVal;
var newColor = new Color();
newColor.SetHSV(newHue, newSat, newVal);
return newColor;
}
function calculateHSV()
{
var max = Math.max(Math.max(red, green), blue);
var min = Math.min(Math.min(red, green), blue);
value = max;
saturation = 0;
if(max != 0)
saturation = 1 - min/max;
hue = 0;
if(min == max)
return;
var delta = (max - min);
if (red == max)
hue = (green - blue) / delta;
else if (green == max)
hue = 2 + ((blue - red) / delta);
else
hue = 4 + ((red - green) / delta);
hue = hue * 60;
if(hue < 0)
hue += 360;
}
function calculateRGB()
{
red = value;
green = value;
blue = value;
if(value == 0 || saturation == 0)
return;
var tHue = (hue / 60);
var i = Math.floor(tHue);
var f = tHue - i;
var p = value * (1 - saturation);
var q = value * (1 - saturation * f);
var t = value * (1 - saturation * (1 - f));
switch(i)
{
case 0:
red = value; green = t; blue = p;
break;
case 1:
red = q; green = value; blue = p;
break;
case 2:
red = p; green = value; blue = t;
break;
case 3:
red = p; green = q; blue = value;
break;
case 4:
red = t; green = p; blue = value;
break;
default:
red = value; green = p; blue = q;
break;
}
}
}
}
();
And of course, you can work with it like this:
var red = Colors.ColorFromRGB(255,0,0);
var complement = red.Complement();
//Prints out: "#00FFFF"
alert(complement.HexString());
And that is it for this introduction to the wacky world of javascript objects. Hope you have a better understanding of the extreme flexibility of objects in javascript, and that you enjoyed this color object example. Feel free to use and expand on this color object - there are probably all sorts of useful functions that could be added to it! If you have any questions/comments about either the object techniques or the color object itself, leave them below.
Thanks for those explanations! I thought I knew quite a bunch about javascript. But with this OO way of coding, plus the JSON one and those XMLHttpRequests, I already feel like I'm way behind. I'll hopefully catch up with you guys thanks to your tutorials. Cheers!
Thanks for spotting that - I've fixed it in all the code examples.
this article is very useful...great job! ;)
I've always wondered about javascript objects and classes. Thanks!
Excellent description. I've seen this explained 15-20 times, this description is the one that seems clearest (and most practical!) to me.
Nicely Done !! Thanks for the post. Starting with a simple object and then building upon it to add more functionality, this tutorial has helped me clarify quite a few concepts. Looking forward to reading your other tutorials.
I may be doing something wrong, but when calling
before
the code doesn't work. It works if I switch their places.
Nice Post, After reading this post my concept about javascript classes and objects are now cleared. Thanks.
When I copy and paste your final code example into a .js file (and insert it into an HTML file using a tag) it doesn't work... The first example does, and it was really awesome, but I'm having a really hard time debugging the js. Any tips?
Does the browser's error console contain any messages?
Figured it out, thanks. It was a combination of my html missing a css file it was trying to link to and not adding the "var red = ..." code to the bottom of your last example. Totally my fault. Thank you both for the help and the quick response.
There will be a performance issue when you create a function object using the function constructor.
http://wisentechnologies.com/it-courses/html-css-javascript-jquery-training.aspx