Deep understanding of dependency injection in JavaScript

Source: Internet
Author: User
Tags abstract reflection regular expression split

  I like to quote this phrase, "program is management of complexity." The computer world is a huge and abstract complex. We simply pack something and release the new tools. Now consider the language you are using to include some built-in abstract functions or low-level operators. This is the same in JavaScript.

  Sooner or later you will need to use the abstract results of other developers--that is, you rely on someone else's code. I prefer modules that depend on freedom (no dependencies), but that's hard to achieve. Even the beautiful black box components that you create are more or less dependent on something. This is where reliance is injected. It is absolutely necessary to manage the ability to rely effectively. This article summarizes my exploration of the problem and some of the solutions.   Vision we have two modules. The first is to be responsible for AJAX request Services (service) and the second is routing (router).     Code as follows: var service = function () {    return {name: ' service '}; var router = function () {    Return {name: ' Router '}; We have another function that needs to use these two modules.   Code as follows: var dosomething = function (other) {    var s = Service ()     var r = router ();}; To make it look more interesting, this function takes a parameter. Of course, we can use the above code completely, but this is obviously not flexible. If we want to use Servicexml or Servicejson, or if we need some test modules. We can't just edit the function body to solve the problem. First, we can solve the dependencies by using the parameters of the function. Namely:   code as follows: var dosomething = function (service, router, other) {    var s = service ();     var r = Router (); }; We implement the functionality we want by passing additional parameters, however, this can create new problems. Imagine if our dosomething method is scattered in our code. If we need to change the dependency condition, we cannot change all the files that call the function.   We need a tool to help us with these. This is the problem that dependency injection tries to solve. Let's write down some of the goals that our dependency injection solution should achieve:   We should be able to register dependencies 1. Injection should accept a function and return a function we need 2. We can't write too many things--we need to streamline the beautifulSyntax 3. Injection should maintain the scope of the passed function 4. The passed function should be able to accept custom parameters, not just dependency description 5. The perfect list, let's implement it. Three, Requirejs/amd method you may have heard of Requirejs, it is a good choice to solve the dependency injection.     Code as follows: define ([' Service ', ' router '], function (service, router) {          //. .. }); The idea is to first describe the dependencies you need, and then write your function. The order of the parameters here is important. As mentioned above, let's write a module called Injector that accepts the same syntax. The code is as follows: var dosomething = injector.resolve ([' Service ', ' router '], function (service, router, other) {    expect (SE Rvice (). Name). to.be (' Service ');     Expect (router (). Name). to.be (' router ');     Expect (other) to.be (' other '); }); DoSomething ("other"); Before I go any further, I should explain the dosomething function body content, I use Expect.js (assertion side of the library) only to ensure that I write the code behavior and I expected the same, a little bit of TDD (test-driven development) method. Here we start our injector module, which is a great single case pattern, so it works well in different parts of our program.   Code as follows: var injector = {    dependencies: {},     register:function (key, value) {        This.dependencies[key] = value;    },     resolve:function (Deps, func, scope) {    &nbsp   This is a very simple object, with two methods, a property to store. All we have to do is check the Deps array and search for the answer in the dependencies variable. All that remains is to invoke the. Apply method and pass the parameters of the previous Func method.   Code as follows: Resolve:function (Deps, func, scope) {    var args = [];     for (var i=0; i<deps.lengt h, D=deps[i]; i++) {        if (This.dependencies[d]) {            Args.push (THIS.DEP ENDENCIES[D]);        } else {            throw new Error (' Can ' t resolve ' + D); &nbs P      }    }     return function () {        Func.apply (Scope | }, Args.concat (Array.prototype.slice.call (arguments, 0));    }        } scope is optional, Array.prototype.slice.call (arguments, 0) is required, Used to convert the arguments variable to a true array. So far it's good. We passed the test. The problem with this implementation is that we need to write the required parts two times, and we can't confuse their order. Additional custom parameters are always located after the dependency.   Iv. Reflection methods according to Wikipedia definition reflection is the ability of a program to check and modify the structure and behavior of an object at runtime. Simply put, in the context of JavaScript, this refers specifically to the source code of the object or function being read and parsed. Let's finish the Dosom we mentioned at the beginning of the articleething function. If you output dosomething.tostring () in the console. You will get the following string: The code is as follows: "Function (service, router, other) {    var s = service ();     var r = router (); "The string returned through this method gives us the ability to traverse the parameters and, more importantly, to get their names." This is actually a angular way to implement its dependency injection. I stole a little lazy, directly intercept the regular expression that gets the parameter in the angular code. The code is as follows:/^functions*[^ (]* (s* ([^)]*))/M We can modify the resolve code like this:   code as follows: Resolve:function () {    var func, DEP s, scope, args = [], self = this;     func = arguments[0];     Deps = func.tostring (). Match (/^functions*[^ (]* (s* ([^)]*))/m) [1].replace (//g, ']. Split (', ');     scope = Arguments[1] | | {};     return function () {        var a = Array.prototype.slice.call (arguments, 0);   &NB Sp     for (var i=0 i<deps.length; i++) {            var d = deps[i];   &NBS P         Args.push (Self.dependencies[d] && d!= '? Self.dependencies[d]: A.shift ());        } &nbsP       func.apply (Scope | | {}, args);    }         The result of our execution of regular expressions is as follows: The code is as follows: ["Function (service, router, other)", "service, router , other "] it looks like we just need a second item. Once we know the space and split the string, we get the deps array. There is only one big change: The code is as follows: var a = Array.prototype.slice.call (arguments, 0); ... Args.push (Self.dependencies[d] && d!= '? Self.dependencies[d]: A.shift ()); We loop through the dependencies array and try to get it from the arguments object if the missing item is found. Thankfully, when the array is empty, the shift method simply returns undefined instead of throwing an error (which benefits from the web's idea). The new injector can be used like this: The following code: var dosomething = injector.resolve (function (service, other, router) {    expect (Ser Vice (). Name). to.be (' Service ');     Expect (router (). Name). to.be (' router ');     Expect (other) to.be (' other '); }); DoSomething ("other"); There is no need to rewrite dependencies and their order can be disrupted. It still works, and we have successfully copied the magic of angular.   However, this practice is not perfect, which is the reflection type injection a very big problem. Compression destroys our logic because it changes the name of the parameter and we will not be able to maintain the correct mapping relationship. For example, dosometing () may look like this after compression: The     code is as follows: Var dosomething=function (e,t,n) {var r=e (); var i=t ()} The solution proposed by the angular team looks like this: &NBsp var dosomething = injector.resolve ([' Service ', ' router ', function (service, router) { }]);   This looks much like the solution we started with. I couldn't find a better solution, so I decided to combine the two approaches. The following is the final version of injector.   Code as follows: var injector = {    dependencies: {},     register:function (key, value) {        This.dependencies[key] = value;    },     resolve:function () {        var func, deps, scope, args = [], self = This         if (typeof arguments[0] = = ' string ') {            func = Argume NTS[1];             Deps = arguments[0].replace (//g, '). Split (', ');             scope = Arguments[2] | | {};        } else {            func = arguments[0];       & nbsp     Deps = func.tostring (). Match (/^functions*[^ (]* (s* ([^)]*))/m) [1].replace (//g, ']. Split (', ');             scope = Arguments[1] | | {};        }         return function () {            VA R A = Array.prototype.slice.call (arguments, 0);             for (var i=0 i<deps.length; i++) {                var d = deps[i];                 Args.push (Self.dependencies[d] && d!= '? Self.dependenc IES[D]: A.shift ());                         func.apply (Scope | | {}, args);        }            } resolve visitors accept two or three parameters, if there are two parameters it actually is the same as the one written earlier in the article. However, if you have three parameters, it converts the first argument and fills the Deps array, and here's a test example: The code is as follows: var dosomething = injector.resolve (' Router,,service ', function (A, B, c {    expect (a (). Name). to.be (' Router ');     expect (b) to.be (' other ');     expect (c (). NAMe). to.be (' Service '); }); DoSomething ("other"); You may notice that there are two commas behind the first argument--note that this is not a clerical error. A null value actually represents the "other" argument (placeholder). This shows how we control the order of parameters.   v. Direct injection scope Sometimes I use the third injection variable, which involves the scope of the action function (in other words, the This object). So, many times you don't need to use this variable. The code is as follows: var injector = {    dependencies: {},     register:function (key, value) {        This.dependencies[key] = value;    },     resolve:function (Deps, func, scope) {      var args = [];   &N Bsp     scope = Scope | | {};         for (Var i=0 i<deps.length, D=deps[i]; i++) {            if ( This.dependencies[d]) {                SCOPE[D] = this.dependencies[d];   &N Bsp        } else {                throw new Error (' Can ' t resolv E ' + D);                     {      &NBSP return function () {            func.apply (Scope | | {}, Array.prototype.slice.call (arguments, 0));        }            } All we do is add dependencies to the scope. The advantage of this is that developers do not have to write dependency parameters; they are already part of a function scope. The code is as follows: var dosomething = injector.resolve ([' Service ', ' router '], function (other) {    expect (This.service (). Name). to.be (' Service ');     Expect (This.router (). Name). to.be (' router ');     Expect (other) to.be (' other '); }); DoSomething ("other"); Vi. concluding remarks In fact, most of us have used dependency injection, but we are not aware of it. Even if you don't know the term, you might use it in your code for millions of years. I hope this article will deepen your understanding of it.  

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.