Sitemap

Line-by-line walkthrough of underscore extend

5 min readMar 10, 2016

--

Underscorejs is an open source library of useful functions that help make your code more compact. Analogus to the ubiquitous ‘$’ in the jQuery object, the underscore object is usually aliased as ‘_’, and functions are just methods on this object, thus accessed via the dot operator.

In this post, I break down line-by-line a particular underscore function _.extend.

What does _.extend do?

In simple terms, it adds properties from other objects (source) on to a target object. Which properties will be added? Own and inherited properties which are enumerable, including those up the prototype chain (read more about enumerable here). This may or may not be what you want, more on which here. Also, if properties of identical keys are present in both the source and the target, the target will be overwritten by the sources. If there a conflicts between the sources themselves, properties of sources later in the argument list will overwrite the properties earlier in the argument list.

Formally,

_.extend(OBJ, X1, X2, …) will copy over properties from X1, X2, … on to OBJ where OBJ = target object and X1, X2, … = source objects.

For purposes of illustration for the rest of this article, lets set up an example. Say I have a car called TeslaOneAB

var TeslaOneAB = {make: ‘KnockOff’, model: ‘OneAB’, type: ‘electric’, wheels: 4}

And I would like to improve it by adding on some upgrades

var Upgrades = {
autopilot: true,
ludicrous: true,
falconWings: function() {
console.log(‘I believe I can fly!’);
}
}

Calling

_.extend(TeslaOneAB, Upgrades)

will result in TeslaOneAB being modified to:

{
autopilot: true,
falconWings: function () {
console.log ('I believe I can fly!');},
ludicrous: true,
make: 'KnockOff',
model: 'OneAB',
type: 'electric',
wheels: 4
}

So yes, _.extend is mutating.

Let’s get on to the source code then. I’ll be using Underscore.js 1.8.3.

The first occurence of extend is at line 1006:

_.extend = createAssigner(_.allKeys);

The official comment tells us this is used to “Extend a given object with all the properties in passed-in object(s).” OK, so this does exactly what we thought. Let’s dig deeper. We see here that _.extend is the return value from calling the function createAssigner with argument _.allKeys.

First, let’s look at the argument _.allKeys:

_.allKeys = function(obj) {
if (!_.isObject(obj)) return [];
var keys = [];
for (var key in obj) keys.push(key);
// Ahem, IE < 9.
if (hasEnumBug) collectNonEnumProps(obj, keys);
return keys;
};

Official comment: “Retrieve all the property names of an object.”

_.allKeys TL;DR

It takes an argument ‘obj’. If ‘obj’ is not an object, it’ll return an empty array and exit the function.

If ‘obj’ is an object, _.allKeys sets up an empty array ‘keys’, then goes on to populate it with keys from ‘obj’. Suppose I pass it the original TeslaOneAB (before the upgrades) from above. Then the final keys array will look like

[“make”, “model”, “type”, “wheels”]

This is what gets returned from _.allKeys.

for (var key in obj) 

will loop over all the keys in obj, with the current key set to ‘key’. The for … in loop will only look for enumerable properties, and will look up the prototype chain. This explains why _.extend also extends enumerable properties only (including those up the prototype chain).

Next, createAssigner.

var createAssigner = function(keysFunc, undefinedOnly) {
return function(obj) {
var length = arguments.length;
if (length < 2 || obj == null) return obj;
for (var index = 1; index < length; index++) {
var source = arguments[index],
keys = keysFunc(source),
l = keys.length;
console.log(keys);
for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
}
}
return obj;
};
};

Official comment: ‘An internal function for creating assigner functions.’ Not very enlightening.

createAssigner TL;DR

[Aside: To fully understand the code here, we’ll need to understand the ‘arguments’ object. If you don’t know it (or need a refresher), try my post on ‘arguments’.]

createAssigner takes in two parameters: keysFunc and undefinedOnly, and returns a function.

Get c0d3r’s stories in your inbox

Join Medium for free to get updates from this writer.

First, take note of two things:

  1. createAssigner is exactly the function assigned to _.extend in line 1006 of underscore.js. And when we called createAssigner in that assignment, we passed in _.allKeys as the argument (the second argument corresponding to parameter ‘undefinedOnly’ as left out, which is OK. See my post on ‘arguments’ linked above). Here it is again:
_.extend = createAssigner(_.allKeys);

Thus keysFunc === allKeys as far as _.extend is concerned.

2. that the returned (anonymous) function will have access to keysFunc and undefinedOnly due to closure.

Let’s start

return function(obj) {

the anonymous function takes in one parameter, but we can pass more. The pattern of _.extend arguments was

_.extend(target, source1, source2, …)

Thus the target object is mapped to ‘obj’ and the rest of the arguments (the sources) will be referenced via the arguments object from index 1 onwards.

var length = arguments.length;

Here we obtain the length of arguments.

_.extend(TeslaOneAB, Upgrades)

In my car upgrade example,

arguments === [TeslaOneAB, Upgrades]

Thus length === 2.

if (length < 2 || obj == null) return obj;

If length < 2, that could mean that we passed in only the target/destination (TeslaOneAB) without the Upgrades object. Thus the function will return the car unchanged.

for (var index = 1; index < length; index++)

Now we iterate over all the sources only. Hence why the index starts at 1, skipping over the first element. Remember, the target (TeslaOneAB) is the first element (index 0) of arguments. Here I only have one source (Upgrades) so the loop will end after one cycle.

var source = arguments[index],
keys = keysFunc(source),

arguments[1] === Upgrades in our example so source === Upgrades.

Remember _.allKeys? ‘keys’ will now be an array of all keys in Upgrades. Here’s Upgrades again:

var Upgrades = {
autopilot: true,
ludicrous: true,
falconWings: function() {
console.log(‘I believe I can fly!’);
}
}

and so

keys === [‘autopilot’, ‘ludicrous’, ‘falconWings’]

Next we have an inner loop iterating over all these keys, 3 in this case.

for (var i = 0; i < l; i++) {
var key = keys[i];
if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key];
}

‘key’ is each one of those keys. Since we never passed in an argument for undefinedOnly in createAssigner, !undefinedOnly is always true, and the entire conditional short circuits to true. It is here that the source property (source[key]) is copied over on to the target (obj[key]). So for the first key (‘autopilot’), this assignment looks like

TeslaOneAB[‘autopilot’] = Upgrades[‘autopilot’] = true

after which TeslaOneAB will (momentarily) look like

TeslaOneAB = {autopilot: true, make: ‘KnockOff’, model: ‘OneAB’, type: ‘electric’, wheels: 4}

[Aside: in use cases for createAssigner other than _.extend, undefinedOnly may be set to true, so the second part of the ‘OR’ conditional tests if the particular property obj[key] is undefined]

return obj;

Finally, the function returns the complete modified target (obj). And our upgraded TeslaOneAB looks like

{
autopilot: true,
falconWings: function () {
console.log ('I believe I can fly!');},
ludicrous: true,
make: 'KnockOff',
model: 'OneAB',
type: 'electric',
wheels: 4
}

It feels faster already! Hopefully this helped. Leave a comment below if you thought this helped or if I made any errors!

--

--