API

Trait(record : Object) : Trait

Trait (aka the "Trait constructor") takes a record describing required and provided properties and returns a trait containing these properties.

The record argument is usually an object literal. Both its identity and its prototype chain are irrelevant when constructing a trait.

Data properties bound to function objects in the argument are marked as method properties in the resulting trait. The prototype of these function objects is frozen by Trait.

Data properties bound to the Trait.required singleton are marked as required properties in the resulting trait.

Show example
var t = Trait({a: 0, b: function(){}});
// t = { a: {value: 0}, b: {value: function(){}, method: true} }
var t2 = Trait({a:Trait.required});
// t2 = { a: {value: undefined, required: true, enumerable: false} }
            
Trait.compose(t1 : Trait, t2 : Trait, ...) : Trait

compose takes any number of argument traits and returns a composite trait combining all of the own properties of the argument traits.

If two or more traits have own properties with the same name, the composite trait will contain a conflict property for that name. In an Ecmascript 5 environment, conflicting properties are represented as accessors that throw upon access. In an Ecmascript 3 environment, they are represented as functions that throw when called.

Two properties are not in conflict if either of them is a required property, or if they refer to identical values (as determined by ===).

compose is a commutative and associative operation, and the ordering of its arguments does not affect its return value.

If compose is invoked with less than 2 arguments, then:

  • compose(t) returns a trait equivalent to t.
  • compose() returns an empty trait.

Show example
var t1 = Trait({a : 0, b: 1});
var t2 = Trait({a : 1, c: 2});
var t = Trait.compose(t1, t2);
// t = Trait({ a: <conflict>, b: 1, c: 2})
            
Trait.resolve(map : Object, t : Trait) : Trait

resolve takes a map describing properties to rename or exclude and a trait and returns a resolved trait in which the indicated properties are renamed or excluded. The map is any object that maps property names either to truthy values that will be coerced to String or to falsy values (such as undefined) which are interpreted as excluding the given property.

resolve can be used to rename or exclude properties that would be in conflict in subsequent compositions. resolve first excludes properties, then renames properties. Required properties are not affected by resolve.

Renaming a property p to p' introduces a new binding for p' in the resolved trait (if p' already exists, it is marked in conflict). The old property name p becomes a required property of the trait. Likewise, excluding a property p turns p into a required property of the trait.

Renaming a property does not affect references to that property in the method body of trait methods. Any such references will continue to refer to the original name.

Show example
var t = Trait.resolve({ a: 'c'}, Trait({a: 1, b: 2 }));
// t = Trait({ a: Trait.required, b: 2, c: 1 })

var t2 = Trait.resolve({a: undefined}, Trait({a: 1, b: 2}));
// t2 = Trait({ a: Trait.required, b: 2 })
            
Trait.override(t1 : Trait, t2 : Trait, ...) : Trait

override takes any number of argument traits and returns a composite trait combining all of the own properties of the argument traits.

If two or more traits have own properties with the same name, the property is overridden, with precedence from left to right. This implies that properties of the leftmost trait are never overridden. Required properties are always overridden by non-required properties. override never creates new conflicts.

override is associative: override(t1,t2,t3) is equivalent to override(t1, override(t2, t3)) or to override(override(t1, t2), t3). override is not commutative: override(t1,t2) is not equivalent to override(t2,t1).

If override is invoked with less than 2 arguments, then:

  • override(t) returns a trait equivalent to t.
  • override() returns an empty trait.

Show example
var t1 = Trait({a : 0, b: 1});
var t2 = Trait({a : 1, c: 2});
var t = Trait.override(t1, t2);
// t = Trait({ a: 0, b: 1, c: 2})
            
Trait.required : Required

required is a singleton object that serves as a placeholder for a trait's required properties.

In principle, required should only be used inside the record definition passed to the Trait constructor.

Show example
var t = Trait({
  once: Trait.required,
  twice: function() { this.once(); this.once(); }
});
            
Trait.create(proto : Object, t : Trait) : Object

create takes a prototype object and a trait and returns an instantiation of the trait. The instantiated object's prototype is proto.

create throws

  • an Error('Missing required property: name'); if the trait contains a required property name.
  • an Error('Remaining conflicting property: name'); if the trait contains a conflicting property name.

create is similar to the Ecmascript 5 built-in Object.create except that it generates high-integrity, "final" objects. In addition to creating a new object from a trait, it also ensures that:

  • the object and all of its accessor and method properties are frozen.
  • the this pseudovariable in all accessors and methods of the object is bound to the instantiated object. Hence, even if methods of the trait instance are extracted and used as funargs, this will retain its correct binding.

Because all methods of an instance created by this function are bound functions, their this pseudovariable is no longer late-bound. Hence, such instances should not be used as the prototype of other objects (because this would not refer to the delegating child object). That is why these instances are called "final" objects.

Use Object.create instead of Trait.create if you want your trait instances to remain malleable or if you want them to act as prototypes of other objects.

Show example
var o = Trait.create(
  Object.prototype,
  Trait({ a:0, b:function(){ return this.a; } }));
// o = Object.freeze({
//  a:0,
//  b: (function() { return this.a; }).bind(o)
// });
            
Trait.eqv(t1 : Trait, t2: Trait) : boolean

eqv takes two traits and returns whether or not they are equivalent.

A trait t1 is equivalent to a trait t2 if both define the same set of property names and for all property names n, the property descriptor t1[n] is equivalent to the property descriptor t2[n]. Two property descriptors are equivalent if they have the same value, accessors and attributes.

The value of two property descriptors is compared using ===.

Show example
Trait.eqv(Trait({a:1,b:2}), Trait({b:2,a:1})); // true
Trait.eqv(Trait({a:Trait.required}), Trait({a:Trait.required})); // true
Trait.eqv(Trait({}), Trait({})); // true
Trait.eqv(Trait({a: <conflict>}),
          Trait({a: <conflict>})); // false
            
Trait.object(record: Object) : Object

object takes as its sole argument a record describing a trait and returns an instance of that trait whose prototype is Object.prototype.

Trait.object({...}) is a shorthand notation for:
Trait.create(Object.prototype, Trait({...})).

Think of object as the equivalent of Javascript's object-literal notation for high-integrity objects.

Show example
var o = Trait.object(
  { a: 0, b: function(){ return this.a; } });
// o = Object.freeze({
//  a:0,
//  b: (function() { return this.a; }).bind(o)
// });
            
Object.create(proto: Object, desc : Trait) : Object

If traits.js is loaded in an Ecmascript 3 engine and Object.create does not exist, traits.js defines it. Otherwise, traits.js assumes the built-in Ecmascript 5 definition.

Object.create takes a prototype object and a trait and returns a new instance, whose prototype is proto and whose properties are described by the trait.

Passing a trait to the Ecmascript 5 built-in function Object.create works because traits are represented as property descriptor maps, which is the format accepted by this function. Unlike Trait.create, Object.create returns malleable, non-frozen objects. However:

  • No exception is thrown if the trait still contains required or conflicting properties. Required properties remain as non-enumerable (in ES5) properties bound to undefined. Conflicting properties are bound to accessors (in ES5) that throw upon an attempt to get or set the property value. In ES3, they are bound to a function that throws when called.
  • Neither the object nor its accessor and method properties are frozen.
  • The this pseudovariable in all accessors and methods of the object is left unbound.

Since Ecmascript 3 does not support accessors or the notion of non-writable, non-enumerable and non-configurable properties, only writable, enumerable and configurable data properties can be created using this emulated version of Ecmascript 5's built-in Object.create function.

Show example
var o = Object.create(
  Object.prototype,
  Object.getOwnProperties({ a:0, b:function(){ return this.a; } }));
// o = {
//  a:0,
//  b: function() { return this.a; }
// };
            
Object.getOwnProperties(record: Object ) : Trait

If traits.js is loaded in an Ecmascript 3 engine and Object.getOwnProperties does not exist, traits.js defines it. Otherwise, traits.js assumes the built-in Ecmascript 5 definition.

Object.getOwnProperties takes a record (usually an object literal) as an argument and returns a property descriptor map that describes the properties of the record.

This method can be used to turn any object into a property descriptor map. The resulting property descriptor map is a valid trait.

Unlike the Trait constructor, Object.getOwnProperties does not transform data properties bound to functions into method properties, or data properties bound to Trait.required into required properties.

Show example
var pdmap = Object.getOwnProperties({a: 0, b: function(){}});
// pdmap = { a: {value: 0}, b: {value: function(){}} }
var pdmap2 = Object.getOwnProperties({a: Trait.required });
// pdmap2 = { a: {value: Trait.required} }
           

The traits.js API is explained in more detail here. See also: a semi-formal specification of traits based on this library.

Bookmark and Share