Function.prototype.apply revisited

You can learn a lot about JavaScript by implementing your own version of apply. In this post I present my own implementation with explanatory notes to highlight hidden complexity:

Function.prototype.apply = function(thisArg, argArray) {
    if (typeof this != "function") {
        throw new Error("apply called on incompatible " +
                    "object (not a function)");
    }
    if (argArray != null && !(argArray instanceof Array)
        && typeof argArray.callee != "function") {
	throw new Error("The 2nd argument to apply must " +
                    "be an array or arguments object");
    }

    thisArg = (thisArg == null) ? window : Object(thisArg);
    thisArg.__applyTemp__ = this;

    // youngpup's hack
    var parameters = [],
        length = (argArray || "").length >>> 0;
    for (var i = 0; i < length; i++) {
	parameters[i] = "argArray[" + i + "]";
    };
    var functionCall =
            "thisArg.__applyTemp__(" + parameters + ")";

    try {
	return eval(functionCall)
    } finally {
	try {
            delete thisArg.__applyTemp__
        } catch (e) {
            /* ignore */
        }
    }
}

Function.prototype.call = function(thisArg) {
    return this.apply(thisArg,
            Array.prototype.slice.apply(arguments, [1]));
}

Download: apply-call.js

Validating apply‘s context and parameters

The ECMAScript specification says that apply may only be called on objects with a [[Call]] property (i.e. functions), and a TypeError should be thrown if an attempt is made to call it on non-function objects. The typeof operator allows us to identify functions easily: typeof object evaluates to "function" if and only if the object has a [[Call]] property.

The second parameter to apply is optional, but if it’s supplied it must be either an array or an arguments object – if not, a TypeError should be thrown. The expression argArray != null is deceptively simple, but exploits an interesting JavaScript fact: object == null evaluates to true if-and-only-if object is null or undefined (so object != null evaluates to true for any value except null or undefined.)

Checking whether an object is an instance of an array is simply a matter of using the instanceof operator with the Array constructor Array (i.e. object instanceof Array.) To check whether something is an arguments object is a little more complicated as the ECMAScript specification does not define an Arguments class or function. We resort to a capability test: we check whether the object has a callee property that’s a function (typeof object.callee == "function".) I think this is a better test than checking for a numeric length property, as both String and NodeList have length properties defined, but neither of them are should be accepted when passed as the second argument to apply.

Getting an appropriate object to use as this

If thisArg is null or undefined, the global-scope must be used as this. Since I’m writing this version of apply for use in a browser: the global-scope is the window object.

JavaScript’s typeof operator allows you to differentiate several types of primitive value (booleans, numbers and strings) To turn primitive values into objects (as required by the specification of apply) we pass them through the Object function. From this point on, you should use instanceof instead of typeof to differentiate different types of object.

Using functions as methods

The purpose of apply is to be able to assign a specific object as this for the duration of a function call. Without native browser-support, the only way to achieve this is to assign the function as a property of the object (thereby making the function a ‘method’ of the object.) Ideally we would generate and check for a non-existent property-name – but this cannot be achieved with 100% reliability on our main target browser (Internet Explorer 5) as there is no way of checking for the existence of a property (the specification defines Object.prototype.hasOwnProperty for this purpose; IE5 doesn’t support it, and it cannot be implemented reliably.) Accepting the futility of the situation, we go with a ‘good enough’ implementation and hope that __applyTemp__ isn’t being used for any other purpose.

Establishing how many parameters to pass

We’ve established that argArray is either null, undefined, an array or an arguments object, we now need to establish how many parameters need passing to the function call. The rather obtuse expression (argArray || "").length >>> 0) is dense code to meet the specification: no arguments (0) are passed if argArray is null or undefined, otherwise the length property is cast to an unsigned 32-bit integer, and the result is the number of parameters that will be passed (it’s extremely unlikely that the result will be any different to using the length property as-is; but since it’s trivial to do things properly, we do so.) A more simplistic and readable version of the code would read: (argArray == null) ? 0 : argArray.length

Calling a function with an arbitrary number of parameters

If you google for implementations of apply you’ll inevitably come across code derived from youngpup’s version. I don’t know if he created the hack, but it’s the inspiration for my version, so credit where credit’s due. The purpose of the hack is to pass a variable number of parameters to a function, without having to consider 1, 2, 3, 4 arguments (etc.) separately. The hack works by building a function-call as a string, then passing the string to eval. For example, to pass two parameters, this would be passed to eval: "thisArg.__applyTemp__(argArray[0], argArray[1])"

Error handling and cleaning-up

Earlier on, we attached the function as a method of the object. To ensure this is a temporary attachment, we have to remove the method once the function call has completed (using delete). We have to be careful though, as the function call may cause an error to be thrown, and we don’t want to swallow or ignore this error. Fortunately, we can use the try/finally construct to run some clean-up code after the function call, regardless of whether the function-call ends in an exception or not.

Ideally, cleaning-up would simply be a matter of writing delete object.property; but in the world of Internet Explorer nothing is that simple. Errors can be thrown when you try to delete properties on host objects (a host object is an object supplied by the browser and accessible through JavaScript, but not defined in the ECMAScript specification. e.g. ActiveXObject.) As we don’t imagine anyone will be interested in errors generated during clean-up, we wrap a simple try/catch around the clean-up code to swallow any errors generated here.

Function.prototype.call

It’s trivial to implement call in terms of apply. The first parameter to call becomes the first parameter to apply. The rest of the parameters are passed by applying Array‘s slice method to the arguments object. This demonstrates our first useful application of apply: converting arguments objects to standard arrays.

Conclusion

To implement apply we’ve had to understand typeof, functions, methods, this, null, the loose equality operator, global-scope, object properties, try/catch/finally, the arguments object, eval, primitive values and errors. This is a lot of ground to cover for such a short function, and I can’t think of any other function that teaches you so much while writing it.

Posted Tuesday, December 26th, 2006 under JavaScript.

6 comments

  1. Márcio Faustino says:

    Hi Ash,

    About using __applyTemp__ as the temporary method name for apply, couldn’t you just use something like this:

    var methodName;
    do {
    	methodName = '_apply' + (Math.floor(Math.random() * 1000000) + 1);
    }
    while (typeof(thisArg[methodName]) != 'undefined');

    Cheers,

  2. Hi Márcio,

    You could, but that still doesn’t guarantee the property methodName is unused (an undefined value is perfectly legitimate.)

    With modern browsers, you can check for the existence of an object property using hasOwnProperty. With your code, you’d change the while-condition to while (thisArg.hasOwnProperty(methodName))

    However, IE5 doesn’t support hasOwnProperty, and I don’t think there’s a perfect fix for it.

  3. Márcio Faustino says:

    You’re right. But for IE4 and up, you can use this:

    Object.hasProperty = function(object, property) {
    	for (var p in object) {
    		if (p == property) {
    			return true;
    		}
    	}
    	
    	return false;
    };

    For example:

    var a = {b: void(0)};
     
    typeof(a.c);    // 'undefined'
    typeof(a.b);    // 'undefined'
     
    'c' in a;       // false
    'b' in a;       // true
     
    // Cross-browser:
    Object.hasProperty(a, 'c');    // false
    Object.hasProperty(a, 'b');    // true
    delete a.b;
    Object.hasProperty(a, 'b');    // false

    Also, don’t forget you can use the syntax: in , instead of hasOwnProperty.

  4. Interesting… I shouldn’t have suggested using hasOwnProperty to check for an object property, as it doesn’t consider the prototype chain.

    Márcio, your Object.hasProperty function is closer in function to hasOwnProperty than to in.

    The difference is clear when checking for a ‘length’ property on an array, or checking for ‘toString’ on any object:

    'toString' in a;   // true
    Object.hasProperty(a, 'toString');  // false
    
    'length' in []; // true
    Object.hasProperty([], 'length'); // false 
    // but... this isn't the same as hasOwnProperty either:
    [].hasOwnProperty('length'); // true

    As I said before, I don’t think there’s a perfect fix; but I’m sure there are cases where your hasProperty function could come in handy.

    (The differences between in used as an operator, and in used in a for-in statement are defined in sections 11.8.7 and 12.6.4 of the ECMAScript Language Spec.)

  5. Hmmm… looks like I was over-confident :-)
    How about this one:

    Object.hasProperty = function(object, property) {
    	if (typeof(object[property]) != 'undefined') {
    		return true;
    	}
    	
    	for (var p in object) {
    		if (p == property) {
    			return true;
    		}
    	}
    	
    	return false;
    };
    
    Object.hasProperty({}, 'toString');       // true
    Object.hasProperty([], 'length');         // true
    
    ({}).hasOwnProperty('toString');          // false
    [].hasOwnProperty('length');              // true
    
    Object.hasProperty({a: void(0)}, 'a');    // true
    Object.hasProperty({}, 'undefined');      // false
  6. That looks like a really good function, and (at the moment) the only way I can break it is through sabotage…

    To demonstrate:

    'tan' in Math;                            // true
    Math.hasOwnProperty('tan');               // true
    Object.hasProperty(Math, 'tan');          // true
    
    Math.tan = void(0)
    
    'tan' in Math;                            // true
    Math.hasOwnProperty('tan');               // true
    Object.hasProperty(Math, 'tan');          // false
    

    Although it can’t handle contrived sabotage; I think the revised hasProperty is ‘good enough’ and I can’t see how it could be improved.