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 = {}; 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 * Creates a class, inheriting either from the provided base class or the
@ -188,7 +198,8 @@ function Class() {};
*/ */
var extend = ( function( extending ) var extend = ( function( extending )
{ {
var class_id = 0; var class_id = 0,
instance_id = 0;
/** /**
* Mimics class inheritance * Mimics class inheritance
@ -215,8 +226,8 @@ var extend = ( function( extending )
hasOwn = Array.prototype.hasOwnProperty; hasOwn = Array.prototype.hasOwnProperty;
var properties = {}, var properties = {},
members = member_builder.initMembers( prototype ),
prop_init = member_builder.initMembers(), prop_init = member_builder.initMembers(),
members = member_builder.initMembers( prototype ),
abstract_methods = abstract_methods =
util.clone( getMeta( base.__cid ).abstractMethods ) util.clone( getMeta( base.__cid ).abstractMethods )
@ -261,7 +272,7 @@ var extend = ( function( extending )
method: function( name, func, is_abstract, keywords ) method: function( name, func, is_abstract, keywords )
{ {
member_builder.buildMethod( member_builder.buildMethod(
members, null, name, func, keywords members, null, name, func, keywords, getMethodInstance
); );
if ( is_abstract ) if ( is_abstract )
@ -294,7 +305,7 @@ var extend = ( function( extending )
// set up the new class // set up the new class
var new_class = createCtor( abstract_methods ); var new_class = createCtor( abstract_methods );
attachPropInit( prototype, prop_init ); attachPropInit( prototype, prop_init, members );
new_class.prototype = prototype; new_class.prototype = prototype;
new_class.constructor = new_class; new_class.constructor = new_class;
@ -334,6 +345,7 @@ var extend = ( function( extending )
{ {
var args = null; var args = null;
// constructor function to be returned
var __self = function() var __self = function()
{ {
if ( !( this instanceof __self ) ) if ( !( this instanceof __self ) )
@ -344,6 +356,10 @@ var extend = ( function( extending )
return new __self(); return new __self();
} }
// generate and store unique instance id
attachInstanceId( this, ++instance_id, __self );
initInstance( instance_id, this );
this.__initProps(); this.__initProps();
// call the constructor, if one was provided // 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 * 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} prototype prototype to attach method to
* @param {Object} properties properties to initialize * @param {Object} properties properties to initialize
* *
* @param {{public: Object, protected: Object, private: Object}} members
*
* @return {undefined} * @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() util.defineSecureProp( prototype, '__initProps', function()
{ {
var inst_props = class_instance[ this.__iid ];
// first initialize the parent's properties, so that ours will overwrite // first initialize the parent's properties, so that ours will overwrite
// them // them
var parent_init = prototype.parent.__initProps; var parent_init = prototype.parent.__initProps;
@ -493,6 +538,25 @@ function attachPropInit( prototype, properties )
// not share references (and therefore, data) // not share references (and therefore, data)
this[ prop ] = util.clone( prop_pub[ prop ] ); 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 * Attaches partially applied isInstanceOf() method to class instance
* *
@ -649,3 +727,26 @@ function getMeta( id )
return class_meta[ 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 {*} value property value
* *
* @param {Object.<string,boolean>} keywords parsed keywords * @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} * @return {undefined}
*/ */
exports.buildMethod = function( members, meta, name, value, keywords, cmp ) exports.buildMethod = function(
members, meta, name, value, keywords, instCallback
)
{ {
var prev; var prev;
@ -104,12 +109,18 @@ exports.buildMethod = function( members, meta, name, value, keywords, cmp )
if ( prev ) if ( prev )
{ {
// override the method // 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 else
{ {
// we are not overriding the method, so simply copy it over // we are not overriding the method, so simply copy it over, wrapping it
dest[ name ] = value; // 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()} super_method method to override
* @param {function()} new_method method to override with * @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 * @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 // return a function that permits referencing the super method via the
// __super property // __super property
var override = function() var override = null;
{
var tmp = this.__super;
// assign _super temporarily for the method invocation so if ( new_method )
// that the method can call the parent method {
override = function()
{
// the _super property will contain the parent method
this.__super = super_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 // 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. // 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 ) exports.buildMemberQuick = function( keywords, preserve_prior )
{ {
@ -66,16 +66,25 @@ exports.assertOnlyVisibility = function( vis, name, value, message )
var check = [ 'public', 'protected', 'private' ], var check = [ 'public', 'protected', 'private' ],
i = check.length, i = check.length,
visi = '', visi = '',
value,
cmp; cmp;
// forEach not used for pre-ES5 browser support // forEach not used for pre-ES5 browser support
while ( i-- ) while ( i-- )
{ {
visi = check[ i ]; visi = check[ i ];
value = exports.members[ visi ][ name ];
cmp = ( visi === vis ) ? value : undefined; cmp = ( visi === vis ) ? value : undefined;
// are we comparing functions?
if ( cmp && exports.funcVal )
{
cmp = exports.funcVal;
value = value();
}
assert.deepEqual( assert.deepEqual(
exports.members[ visi ][ name ], value,
cmp, cmp,
message message
); );

View File

@ -29,22 +29,21 @@ var common = require( './common' ),
// these two variables are declared outside of the class to ensure that they // 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 // will still be set even if the context of the constructor is wrong
var construct_count = 0, var construct_count = 0,
construct_context = null; construct_context = null,
construct_args = null
// create a basic test class // create a basic test class
var Foo = Class.extend( Foo = Class.extend(
{ {
args: null,
__construct: function() __construct: function()
{ {
construct_count++; construct_count++;
construct_context = this; construct_context = this;
construct_args = arguments;
this.args = arguments;
}, },
}); })
;
assert.ok( assert.ok(
( Foo.prototype.__construct instanceof Function ), ( Foo.prototype.__construct instanceof Function ),
@ -67,19 +66,19 @@ assert.equal(
); );
assert.equal( assert.equal(
obj, construct_context.__iid,
construct_context, obj.__iid,
"Constructor should be invoked within the context of the class instance" "Constructor should be invoked within the context of the class instance"
); );
assert.notEqual( assert.notEqual(
obj.args, construct_args,
null, null,
"Constructor arguments should be passed to the constructor" "Constructor arguments should be passed to the constructor"
); );
assert.equal( assert.equal(
obj.args.length, construct_args.length,
args.length, args.length,
"All arguments should be passed to the constructor" "All arguments should be passed to the constructor"
); );
@ -88,7 +87,7 @@ assert.equal(
for ( var i = 0, len = args.length; i < len; i++ ) for ( var i = 0, len = args.length; i < len; i++ )
{ {
assert.equal( assert.equal(
obj.args[ i ], construct_args[ i ],
args[ i ], args[ i ],
"Arguments should be passed to the constructor: " + i "Arguments should be passed to the constructor: " + i
); );
@ -112,16 +111,16 @@ assert.equal(
); );
assert.equal( assert.equal(
construct_context, construct_context.__iid,
subobj, subobj.__iid,
"Parent constructor is run in context of the subtype" "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 // this should be implied by the previous test, but let's add it for some peace
// of mind // of mind
assert.ok( assert.ok(
( ( subobj.args[ 0 ] === args2[ 0 ] ) ( ( construct_args[ 0 ] === args2[ 0 ] )
&& ( subobj.args[ 1 ] == args2[ 1 ] ) && ( construct_args[ 1 ] == args2[ 1 ] )
), ),
"Parent constructor sets values on subtype" "Parent constructor sets values on subtype"
); );
@ -135,14 +134,14 @@ assert.ok(
); );
assert.equal( assert.equal(
construct_context, construct_context.__iid,
subobj2, subobj2.__iid,
"Self-invoking constructor is run in the context of the new object" "Self-invoking constructor is run in the context of the new object"
); );
assert.ok( assert.ok(
( ( subobj2.args[ 0 ] === args2[ 0 ] ) ( ( construct_args[ 0 ] === args2[ 0 ] )
&& ( subobj2.args[ 1 ] == args2[ 1 ] ) && ( construct_args[ 1 ] == args2[ 1 ] )
), ),
"Self-invoking constructor receives arguments" "Self-invoking constructor receives arguments"
); );

View File

@ -30,9 +30,9 @@ var common = require( './common' ),
prot = 'bar', prot = 'bar',
priv = 'baz', priv = 'baz',
pubf = function() {}, pubf = function() { return pub; },
protf = function() {}, protf = function() { return prot; },
privf = function() {}, privf = function() { return priv; },
// new anonymous class instance // new anonymous class instance
foo = Class.extend( { foo = Class.extend( {
@ -43,6 +43,13 @@ var common = require( './common' ),
'public pubf': pubf, 'public pubf': pubf,
'protected protf': protf, 'protected protf': protf,
'private privf': privf, '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( assert.equal(
foo.pubf, foo.pubf(),
pubf, pub,
"Public methods are accessible via public interface" "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 = 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; mb_common.buildMember = common.require( 'member_builder' ).buildMethod;
// do assertions common to all member builders // do assertions common to all member builders
@ -51,6 +52,7 @@ mb_common.assertCommon();
( function testCannotOverridePropertyWithMethod() ( function testCannotOverridePropertyWithMethod()
{ {
mb_common.value = 'moofoo'; mb_common.value = 'moofoo';
mb_common.funcVal = undefined;
mb_common.buildMemberQuick(); mb_common.buildMemberQuick();
assert.throws( function() assert.throws( function()
@ -151,3 +153,74 @@ mb_common.assertCommon();
}, TypeError, "Cannot override concrete method with abstract method" ); }, 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"
);
} )();