Command Pattern Deconstructed
I was rewriting the Underscore.js library in CoffeeScript when I came accross the invoke function. It reminded me of the execution method from the command pattern in Addy Osmani's book Learning JavaScript Design Patterns:
carManager.execute = function ( name ) { return carManager[name] && carManager[name].apply( carManager, [].slice.call(arguments, 1) ); };
This method takes an argument called name
which holds a string.
The string represents the method name we want to call on the
carManager
object.
&& Operator Short Circuiting
carManager[name] && ...
means 'if the method we want to call on
carManager
exist then evaluate the right hand side of the expression and return its value'.
It is worth noting here that when the &&
operator is used, if
every part of the expression evaluates to true then the value of the last
expression evaluated will be returned. For example:
'hey' && 'lol' && 32
Will return 32
, since it was the last expression to be evaluated. Likewise, if
you have any expressions that evaluate to false, then the first expression which
evaluates to false in the chain of operations will be returned.
undefined && false && 44
Will return undefined
. However, if we switched the order of
false
and undefined
then false
would be
returned instead. If carManager[name]
does not exist then undefined
will be returned, otherwise the second part of expression will be executed:
Using apply to Invoke Methods
carManager[name].apply( carManager, [].slice.call(arguments, 1) );
is essentially the same thing as
carManager[name](arguments.slice(1));
[].slice
is initializing an empty array instance and using it to
gain access to the slice
method. We when use call
on
the slice
method and pass in (arguments, 1)
, which
simply returns a new array of arguments including all but the first. In this
case we are only excluding the first argument because the first argument is the
name
variable which we are already have access to.
Writing Specs for _.invoke
The following Jasmine specs describe the expected behavior for
_.invoke
:
describe "invoke", -> it "should call sort method on each element in an array and return results in an array", -> result = _.invoke([[5, 1, 7], [3, 2, 1]], "sort"); expect(result).toEqual([[1, 5, 7], [1, 2, 3]]) it "should call sort method on each value in an object and return results in an array", -> result = _.invoke({a: [5, 1, 7], b: [3, 2, 66]}, "sort") expect(result).toEqual([[1, 5, 7], [2, 3, 66]]) it "should pass extra arguments onto method invocation", -> result = _.invoke(["lol"], "concat", "bbq") expect(result).toEqual(["lolbbq"])
_.invoke
accepts a container, a method name, and any amount of
extra arguments. This method then calls the method which corresponds to the
method name on each item in the container and returns an array of the
results. Any additional arguments passed into _.invoke
will be
applied to the invocation of each item in the container. We can write this
entire method by modeling the pattern deconstructed above:
_.invoke = (container, methodName) -> if Array.isArray(container) for element in container element[methodName].apply(element, [].slice.call(arguments, 2)) else for key of container container[key][methodName].apply(container[key], [].slice.call(arguments, 2))