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 * 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. * This file is part of GNU ease.js.
* *
@ -19,10 +19,26 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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.Class = require( './lib/class' );
exports.AbstractClass = require( './lib/class_abstract' ); exports.AbstractClass = require( './lib/class_abstract' );
exports.FinalClass = require( './lib/class_final' ); exports.FinalClass = require( './lib/class_final' );
exports.Interface = require( './lib/interface' ); exports.Interface = require( './lib/interface' );
exports.Trait = require( './lib/Trait' ); exports.Trait = require( './lib/Trait' );
exports.version = require( './lib/version' ); exports.version = require( './lib/version' );

View File

@ -28,6 +28,8 @@ var util = require( './util' ),
Warning = require( './warn' ).Warning, Warning = require( './warn' ).Warning,
Symbol = require( './util/Symbol' ), Symbol = require( './util/Symbol' ),
parseKeywords = require( './prop_parser' ).parseKeywords,
hasOwn = Object.prototype.hasOwnProperty, hasOwn = Object.prototype.hasOwnProperty,
@ -50,15 +52,27 @@ var util = require( './util' ),
/** /**
* Hash of reserved members * Hash of reserved members
* *
* These methods cannot be defined in the class. They are for internal use * These methods cannot be defined in the class; they are for internal
* only. We must check both properties and methods to ensure that neither is * use only. We must check both properties and methods to ensure that
* defined. * neither is defined.
* *
* @type {Object.<string,boolean>} * @type {Object.<string,boolean>}
*/ */
reserved_members = { reserved_members = {
'__initProps': true, '__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 ); parser.method && hjoin( 'method', handlers.method );
} }
handlers.keywordParser = _keywordParser;
// parse members and process accumulated member state // parse members and process accumulated member state
util.propParse( props, handlers, context ); util.propParse( props, handlers, context );
this._memberBuilder.end( context.state ); 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 ) function _parseEach( name, value, keywords )
{ {
var defs = this.defs; var defs = this.defs;

View File

@ -1,7 +1,8 @@
/** /**
* Property keyword parser module * 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. * This file is part of GNU ease.js.
* *
@ -60,7 +61,7 @@ exports.kmasks = _kmasks;
* *
* @param {string} prop property string, which may contain keywords * @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 ) exports.parseKeywords = function ( prop )
{ {

View File

@ -1,7 +1,7 @@
/** /**
* Tests class module constructor creation * 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. * This file is part of GNU ease.js.
* *
@ -21,6 +21,24 @@
require( 'common' ).testCase( 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() setUp: function()
{ {
this.Sut = this.require( 'class' ); 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 * defining the class. (Note that the case of ensuring that it is not
* called when creating a subtype is handled by the ExtendTest case.) * 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; var called = false,
this.Sut.extend( { __construct: function() { called = true; } } ); dfn = {};
this.Sut.extend(
this.mkctor( name, function() { called = true; } )
);
this.assertNotEqual( called, true ); this.assertNotEqual( called, true );
}, },
@ -47,14 +70,14 @@ require( 'common' ).testCase(
* is instantiated. Further, it should only be called a single time, * is instantiated. Further, it should only be called a single time,
* which is particularly important if it produces side-effects. * 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 called = 0;
var Foo = this.Sut.extend( var Foo = this.Sut.extend(
{ this.mkctor( name, function() { called++; } )
__construct: function() { called++; } );
} );
// note that we're not yet testing the more consise new-less // note that we're not yet testing the more consise new-less
// invocation style // invocation style
@ -67,17 +90,20 @@ require( 'common' ).testCase(
* Once invoked, the __construct method should be bound to the newly * Once invoked, the __construct method should be bound to the newly
* created instance. * created instance.
*/ */
'Constructor should be invoked within context of new instance': '@each(ctors) Should be invoked within context of new instance':
function() function( name )
{ {
var expected = Math.random(); var expected = Math.random();
var Foo = this.Sut.extend( var dfn = this.mkctor( name, function()
{ {
val: null, this.val = expected;
__construct: function() { this.val = expected; }
} ); } );
dfn.val = null;
var Foo = this.Sut.extend( dfn );
// if `this' was bound to the instance, then __construct should set // if `this' was bound to the instance, then __construct should set
// VAL to EXPECTED // VAL to EXPECTED
var inst = new Foo(); var inst = new Foo();
@ -90,19 +116,18 @@ require( 'common' ).testCase(
* ``class'') should be passed to __construct, unchanged and * ``class'') should be passed to __construct, unchanged and
* uncopied---that is, references should be retained. * uncopied---that is, references should be retained.
*/ */
'Constructor arguments should be passed unchanged to __construct': '@each(ctors) Arguments should be passed unchanged to __construct':
function() function( name )
{ {
var args = [ "foo", { bar: 'baz' }, [ 'moo', 'cow' ] ], var args = [ "foo", { bar: 'baz' }, [ 'moo', 'cow' ] ],
given = null; given = null;
var Foo = this.Sut.extend( var Foo = this.Sut.extend(
{ this.mkctor( name, function()
__construct: function()
{ {
given = Array.prototype.slice.call( arguments, 0 ); given = Array.prototype.slice.call( arguments, 0 );
} } )
} ); );
new Foo( args[ 0 ], args[ 1 ], args[ 2 ] ); 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 * the name __construct---is modelled after PHP; Java classes, for
* instance, do not inherit their parents' constructors. * instance, do not inherit their parents' constructors.
*/ */
'Parent constructor should be invoked for subtype if not overridden': '@each(ctors)Parent constructor invoked for subtype if not overridden':
function() function( name )
{ {
var called = false; var called = false;
var Sub = this.Sut.extend( var dfn = {};
{ dfn[ name ] = function() { called = true; };
__construct: function() { called = true; }
} ).extend( {} ); var Sub = this.Sut.extend( dfn )
.extend( {} );
new Sub(); new Sub();
this.assertOk( called ); this.assertOk( called );
@ -172,18 +198,22 @@ require( 'common' ).testCase(
* __construct, since public is the default and there is no other * __construct, since public is the default and there is no other
* option.) * option.)
*/ */
'__construct must be public': function() '@each(ctors) Constructor must be public': function( name )
{ {
var Sut = this.Sut; var Sut = this.Sut;
this.assertThrows( function() this.assertThrows( function()
{ {
Sut( { 'protected __construct': function() {} } ); var dfn = {};
dfn[ 'protected ' + name ] = function() {};
Sut( dfn );
}, TypeError, "Constructor should not be able to be protected" ); }, TypeError, "Constructor should not be able to be protected" );
this.assertThrows( function() this.assertThrows( function()
{ {
Sut( { 'private __construct': function() {} } ); var dfn = {};
dfn[ 'private ' + name ] = function() {};
Sut( dfn );
}, TypeError, "Constructor should not be able to be private" ); }, TypeError, "Constructor should not be able to be private" );
}, },
@ -200,4 +230,35 @@ require( 'common' ).testCase(
var Foo = this.Sut.extend( {} ); var Foo = this.Sut.extend( {} );
this.assertStrictEqual( Foo().constructor, Foo ); 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 * 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. * This file is part of GNU ease.js.
* *
@ -88,7 +88,7 @@ require( 'common' ).testCase(
*/ */
'Proper members are reserved': function() 'Proper members are reserved': function()
{ {
var chk = [ '__initProps', 'constructor' ], var chk = [ '__initProps' ],
i = chk.length, i = chk.length,
reserved = this.Sut.getReservedMembers(); reserved = this.Sut.getReservedMembers();

View File

@ -1,7 +1,7 @@
/** /**
* Tests index.js * 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. * This file is part of GNU ease.js.
* *
@ -63,4 +63,27 @@ require( 'common' ).testCase(
{ {
this.exportedAs( 'version', 'version' ); 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() ) );
},
} ); } );