The Web Security Topics Series
Modules
[...]
Creating and Using Modules
Modules are stored in files, with a single module per file. A module is just some JavaScript code. The module mechanism ensures that any variables that are declared inside the module are inaccessible from outside it. To make an object available outside the module, it must be explicitly exported by assigning it to the exports
property of a special variable called module
. The data hiding is done by wrapping the module in a closure, but the module system does this transparently, so you do not need to be concerned about the implementation.
If a program needs to use a module, it must call the require
function, passing it a path name that points to the file in which the module is stored. The value returned by this call to require
is the object that was assigned to module.exports
in the file.
[...]
The object that was used as a prototype for counters in Listing 14 in the previous chapter could usefully be made into a module. This can be done by storing the code shown in Listing 23 in a file, which might be called counter-module.js
.
Listing 23
module.exports = {
setUp: function(max, min) {
// ...
},
next: function() {
// ...
},
previous: function() {
// ...
},
rewind: function() {
// ...
},
value: function() {
// ...
}
}
The object literal comprising the five methods that implement the counter is assigned to module.exports
instead of being stored in a variable as before. (Some programmers prefer to assign it to a variable first and then assign the value of that variable to module.exports
at the bottom of the file, so that it is clear what is being exported. This is just a stylistic preference as there is no advantage either way.)
Any program that needs to use counters can load this module and use the result exactly like any other object. It can be assigned to a variable (and objects imported from modules usually are) and used to create other objects, as shown in Listing 24, or its methods may be called.
Listing 24
var counter = require('counter-module')
var randomAcessCounter = Object.create(counter, {
goTo: {
// ...
}
})
Clearly, being able to use modules is much more convenient and maintainable than having to copy and paste the code of reused objects into every program that uses them.
[...]
Listing 25 shows how a module for inserting commas or points into long numbers could export the two functions described at the end of the chapter on Functions.
Listing 25###
var applyToInteger = function (f, n) {
// ...
}
var insertSeparators = function (separator, n) {
// ...
}
var curry = function(f) {
// ...
}
var insertSeparatorsFunction = curry(insertSeparators)
module.exports.insertCommas = insertSeparatorsFunction(',')
module.exports.insertPoints = insertSeparatorsFunction('.')
The auxiliary functions applyToInteger
, insertSeparators
and curry
are not exported, so there is no way of accessing them from outside the module, but the two exported functions are available by way of the object returned by require
, as in Listing 26.
Listing 26
var separators = require('separators-module'),
insertCommas = separators.insertCommas,
insertPoints = separators.insertPoints
Now insertCommas
and insertPoints
can be used just as before. There is no requirement that the names of the variables used to hold the imported functions should be the same as the property names in the exported module, but they often are.
[...]
A module can export a function that takes arguments and returns a value – an object or another function – that depends on the value of the argument. When the module is required, this function can be called immediately with an argument.
As an example, suppose that instead of exporting insertCommas
and insertPoints
, as in Listing 25, separators-module
exports the function previously assigned to insertSeparatorsFunction
. That is, the last three lines of code in Listing 25 are replaced by
module.exports = curry(insertSeparators)
The value of require('separators-module')
would then be that function, so the following code would make sense, and assign the expected function to insertCommas
.
var insertCommas = require('separators-module)(',')
Most often, when this technique is used, the module consists entirely of the assignment of a function taking an argument to module.exports
. Somewhere in the body of the function, an object with methods is returned. Since this object’s properties are closures they can refer to the argument to the module. The module can provide some abstract functions. When an object is passed into the module, these functions may become specific operations on that object.
To make this vague description more concrete, consider the module in Listing 27, which implements a map
function that applies some function to each value in a sequence, and returns an array of the results.
Listing 27
module.exports = function(obj) {
return {
map: function(f) {
if (obj.isEmpty()) return []
var maps = function(o) {
if (o.atEnd())
return [f(o.value)]
else
return [f(o.value)].concat(maps(o.next()))
}
return maps(obj.rewind())
}
}
}
Each value is generated by calling the value
property of the object passed into the module; the object’s next
member is called to move through the sequence, and isEmpty
and atEnd
are used to identify the special case of an empty sequence, and to determine when the process should terminate. An auxiliary function called maps
has been used to perform the actual iteration recursively. A while
loop could be used instead.
The map
function will work on any object that has a value
property and next
, isEmpty
and atEnd
methods. It might be used to map a function on a sequence of messages retrieved from a queue or records retrieved from a database, provided the methods are synchronous. (An asynchronous version is left as an exercise.)
As Listing 28 shows, it’s easy to add the missing pair of methods to the protoCounter
prototype from the previous chapter.
Listing 28
var protoCounter = {
// setUp, next, previous, rewind as before
atEnd: function() {
return this._current == this._max
},
isEmpty: function() {
this._max < this._min
}
}
// add value property as before
The value returned by isEmpty
should always be false
, because setUp
throws an error unless there is at least one possible value. However, since it is possible to assign to _min
and _max
, it is necessary to perform the test.
With these added methods, it is possible to create objects that can be passed into the module that exports map
. If this module is held in a file called iterator.js
, a small counter could be created and passed to the module in the manner shown in Listing 29.
Listing 29
var c = Object.create(protoCounter)
c.setUp(4)
var c_iterator = require('iterator')(c)
The object assigned to c_iterator
has a map
method that applies its argument, a function, to each element of the sequence generated by c
, so c_iterator.map(function(n) { return 2*n })
would be equal to [2, 4, 6, 8]
.
[...]
End of Extract
[Extracted from Javascript on the Server Using Node.js and Express by Nigel Chapman and Jenny Chapman]