Redesigning Mangler.js type handling

Mangler.js‘s type handling has undergone a major redesign which retired the flawed Mangler.getType() function and changed the type parameter on Mangler.registerType(). With the recent changes the library now supports code minification, objects built from anonymous constructors and constructors with the same name in different scopes.

Read on to see what the issue was, how it got handled, and to see the newly introduced Mangler.compareType() function for type checking.

Original implementation

Originally, Mangler.js’ type handling was based on the Mangler.getType() function, which returned a string representation of an object instance’s type:

fn.getType = function(obj) {
	var name;

	if(typeof obj === 'undefined') {
		name = 'undefined';
	} else if(obj === null) {
		name = 'null';
	} else if(typeof obj === 'object') {
		name = Object.prototype.toString.call(obj).match(/^\[object (.*)\]$/)[1];
		if(name === 'Object') {
			name = obj.constructor.toString().match(/^function ([^\(]*)\(/)[1];
			if(!name) name = '$unknown';
		}
	} else if(typeof obj === 'function') {
		name = 'function';
	} else {
		name = '$value';
	}

	return name;
}

As you can see, it extracted the type from .toString()‘s output: [object Something]. For native types it returns the type name, but for other objects it just returns a generic [object Object]. In this case I added a second step, which extracts its constructor’s function name.

The returned type string was then used to read a property from the internal types object, that contained the handler functions for cloning and iterating through the object.

When registering type handlers with Mangler.registerType(), you simply had to pass the object’s type name, matching the one returned by the above function.

Fundamental flaws

It’s easy to see how this approach cannot be reliably used in most situations:

  • It can’t handle anonymous, unnamed constructors. If there is no function name specified after the function keyword, getType() just returns '$unknown'. For example there was no way to write an iterator for jQuery objects.
  • It doesn’t support minified code. Thorough minification algorithms shorten identifier names, making it impossible to match them against hard-coded strings. The library itself didn’t survive minification, as it referenced the internal ManglerObject by name.
  • Names could clash across scopes. Nothing stops you from creating two different objects with the same constructor name in different scopes. In this case only one of them could be registered. Even worse, a handler written for one could be run on instances for the other, leading to run-time errors and exceptions.

The new implementation

The public getType() function was replaced by the internal resolveType():

var handlers = [];
var constructors = [];

// Return type index in list from instance or constructor
// Returns -1 if not found or parameter is not an object/function
function resolveType(obj, register) {
	if(obj === null) return -1;
	if(typeof obj === 'object') obj = obj.constructor;
	if(typeof obj !== 'function') return -1;

	var index = constructors.indexOf(obj);
	if(index === -1 && register) {
		// Register unknown constructor
		index = constructors.length;
		constructors[index] = obj;
		handlers[index] = {};
	}

	return index;
}

Instead of storing type handlers in an object literal keyed on strings, it stores them in an internal handlers array. To know which handler is which, a constructors array was created which is kept in sync, so that items with the same index belong together.

When looking for handlers of a certain object instance, the object’s constructor is looked up in the constructors array, which stores function references to all registered objects’ constructors. Then the corresponding item in the handlers array is used.

When registering a new object type, instead of passing a string with the object’s type name in the first parameter of Mangler.registerType(), you simply pass either the object’s constructor function or an object instance itself. This change fixes all issues mentioned above. It’s never been easier to enable Mangler.js to use the standard iterator method on jQuery objects:

Mangler.registerType($, { each: true });

Type checking

To fill the void left by the removal of getType(), a new function was added to check if an object is of a certain type: Mangler.compareType(a, b)

You can pass in two object instances and it returns true if they are of the same type, i.e. if their constructor is the same function. You can also pass an object instance and a constructor function to check against.

Functions are handled a bit differently: passing two functions always returns false, unless you’re checking against the Function global.

Mangler.compareType('hello', 'world');   // true
Mangler.compareType(0, false);           // false

function ObjectA() {}
function ObjectB() {}

var a = new ObjectA();
var b = new ObjectB();

Mangler.compareType(a, b);                // false
Mangler.compareType(a, ObjectA);          // true
Mangler.compareType(a, new ObjectA());    // true
Mangler.compareType(ObjectA, ObjectB);    // false
Mangler.compareType(ObjectA, Function);   // true
Share Button
Posted in Blog Tagged with: , ,

Leave a Reply