1
0
Fork 0

ES6-style constructors

Included with this change is a simple "wrapper" implementation:

```
// equivalent
easejs( Foo );
Class.extend( Foo, {} );
```
master
Mike Gerwitz 2015-09-16 00:15:50 -04:00
commit cbf98cccf1
No known key found for this signature in database
GPG Key ID: F22BB8158EE30EAB
6 changed files with 200 additions and 41 deletions

View File

@ -1,7 +1,7 @@
/**
* Provides ease of access to all submodules
*
* Copyright (C) 2010, 2011, 2013, 2014 Free Software Foundation, Inc.
* Copyright (C) 2010, 2011, 2013, 2014, 2015 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
@ -19,10 +19,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Wrap a prototype using ease.js
*
* This function is the entry point for ease.js; its fields expose all of
* its core features. When invoked, it wraps the given prototype using
* ease.js, producing an ease.js Class. This is more natural when using the
* ECMAScript 6 `class` syntax to define prototypes.
*
* @param {Function} proto prototype to wrap
*
* @return {Function} ease.js Class wrapping PROTO
*/
var exports = module.exports = function( proto )
{
return exports.Class.extend( proto, {} );
};
exports.Class = require( './lib/class' );
exports.AbstractClass = require( './lib/class_abstract' );
exports.FinalClass = require( './lib/class_final' );
exports.Interface = require( './lib/interface' );
exports.Trait = require( './lib/Trait' );
exports.version = require( './lib/version' );

View File

@ -28,6 +28,8 @@ var util = require( './util' ),
Warning = require( './warn' ).Warning,
Symbol = require( './util/Symbol' ),
parseKeywords = require( './prop_parser' ).parseKeywords,
hasOwn = Object.prototype.hasOwnProperty,
@ -50,15 +52,27 @@ var util = require( './util' ),
/**
* Hash of reserved members
*
* These methods cannot be defined in the class. They are for internal use
* only. We must check both properties and methods to ensure that neither is
* defined.
* These methods cannot be defined in the class; they are for internal
* use only. We must check both properties and methods to ensure that
* neither is defined.
*
* @type {Object.<string,boolean>}
*/
reserved_members = {
'__initProps': true,
'constructor': true,
},
/**
* Hash of aliased members
*
* These are members that alias to another. Ideally, this should be a
* very small list. It is useful for introducing features without
* deprecating old.
*
* @type {Object.<string,string>}
*/
aliased_members = {
'constructor': '__construct',
},
/**
@ -641,12 +655,56 @@ exports.prototype.buildMembers = function buildMembers(
parser.method && hjoin( 'method', handlers.method );
}
handlers.keywordParser = _keywordParser;
// parse members and process accumulated member state
util.propParse( props, handlers, context );
this._memberBuilder.end( context.state );
}
/**
* Member keyword parser
*
* In reality, this parser is simply intended to override names where there
* are applicable aliases; all keyword parsing is kept to the original
* implementation.
*
* @param {string} prop property to parse
*
* @return {{name: string, bitwords: number, keywords: Object.<string, boolean>}}
*/
function _keywordParser( prop )
{
var result = parseKeywords( prop ),
alias = _getMemberAlias( result.name );
if ( alias !== undefined )
{
result.name = alias;
}
return result;
}
/**
* Return a member alias for NAME
*
* If NAME has no alias, then the result is `undefined`.
*
* @param {string} name member name
*
* @return {string|undefined}
*/
function _getMemberAlias( name )
{
return ( hasOwn.call( aliased_members, name ) )
? aliased_members[ name ]
: undefined;
}
function _parseEach( name, value, keywords )
{
var defs = this.defs;

View File

@ -1,7 +1,8 @@
/**
* Property keyword parser module
*
* Copyright (C) 2010, 2011, 2012, 2013, 2014 Free Software Foundation, Inc.
* Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015
* Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
@ -60,7 +61,7 @@ exports.kmasks = _kmasks;
*
* @param {string} prop property string, which may contain keywords
*
* @return {{name: string, keywords: Object.<string, boolean>}}
* @return {{name: string, bitwords: number, keywords: Object.<string, boolean>}}
*/
exports.parseKeywords = function ( prop )
{

View File

@ -1,7 +1,7 @@
/**
* Tests class module constructor creation
*
* Copyright (C) 2014 Free Software Foundation, Inc.
* Copyright (C) 2014, 2015 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
@ -21,6 +21,24 @@
require( 'common' ).testCase(
{
caseSetUp: function()
{
// ease.js was written long before ES6 drafts began providing class
// support. Now that they do, we should support their constructor
// decision as well as our own.
this.ctors = [ '__construct', 'constructor' ];
// we only use ES3 features, thus this
this.mkctor = function( name, f )
{
var o = {};
o[ name ] = f;
return o;
};
},
setUp: function()
{
this.Sut = this.require( 'class' );
@ -32,10 +50,15 @@ require( 'common' ).testCase(
* defining the class. (Note that the case of ensuring that it is not
* called when creating a subtype is handled by the ExtendTest case.)
*/
'Constructor should not be invoked before instantiation': function()
'@each(ctors) Should not be invoked before instantiation':
function( name )
{
var called = false;
this.Sut.extend( { __construct: function() { called = true; } } );
var called = false,
dfn = {};
this.Sut.extend(
this.mkctor( name, function() { called = true; } )
);
this.assertNotEqual( called, true );
},
@ -47,14 +70,14 @@ require( 'common' ).testCase(
* is instantiated. Further, it should only be called a single time,
* which is particularly important if it produces side-effects.
*/
'Constructor should be invoked once upon instantiation': function()
'@each(ctors) Should be invoked once upon instantiation':
function( name )
{
var called = 0;
var Foo = this.Sut.extend(
{
__construct: function() { called++; }
} );
this.mkctor( name, function() { called++; } )
);
// note that we're not yet testing the more consise new-less
// invocation style
@ -67,17 +90,20 @@ require( 'common' ).testCase(
* Once invoked, the __construct method should be bound to the newly
* created instance.
*/
'Constructor should be invoked within context of new instance':
function()
'@each(ctors) Should be invoked within context of new instance':
function( name )
{
var expected = Math.random();
var Foo = this.Sut.extend(
var dfn = this.mkctor( name, function()
{
val: null,
__construct: function() { this.val = expected; }
this.val = expected;
} );
dfn.val = null;
var Foo = this.Sut.extend( dfn );
// if `this' was bound to the instance, then __construct should set
// VAL to EXPECTED
var inst = new Foo();
@ -90,19 +116,18 @@ require( 'common' ).testCase(
* ``class'') should be passed to __construct, unchanged and
* uncopied---that is, references should be retained.
*/
'Constructor arguments should be passed unchanged to __construct':
function()
'@each(ctors) Arguments should be passed unchanged to __construct':
function( name )
{
var args = [ "foo", { bar: 'baz' }, [ 'moo', 'cow' ] ],
given = null;
var Foo = this.Sut.extend(
{
__construct: function()
this.mkctor( name, function()
{
given = Array.prototype.slice.call( arguments, 0 );
}
} );
} )
);
new Foo( args[ 0 ], args[ 1 ], args[ 2 ] );
@ -125,15 +150,16 @@ require( 'common' ).testCase(
* the name __construct---is modelled after PHP; Java classes, for
* instance, do not inherit their parents' constructors.
*/
'Parent constructor should be invoked for subtype if not overridden':
function()
'@each(ctors)Parent constructor invoked for subtype if not overridden':
function( name )
{
var called = false;
var Sub = this.Sut.extend(
{
__construct: function() { called = true; }
} ).extend( {} );
var dfn = {};
dfn[ name ] = function() { called = true; };
var Sub = this.Sut.extend( dfn )
.extend( {} );
new Sub();
this.assertOk( called );
@ -172,18 +198,22 @@ require( 'common' ).testCase(
* __construct, since public is the default and there is no other
* option.)
*/
'__construct must be public': function()
'@each(ctors) Constructor must be public': function( name )
{
var Sut = this.Sut;
this.assertThrows( function()
{
Sut( { 'protected __construct': function() {} } );
var dfn = {};
dfn[ 'protected ' + name ] = function() {};
Sut( dfn );
}, TypeError, "Constructor should not be able to be protected" );
this.assertThrows( function()
{
Sut( { 'private __construct': function() {} } );
var dfn = {};
dfn[ 'private ' + name ] = function() {};
Sut( dfn );
}, TypeError, "Constructor should not be able to be private" );
},
@ -200,4 +230,35 @@ require( 'common' ).testCase(
var Foo = this.Sut.extend( {} );
this.assertStrictEqual( Foo().constructor, Foo );
},
/**
* We support multiple constructor styles; only one may be provided.
*
* This error should happen as a consequence of other method checks that
* prohibit redefinitions.
*/
'Cannot provide multiple constructor styles': function()
{
var Sut = this.Sut,
len = this.ctors.length;
// this will not test every permutation, but will hopefully be a
// reasonable test in the event that any additional constructors are
// added in the future (we start at 1 because it'll wrap modulo LEN)
for ( var i = 1; i < len; i++ )
{
var dfn = {},
a = this.ctors[ i ],
b = this.ctors[ ( i + 1 ) % len ];
dfn[ a ] = function() {};
dfn[ b ] = function() {};
this.assertThrows( function()
{
Sut( dfn );
}, Error, "Multiple constructors should not be permitted" );
}
},
} );

View File

@ -1,7 +1,7 @@
/**
* Tests class builder member restrictions
*
* Copyright (C) 2014 Free Software Foundation, Inc.
* Copyright (C) 2014, 2015 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
@ -88,7 +88,7 @@ require( 'common' ).testCase(
*/
'Proper members are reserved': function()
{
var chk = [ '__initProps', 'constructor' ],
var chk = [ '__initProps' ],
i = chk.length,
reserved = this.Sut.getReservedMembers();

View File

@ -1,7 +1,7 @@
/**
* Tests index.js
*
* Copyright (C) 2014 Free Software Foundation, Inc.
* Copyright (C) 2014, 2015 Free Software Foundation, Inc.
*
* This file is part of GNU ease.js.
*
@ -63,4 +63,27 @@ require( 'common' ).testCase(
{
this.exportedAs( 'version', 'version' );
},
/**
* Since ECMAScript 6 introduces the ability to define prototypes using
* the `class` keyword and some syntatic sugar, it is awkward to use the
* traditional ease.js class abstraction with it. Instead, if a user
* wishes to wrap a prototype defined in this manner (to take advantage
* of ease.js' features), it would be more appropriate to make it look
* like we're doing just that.
*
* This is a shorthand for Class.extend( proto, {} ).
*/
'Invoking is equivalent to extending with empty definition': function()
{
var proto = function() {},
result = this.Sut( proto );
this.assertOk( this.Sut.Class.isClass( result ) );
// this is not a comprehensive test (once we add reflection of some
// sort, verify that nothing is added to the prototype)
this.assertOk( this.Sut.Class.isA( proto, result() ) );
},
} );