Architecting Your Own Angular Framework
Internal Dependency Injection
The Injector's Magic
In AngularJS, when you ask for a service like $scope or $http in your controller, it feels like magic. You just list it as a parameter, and it appears. This isn't magic, though. It's the work of a core component called the $injector.
The
$injectoris a service locator. It maintains a registry of all the services your application knows about and is responsible for creating and handing them out whenever they're requested.
Think of it like a central supply closet. When a component needs a tool (a service), it doesn't build it from scratch. It just tells the $injector the name of the tool, and the injector finds it, builds it if it's the first time, and delivers it. This process is what we call Dependency Injection (DI).
How It Knows What to Inject
For the $injector to work, it must know which services to provide to a function. It does this by reading the function's parameter names. AngularJS offers three ways to "annotate" or declare these dependencies so the injector can find them.
First is the implicit annotation. This is the simplest style. You just list the services you need as arguments to your function.
myApp.controller('MyController', function($scope, greeterService) {
// ...
});
Here, the injector inspects the function's definition, converts it to a string, and uses regular expressions to parse out the parameter names: "$scope" and "greeterService". It then fetches these services from its registry. Simple, but it has a major weakness: it breaks when you minify your code, because minifiers often rename variables to save space (e.g., $scope becomes a).
To solve this, we have inline array annotation. This is the most common and recommended style.
myApp.controller('MyController', ['$scope', 'greeterService', function($scope, greeterService) {
// ...
}]);
In this style, you provide an array. The array contains a list of strings (the service names) followed by the function itself. The injector uses the strings, not the function's parameter names, to look up dependencies. Since strings don't get minified, this method is safe for production code.
Finally, there's the $inject property annotation.
function MyController($scope, greeterService) {
// ...
}
MyController.$inject = ['$scope', 'greeterService'];
myApp.controller('MyController', MyController);
This approach is useful when you define your controller function separately. You add an $inject property to your function, and that property is an array of service name strings. Like the inline array, this is minification-safe. It's functionally the same as the inline method but can sometimes lead to cleaner code in larger applications.
The Service Recipe Book
You can register dependencies in AngularJS in several ways, primarily using provider, factory, and service. While they seem different, they are all built on the same foundation. They are essentially different "recipes" for telling the injector how to create your object.
The Provider is the most powerful and verbose recipe. It's an object with a $get method. This method is the factory function that the $injector calls to create your service instance. The key benefit of a provider is that it can be configured during the application's configuration phase, before any services are created.
A Factory is a simpler way to create a service. It's just a function that returns an object. Behind the scenes, AngularJS creates a provider for you and sets the factory function as the provider's $get method. You lose the configuration ability, but gain simplicity.
myApp.factory('myFactory', function() {
return { greet: () => 'Hello!' };
});
A Service is even simpler. It takes a constructor function. When the service is needed, the injector calls this function with the new keyword to create an instance. It's useful when your service is best represented by a custom type or class.
myApp.service('myService', function() {
this.greet = function() { return 'Hello!'; };
});
Ultimately, factory and service are just convenient shortcuts over the more fundamental provider recipe.
Managing Singletons and Dependencies
A critical job of the $injector is managing the lifecycle of services. In AngularJS, all services are singletons. This means the injector creates exactly one instance of each service the first time it's requested.
When you ask for a service, the injector first checks an internal cache. If an instance of that service already exists, it returns the cached instance immediately. If not, it uses the appropriate recipe (the provider's $get method) to create the instance, stores it in the cache for future use, and then returns it.
This ensures that all parts of your application share the exact same instance of a service, making it a perfect place to share data and state across different controllers and directives.
By injecting dependencies, Angular encourages a modular and loosely coupled architecture, where components can be easily interchanged or tested in isolation.
What about when services depend on other services? The injector handles this recursively. If Service A needs Service B, the injector first resolves Service B (creating it if necessary) before it finishes creating Service A.
This can lead to a problem: circular dependencies. For example, if Service A needs Service B, and Service B needs Service A. When the injector tries to create A, it must first create B. But to create B, it needs A, which is already in the process of being created. AngularJS is smart enough to detect this loop and will throw an error to prevent an infinite recursion, saving you from a stack overflow.
What is the primary role of the $injector service in an AngularJS application?
Which method of dependency annotation in AngularJS is NOT safe for code minification?
Understanding how the injector works under the hood demystifies AngularJS and is a key step toward mastering the framework and building more complex, maintainable applications.