diff --git a/lib/class.js b/lib/class.js index b0af43c..cd3c047 100644 --- a/lib/class.js +++ b/lib/class.js @@ -34,6 +34,16 @@ var util = require( './util' ), */ var class_meta = {}; +/** + * Stores class instance visibility object + * + * For each instance id, an object exists that contains the private and + * protected members. + * + * @type {Object.} + */ +var class_instance = {}; + /** * Creates a class, inheriting either from the provided base class or the @@ -188,7 +198,8 @@ function Class() {}; */ var extend = ( function( extending ) { - var class_id = 0; + var class_id = 0, + instance_id = 0; /** * Mimics class inheritance @@ -215,8 +226,8 @@ var extend = ( function( extending ) hasOwn = Array.prototype.hasOwnProperty; var properties = {}, - members = member_builder.initMembers( prototype ), prop_init = member_builder.initMembers(), + members = member_builder.initMembers( prototype ), abstract_methods = util.clone( getMeta( base.__cid ).abstractMethods ) @@ -261,7 +272,7 @@ var extend = ( function( extending ) method: function( name, func, is_abstract, keywords ) { member_builder.buildMethod( - members, null, name, func, keywords + members, null, name, func, keywords, getMethodInstance ); if ( is_abstract ) @@ -294,7 +305,7 @@ var extend = ( function( extending ) // set up the new class var new_class = createCtor( abstract_methods ); - attachPropInit( prototype, prop_init ); + attachPropInit( prototype, prop_init, members ); new_class.prototype = prototype; new_class.constructor = new_class; @@ -334,6 +345,7 @@ var extend = ( function( extending ) { var args = null; + // constructor function to be returned var __self = function() { if ( !( this instanceof __self ) ) @@ -344,6 +356,10 @@ var extend = ( function( extending ) return new __self(); } + // generate and store unique instance id + attachInstanceId( this, ++instance_id, __self ); + + initInstance( instance_id, this ); this.__initProps(); // call the constructor, if one was provided @@ -455,6 +471,29 @@ function setupProps( func, abstract_methods, class_id ) } +/** + * Initializes class instance + * + * This process will create the instance visibility object containing private + * and protected members. The class instance is part of the prototype chain. + * This will be passed to all methods when invoked, permitting them to access + * the private and protected members while keeping them encapsulated. + * + * @param {number} iid instance id + * @param {Object} instance instance to initialize + * + * @return {undefined} + */ +function initInstance( iid, instance ) +{ + var prot = function() {}; + prot.prototype = instance; + + // add the visibility objects to the data object for this class instance + class_instance[ iid ] = new prot(); +} + + /** * Attaches __initProps() method to the class prototype * @@ -469,14 +508,20 @@ function setupProps( func, abstract_methods, class_id ) * @param {Object} prototype prototype to attach method to * @param {Object} properties properties to initialize * + * @param {{public: Object, protected: Object, private: Object}} members + * * @return {undefined} */ -function attachPropInit( prototype, properties ) +function attachPropInit( prototype, properties, members ) { - var prop_pub = properties[ 'public' ]; + var prop_pub = properties[ 'public' ], + prop_prot = properties[ 'protected' ] + ; util.defineSecureProp( prototype, '__initProps', function() { + var inst_props = class_instance[ this.__iid ]; + // first initialize the parent's properties, so that ours will overwrite // them var parent_init = prototype.parent.__initProps; @@ -493,6 +538,25 @@ function attachPropInit( prototype, properties ) // not share references (and therefore, data) this[ prop ] = util.clone( prop_pub[ prop ] ); } + + var methods_protected = members[ 'protected' ], + hasOwn = Array.prototype.hasOwnProperty + ; + + // copy over the methods + for ( method_name in methods_protected ) + { + if ( hasOwn.call( methods_protected, method_name ) ) + { + inst_props[ method_name ] = methods_protected[ method_name ]; + } + } + + // initialize protected properties and store in instance data + for ( prop in prop_prot ) + { + inst_props[ prop ] = util.clone( prop_prot[ prop ] ); + } }); } @@ -586,6 +650,20 @@ function attachId( func, id ) } +/** + * Attaches an instance identifier to a class instance + * + * @param {Object} instance class instance + * @param {number} iid instance id + * + * @return {undefined} + */ +function attachInstanceId( instance, iid ) +{ + util.defineSecureProp( instance, '__iid', iid ); +} + + /** * Attaches partially applied isInstanceOf() method to class instance * @@ -615,7 +693,7 @@ function attachInstanceOf( instance ) function createMeta( func, parent_id ) { var id = func.__cid, - parent_meta = ( ( parent_id ) ? getMeta( parent_id) : undefined ); + parent_meta = ( ( parent_id ) ? getMeta( parent_id ) : undefined ); // copy the parent prototype's metadata if it exists (inherit metadata) if ( parent_meta ) @@ -649,3 +727,26 @@ function getMeta( id ) return class_meta[ id ] || {}; } + +/** + * Returns the instance object associated with the given method + * + * The instance object contains the protected and private members. This object + * can be passed as the context when calling a method in order to give that + * method access to those members. + * + * @param {function()} method method to look up instance object for + * + * @return {Object,null} instance object if found, otherwise null + */ +function getMethodInstance( method ) +{ + var iid = method.__iid, + data = class_instance[ method.__iid ]; + + return ( iid && data ) + ? data + : null + ; +} + diff --git a/lib/member_builder.js b/lib/member_builder.js index 8d22af9..e5fec21 100644 --- a/lib/member_builder.js +++ b/lib/member_builder.js @@ -59,10 +59,15 @@ exports.initMembers = function( mpublic, mprotected, mprivate ) * @param {*} value property value * * @param {Object.} keywords parsed keywords + + * @param {Object=} instCallback function to call in order to retrieve + * object to bind 'this' keyword to * * @return {undefined} */ -exports.buildMethod = function( members, meta, name, value, keywords, cmp ) +exports.buildMethod = function( + members, meta, name, value, keywords, instCallback +) { var prev; @@ -104,12 +109,18 @@ exports.buildMethod = function( members, meta, name, value, keywords, cmp ) if ( prev ) { // override the method - dest[ name ] = overrideMethod( prev, value ); + dest[ name ] = overrideMethod( prev, value, instCallback ); + } + else if ( keywords[ 'abstract' ] ) + { + // we do not want to wrap abstract methods, since they are not callable + dest[ name ] = value; } else { - // we are not overriding the method, so simply copy it over - dest[ name ] = value; + // we are not overriding the method, so simply copy it over, wrapping it + // to ensure privileged calls will work properly + dest[ name ] = overrideMethod( value, null, instCallback ); } }; @@ -270,24 +281,42 @@ function scanMembers( members, name, cmp ) * @param {function()} super_method method to override * @param {function()} new_method method to override with * + * @param {Object=} instCallback function to call in order to retrieve + * object to bind 'this' keyword to + * * @return {function()} override method */ -function overrideMethod( super_method, new_method ) +function overrideMethod( super_method, new_method, instCallback ) { + instCallback = instCallback || function() {}; + // return a function that permits referencing the super method via the // __super property - var override = function() + var override = null; + + if ( new_method ) { - var tmp = this.__super; + override = function() + { + // the _super property will contain the parent method + this.__super = super_method; - // assign _super temporarily for the method invocation so - // that the method can call the parent method - this.__super = super_method; - var retval = new_method.apply( this, arguments ); - this.__super = tmp; + var retval = new_method.apply( + ( instCallback( this ) || this ), arguments + ); - return retval; - }; + return retval; + }; + } + else + { + override = function() + { + return super_method.apply( + ( instCallback( this ) || this ), arguments + ); + }; + } // This is a trick to work around the fact that we cannot set the length // property of a function. Instead, we define our own property - __length. diff --git a/test/inc-member_builder-common.js b/test/inc-member_builder-common.js index 9a61d19..5aca382 100644 --- a/test/inc-member_builder-common.js +++ b/test/inc-member_builder-common.js @@ -35,7 +35,7 @@ exports.buildMember = null; /** - * Partially applied function to quickly build properties using common test data + * Quickly build properties using common test data */ exports.buildMemberQuick = function( keywords, preserve_prior ) { @@ -66,16 +66,25 @@ exports.assertOnlyVisibility = function( vis, name, value, message ) var check = [ 'public', 'protected', 'private' ], i = check.length, visi = '', + value, cmp; // forEach not used for pre-ES5 browser support while ( i-- ) { - visi = check[ i ]; - cmp = ( visi === vis ) ? value : undefined; + visi = check[ i ]; + value = exports.members[ visi ][ name ]; + cmp = ( visi === vis ) ? value : undefined; + + // are we comparing functions? + if ( cmp && exports.funcVal ) + { + cmp = exports.funcVal; + value = value(); + } assert.deepEqual( - exports.members[ visi ][ name ], + value, cmp, message ); diff --git a/test/test-class-constructor.js b/test/test-class-constructor.js index c39dcf9..2262570 100644 --- a/test/test-class-constructor.js +++ b/test/test-class-constructor.js @@ -29,22 +29,21 @@ var common = require( './common' ), // these two variables are declared outside of the class to ensure that they // will still be set even if the context of the constructor is wrong var construct_count = 0, - construct_context = null; + construct_context = null, + construct_args = null // create a basic test class -var Foo = Class.extend( -{ - args: null, - - - __construct: function() + Foo = Class.extend( { - construct_count++; - construct_context = this; + __construct: function() + { + construct_count++; + construct_context = this; + construct_args = arguments; + }, + }) +; - this.args = arguments; - }, -}); assert.ok( ( Foo.prototype.__construct instanceof Function ), @@ -67,19 +66,19 @@ assert.equal( ); assert.equal( - obj, - construct_context, + construct_context.__iid, + obj.__iid, "Constructor should be invoked within the context of the class instance" ); assert.notEqual( - obj.args, + construct_args, null, "Constructor arguments should be passed to the constructor" ); assert.equal( - obj.args.length, + construct_args.length, args.length, "All arguments should be passed to the constructor" ); @@ -88,7 +87,7 @@ assert.equal( for ( var i = 0, len = args.length; i < len; i++ ) { assert.equal( - obj.args[ i ], + construct_args[ i ], args[ i ], "Arguments should be passed to the constructor: " + i ); @@ -112,16 +111,16 @@ assert.equal( ); assert.equal( - construct_context, - subobj, + construct_context.__iid, + subobj.__iid, "Parent constructor is run in context of the subtype" ); // this should be implied by the previous test, but let's add it for some peace // of mind assert.ok( - ( ( subobj.args[ 0 ] === args2[ 0 ] ) - && ( subobj.args[ 1 ] == args2[ 1 ] ) + ( ( construct_args[ 0 ] === args2[ 0 ] ) + && ( construct_args[ 1 ] == args2[ 1 ] ) ), "Parent constructor sets values on subtype" ); @@ -135,14 +134,14 @@ assert.ok( ); assert.equal( - construct_context, - subobj2, + construct_context.__iid, + subobj2.__iid, "Self-invoking constructor is run in the context of the new object" ); assert.ok( - ( ( subobj2.args[ 0 ] === args2[ 0 ] ) - && ( subobj2.args[ 1 ] == args2[ 1 ] ) + ( ( construct_args[ 0 ] === args2[ 0 ] ) + && ( construct_args[ 1 ] == args2[ 1 ] ) ), "Self-invoking constructor receives arguments" ); diff --git a/test/test-class-visibility.js b/test/test-class-visibility.js index 6f2647f..ad8a18a 100644 --- a/test/test-class-visibility.js +++ b/test/test-class-visibility.js @@ -30,9 +30,9 @@ var common = require( './common' ), prot = 'bar', priv = 'baz', - pubf = function() {}, - protf = function() {}, - privf = function() {}, + pubf = function() { return pub; }, + protf = function() { return prot; }, + privf = function() { return priv; }, // new anonymous class instance foo = Class.extend( { @@ -43,6 +43,13 @@ var common = require( './common' ), 'public pubf': pubf, 'protected protf': protf, 'private privf': privf, + + 'public getProp': function( name ) + { + // return property, allowing us to break encapsulation for + // protected/private properties (for testing purposes) + return this[ name ]; + }, })(); @@ -55,8 +62,8 @@ var common = require( './common' ), ); assert.equal( - foo.pubf, - pubf, + foo.pubf(), + pub, "Public methods are accessible via public interface" ); } )(); @@ -89,3 +96,24 @@ var common = require( './common' ), ); } )(); + +/** + * Protected members should be accessible from within class methods + */ +( function testProtectedMembersAreAccessibleInternally() +{ + assert.equal( + foo.getProp( 'peeps' ), + prot, + "Protected properties are available internally" + ); + + // invoke rather than checking for equality, because the method may be + // wrapped + assert.equal( + foo.getProp( 'protf' )(), + prot, + "Protected methods are available internally" + ); +} )(); + diff --git a/test/test-member_builder-method.js b/test/test-member_builder-method.js index c5c711d..ab24e18 100644 --- a/test/test-member_builder-method.js +++ b/test/test-member_builder-method.js @@ -27,7 +27,8 @@ var common = require( './common' ), mb_common = require( './inc-member_builder-common' ) ; -mb_common.value = function() {}; +mb_common.funcVal = 'foobar'; +mb_common.value = function() { return mb_common.funcVal; }; mb_common.buildMember = common.require( 'member_builder' ).buildMethod; // do assertions common to all member builders @@ -50,7 +51,8 @@ mb_common.assertCommon(); */ ( function testCannotOverridePropertyWithMethod() { - mb_common.value = 'moofoo'; + mb_common.value = 'moofoo'; + mb_common.funcVal = undefined; mb_common.buildMemberQuick(); assert.throws( function() @@ -151,3 +153,74 @@ mb_common.assertCommon(); }, TypeError, "Cannot override concrete method with abstract method" ); } )(); + +/** + * One of the powerful features of the method builder is the ability to pass in + * an instance to be bound to 'this' when invoking a method. This has some + * important consequences, such as the ability to implement protected/private + * members. + */ +( function testMethodInvocationBindsThisToPassedInstance() +{ + var instance = function() {}, + val = 'fooboo', + val2 = 'fooboo2', + iid = 1, + + func = function() + { + return this.foo; + }, + + func2 = function() + { + return this.foo2; + }, + + called = false, + instCallback = function() + { + called = true; + return instance; + }, + + members = { 'public': {}, 'protected': {}, 'private': {} } + ; + + // set instance values + instance.foo = val; + instance.foo2 = val2; + + // concrete method + mb_common.buildMember( + members, + exports.meta, + 'func', + func, + [ 'public' ], + instCallback + ); + + assert.equal( + members[ 'public' ].func(), + val, + "Calling method will bind 'this' to passed instance" + ); + + // override method + mb_common.buildMember( + members, + exports.meta, + 'func', + func2, + [ 'public' ], + instCallback + ); + + assert.equal( + members[ 'public' ].func(), + val2, + "Calling method override will bind 'this' to passed instance" + ); +} )(); +