闭包

什么是闭包

  • 闭包三步:

    1.外层函数嵌套内层函数

    2.内层函数使用外层函数的局部变量

    3.把内层函数作为外层函数的返回值

    经过这样的三步就可以形成一个闭包

  • 闭包就是函数不在定义的词法作用域内被调用,但是仍然可以访问词法作用域中定义的变量。

  • 闭包是在一个函数 A 内部有一个函数 B,通过函数 B 记录访问函数 A 内的变量。

    因为作用域的关系,函数A外部无法直接访问内部数据,而通过闭包这种方法可以让我们可以间接访问函数内部的私有变量,利用这一特性我们可以用来封装私有变量,实现数据寄存等

  • 闭包就是指有权访问另一个函数作用域中的变量的函数

闭包出现的原因

由于js引擎的垃圾回收机制, 在执行我们的代码的时候,js维护着一个调用栈。在函数执行完成的时候,由垃圾回收机制去处理这个调用栈(调用栈内包含函数的词法作用域), 要销毁调用栈的时候,发现还存在引用,那么垃圾回机制就不处理它。这就导致这个函数的词法作用域保留了下来,也让该函数具有了数据持久性。有利也有弊,基于垃圾回收机制,如果你的闭包内存有大量数据, 那么它是不会被清除的, 这就需要我们自己手动的去处理它。

闭包的作用是什么

  1. 封装私有变量,属性私有化
function create_counter(initial) {
        var x = initial || 0;
        return {
            inc: function () {
                x += 1;
                return x;
            }
        }
   }
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3

var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在返回的对象中,实现了一个闭包,该闭包携带了局部变量x,并且,从外部代码根本无法访问到变量x。换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来。

  1. 模拟模块,实现模块化
function module() {
    let inner = 1;
    let increaseInner = function() {
        inner++;
    }
    let decreaseInner = function() {
        inner--;
    }
    let getInner = function() {
        return inner;
    }
    return {
        increaseInner,
        decreaseInner,
        getInner
    }
}
let api = module();
console.log(api.getInner());
api.increaseInner();
console.log(api.getInner());
api.decreaseInner();
console.log(api.getInner());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 用闭包模仿块级作用域
  • 例1
// IIFE的目的是为了隔离作用域,防止污染全局命名空间。
for (var i = 0; i < 5; i++) {
    (function(i) {
        //这里是块级作用域
        setTimeout(function() {
            console.log(i)
        }, 1000);
    })(i);
}
1
2
3
4
5
6
7
8
9
  • 例2
for(var i=0; i<10; i++){
     console.log(i)
}
alert(i)  // 变量提升,弹出10

//为了避免i的提升可以这样做
(function () {
    for(var i=0; i<10; i++){
         console.log(i)
    }
)()
alert(i)   // undefined   因为i随着闭包函数的退出,执行环境销毁,变量回收
1
2
3
4
5
6
7
8
9
10
11
12

4.函数绑定

function bind(fn, context){
    return function(){
        return fn.apply(context, arguments); 
 };
}
1
2
3
4
5

5.函数柯里化

调用另一个函数并为它传入要柯里化的函数和必要参数。使用一个闭包返回一个函数

function curry(fn){
        var args = Array.prototype.slice.call(arguments, 1);
        return function(){
            var innerArgs = Array.prototype.slice.call(arguments);
            var finalArgs = args.concat(innerArgs);
            return fn.apply(null, finalArgs);
    };
 }
1
2
3
4
5
6
7
8

6.使用闭包实现递归

闭包的缺点

  • 闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
  • 如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。 闭包会导致原有作用域链不释放,造成内存泄露

闭包的注意事项

  • 通常,函数的作用域及其所有变量都会在函数执行结束后被销毁,被垃圾回收机制回收。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。
function makeAdd(x) {
    return function(y) {
      return x + y;
    };
  }

  var add1 = makeAdder(5);
  var add2 = makeAdder(10);

  console.log(add1(4));  // 9
  console.log(add2(3)); // 13

  // 释放对闭包的引用
  add5 = null;
  add10 = null;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 闭包中的this
 var name = "The Window";
 var obj = {
     name: "My Object",
     getName: function(){
         return function(){
             return this.name;
      };
     }
   };
   console.log(obj.getName()());  // The Window
   //将这一步分解:console.log( function(){return this.name;};() ); 
1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
 var obj = {
   name: "My Object",
   getName: function(){
       var that = this;
       return function(){
          return that.name;
      };
    }
 };
 console.log(obj.getName()());  // My Object
1
2
3
4
5
6
7
8
9
10
11
  • 由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,在绝对必要的情况下再考虑使用闭包。虽然像 V8 等优化后的 JavaScript 引擎会尝试回收被闭包占用的内存,但是闭包还是要慎重使用。

性能考量

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是,每个对象的创建)。 这是个很典型的例子,学到构造函数和原型的时候总会有提及。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}
1
2
3
4
5
6
7
8
9
10
11

在上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};
1
2
3
4
5
6
7
8
9
10
11
12

但我们不建议重新定义原型。可改成如下例子:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};
1
2
3
4
5
6
7
8
9
10

闭包面试题

  • 题1
function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?
//问:三行a,b,c的输出分别是什么?

//答案:
//a: undefined,0,0,0
//b: undefined,0,1,2
//c: undefined,0,1,1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • 题2

循环中使用闭包解决 var 定义函数的问题

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

首先因为 setTimeout 是个异步函数,所有会先把循环全部执行完毕,这时候 i就是 6 了,所以会输出一堆 6。

解决办法两种,第一种使用闭包

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

第二种就是使用 setTimeout 的第三个参数

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

第三种就是使用 let 定义 i 了

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
1
2
3
4
5

因为对于 let 来说,他会创建一个块级作用域,相当于

{ // 形成块级作用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18