1
0
Fork 0

Initial implementation of protected members

- This was quite the pain in the ass
- There are additional considerations. I DO NOT recommend using this commit. Check out a later commit.
closure/master
Mike Gerwitz 2011-03-02 20:43:24 -05:00
parent 2af7bcf45d
commit 74c2fc57c1
6 changed files with 296 additions and 57 deletions

View File

@ -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.<number, 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
*
@ -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
;
}

View File

@ -59,10 +59,15 @@ exports.initMembers = function( mpublic, mprotected, mprivate )
* @param {*} value property value
*
* @param {Object.<string,boolean>} 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 tmp = this.__super;
var override = null;
// assign _super temporarily for the method invocation so
// that the method can call the parent method
if ( new_method )
{
override = function()
{
// the _super property will contain 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;
};
}
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.

View File

@ -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 ];
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
);

View File

@ -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(
Foo = Class.extend(
{
args: null,
__construct: function()
{
construct_count++;
construct_context = this;
this.args = arguments;
construct_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"
);

View File

@ -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"
);
} )();

View File

@ -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
@ -51,6 +52,7 @@ mb_common.assertCommon();
( function testCannotOverridePropertyWithMethod()
{
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"
);
} )();