
|
JavaScript IntrospectionThis tutorial introduces you to JavaScript introspection. Being able to interrogate an object to discover its properties can be a great help when debugging. Additionally, given the great number of implementations of JavaScript (not all of which are compatible between themselves), the possibility of interrogating an object to find if it contains specific functions is essential in developing cross browser, and cross implementation code.
The Scripts/Introspector.js file, explained in this tutorial, contains three helper functions; typeOf, an enhancement to the typeof keyword, exists, a function to test if an object contains a specified property, and introspect, which produces a list of the specified object's properties.
The NamespaceFirst, we'll take a look at the JavaScript code. There are several important concepts here, which I think are worth describing in some detail. I'll start with the syger namespace:
var syger = {
// ...
};
All variables and functions that you create, unless properties of an object, will be added to the Global Object. Within the web browser, this is window. Apart from the generally agreed concept that global variables are a bad programming practice, the Global Object will quickly become cluttered with properties. There may even be naming collisions.
Here I am introducing three new methods, but by making them properties of an object (syger), I am only adding one variable to the Global Object. True it is only a variable, but it does work as a namespace. The drawback is that I will have to write syger.exists(...), instead of just exists(...), but this is a price that I am more than willing to pay to avoid name collisions.
The typeOf FunctionThere are three functions in this tutorial, and I'll start with the simplest of them, typeOf:
/**
Checks the type of a given object.
@param obj the object to check.
@returns one of; "boolean", "number", "string", "object",
"function", or "null".
*/
typeOf : function (obj) {
type = typeof obj;
return type === "object" && !obj ? "null" : type;
}
Unfortunately, the typeof keyword returns "object" even when the object is null. This is generally considered an error, which typeOf corrects.
The first thing to note is that I have used javadoc style commenting for the function. This allows me to create HTML documentation from the JavaScript source code using the JSDoc tool. It follows a very similar syntax to Java's javadoc tool.
JavaScript is a case sensitive language so typeOf will not clash with the keyword typeof. Since it's inside an object definition, I can use object literal syntax; { name : value, name : value, ... }.
The function body uses the ternary operation (?:) to return the typeof value for all objects that are truthy, or the string "null" otherwise.
In JavaScript, truthy means any object that is not 0, NaN, "", null or undefined. Only the last two refer to objects.
The exists FunctionThe next function is exists. This function will check that, for a specified object, the named property exists and has the required type:
/**
Checks if a property of a specified object has the given type.
@param obj the object to check.
@param name the property name.
@param type the property type (optional, default is "function").
@returns true if the property exists and has the specified type,
otherwise false.
*/
exists: function (obj, name, type) {
type = type || "function";
return (obj ? this.typeOf(obj[name]) : "null") === type;
}
Again, there are a few interesting points to outline. The JavaScript interpreter will match a method call to a defined function, irrespective of the number of method arguments provided. This can make function overloading a little tricky, but it offers a simple mechanism to provide optional function arguments.
Essentially, this means that I can call the syger.exists function, as follows
syger.exists(this, "puts"); syger.exists(this, "puts", "function"); and both will give identical results.
The first line of the function takes care of setting type to a valid value, if it wasn't present in the argument list. This is a short cut which takes advantage of the fact that JavaScript returns the object within a boolean expression, rather than true or false. It can be read as set type to itself if truthy, otherwise set it to the string "function".
I also use the equality operator === to check the type. This is not a typing mistake, JavaScript has two equality operators. The first, and probably more familiar is the == operator. It uses type coercion when making the comparison, which means that the expression 1 == "1" will return true, because the number on the left hand side is coerced to a string. The second type of equality operator does not coerce the values, so 1 === "1" will return false.
The introspect FunctionFinally, there is the introspect function itself:
/**
Introspects an object.
@param name the object name.
@param obj the object to introspect.
@param indent the indentation (optional, defaults to "").
@param levels the introspection nesting level (defaults to 1).
@returns a plain text analysis of the object.
*/
introspect : function (name, obj, indent, levels) {
indent = indent || "";
if (this.typeOf(levels) !== "number") levels = 1;
var objType = this.typeOf(obj);
var result = [indent, name, " ", objType, " :"].join('');
if (objType === "object") {
if (level > 0) {
indent = [indent, " "].join('');
for (prop in obj) {
var prop = this.introspect(prop, obj[prop], indent, level - 1);
result = [result, "\n", prop].join('');
}
return result;
}
else {
return [result, " ..."].join('');
}
}
else if (objType === "null") {
return [result, " null"].join('');
}
return [result, " ", obj].join('');
}
For a given named object, this function will display the object's type, and the types of all its enumerable properties. Since some of these may also be objects, these will also be displayed. To achieve this, the function calls itself recursively.
Obviously, if the object hierarchy is very deep, and you have set the levels value very high, you will get a lot of text back. Fortunately, JavaScript rarely requires such an architecture.
Here, the var keyword is used to ensure that the variables remain inside the function scope. I make a point of checking that all variables within a function are preceded by var, otherwise they will become a property of the Global Object.
I use the Array.join() function to concatenate a series of strings, in favour of using the + operator, because it will only create one new string. Strings in JavaScript are immutable, so a statement like
"Now" + " is" + " the" + " right" + " time" will create four strings
"Now is" + "the" + "right" + "time" "Now is the" + "right" + "time" "Now is the right" + "time" "Now is the right time" three of which will be discarded, to be swept up by the garbage collector at some later time.
The levels parameter was added in a second moment, thanks to observations made by Ming-fai Ma, who demonstrated that the function can loop until the stack overflows, when there are circular references.
Using the CodeNow we can execute the example test code, provided in the Scripts/IntrospectExample.js file, which is shown below:
// Create a short cut to the normal output device
if (!syger.exists(this, "puts")) {
var puts = function (str) {
document.write(str);
};
}
var obj = "string value";
puts(syger.introspect("string variable", obj) + "\n");
if (syger.exists(obj, "indexOf")) {
puts('string variable.indexOf(" ") returns ' + obj.indexOf(" ") + "\n\n");
} else {
puts('string variable has no "indexOf" method\n\n');
}
obj = 1.2345;
puts(syger.introspect("numeric variable", obj) + "\n");
if (syger.exists(obj, "toFixed")) {
puts('numeric variable.toFixed(1) returns ' + obj.toFixed(1) + "\n\n");
} else {
puts('numeric variable has no "toFixed" method\n\n');
}
obj = {
first: 1,
second: "2nd",
third: function () { return "3rd"; },
fourth: new Object(),
fifth: null
};
obj.sixth = obj;
puts(syger.introspect("user_defined_object", obj) + "\n");
if (syger.exists(obj, "third")) {
puts('user_defined_object.third() returns ' + obj.third());
} else {
puts('user_defined_object has no "third" method');
}
The five lines of code at the start simply create a short cut puts to the longer function document.write. The surrounding test creates the short cut only if no previous definition of puts has been made. In the ASP version of this tutorial I can use exactly the same code, by previously defining a puts variable which is a short cut to Response.Write, the standard output function in the ASP environment.
Another thing to note is that I am setting the variable to a function definition. In JavaScript function definitions are also objects. Note also the ; after the final function brace. This terminates the statement puts = ... ;, and is required.
Now we can run the example code, which gives the following results:
Note: if you don't see anything in monospaced type above this line, you probably have JavaScript disabled in your browser. This is a live demonstration, so you'll need to enable JavaScript!
The first and second examples demonstrate the results of introspecting a string variable and a numeric variable respectively. As you can see, it would appear that neither have any methods, even though the standard documentation shows that the indexOf or toFixed method should be present. The methods do exist, of course, which is shown in the following line.
In both examples we have been introspecting two native objects of JavaScript. Native objects do not always provide information for introspection.
The last example, where we introspect an object we have defined, gives the expected results, including the function definition. I also deliberately set the sixth value to be circular.
The next tutorial discusses inheritance.
All the scripts in these tutorials are available for download as two compressed archives; Scripts.zip and AspScripts.zip, both distributed under the GNU Lesser General Public License.
ContactsSyger can be contacted for consultancy work on any of the topics mentioned in this article, by sending an email to info@syger.it.
|
Tag cloud: |