Back to homepage.

Writings.

Private/protected without using caller

This is a followup to my previous post on private and protected fields in JavaScript; please read that first. Up to speed? Good. Now, the main issue with the implementation I described in that post is the use of the caller property on functions; this is an error in ES5 strict, and therefore not acceptable to many people. Well, the technique is still perfectly possible without it (and indeed by doing so adds support for Safari and Opera 10.10, just leaving IE out of the party). The code presented below is meant to be self-documenting again and is broadly similar to that in the previous post. The main advantage of this version is the extra browser support and the compliance with ES5 strict (barring syntax changes to getters/setters). It also removes the need to pass a method name to the parent method. The disadvantage is that wrappers have to be introduced around every method (and constructor functions), which could be significantly less efficient, although I haven't benchmarked it to test exactly what impact this makes. Also, it's open to the same security hole as Mootools: callbacks are able to access the private and protected variables of the function that calls them. I do have a solution to this though: methods can pass the callback function to Class.invokeCallback, along with any arguments, and this will ensure that the callback is invoked without access to any private data.

var Class = (function() { // Open closure

    var caller = null;

    // Functions within the closure can only be accessed by other functions
    // within the closure (this is the standard way of doing private methods)

    // This function checks whether the calling method belongs to a certain
    // class (i.e. whether it is contained within the class's prototype object
    // For protected fields, the prototype of the calling object's constructor is passed.
    // For private fields, the prototype in which the variable is defined is passed.
    function callerInPrototype(proto, mayBeInSuperClass) {

        // If access attempted in global context, caller may be null
        if (!caller) { return false; }

        // Implemented methods have an _name property set.
        var name = caller._name;

        // Method trying to access the variable must be in
        // the class prototype chain for protected.
        // For private, it must be in the prototype of the class
        // defining the var.
        if (name in proto && !proto.__lookupGetter__(name) && proto[name] === caller &&
                (mayBeInSuperClass || proto.hasOwnProperty(name))) {
            return true;
        }
        return false;
    }

    function addPrivateOrProtectedProperty(proto, name, value, isPrivate) {

        // Doesn't work in IE as it doesn't support getters and setters.

        // When obj[name] is accessed, this function will be called instead:
        proto.__defineGetter__(name, function() {
            if (callerInPrototype(isPrivate ? proto : this.constructor.prototype, !isPrivate)) {
                return value;
            }
            else {
                throw "Error: cannot access " +
                    (isPrivate ? "private " : "protected ") + name + " property.";
            }
        });

        // When a method tries to write to obj[name], we define a new getter on the object
        // that returns the new value (after performing the same checks to maintain the
        // private/protected status), masking the getter set on the prototype of the object.
        proto.__defineSetter__(name, function(value) {
            if (callerInPrototype(isPrivate ? proto : this.constructor.prototype, !isPrivate)) {
                // Define a new getter with closure over the value supplied as an argument
                // to the setter. Note the new getter is defined on the object *instance*
                // not the prototype (like the one above), so writing to one instance of
                // an object won't affect the properties on another.
                this.__defineGetter__(name, function() {
                    if (callerInPrototype(isPrivate ? proto : this.constructor.prototype,
                            !isPrivate)) {
                        return value;
                    }
                    else {
                        throw "Error: cannot access " +
                            (isPrivate ? "private " : "protected ") + name + " property.";
                    }
                });
            }
            else {
                throw "Error: cannot access " +
                    (isPrivate ? "private " : "protected ") + name + " property.";
            }
        });
    }

    function callParent() {

        var name = caller && caller._name;
        if (!name) { throw "Unrecognised caller of parent()"; }

        // The object calling this function will set itself as the 'this' parameter.
        // The constructor field (inherited through the object prototype)
        // points to the constructing function for the object. In the Class
        // setup function we add a pointer from this to the parent constructor
        var parent = this.constructor.parent;

        // If the method name is not init, we need to look in the parent
        // constructor's prototype for the desired method.
        if (parent) {
            var method = parent.prototype[name];
            if (method instanceof Function) {
                return method.apply(this, arguments);
            }
        }

        throw "Method " + name + " not found in superclass.";
    }

    function Class(params) {

        if (params instanceof Function) {
            params = {init: params};
        }

        var newClass = function() {
            // init is wrapped so has access to private/public variables.
            if (this.init) {
                this.init.apply(this, arguments);
            }
        };
        // 'this' is a reference to a new Class object,
        // so mixin everything from the prototype into the
        // actual class constructor
        for (var prop in this) {
            newClass[prop] = this[prop];
        }
        newClass._name = 'init';

        var parent = params.Extends;
        delete params.Extends;
        if (parent) {
            // Create new parent object as prototype
            newClass.prototype = Class.instantiate(parent);
            // Set pointer from constructor to parent constructor
            newClass.parent = parent;
            // If not already implemented by parent prototype, implement
            // the parent method for calling superclass methods.
            if (!parent.parent) {
                addPrivateOrProtectedProperty(newClass.prototype, 'parent', callParent, false);
            }
        }

        newClass.implement(params);

        newClass.constructor = Class;
        newClass.prototype.constructor = newClass;

        return newClass;
    }

    var emptyConstructor = function(){};
    Class.instantiate = function(constructor) {
        emptyConstructor.prototype = constructor.prototype;
        return new emptyConstructor();
    };

    var mayAccessWrapped = false;
    function wrapmethod(method) {

        mayAccessWrapped = true;
        if (method.__getWrappedMethod) {
            method = method.__getWrappedMethod();
        }
        mayAccessWrapped = false;

        var wrapped = function wrapper() {
            var prevCaller = caller;
            caller = wrapper;
            var returns;
            try {
                returns = method.apply(this, arguments);
            }
            finally {
                caller = prevCaller;
            }
            return returns;
        };
        wrapped.__getWrappedMethod = function() {
            if (mayAccessWrapped) { return method; }
            throw "Error: only the wrapping function may access the wrapped method";
        }
        return wrapped;
    }

    Class.invokeCallback = function(f) {
        var args = Array.prototype.slice.call(arguments, 1);
        wrapmethod(f)(args);
    };

    Class.prototype.implement = function(key, value, doNotOverride) {

        if (typeof key === 'object') {
            for (var prop in key) {
                this.implement(prop, key[prop], value);
            }
            return this;
        }

        var proto = this.prototype;

        // Split name, e.g. 'protected methodName' -> ['protected', 'methodName']
        var tokens = key.split(' ');
        // Actual property name is final object in array.
        var name = tokens.pop();

        // Check if it already exists
        if (doNotOverride && name in proto) {
            return this;
        }

        // If there's a visibility modifier it (should)
        // be the next (and indeed only) object in the array
        var visibility = tokens.pop();

        // Record the method name as a property on the
        // function object to allow super functions to be called.
        if (value instanceof Function && !value._name) {
            value = wrapmethod(value);
            value._name = name;
        }

        // And actually add the property
        var isPrivate = (visibility === 'private');
        var isProtected = (visibility === 'protected');
        if (isPrivate || isProtected) {
            addPrivateOrProtectedProperty(proto, name, value, isPrivate);
        }
        else {
            proto[name] = value;
        }
        return this;
    };

    return Class;

})(); // End Class closure

Neat, huh?