闭包是什么?如何使用闭包?

闭包是什么?如何使用闭包

闭包和原型应该是js里对初学者来说比较难以掌握的点了,今天我们就来好好聊一下什么是闭包。

先看看mdn的定义:

闭包是函数和声明该函数的词法环境的组合。

什么意思呢?先看一段代码吧

function init() {
    var name = "Mozilla"; // name 是一个被 init 创建的局部变量
    function displayName() { // displayName() 是内部函数,一个闭包
        console.log(name); // 使用了父函数中声明的变量
    }
    displayName();
}
init();

在函数init()里还有一个函数,displayName()函数。这个函数没有局部变量,但是他可以访问到外部函数中的变量,但是,如果

有同名变量name在displayName中被定义,则会使用displayName()中定义的name。

这就是词法作用域的意思。

而什么又是闭包呢?

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

来看代码,

运行这段代码和之前的 init() 示例的效果完全一样。其中的不同 — 也是有意思的地方 — 在于内部函数 displayName() 在执行前,被外部函数返回。
第一眼看上去,也许不能直观的看出这段代码能够正常运行。在一些编程语言中,函数中的局部变量仅在函数的执行期间可用。一旦 makeFunc() 执行完毕,我们会认为 name 变量将不能被访问。然而,因为代码运行得没问题,所以很显然在 JavaScript 中并不是这样的。

这个谜题的答案是,JavaScript中的函数会形成闭包。 闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。在我们的例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用,而 displayName 实例仍可访问其词法作用域中的变量,即可以访问到 name 。由此,当 myFunc 被调用时,name 仍可被访问,其值 Mozilla 就被传递到alert中。

再来看一段代码,

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

在这个示例中,我们定义了 makeAdder(x) 函数,它接受一个参数 x ,并返回一个新的函数。返回的函数接受一个参数 y,并返回x+y的值。

从本质上讲,makeAdder 是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。

add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5的环境中,x 为 5。而在 add10 中,x 则为 10。

闭包可以这样理解,好比一个餐厅,盘子总是有限的,所以服务员会去巡台回收空盘子,但还装着菜的盘子他怎么敢收? 当然,你自己手动倒掉了盘子里面的菜(=null),那盘子就会被收走了,这就是所谓的内存回收机制。

来看看常见的问题。

1.概念很抽象,我们到底什么时候使用闭包呢?

通常你使用只有一个方法的对象的地方,都可以使用闭包

举个例子

function makeSizer(size) {
    return function() {
      document.body.style.fontSize = size + 'px';
    };
  }
  
  var size12 = makeSizer(22);
  var size14 = makeSizer(24);
  var size16 = makeSizer(26);
  document.getElementById('size-22').onclick = size12;
  document.getElementById('size-24').onclick = size14;
  document.getElementById('size-26').onclick = size16;

这就是一个闭包的使用案例,

  22
  24
  26

当点击a标签的时候切换字体的大小。

2.有哪些常见的问题?

在循环中使用闭包,容易出错

来看代码

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

运行这段代码后,您会发现它没有达到想要的效果。无论焦点在哪个input上,显示的都是关于年龄的信息。

原因是赋值给 onfocus 的是闭包。这些闭包是由他们的函数定义和在 setupHelp 作用域中捕获的环境所组成的。这三个闭包在循环中被创建,但他们共享了同一个词法作用域,在这个作用域中存在一个变量item。当onfocus的回调执行时,item.help的值被决定。由于循环在事件触发之前早已执行完毕,变量对象item(被三个闭包所共享)已经指向了helpText的最后一项。

解决这个问题的一种方案是使用更多的闭包或者使用let关键字。

3.闭包有什么缺点?

因为闭包一直存在与内存中,所以会造成性能的降低,需要手动将不需要的闭包解除掉,可以用(=null)来处理

参考文献:

MDN闭包

深入理解javascript原型和闭包(15)——闭包

带你一分钟了解闭包

你可能感兴趣的:(闭包是什么?如何使用闭包?)