AngularJS Dependency Injection from The Ground Up

Posted on by Tero Parviainen (@teropa)

Much has been written about Angular's dependency injection framework. As I've been recently writing the dependency injection chapters of Build Your Own AngularJS, I thought I might as well join the club and provide my own introduction to Angular DI.

This article addresses the dependency injector of Angular 1.x, which is the version everyone is currently using. The 2.0 version will be very different, and is an interesting topic in its own right. I've written a bit about it earlier.

There's also a previous article by Merrick Christensen out there that takes a similar approach. You might want to take a look at that to get an alternate perspective. I guess you could consider this article as an extended version of Merrick's article.

As an Angular developer, I appreciate what the dependency injector does for me. It provides structure to my applications. It gives me the tools to organize my code into coherent components and then combine them in a loosely coupled way.

Dependency injection is a boon to testing too; testability is indeed often given as the primary motivation for doing DI, though for me its more like a positive side effect of the primary benefit, which is the loosely coupled design you get.

As much as I appreciate it, I also recognize that Angular's dependency injector is one of the most criticised parts of the framework. Some people don't like the idea of a JavaScript DI framework in general, while some people have more of an issue with Angular's implementation specifically. I think the critics do have a point - there are definitely some issues with the way DI is implemented in Angular - but it is not nearly as bad as it's sometimes made out to be.

In this article we'll build up a simplified version of the Angular injector. It'll hopefully help you understand what exactly the injector does. The content here is an adaptation of a talk I've given at Front-Trends, ScotlandJS, HelsinkiJS, and JSCamp Romania in the past few weeks.

The Application

We'll be working with a simple example application. We'll transform it bit by bit into something that uses Angular's dependency injection features instead of wiring things up manually. As we use a new DI feature, we'll also implement that feature into our simplified Angular injector.

Our application is in the domain of space exploration. It begins with a simple function that launches a rocket into space. As most browsers cannot actually launch rockets into space, we'll simulate this with a logging statement:

function launchRocket() {
  console.log('Launching rocket at '+new Date);
}

The application also has a constructor function called BigRedButton:

function BigRedButton() {
}

A big red button object has a lid that you can open or close:

BigRedButton.prototype.openLid = function() {
  this._open = true;
};
BigRedButton.prototype.closeLid = function() {
  this._open = false;
};

You can also push a big red button. When you call its push method, it will launch the rocket, but only if the lid is currently open:

BigRedButton.prototype.push = function() {
  if (this._open) {
    launchRocket();
  }
};

Finally, we have our main space exploration program, which creates a big red button, opens its lid, pushes it, and finally closes its lid:

function spaceProgram() {
  var button = new BigRedButton();
  button.openLid();
  button.push();
  button.closeLid();
}

Here's the complete initial version of the app that you can play with:

Manual Dependency Injection

Our example application does what it's supposed to do, but there are several ways in which we might want to improve it.

To begin with, if we look at the BigRedButton implementation, we see that it is currently completely single-purpose: A big red button can only be used for launching a rocket. Big red buttons are quite useful things though, and we can easily think of other use cases for them. It would make sense to make BigRedButton more generic so that it can be used for other purposes too. We can do that if we give the button action to the constructor as an argument instead of hardcoding it into push:

function BigRedButton(buttonAction) {
  this.action = buttonAction;
}
BigRedButton.prototype.openLid = function() {
  this._open = true;
};
BigRedButton.prototype.closeLid = function() {
  this._.open = false;
};
BigRedButton.prototype.push = function() {
  if (this._open) {
    this.action();
  }
};

We can now use big red buttons for anything! The decision about what the button should do shifts to whoever constructs the button, which in this case is spaceProgram:

function spaceProgram() {
  var button = new BigRedButton(launchRocket);
  button.openLid();
  button.push();
  button.closeLid();
}

We can also employ a similar trick to the space program itself. It currently constructs the button it is using, but it could also receive the button to use as an argument:

function spaceProgram(button) {
  button.openLid();
  button.push();
  button.closeLid();
}

Here we have again shifted the responsibility of figuring out which button the space program actually uses to the caller of spaceProgram:

spaceProgram(new BigRedButton(launchRocket));

What we have done here is transformed BigRedButton and spaceProgram to dependency injection style. Instead of looking up functions from the global scope or constructing objects, they receive those things as arguments. The decisions about what exact functions or objects to use are shifted outside of the functions. In the process, we have made things more reusable: A big red button can be used in several contexts, and a space program can use any button.

Here's the complete version of the app that uses manual dependency injection:

In order to do dependency injection, you don't necessarily need a framework. It is just a way to organize code in a way that separates the construction and wiring up of things from the code that actually executes program logic. Indeed, if your application is as small as this one, you don't need or probably even want to use a framework for dependency injection.

When applications grow larger though, it becomes more and more cumbersome to maintain the dependency graph manually. This is where you might want to use a DI framework, such as the one that comes with AngularJS.

Injector 1: Constants And Invocation

When you use Angular's dependency injection features, what you're dealing with is an object called an injector. The injector is where you register application components. You can then ask it to inject those components wherever they might be needed.

The injector is usually baked into the structure of your application in such a way that you actually rarely use it directly. But you can also use it as a standalone object, as we will do here.

We'll create an injector and then register a component to it: The button. We basically tell the injector "here's something called 'button'. It is just a simple value that you can give to anyone who needs it":

var injector = createInjector();
injector.constant('button', new BigRedButton(launchRocket));

Constants are perhaps the simplest kind of application component in Angular. They're just values you give to the injector as-is, like we do here.

Once we have registered a component, we can ask the injector to inject it somewhere. We can, for instance, ask the injector to invoke a function for us and, while doing that, provide it with any dependencies it might need as arguments:

var injector = createInjector();
injector.constant('button', new BigRedButton(launchRocket));
injector.invoke(spaceProgram);

The spaceProgram takes the button as an argument but with dependency injection we no longer need to think about that when we invoke it. We can rely on the injector to figure out the arguments based on what has been configured before.

Let's implement this first version of the injector. What we need is a function called createInjector that returns an injector object:

function createInjector() {

  return {

  };
}

The injector object should have two methods: constant and invoke. We'll implement them as local functions inside the createInjector closure. The constant function takes the name and value of the constant, and the invoke function takes the function to invoke:

function createInjector() {

  function constant(name, value) {

  }

  function invoke(fn) {

  }

  return {
    constant: constant,
    invoke: invoke
  };
}

What we need to do is remember what has been registered with the constant function, and the look those things up in the invoke function. For remembering the registrations we can use a local cache object, into which we'll just put constants as they are registered:

function createInjector() {

  var cache = {};

  function constant(name, value) {
    cache[name] = value;
  }

  function invoke(fn) {

  }

  return {
    constant: constant,
    invoke: invoke
  };
}

The invocation part is more challenging. What we need is a way to figure out, from the given function, what dependencies it has. If you've used Angular, you'll know that there are several ways to annotate the dependencies of a function. Arguably the most interesting approach is to not annotate the dependencies but to look them up on the fly.

For this purpose, we need to employ some dark trickery. Specifically, we'll make use of the fact that every JavaScript function has a method called toString, and that method can be used to obtain the functions's source code:

spaceProgram.toString() // => "function spaceProgram (button) { ... }"

That source code will include the function's argument list. We can use a regular expression to extract that argument list to obtain the names of the function's arguments. The regular expression we need to craft is this:

| ^function | The source code always begins with the text 'function' | | \s* | It is followed by (optionally) some white space | | [^\(]* | Then there is (optionally) the function's name, which we define as a succession of characters other than the opening parenthesis. | | \( | Then comes the argument list, which starts with an opening parenthesis. | | ( | The argument list is captured into a capturing group, because it is the part we're interested in. | | [^\)]* | What we capture is a succession of characters other than the closing parenthesis. | | ) | Then we close the capturing group. | | \) | Finally we match the closing parenthesis. |

What we get using this regular expression is a two-item match result array, where the second item has the contents of the capturing group - the argument list. When we split that at the commas, we get an array of the function's argument names:

spaceProgram
  .toString()
  .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1]
  .split(',') // => ['button']

AngularJS also uses some additional regular expressions to strip any comments that the argument list might include, and to deal with the fact that the function signature could be split on multiple lines. But this is the core of the implementation.

If we pull this code into our injector, we can use it to get the array of argument names of the given function. Additionally, we will also remove all whitespace from the argument names (because there might be some around the commas):

function createInjector() {

  var cache = {};

  function constant(name, value) {
    cache[name] = value;
  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
  }

  return {
    constant: constant,
    invoke: invoke
  };
}

When we have the argument names, we can obtain the concrete arguments by simply looking up each argument name from the cache. Then we can invoke the given function with those arguments using apply:

function createInjector() {

  var cache = {};

  function constant(name, value) {
    cache[name] = value;
  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      return cache[argName];
    });
    return fn.apply(null, args);
  }

  return {
    constant: constant,
    invoke: invoke
  };
}

This completes our first injector iteration. Here's the full code so far:

This magical dependency lookup style is often criticised because it has some severe limitations: It breaks when you minify your code, for example, because the minification process changes the names of function arguments. For that reason, the other dependency annotation methods are often preferred, and Angular 1.3 even introduces a so-called "strict DI" mode, which will throw exceptions when you attempt to do non-annotated dependency injection.

Injector 2: Factories

Our injector can now do constants and function invocation, but there's more it could be doing for us. For intance, we're still having to manually construct the dependency between the big red button and the launchRocket function:

new BigRedButton(launchRocket)

This is exactly the sort of thing the injector can be used for. To explore how, let's first convert the button from a constant to something called a factory:

injector.factory('button', function() {
  return new BigRedButton(launchRocket);
});

This is a factory function for the button. Instead of giving the injector the constant value directly, we give it a function that returns that value instead. We expect the injector to call that function when it needs the dependency.

At first glance it doesn't look like this indirection is buying us much. The reason for doing it becomes apparent when we introduce a dependency for the button itself. The factory gives us a place to inject that dependency:

injector.factory('button', function(buttonAction) {
  return new BigRedButton(buttonAction);
});
injector.constant('buttonAction', launchRocket);

We've now broken the direct dependency between the construction of the button and the launchRocket function. When we construct a button, we don't need to know what the concrete action will be. It is configured elsewhere. With a constant there would be no way to do that, but the factory function gives us an injection point.

To implement factories, we need a new method to our injector, and a new implementation function for it:

function createInjector() {

  var cache = {};

  function constant(name, value) {
    cache[name] = value;
  }

  function factory(name, factoryFn) {

  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      return cache[argName];
    });
    return fn.apply(null, args);
  }

  return {
    constant: constant,
    factory: factory,
    invoke: invoke
  };
}

Let's think about what we should do in the factory function. We have the cache object for caching dependencies, but we can't really put the factory function there. The cache is for concrete dependencies, not the factories that produce them. But we also cannot invoke the factory function at this point. If you look at the way we set up the button factory and the buttonAction constant, you'll see that we we registered the button action after the button. That means that at the time when the button factory is registered, the action doesn't exist yet. This is one of the major benefits Angular's DI framework buys you: You don't have to load and execute your code in the order of its dependency graph. The components can be loaded in any order, and the injector will figure out the order in which to instantiate them.

As framework authors, that means we need to do some extra work to make this happen. What we basically need is two separate caches: One called instanceCache, which is the object we previously called just cache, and one called providerCache, in which we'll put factories:

function createInjector() {

  var instanceCache = {};
  var providerCache = {};

  function constant(name, value) {
    instanceCache[name] = value;
  }

  function factory(name, factoryFn) {
    providerCache[name] = factoryFn;
  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      return instanceCache[argName];
    });
    return fn.apply(null, args);
  }

  return {
    constant: constant,
    factory: factory,
    invoke: invoke
  };
}

Where we will actually invoke factories is during dependency lookup: We will first look at the instance cache to see if it contains what we need. If it doesn't, we'll look at the provider cache. If there's something there, that'll be the factory function:

function createInjector() {

  var instanceCache = {};
  var providerCache = {};

  function constant(name, value) {
    instanceCache[name] = value;
  }

  function factory(name, factoryFn) {
    providerCache[name] = factoryFn;
  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      if (instanceCache.hasOwnProperty(argName)) {
        return instanceCache[argName];
      } else if (providerCache.hasOwnProperty(argName)) {
        var provider = providerCache[argName];
      }
    });
    return fn.apply(null, args);
  }

  return {
    constant: constant,
    factory: factory,
    invoke: invoke
  };
}

The factory function is a function that we should call with dependency injection to produce the dependency instance. Luckily, we have just the function for that already. It's the one we're currently in - invoke:

function createInjector() {

  var instanceCache = {};
  var providerCache = {};

  function constant(name, value) {
    instanceCache[name] = value;
  }

  function factory(name, factoryFn) {
    providerCache[name] = factoryFn;
  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      if (instanceCache.hasOwnProperty(argName)) {
        return instanceCache[argName];
      } else if (providerCache.hasOwnProperty(argName)) {
        var provider = providerCache[argName];
        var instance = invoke(provider);
        return instance;
      }
    });
    return fn.apply(null, args);
  }

  return {
    constant: constant,
    factory: factory,
    invoke: invoke
  };
}

So we have a recursive call to invoke. This is what makes it possible for us to have transitive dependencies: Our dependencies may have factories that have their own dependencies, and so on.

In Angular's dependency injection framework, every object is a singleton. That means that whenever you ask for the same dependency twice, you will receive the same exact object each time. The way this works is that when we construct a dependency using a factory, we immediately put that dependency into the instance cache. The next time someone needs it they will get the cached object and we will never call the factory again:

function createInjector() {

  var instanceCache = {};
  var providerCache = {};

  function constant(name, value) {
    instanceCache[name] = value;
  }

  function factory(name, factoryFn) {
    providerCache[name] = factoryFn;
  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      if (instanceCache.hasOwnProperty(argName)) {
        return instanceCache[argName];
      } else if (providerCache.hasOwnProperty(argName)) {
        var provider = providerCache[argName];
        var instance = invoke(provider);
        instanceCache[argName] = instance;
        return instance;
      }
    });
    return fn.apply(null, args);
  }

  return {
    constant: constant,
    factory: factory,
    invoke: invoke
  };
}

That completes our second injector iteration. Here's the code:

Injector 3: Services

If we look at the way we're constructing our button, we see that there's some unfortunate duplication going on:

injector.factory('button', function(buttonAction) {
  return new BigRedButton(buttonAction);
});

We have a factory function into which we inject buttonAction. But all we do in that factory function is call the BigRedButton constructor, passing in the same arguments. Could we get rid of this seemingly unnecessary wrapper?

We can, in fact, give BigRedButton to the injector directly, if we just register it as a service instead of a factory:

injector.service('button', BigRedButton);

A service takes a constructor function instead of a plain old function. In addition to invoking that function with dependency injection, it also sets up a new object with the function's prototype chain. It is basically a dependency-injected version of JavaScript's new operator.

Again, we need a new method and the corresponding implementation function to the injector:

function createInjector() {

  var instanceCache = {};
  var providerCache = {};

  function constant(name, value) {
    instanceCache[name] = value;
  }

  function factory(name, factoryFn) {
    providerCache[name] = factoryFn;
  }

  function service(name, Constructor) {

  }

  function invoke(fn) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      if (instanceCache.hasOwnProperty(argName)) {
        return instanceCache[argName];
      } else if (providerCache.hasOwnProperty(argName)) {
        var provider = providerCache[argName];
        var instance = invoke(provider);
        instanceCache[argName] = instance;
        return instance;
      }
    });
    return fn.apply(null, args);
  }

  return {
    constant: constant,
    factory: factory,
    service: service,
    invoke: invoke
  };
}

What we need to do in service is two things: 1) Construct a new object with the prototype chain of the given constructor, and 2) Invoke the given constructor with dependency injection.

The first step can be achieved by using the Object.create function introduced in ECMAScript 5. We'll implement service on top of the factory abstraction we already have, by registering an anonymous function as a factory, and doing the service construction there:

function service(name, Constructor) {
  factory(name, function() {
    var instance = Object.create(Constructor.prototype);
  });
}

This creates a new object whose prototype is the prototype of Constructor.

Angular.js doesn't use `Object.create` because it needs to run on browsers that don't support it. Instead, it achieves the same result with a manual contortion that involves an ad-hoc local constructor function.

Secondly, we need to actually invoke the constructor. We already have the invoke function for calling a function with dependency injection. But, crucially, a constructor function should also have its receiver (this) bound to the new object, because that's what the new operator does. For instance, our BigRedButton constructor uses this to set this.action. To achieve this, we'll just extend invoke with an optional second arguments that can be used as the function's receiver. By just passing it on to apply we can make sure that it is used as the receiver (when provided):

function createInjector() {

  var instanceCache = {};
  var providerCache = {};

  function constant(name, value) {
    instanceCache[name] = value;
  }

  function factory(name, factoryFn) {
    providerCache[name] = factoryFn;
  }

  function service(name, Constructor) {
    factory(name, function() {
      var instance = Object.create(Constructor.prototype);
      invoke(Constructor, instance);
      return instance;
    });
  }

  function invoke(fn, self) {
    var argsString = fn.toString()
      .match(/^function\s*[^\(]*\(([^\)]*)\)/)[1];
    var argNames = argsString.split(',').map(function(argName) {
      return argName.replace(/\s*/g, '');
    });
    var args = argNames.map(function(argName) {
      if (instanceCache.hasOwnProperty(argName)) {
        return instanceCache[argName];
      } else if (providerCache.hasOwnProperty(argName)) {
        var provider = providerCache[argName];
        var instance = invoke(provider);
        instanceCache[argName] = instance;
        return instance;
      }
    });
    return fn.apply(self, args);
  }

  return {
    constant: constant,
    factory: factory,
    service: service,
    invoke: invoke
  };
}

And there we have an Angular-style dependency injector that can inject constants, factories, and services into functions!

Here's the final source code:

Completing The Picture

The injector built in this article is a simplification of what AngularJS actually ships with. Most crucially, the concept of providers was omitted. Providers are a low-level building block that both factories and service are actually implemented on. Another omitted concept is modules, which are used to group dependencies together. In real Angular applications, you do not register your components directly on the injector, but on modules.

The book will contain the full implementation of Angular's dependency injection, spread over three chapters.

Know Your AngularJS Inside Out

Build Your Own AngularJS

Build Your Own AngularJS helps you understand everything there is to understand about AngularJS (1.x). By creating your very own implementation of AngularJS piece by piece, you gain deep insight into what makes this framework tick. Say goodbye to fixing problems by trial and error and hello to reasoning your way through them

eBook Available Now

comments powered by Disqus