In my webinar today on reading jQuery source code, I was talking about the extend
method. I started out by talking about what a typical extend method would do before going onto the jQuery-specific one.
photo © 2008 greg westfall | more info (via: Wylio)A while back I wrote a simple one, more as a pedagogical exercise, rather than any real attempt to create the be-all and end-all version:
var extend = function () { var obj, name, shift = Array.prototype.shift, target = shift.call(arguments); target = target || {}; obj = shift.call(arguments); while (obj) { for (name in obj) { if (obj.hasOwnProperty(name)) { target[name] = obj[name]; } } obj = shift.call(arguments); }; return target; };
So, essentially, the code assumes that the first argument to the function is the target object and there will be zero or more source objects defined that will provide properties to the target object. The source objects are peeled off the arguments array one by one. The target object is returned (and of course is modified by the function).
Here’s an example of it in action (we extend an empty object with some default values and then some changed values):
var defaultOptions = { animate: true, color: "red" }; var myOptions = { color: "blue" }; var actualOptions = extend({}, defaultOptions, myOptions); // result: { animate=true, color="blue"}
Anyway, this is not whether this code is correct or not, but about my first attempt at writing it. I initially had something like this:
var extend = function (target) { var obj, name, shift = Array.prototype.shift; obj = shift.call(arguments); obj = shift.call(arguments); while (obj) { for (name in obj) { if (obj.hasOwnProperty(name)) { target[name] = obj[name]; } } obj = shift.call(arguments); }; return target; };
I was assuming that there was always a target object, so I declared it in the function declaration itself. I then shifted off (and discarded) the first element of the arguments array (which was the target object) and from then on, it was the same as before. Except: it didn’t work. What the heck was going on?
To help resolve the problem, I wrote the following code:
var cl = function (obj, text) { if (text) { console.log(obj); } else { console.log('<' + obj + '>'); } }; var arraycl = function (a) { for (var i = 0; i < a.length; i++) { cl(a[i]); } cl('length: ' + a.length); }; var f = function (a, b, c) { cl('===before manipulation', true); cl(a); cl(b); cl(c); cl('arguments array', true); arraycl(arguments); cl('===remove first argument', true); Array.prototype.shift.call(arguments); cl(a); cl(b); cl(c); cl('arguments array', true); arraycl(arguments); }; f("one", "two", "three");
Ignore the first two functions there, they’re just helper functions. The real meat is in the f
function. It uses three parameters, a
, b
, and c
. It first logs the values of those parameters, and then the elements of the arguments array. All well and good, and here is the result of that part of the f
function:
===before manipulation <one> <two> <three> arguments array <one> <two> <three> <length: 3>
This is exactly what I’d expect: the named parameters have the correct values from the call site and the arguments array reflects that. No sweat.
Now the f
function shifts off the first parameter of the arguments array. Here is the result of that:
===remove first argument <two> <three> <three> arguments array <two> <three> <length: 2>
First thing to note is that the arguments array has lost its first element and hence now has length 2. The next really wacky thing is that removing the first element from the array has actually changed the values of the named parameters, a
and b
. a
now has the value of the second parameter passed in, and b
the value of the third. c
on the other hand still has the value of the third parameter and has not been set to undefined
, for example. Hence, sometimes the named parameters reflect the current version of the arguments array, and ... sometimes not.
The lesson to be learned here is that modifying the arguments array in a function will also modify the values of the named parameters as well. This seemed counter-intuitive to me at first glance (hence my first cut at an extend function where I assumed that the named parameters kept their values even though I was manipulating the arguments array).
My recommendation is, if you are going to change the arguments array inside a function, don’t use named parameters as well.
Now playing:
Eurythmics - Sisters Are Doin' It for Themselves
(from Be Yourself Tonight)
1 Response
#1 Nijikokun said...
30-Nov-12 5:54 PMYou should use
Array.prototype.splice
instead:var args = Array.prototype.splice.call(arguments, (0 - n));
0 to n signifies how many you want to remove from the array, those would be your defined arguments in the function parens.Leave a response
Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.
_emphasis_
**strong**
[text](url)
`IEnumerable`
* an item
1. an item
> Now is the time...
Preview of response