I played with JavaScript closures before, for example changing the variables in the parent scope by recompiling the inner function. Now I found an even weirder trick to change the function's argument values after the function has been called.
Imagine we have a parent function with a couple of named parameters.
The parent function returns another function named add
.
The function add
always returns the sum of the arguments in the parent function.
1 | function parent(a, b) { |
Question:
Will adder() always return 1? Or is there a way to change its result?
Note that the outside code has time to try to modify the environment between the
call to the parent
and adder
1 | var adder = parent(0, 1); |
Well, all parameters in a function are stored at runtime inside a special array-like
object arguments
. Can we return it from the parent
function? Yes we can.
1 | function parent(a, b) { |
Then we can modify the returned arguments by index to override the value of a
and b
!
1 | function parent(a, b) { |
the output shows the changed values between the parent
run and the add
execution
1 | in parent, a 0 b 1 |
Can we modify both arguments? Yes.
1 | function parent(a, b) { |
Do we need to pass "dummy" arguments to the parent
? We intend to override them any way.
1 | function parent(a, b) { |
Turns out - we do need to pass some dummy arguments to the parent
function, otherwise
the arguments
object is not initialized. The number of dummy or placeholder arguments
should be at least the number of arguments we intend to replace.
1 | function parent(a, b) { |
We need both arguments, even if we pass undefined
values. Thus we can simply pass
the needed number of undefined values when calling the parent
function.
1 | function parent(a, b) { |
Note
The above example does NOT work in strict mode :( Modifying the arguments
after
the execution is forbidden.
Use case
Before you think this is a very contrived example without a practical use, let me show how we applied it. We have an open source library ng-describe for unit testing AngularJS code. Typical Angular unit tests are very verbose, with lots of boilerplate code just to inject the necessary dependencies from different modules.
1 | // typical AngularJS unit test |
The same unit test can be written using ng-describe much shorter
1 | ngDescribe({ |
One can inject multiple dependencies by providing a list of names.
Each dependency will be placed into the deps
object passed into the test
callback function.
1 | ngDescribe({ |
We need the dependencies object because the actual injection happens before
each unit test; we don't want to initialize all the dependencies just once!
Thus when the ngDescribe
code executes the test callback, we only have the
names of the dependencies. In a sense, we are scheduling each unit test
by placing it in the queue with a corresponding beforeEach
function.
1 | // view of the test queue |
This is very similar to our initial parent - add
example. The test callback
is the parent, and each unit test function it
is the inner one. The unit tests
are placed onto the test runner queue; they will be executed later.
This gives us a way to implement a better API - instead of using an intermediate
object deps
that we can fill from the outside in beforeEach
step, we can just
list of the dependencies to be injected in the test callback.
1 | ngDescribe({ |
Before we run the test callback, we can grab the names of the parameters it expects, there is Angular $injector.annotate utility that does it by inspecting function's source code. Thus our code can do something like this:
1 | // view of the test queue |
But how do grab the reference to the arguments
object from inside testCallback
?
We could ask the people writing unit tests to return it, but this would be bad :(
1 | ngDescribe({ |
Here we apply another trick: we can change the source of the testCallback
on the fly
and add return arguments
statement! Our logic then becomes something like this
1 | // view of the test queue |
Notice that we need to place injected values into deps
structure (which now points)
at the arguments
returned by the rewritten testCallback
function. We place the
values by index, making sure the injected value foo
goes into arguments[0]
,
bar
goes into arguments[1]
, etc.
The result is that when each unit test it
runs, the named parameters in the parent scope
testCallback
are initialized. Nice! You can see the code for yourself inside
ng-describe.js in the function runTestCallbackExposeArguments
.