Values is a library for improving your application code by adding value semantics. Lots of concepts in our apps don't have a specific 'identity' - for instance every
thisx = x;thisy = y;var a = 00;var b = 00;assert a == b ; // fails - based on object identity
In contrast, Values ensures that we can compare two value objects based on their value:
var Point = vodefine"x""y";var a = 00;var b = 00;assert a == b ; // succeeds - follows value semantics
npm install values
values supports require.js and other AMD loaders, or you can simply include it as normal and it'll define
So values should be comparable by value.
valueOf is the method JS gives us to control how our objects are compared, but unfortunately it doesn't work for
===, only the inequality operators. Equality operations for objects are always based on identity. Values.js works around this by ensuring the same object is returned for the same arguments to a value object constructor.
var a = 110;var b = 110;assert a === b ;
If your value object can meaningfully use the inequality operators
> - for instance a set can be bigger than another set - then define a
valueOf method to return a comparable value (a String or Number). That'll cover all comparisons for your value object!
var Line = vodefine"x1""y1""x2""y2";// Pythagorus' theoremreturn Mathsqrt Mathpowthisy2 - thisy12 + Mathpowthisx2 - thisx12 ;var a = 0 0 0 10;var b = 1919 2020;assert a > b ;
ValueObjects should be immutable. Like numbers, it doesn't make sense to 'change' (mutate) a value, you simply have a new one. Allowing values to change in place leads to confusing semantics:
var today = MutableDateLibrarytoday;var event = at: today text: "started using values" ;// `addDays()` is implemented mutably, changing the date in place and returning itvar remindAt = eventataddDays1;// fails! today has been changedassert todaytimestamp === MutableDateLibrarytodaytimestamp ;
This really happens, and we've probably all made something that should be a value type mutable. The above is equally true for: intervals, ranges, dates and sets of any type.
Value objects created by Values are immutable - you can't change fields in ES5, and trying to do so in strict mode will raise an exception. If your unit tests enforce immutable use of your value objects, your application will be correct even in non ES5 environments.
Rather than requiring you to use a subclassing mechanism, Values.js exposes functions that allow you to compose your own value objects and setup their constructor and prototype as usual.
vo.memoizedConstructor is used to fulfil the value equality semantics and
vo.setup sets the field values immutably. Finally,
vo.setupPrototype adds the
derive method and
fields array it uses to the prototype.
varvar existing = vomemoizedConstructorPeriodthisarguments;ifexisting return existing;vosetupthisperiodFieldsarguments;;var periodFields = "from""to";vosetupPrototypePeriodperiodFields
A quick way to define VOs which don't require custom constructors (effectively just doing the above) is also provided.
var Period = vodefine"from""to";
To create a new version of a value object based on an old one, use the
derive method. This eases the creation of modified value objects, without losing the benefits of their immutability.
var periodA = 20122015;var periodB = periodAderivefrom:2013;assertperiodAfrom === 2012;assertperiodBfrom === 2013;var periodC = periodBderivefrom: 2012;assertperiodA === periodC;
The derive method takes a map of named arguments.
You'd use the
derive method to update references to values in variables or as object properties. Values are used in mutable systems, they're just immutable themselves.
If a value object of same type with the same fields exists, returns that value object. If not, will create and return a new instance.
You can supply a function as an optional third argument to specify how the parameters are hashed. This is useful if your value objects have fields that can be more quickly hashed than via JSON.stringify (the default hasher).
Sets immutable fields on instance.
derive method, and the
fields array it needs, to the constructor's prototype.
vo.define(fieldName1 [, fieldNameN ... ])
Defines a new value object constructor with the specified field names.
Instance method that returns a new value object with field values taken by preference from newValuesMap, with any missing fields taken from the existing value object
derive is called on.
In environments with
In environments without
WeakMap all value objects will be retained. If you ensure your value objects are themselves small (especially avoiding them holding references to large objects) this should be fine.
For instance, if we create 100,000
Point, and 100,000
var Point = vo.define("x","y");for(var i=0; i < 1e5; i++) new Point(Math.random(),Math.random());var Person = vo.define("name","age");for(var i=0; i < 1e5; i++) new Person(Math.random().toString(),Math.random());
we use 10mb of memory: 4mb for the points, and 6mb for the people.
Does this matter? It depends. The above is a worst case as none of the instances or strings are shared. However, since value semantics make sense when you have values that are identical (and therefore share the same instance), 200k unique instances in 10mb (or 1 million in 50mb) gives your application a lot of room.
Values is designed to be extremely extensible, so uses only advisory privacy. This allows redefinition of its implementation without modifying the code (and therefore having to maintain a patched version). This post explains how to modify public and private function behaviour.
- Small (~160 lines, <1.5kb uglified + gzipped)
- Contracts upheld strongly in all ES5 environments (with strict mode)
- Immutability is about ensuring application level validity, so your unit tests will catch any problems when run in ES5/6. If you have good coverage, it doesn't matter if older browsers (IE7) won't enforce immutability at run-time.