开发者

Javascript Closures - What are the negatives?

Question: There seem to be many benefits to Closures, but what are the negatives (memory leakage? obfuscation problems? bandwidth increasage?)? Additionally, is my understanding of Closures correct? Finally, once closures are created, can they be destroyed?

I've been reading a little bit about Javascript Closures. I hope someone a little more knowledgeable will guide my assertions, correcting me where wrong.

Benefits of Closures:

  1. Encapsulate the variables to a local scope, by using an internal function. The anonymity of the function is insignificant.

What I've found helpful is to do some basic testing, regarding local/global scope:

<script type="text/javascript">

   var global_text  = "";
   var global_count = 0;
   var global_num1  = 10;
   var global_num2  = 2开发者_如何学JAVA0;
   var global_num3  = 30;

   function outerFunc() {

      var local_count = local_count || 0;

      alert("global_num1: " + global_num1);    // global_num1: undefined
      var global_num1  = global_num1 || 0;
      alert("global_num1: " + global_num1);    // global_num1: 0

      alert("global_num2: " + global_num2);    // global_num2: 20
      global_num2  = global_num2 || 0;         // (notice) no definition with 'var'
      alert("global_num2: " + global_num2);    // global_num2: 20
      global_num2  = 0;

      alert("local_count: " + local_count);    // local_count: 0

      function output() {
         global_num3++;

         alert("local_count:  " + local_count  + "\n" +
               "global_count: " + global_count + "\n" +
               "global_text:  " + global_text
              );

         local_count++; 
      }

      local_count++;
      global_count++;

      return output;  
   }  

   var myFunc = outerFunc();

   myFunc();
      /* Outputs:
       **********************
       * local_count:  1
       * global_count: 1
       * global_text: 
       **********************/

   global_text = "global";
   myFunc();
      /* Outputs:
       **********************
       * local_count:  2
       * global_count: 1
       * global_text:  global
       **********************/

   var local_count = 100;
   myFunc();
      /* Outputs:
       **********************
       * local_count:  3
       * global_count: 1
       * global_text:  global
       **********************/


   alert("global_num1: " + global_num1);      // global_num1: 10
   alert("global_num2: " + global_num2);      // global_num2: 0
   alert("global_num3: " + global_num3);      // global_num3: 33

</script>

Interesting things I took out of it:

  1. The alerts in outerFunc are only called once, which is when the outerFunc call is assigned to myFunc (myFunc = outerFunc()). This assignment seems to keep the outerFunc open, in what I would like to call a persistent state.

  2. Everytime myFunc is called, the return is executed. In this case, the return is the internal function.

  3. Something really interesting is the localization that occurs when defining local variables. Notice the difference in the first alert between global_num1 and global_num2, even before the variable is trying to be created, global_num1 is considered undefined because the 'var' was used to signify a local variable to that function. -- This has been talked about before, in the order of operation for the Javascript engine, it's just nice to see this put to work.

  4. Globals can still be used, but local variables will override them. Notice before the third myFunc call, a global variable called local_count is created, but it as no effect on the internal function, which has a variable that goes by the same name. Conversely, each function call has the ability to modify global variables, as noticed by global_var3.

Post Thoughts: Even though the code is straightforward, it is cluttered by alerts for you guys, so you can plug and play.

I know there are other examples of closures, many of which use anonymous functions in combination with looping structures, but I think this is good for a 101-starter course to see the effects.

The one thing I'm concerned with is the negative impact closures will have on memory. Because it keeps the function environment open, it is also keeping those variables stored in memory, which may/may not have performance implications, especially regarding DOM traversals and garbage collection. I'm also not sure what kind of role this will play in terms of memory leakage and I'm not sure if the closure can be removed from memory by a simple "delete myFunc;."

Hope this helps someone,

vol7ron


Closures bring a lot of benefits...but also a number of gotchas. The same thing that makes them powerful also makes them quite capable of making a mess if you're not careful.

Besides the issue with circular references (which isn't really as much of a problem anymore, since IE6 is hardly used at all outside of China), there's at least one other huge potential negative: They can complicate scope. When used well, they improve modularity and compatibility by allowing functions to share data without exposing it...but when used badly, it can become difficult if not impossible to trace exactly where a variable is set or changed.

JavaScript without closures has three* scopes for variables: block-level, function-level, and global. There is no object-level scope. Without closures, you know a variable is either declared in the current function, or in the global object (because that's where global variables live).

With closures, you no longer have that assurance. Each nested function introduces another level of scope, and any closures created within that function see (mostly) the same variables as the containing function does. The big problem is that each function can define its own variables at will that hide the outer ones.

Using closures properly requires that you (a) be aware of how closures and var affect scope, and (b) keep track of which scope your variables are in. Otherwise, variables can be accidentally shared (or pseudo-variables lost!), and all sorts of wackiness can ensue.


Consider this example:

function ScopeIssues(count) {
    var funcs = [];
    for (var i = 0; i < count; ++i) {
        funcs[i] = function() { console.log(i); }
    }
    return funcs;
}

Short, straightforward...and almost certainly broken. Watch:

x = ScopeIssues(10);

x[0]();   // outputs 10
x[1]();   // does too
x[2]();   // same here
x[3]();   // guess

Every function in the array outputs count. What's going on here? You're seeing the effects of combining closures with a misunderstanding of closed-over variables and scope.

When the closures are created, they're not using the value of i at the time they were created to determine what to output. They're using the variable i, which is shared with the outer function and is still changing. When they output it, they're outputting the value as of the time it is called. That will be equal to count, the value that caused the loop to stop.

To fix this before let existed, you'd need another closure.

function Corrected(count) {
    var funcs = [];
    for (var i = 0; i < count; ++i) {
        (function(which) {
            funcs[i] = function() { console.log(which); };
        })(i);
    }
    return funcs;
}

x = Corrected(10);

x[0]();  // outputs 0
x[1]();  // outputs 1
x[2]();  // outputs 2
x[3]();  // outputs 3

As of ES7, you can use let instead of var, and each iteration of the loop will basically get its own version of i.

function WorksToo(count) {
    var funcs = [];
    for (let i = 0; i < count; ++i) {
        funcs[i] = function() { console.log(i); }
    }
    return funcs;
}

x = WorksToo(10);

x[0]();  // outputs 0
x[1]();  // outputs 1
x[2]();  // outputs 2
x[3]();  // outputs 3

But that comes with complications of its own -- variables with the same name and purpose, in the same block of code, are now effectively disconnected. So you don't want to just always use let either. The only real fix is to all-around be much more aware of scope.


Another example:

value = 'global variable';

function A() {
    var value = 'local variable';
    this.value = 'instance variable';
    (function() { console.log(this.value); })();
}

a = new A();  // outputs 'global variable'

this and arguments are different; unlike nearly everything else, they are not shared across closure boundaries?. Every function call redefines them -- and unless you call the function like

  • obj.func(...),
  • func.call(obj, ...),
  • func.apply(obj, [...]), or
  • var obj_func = func.bind(obj); obj_func(...)

to specify a this, then you'll get the default value for this: the global object.^

The most common idiom to get around the this issue is to declare a variable and set its value to this. The most common names i've seen are that and self.

function A() {
    var self = this;
    this.value = 'some value';
    (function() { console.log(self.value); })();
}

But that makes self a real variable, with all the potential oddness that entails. Fortunately, it's rare to want to change the value of self without redefining the variable...but within a nested function, redefining self of course redefines it for all the functions nested within it as well. And you can't do something like

function X() {
    var self = this;
    var Y = function() {
        var outer = self;
        var self = this;
    };
}

because of hoisting. JavaScript effectively moves all the variable declarations to the top of the function. That makes the above code equivalent to

function X() {
    var self, Y;
    self = this;
    Y = function() {
        var outer, self;
        outer = self;
        self = this;
    };
}

self is already a local variable before outer = self runs, so outer gets the local value -- which at this point, is undefined. You've just lost your reference to the outer self.


* As of ES7. Previously, there were only two, and variables were even easier to track down. :P

? Functions declared using lambda syntax (new to ES7) don't redefine this and arguments. Which potentially complicates the matter even more.

^ Newer interpreters support a so-called "strict mode": an opt-in feature that aims to make certain iffy code patterns either fail entirely or cause less damage. In strict mode, this defaults to undefined rather than the global object. But it's still some whole other value than you usually intended to mess with.


You may get a raft of good answers. One certain negative is the Internet Explorer circular reference memory leak. Basically, "circular" references to DOM objects are not recognized as collectible by JScript. It's easy to create what IE considers a circular reference using closures. Several examples are provided in the second link.

  • Microsoft KB Article re IE6 Memory Leak
  • Mitigation Efforts in Later Versions

In IE6, the only way to reclaim the memory is to terminate the whole process. In IE7 they improved it so that when you navigate away from the page in question (or close it), the memory is reclaimed. In IE8, DOM objects are better understood by JScript and are collected as you'd expect they should be.

The suggested workaround for IE6 (besides terminating the process!) is not to use closures.


Closures may cause memory leaks, however Mozilla has made attempts to optimize their garbage collection engine to prevent this.

I'm unsure how Chrome handles closures. I think they're on par with Mozilla, but I don't want to say for sure. IE8 is definitely improved over the earlier versions of IE - it's almost a whole new browser, there are still some nuances.

You should also benchmark the code to see if there's any improvement in speed.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜