A procedure is a chunk of coding set aside for some reason. These reasons can be:
getElementById
, Number
, alert
—most of the functions and methods I've used so far.true
or false
value is too complex to use in a conditionAs I mentioned back in Dynamic Scripting, there are two types of procedures: functions, which stand on their own, and methods, which are associated with objects. For example, the function Number
is not associated with any object, but the method pow
is associated with the Math
object.
Calling a procedure means you put it to work.
Before we talk about this, I want to make something very clear: you have the same number of scripts as you do script
elements. Whether each script are contained in the actual element or a separate file or a mix of these; it doesn't matter. One script for each script
element.
With that in mind, a procedure must be in one of two places:
Remember when I said that an element must exist before you start working with it? Scripts are no exception. This is one of the reasons that, while scripts that immediately take effect are best placed at the end of the body
element, scripts consisting entirely of procedures usually go in the head
element.
A procedure always starts with the keyword function
, followed by the procedure name (let's call this once
, referring to the fact that it gets all text from a P_Full_Text
p
element.
No, there needs to be no capitalization of the name; it's just a habit of mine.
After the procedure name, you have a pair of parentheses, then a pair of braces:
function
P_Full_Text
(){}You have to be careful with the procedure name, since if you have two procedures with the same name used by the same page, they will collide. Even if they're part of two separate external script files, if you use them with the same page there will be trouble. I know this to my frustration.
Between the braces goes whatever coding you desire: loops, if statements, calls to other procedures—even other procedures!
Remember the while loop that extracted the text of a p
element and displayed it in an alert box that I demonstrated back in Loops? Let's use that in P_Full_Text
:
function
P_Full_Text
(){var
p_child = document
.getElementById
("p_1"
).childNodes
;var
p_text = ""
;for
(var
loop = 0; loop < p_child.length
; loop++){nodeType
== 3)?(p_child[loop].data
):(p_child[loop].firstChild
.data
);alert
(p_text);To call this particular procedure, all you need is the following code:
P_Full_Text
();Seriously, that's it. The following result is this pop-up box.
If you want the procedure to return a value (and a lot of procedures do), you need the keyword return
, which says return this value
.
So if I want P_Full_Text
to return the value of the text, I can add this line to the end of the procedure:
return
p_text;Here's the final result (the alert
function has been replaced by the code that returns the value).
P_Full_Text
function
P_Full_Text
(){var
p_child = document
.getElementById
("p_1"
).childNodes
;var
p_text = ""
;for
(var
loop = 0; loop < p_child.length
; loop++){nodeType
== 3)?(p_child[loop].data
):(p_child[loop].firstChild
.data
);return
p_text;When I call this procedure, it is best to call it so the value it returns is stored in a variable, or even displayed in an alert box:
P_Full_Text
alert
(P_Full_Text
());Remember when I mentioned that the code inside parentheses is executed before the processing outside back in Numbers and Math? This holds true for procedures as well: the call to P_Full_Text
is inside a pair of parentheses (those belonging to the alert
function), so it is executed first.
Once the procedure returns a value, it is finished and the procedure will not execute anything after it returns a value. So how can you return multiple values? Use an array to hold the variables that the procedure generates, and once that procedure returns the array, you can use coding to pick out the information you want.
A procedure can have values passed to it when it is called. For example, you've often seen
throughout my examples. In the example pages, there is a getElementById
("p_1"
)p
element with the ID p_1
. When I create a reference to it, I call the method getElementById
. The value I pass to that procedure is the ID of the element that I want—in this case, p_1
.
I haven't the foggiest idea what the method getElementById
looks like, so I'll use P_Full_Text
instead. And, instead of letting the procedure decide which element is referenced, I'll tell the procedure which element to reference. The procedure variable used to pass the value is named p_id.
P_Full_Text
To Receive A Valuefunction
P_Full_Text
(p_id){var
p_child = document
.getElementById
(p_id).childNodes
;var
p_text = ""
;for
(var
loop = 0; loop < p_child.length
; loop++){nodeType
== 3)?(p_child[loop].data
):(p_child[loop].firstChild
.data
);return
p_text;Calling the procedure and storing the value in the variable para_text I get:
P_Full_Text
Via A Procedure Callalert
(P_Full_Text
("p_1"
));This will get me the text stored in the element with the ID p_1
.
But say I wanted to add in a second variable that decides whether I want the text of an element with a specific ID, or the number of child nodes that element has. The second variable is separated from the first by a comma, and the code updated thusly (I'm using the if-else shorthand I talked about in Decisions):
P_Full_Text
To Receive 2 Valuesfunction
P_Full_Text
(p_id, what){var
p_child = document
.getElementById
(p_id).childNodes
;if
(what == "text"
){var
p_text = ""
;for
(var
loop = 0; loop < p_child.length
; loop++){nodeType
== 3)?(p_child[loop].data
):(p_child[loop].firstChild
.data
);return
(what == "text"
)?p_text:p_child.length
;In the following example, the webpage created in Your First Webpage has a table added to it to show a) the number of child nodes each p
element, and b) the plain text of it. Below is the code for the page along with a touch of styling, so that the table is be easier to read.
There's something I want you to notice: I have two script
elements: one in the head
element, one in the body
element. This is because I want to demonstrate that separating the procedures from the scripts that call them is possible—and often advisable, since your procedures may be used over and over again, but the scripts that call them may vary widely.
Below, then, are the two scripts: one containing the procedure, one containing the procedure calls.
function
P_Full_Text
(p_id, what){var
p_child = document
.getElementById
(p_id).childNodes
;if
(what == "text"
){var
p_text = ""
;for
(var
loop = 0; loop < p_child.length
; loop++){nodeType
== 3)?(p_child[loop].data
):(p_child[loop].firstChild
.data
);return
(what == "text"
)?p_text:p_child.length
;document
.getElementById
("td_1-1"
).firstChild
.data
= P_Full_Text
("p_1"
);document
.getElementById
("td_1-2"
).firstChild
.data
= P_Full_Text
("p_1"
, "text"
);document
.getElementById
("td_2-1"
).firstChild
.data
= P_Full_Text
("p_2"
);document
.getElementById
("td_2-2"
).firstChild
.data
= P_Full_Text
("p_2"
, "text"
);The fact that the script with the procedure is in the head
and therefore before any p
element nodes does not cause an error because this procedure is not yet called; its code and reference have not been run yet. Since the script calls this procedure is at the bottom of the page, the nodes P_Full_Text
is intended to refer to have already been processed when its code is run.
The page below displays why external script files are helpful. Look how the JavaScript expands the HTML document:
function
P_Full_Text
(p_id, what){var
p_child = document
.getElementById
(p_id).childNodes
;if
(what == "text"
){var
p_text = ""
;for
(var
loop = 0; loop < p_child.length
; loop++){nodeType
== 3)?(p_child[loop].data
):(p_child[loop].firstChild
.data
);return
(what == "text"
)?p_text:p_child.length
;document
.getElementById
("td_1-1"
).firstChild
.data
= P_Full_Text
("p_1"
);document
.getElementById
("td_1-2"
).firstChild
.data
= P_Full_Text
("p_1"
, "text"
);document
.getElementById
("td_2-1"
).firstChild
.data
= P_Full_Text
("p_2"
);document
.getElementById
("td_2-2"
).firstChild
.data
= P_Full_Text
("p_2"
, "text"
);This following page puts both scripts and the stylesheet in separate files:
p_stats_split.html
function
P_Full_Text
(p_id, what){var
p_child = document
.getElementById
(p_id).childNodes
;if
(what == "text"
){var
p_text = ""
;for
(var
loop = 0; loop < p_child.length
; loop++){nodeType
== 3)?(p_child[loop].data
):(p_child[loop].firstChild
.data
);return
(what == "text"
)?p_text:p_child.length
;document
.getElementById
("td_1-1"
).firstChild
.data
= P_Full_Text
("p_1"
);document
.getElementById
("td_1-2"
).firstChild
.data
= P_Full_Text
("p_1"
, "text"
);document
.getElementById
("td_2-1"
).firstChild
.data
= P_Full_Text
("p_2"
);document
.getElementById
("td_2-2"
).firstChild
.data
= P_Full_Text
("p_2"
, "text"
);This is much easier to keep track of, since now you've got smaller files to maintain. The final result for both pages is the same:
The biggest difference is you wouldn't have to redo the JavaScript for every webpage, making it easier to maintain.
There is a reason why the keyword return
exists: A variable declared inside a procedure does not exist outside the function. This limited existence is known as a variable's scope.
Permit me to demonstrate by creating script with a procedure which assigns a number to a variable (but doesn't return it), calls that procedure, and tries to write the value of that variable to a specified element which will have the text Nothing written here.
before the script is run:
function
number
(){var
num = 7;number
();document
.getElementById
("JS"
).firstChild
.data
= num;Clearly, I did something wrong, since the text hasn't changed. Not only that, but I get the following errors from various browsers:
When both FireFox and Internet Explorer say there's something wrong about your script, you've definitely goofed. The reason all these return an error is simple: num does not exist outside of number
.
When you nest a procedure inside a procedure, the same applies.
Now, a variable declared outside of a procedure is known as a global variable. Global variables may be reference and changed within a procedure and outside it:
function
number
(){var
num;number
();document
.getElementById
("JS"
).firstChild
.data
= num;Similarily, procedures that are not nested in other procedures may be called within any procedures.
This limited scope may be used to your advantage. One way that JavaScript code gets bloated is long variable names, but when scope limits the influence of variables, you can shorten those variables down to one letter each. In the function P_Full_Text
, the following changes are made to the variable names:
Original Name | Shortened Name |
---|---|
loop | l |
p_child | c |
p_id | i |
p_text | t |
what | w |
function
P_Full_Text
(i, w){var
c = document
.getElementById
(i).childNodes
;if
(w == "text"
){var
t = ""
;for
(var
l = 0; l < c.length
; l++){nodeType
== 3)?(c[l].data
):(c[l].firstChild
.data
);return
(w == "text"
)?t:c.length
;Procedures may contain other procedures. Like variables, these inner procedures may not be used outside the procedure they are nested in; to get their values, those values have to be passed to the procedure they are nested in. Below, I adjusted the function P_Full_Text
so that the code that extracts the text from the p
element is in its own procedure (in this case, it's a function).
function
P_Full_Text
(i, w){var
c = document
.getElementById
(i).childNodes
;function
GetText
(cl){var
t = ""
;for
(var
l = 0; l < cl.length
; l++){nodeType
== 3)?(cl[l].data
):(cl[l].firstChild
.data
);return
t;return
(w == "text"
)?GetText
(c):c.length
;What the above does is get the child node list of a p
element. Depending on what the second value passed is (that is, the value w is set to), it either returns the number of child nodes or it uses the nested function Get_Text
to extract the paragraph's text.
This would work even if I had GetText
after the keyword return
because I call the procedure before anything is returned. Its actual position within the function P_Full_Text
is irrelevant—sort of. I would still get a warning from my JavaScript error console about this procedure not always returning a value. That warning goes away when I place return
at the end of the procedure.
One of the more mind-bending challenges in any programming language is a recursive procedure, which means a procedure that calls itself. This can lead to infinite loops, so you'll want a way to avoid that.
So why would I want a recursive procedure anyways?
Well, my current script will only extract text so long as the nesting level never goes past one—that is, from the p
element's children. But say I wanted to extract the full text of a p
element regardless of nesting? I would have to extract the text from those elements in the same way I extract the text from a p
element—and I would have to allow the script to go as far as I need it to go.
In all practicality, it is impossible for this kind of recursion to go on indefinitely if it is properly programmed—if a file of any kind can be stored on a computer, there's a limit to its size and therefore a limit to the nesting in an (X)HTML document.
Below, I've updated the page created in Your First Webpage to include a third paragraph, just to illustrate things better (the table below the paragraphs is not read by the script).
Below is the code for those three paragraphs.
p
Elements Of The Above PageSo this page has inline elements with multiple child nodes, multiple layers of inline elements with only a single text node, and just to make everything even more challenging, there's an empty element in the third paragraph. :-)
So, let's start out with the functions P_Full_Text
and GetText
. However, we are going to pretty much rewrite the contents of the for
loop in GetText
, so let's start by getting rid of what's there.
GetText
Ready To Be Rebuiltfunction
P_Full_Text
(i, w){var
c = document
.getElementById
(i).childNodes
;function
GetText
(cl){var
t = ""
;for
(var
l = 0; l < cl.length
; l++){return
t;return
(w == "text"
)?GetText
(c):c.length
;Now this is where logic really comes into play. The only two types of nodes that we need to get at are element nodes (type 1) and text nodes (type 3). None of the p
elements contain any other type—well okay, they also contain attribute nodes, but those are really beside the point since they don't show up in a childNodes
list. The childNodes
property only returns types 1 and 3. Type 3 we can process right away, type 1 is the one that gets complicated, so lets set up an if
/else
statement to split the two types of nodes right away and deal with type 3 (text nodes).
GetText
function
P_Full_Text
(i, w){var
c = document
.getElementById
(i).childNodes
;function
GetText
(cl){var
t = ""
;for
(var
l = 0; l < cl.length
; l++){if
(cl[l].nodeType
== 1){else
{t += cl[l].data
;}return
t;return
(w == "text"
)?GetText
(c):c.length
;Now, before we do anything with an element node, I want to remind you of something: you cannot manipulate the values of a node list like you can those of an array. That means that, since cl is a node list, you cannot change the value contained in cl[l]
. I tell you this with the full authority of someone who made precisely such an error, and I was almost tearing my hair out trying to fix a resulting infinite loop before I clued into my goof. Therefore, we must put the value in cl[l]
into a variable we can work with (let's call it n, short for node
).
Once we have done that, we'll need a way to burrow
through the levels of nesting. Keep in mind the following:
Therefore, it seems to make sense that we should use a while
loop that gets the first child of a node and runs so long as the length of the child node list is equal to 1.
When the loop is finished, we can take the node contained in n, and see how long its child node list is. It will be either 0 or more than 1. If it's equal to 0, then we can process the text node. If it's more than 1, then things get interesting. We'll do that as an
statement.if
/else
GetText
function
P_Full_Text
(i, w){var
c = document
.getElementById
(i).childNodes
;function
GetText
(cl){var
t = ""
;for
(var
l = 0; l < cl.length
; l++){if
(cl[l].nodeType
== 1){var
n = cl[l];while
(n.childNodes
.length
== 1){n = n.firstChild
}if
(n.childNodes
.length
> 1){else
{t += n.data
;}else
{t += cl[l].data
;}return
t;return
(w == "text"
)?GetText
(c):c.length
;So what to do if the child node has multiple nodes? To extract its text, we need a procedure that will accept a node list and extract text from the selection of text and element nodes sent to it. As it so happens, we have one already: the GetText
function itself. Placing a procedure call to GetText
will cause it to be triggered with a whole new node list, the burrowing and checking starts again, and it might find other elements in one of those elements, and all around the mulberry bush we go.
In other words, GetText
will be a recursive procedure.
GetText
function
P_Full_Text
(i, w){var
c = document
.getElementById
(i).childNodes
;function
GetText
(cl){var
t = ""
;for
(var
l = 0; l < cl.length
; l++){if
(cl[l].nodeType
== 1){while
(n.childNodes
.length
== 1){n = n.firstChild
}if
(n.childNodes
.length
> 1){GetText
(n.childNodes
); //Recursive function call hereelse
{t += n.data
;}else
{t += cl[l].data
;}return
t;return
(w == "text"
)?GetText
(c):c.length
;Now, this will not be an infinite loop for one simple reason: in this context, infinite looping requires infinite nesting requires infinite file size. If the file size is finite, eventually this script will come to the last level of nesting and the iterations of GetText
will end. Currently, there is no computer in the world that I know of that can contain such an infinite file.
With that in mind, I'd like to point out one last thing: this script is not yet finished; it needs one final touch. Earlier, I said: Text nodes do not have child nodes (thus its list of child nodes will have a length of 0).
The same is true for an empty element, and we've got an img
element in the third paragraph.
So, what can be said about an img
element?
a
element, therefore it will be included in one of the iterations of the for
loop.nodeType
1), therefore it will be processed as an element node instead of a text node.
while
loop is skipped altogether.if
statement will not come into play.if
statement will...I don't think I need to remind you that an empty element is not a text node.
Therefore, we have to add something that will make sure that text nodes get processed, and empty elememts are ignored. We do this by turning the else
statement into an else if
statement, allowing us to add a condition that makes sure that what is processed is a text node.
GetText
Procedurefunction
P_Full_Text
(i, w){var
c = document
.getElementById
(i).childNodes
;function
GetText
(cl){var
t = ""
;for
(var
l = 0; l < cl.length
; l++){if
(cl[l].nodeType
== 1){while
(n.childNodes
.length
== 1){n = n.firstChild
}if
(n.childNodes
.length
> 1){GetText
(n.childNodes
); //Recursive function call hereelse
if
(n.nodeType
== 3){t += n.data
;}else
{t += cl[l].data
;}return
t;return
(w == "text"
)?GetText
(c):c.length
;Kind of a head-spinner, isn't it? But, using logic and an understanding of precisely what you want to do, it is doäble.
I'd like to explain a statement I made at the beginning of this chapter: that a procedure can return a boolean value when the coding required is too complex to fit into the condition of an if statement or while loop. The example I'm going to use could technically be used in a condition, but would rapidly turn into a pain in the butt.
In Decisions, I demonstrated an if condition that depended on only one condition proving true and the other proving false:
if
("five"
&& bar != 5) ||"five"
&& bar == 5)Here is a similar condition, allowing only one of three conditions to be true:
if
("five"
&& bar != 5 && dom_node.nodeType
!= 3) ||"five"
&& bar == 5 && dom_node.nodeType
!= 3) ||"five"
&& bar != 5 && dom_node.nodeType
== 3)Imagine the fun if this required ten conditions...
In Decisions, I demonstrated that assigning a condition to a variable would result in that variable holding a boolean value, depending on the result of that condition. You can assign boolean values to an array in similar fashion:
var
conditions = new
Array
("five"
),nodeType
== 3)This will actually result in an array of boolean values, which is exactly what we want for the procedure we are creating. Let's call this procedure XOR
after a keyword does exactly this in other programming languages (I think it means Exclusive OR
). The desired effect can be achieved in two ways:
true
(or until it runs out of elements). A second loop checks the rest of the values to see if any of those are true
.true
.XOR
Function Using The First Methodfunction
XOR
(conds){var
bool = false
;var
count = 0;while
(!bool && count < conds.length
){while
(bool && count < conds.length
){return
bool;Note the subtle distinction between the first and second while loops: the first runs so long as bool is false
, ending when bool is assigned a value of true
. The second runs so long as bool is true
, ending when bool is assigned a value of false
)—that is, because of the use of !
, literally not
.true
The second method uses one loop, and goes on so long as the number of true
elements is less than or equal to 1.
XOR
Function Using The Second Methodfunction
XOR
(conds){var
trucount = 0;var
count = 0;while
(!trucount <= 1 && count < conds.length
){return
(trucount == 1);The second method requires a bit less coding and allows for some extra flexibility, which I'll get to in just a bit.
In either case, the function call is identical:
XOR
In A Conditionif
(XOR
(["five"
),nodeType
== 3)Here's the flexibility I mentioned earlier: the second method allows you to pass a second variable to determine how many conditions you want to prove true.
XOR
Function With Added Flexibilityfunction
XOR
(conds, trulimit){if
(trulimit === undefined
){trulimit = 1;}var
trucount = 0;var
count = 0;while
(!trucount <= trulimit && count < conds.length
){return
(trucount == trulimit);The if statement at the beginning of XOR
means that if trulimit isn't set in the function call (that is, if the second value is ommitted), then it is set to its default value of 1.
XOR
In A Conditiontrue
if
(XOR
(["five"
),nodeType
== 3)true
if
(XOR
(["five"
),nodeType
== 3)In any case, using a function in a condition allows for such code to become much simpler—and in some cases, even possible. For an example in the possible
category, I used a function called InArray
in the script for an old page that allowed you to experiment with colours to check if a string is in an array of colour names.
InArray
Functionfunction
InArray
($arr, $arr_val){var
$bool = false
;var
$count = 0;do
{true
:false
;while
($count < $arr.length
&& !$bool);return
$bool;InArray
($valid_cols, $nm.toLowerCase
());In this particular script, $nm stands for a colour name, and $valid_cols refers to an array of seventeen colour names that the W3C recognizes as valid CSS names. For some reason, I used dollar signs ("$") in front of my variable names in this script. I think it was because I do so much work with PHP, which requires them. Many coders use dollar signs in front of variable names to make it clear that they ARE variables, but they aren't required in JavaScript.