文章目录
  1. 1. 为什么要用闭包?
    1. 1.1. 作用域
      1. 1.1.1. 函数访问全局变量
      2. 1.1.2. 外部访问函数局部变量
  2. 2. 闭包是什么?
    1. 2.1. 改变私有变量?
    2. 2.2. 闭包的优点
    3. 2.3. 闭包的用途
  3. 3. 使用闭包的注意点
    1. 3.1. 循环中的闭包
      1. 3.1.1. 例一:
      2. 3.1.2. 避免引用错误
      3. 3.1.3. 例二:
      4. 3.1.4. 避免引用错误

本文本着3个目的进行探究
1.闭包是什么
2.为什么要用闭包
3.闭包的应用场景
我的傻瓜式学习笔记,给自己备忘,也希望能帮助到一些人,如有错误欢迎指正。O(∩_∩)O~


为什么要用闭包?

作用域

函数是JavaScript中唯一拥有自身作用域的结构。(JavaScript不支持块级作用域)
意味着定义在函数中的参数和变量在函数外部是不可见的,而在一个函数内部任何位置定义的变量,在该函数内部任何地方都可见。

一个值得注意的例子:

1
2
3
4
5
6
7
8
9
10
11
12
var foo = function(){
var a = 3,b = 5;
var bar = function(){
var b = 7,c = 11;
//此时,a为3,b为7,c为11。
a += b+c;
//此时,a为21,b为7,c为11。
};
//此时,a为3,b为5,而c还没有定义
bar();
//此时,a为21,b为5
};

函数访问全局变量

JavaScript中函数内部可以直接访问全局变量。

1
2
3
4
5
6
7
var n=999;
function f1(){
console.log(n);
}
//test
f1(); // 999

函数外部无法读取函数内的局部变量。

1
2
3
4
5
6
7
8
var m=123;
function f1(){
var n=999;
}
//test
console.log(m); // output:123
console.log(n); // undefined
1
2
3
4
5
6
7
8
9
var m=123;
function f1(){
var n=999;
}
f1();
//test
console.log(m); // output:123
console.log(n); // undefined

注意:此时的n,如果声明时没有用var命令,在执行了声明它的函数后,事实上是产生了一个全局变量!

1
2
3
4
5
6
7
8
var m=123;
function f1(){
n=999;
}
//test
console.log(m); // output:123
console.log(n); // undefined
1
2
3
4
5
6
7
8
9
var m=123;
function f1(){
n=999;
}
f1();
//test
console.log(m); // output:123
console.log(n); // output:999
1
2
3
4
5
6
7
8
var n=123;
function f1(){
n=999;
}
f1();
//test
console.log(n); // output:999

外部访问函数局部变量

正常情况下JavaScript无法获得函数内部的局部变量的值,可是我们有时需要获取,我们应该怎样做?

1
2
3
4
5
6
7
8
9
function counter(){
var count=5;
function get(){
console.log(count);
}
}
//test
counter(); //木有反应

以上代码木有反应,因为我们执行了counter函数却没执行counter函数里面的个体函数哟..
我们在counter里面调用一下get();这样执行counter函数即执行了get函数。

1
2
3
4
5
6
7
8
9
10
function counter(){
var count=5;
function get(){
console.log(count);
}
get();
}
//test
counter(); //output:5

这样我们便访问到了counter的局部变量,哈哈,大功告成!…了吗?
我们这样做和一下代码有什么区别?!

1
2
3
4
5
6
7
function counter(){
var count=5;
console.log(count);
}
//test
counter(); //output:5

这样做行不通,我们另寻他径。

1
2
3
4
5
6
7
8
9
10
11
function counter(){
var count=5;
function get(){
return count;
}
return get;
}
//test
var foo=counter();
console.log(foo()); //output:5

我们返回了一个函数名get,当执行了counter函数后返回了counter函数内的get函数的引用并赋值给foo,在外面执行了foo()相当于调用了get(),我们获取到了局部变量count!!!~

以下例子更加灵活

1
2
3
4
5
6
7
8
9
10
11
12
function counter(){
var count=5;
return {
get:function(){
return count;
}
}
}
//test
var foo=counter();
console.log(foo.get()); //output:5

这次我们return了一个对象字面量,在执行counter函数后返回了一个对象字面量并赋给一个foo变量,里面有个名为get的key,其value为一个匿名函数(是否是匿名并没有关系)。我们执行字面量中的get方法,得到了count。

我们希望能在一个函数中访问局部变量,完成更多的事,不仅仅是这一个用途而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function counter(){
var count=5;
return {
get:function(){
return count;
},
increment:function(){
count++;
}
}
}
//test
var foo=counter();
console.log(foo.get()); //output:5
foo.increment();
console.log(foo.get()); //output:6

以上例子也可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function counter(){
var count=5;
function get(){
return count;
}
function increment(){
count++;
}
return {
get:get,
increment:increment
}
}
//test
var foo=counter();
console.log(foo.get()); //output:5
foo.increment();
console.log(foo.get()); //output:6

再改进一下,使他更有意义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function counter(start){
var count=start;
function get(){
return count;
}
function increment(){
count++;
}
return {
get:get,
increment:increment
}
}
//test
var foo=counter(4);
console.log(foo.get()); //output:4
foo.increment();
console.log(foo.get()); //output:5

至此,我们已经应用到了闭包。
这个例子中的counter函数返回两个闭包,函数get和函数increment。这两个函数都维持着对外部作用域 counter 的引用,因此总可以访问此作用域内定义的变量 count。

以上例子,你还可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var foo = (function (){
var count=0;
function setCount(start){
count=start;
}
function getCount(){
return count;
}
function increment(){
count++;
}
return {
setCount:setCount,
getCount:getCount,
increment:increment
}
}());
//test
console.log(foo.getCount()); //output:0
foo.increment();
console.log(foo.getCount()); //output:1
//重新设置count
foo.setCount(4);
console.log(foo.getCount()); //output:4
foo.increment();
console.log(foo.getCount()); //output:5

此处我们直接把调用函数后的结果赋值给foo,注意最后一行的(),成为立即执行函数。
之后我们可以直接使用foo.xx的方式来访问相关函数。
这也是JavaScript模块化编程的一种写法。

关于立即执行函数,以下两种写法均可,看个人习惯:

1
2
3
4
//写法一
(function () { /* code */ } ());
//写法二
(function () { /* code */ })();

(function(){}())是使用了强制运算符执行函数调用运算,(function(){})()是通过函数调用运算符操作函数引用。两者功能上是一致的,只是运算过程不同。

闭包是什么?

闭包是 JavaScript一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域中的变量。 因为函数是JavaScript中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。

闭包在书中是这样被描述的

闭包是函数和执行它的作用域组成的综合体 — 《JavaScript权威指南》

闭包是一种在函数内访问和操作外部变量的方式;所有的函数都是闭包;函数可以访问它被创建时的上下文环境,成为闭包 — 《JavaScript语言精粹》

内部函数比它的外部函数具有更长的生命周期

更简单的定义 — 闭包就是能够读取其他函数内部变量的函数。

由于在JavaScript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

改变私有变量?

我们能否在闭包外面改变私有变量呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function counter(start){
var count=start;
function get(){
return count;
}
function increment(){
count++;
}
return {
get:get,
increment:increment
}
}
var foo=counter(4);
foo.hack = function(){
count = 999;
};
//test
console.log(foo.get()); //output:4

这种做法,答案是NO。foo.hack没有定义在counter作用域内,以上代码不会改变定义在counter作用域中的count值。

闭包的优点

  • 实现了私有变量
  • 保护命名空间
  • 避免污染全局变量

闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

循环中的闭包

一个常见的错误,出现在循环中使用闭包,假设我们需要在每次循环中访问循环序号

例一:

1
2
3
4
5
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}

执行结果为:
closure

上面的代码不会输出数字 0 到 9,而是会输出数字 10 十次。

当 console.log 被调用的时候,匿名函数保持对外部变量 i 的引用,此时 for循环已经结束, i 的值被修改成了 10.

为了得到想要的结果,需要在每次循环中创建变量 i 的拷贝。

避免引用错误

为了正确的获得循环序号,最好使用匿名包裹器。

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
(function(e) {
setTimeout(function() {
console.log(e);
}, 1000);
})(i);
}

执行结果为:
closure

外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。

当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。

有另一个方法完成同样的工作;那就是从匿名包装器中返回一个函数。这和上面的代码效果一样。

1
2
3
4
5
6
7
for(var i = 0; i < 10; i++) {
setTimeout((function(e) {
return function() {
console.log(e);
}
})(i), 1000)
}

执行结果为:
closure

例二:

糟糕的例子:

1
2
3
4
5
6
7
8
var foo=function(nodes){
var i;
for(i=0; i<nodes.length; i+=1){
node[i].onclick=function(e){
console.log(i);
};
}
};

错误来源于在循环中创建函数,其本意是想传递给每个事件处理器一个唯一值(i)。但它未能达到目的,因为事件处理器函数绑定了变量i本身,而不是
函数在构造时的变量i的值。

避免引用错误

例一的解法:

1
2
3
4
5
6
7
8
9
10
var foo=function(nodes){
var i;
for(i=0; i<nodes.length; i+=1){
(function(n){
node[n].onclick=function(e){
console.log(n);
};
})(i);
}
};

或者你可以这样做,创建一个辅助函数:

1
2
3
4
5
6
7
8
9
10
11
var foo=function(nodes){
var helper=function(i){
return function(e){
console.log(i);
};
};
var i;
for(i=0; i<nodes.length; i+=1){
node[i].onclick=helper(i);
}
};

避免在循环中创建函数,在循环中创建函数,会导致错误,只会带来无谓的计算及引起混淆。
在循环之外创建一个辅助函数,让这个辅助函数再返回一个绑定了当前i值的函数,这样就不会导致混淆了。


参考文献:
[1] 学习Javascript闭包(Closure)
[2] JavaScript - The Good Parts
[3] JavaScript 秘密花园 - 闭包和引用
[4] 潜力无限的编程语言JavaScript

文章目录
  1. 1. 为什么要用闭包?
    1. 1.1. 作用域
      1. 1.1.1. 函数访问全局变量
      2. 1.1.2. 外部访问函数局部变量
  2. 2. 闭包是什么?
    1. 2.1. 改变私有变量?
    2. 2.2. 闭包的优点
    3. 2.3. 闭包的用途
  3. 3. 使用闭包的注意点
    1. 3.1. 循环中的闭包
      1. 3.1.1. 例一:
      2. 3.1.2. 避免引用错误
      3. 3.1.3. 例二:
      4. 3.1.4. 避免引用错误