Javascript闭包理解

2018-11-24  本文已影响0人  troublemak_19be
何为闭包?

闭包(Closure)是一个封闭的作用域,它可以访问外部作用域的变量。

说起来比较抽象,实际上闭包就是一个函数,函数内部可以访问外部的变量,比如下面这个例子:

function sayHello() {
  const greet = 'hello world';
  function helloFunc() {
    console.log(greet);
  }
  helloFunc();
}
sayHello(); // hello world

helloFunc函数内虽然没有定义greet变量,但是它的外层函数sayHello函数里定义了greet变量,所以最后成功输出hello world,这个例子里面helloFunc就是一个闭包,寻找变量的过程是按照作用域链来寻找的。

闭包的作用

我们知道javascript外部作用域无法访问内部作用的值。

function func() {
  var name = 'jack';
}
console.log(name);  // undefined

但是函数内部却可以访问外部变量。

var name = 'jack';
function func() {
  console.log(name);
}
func(); // jack

所以函数内部可以修改外部状态,我们可以让函数拥有状态,比如迭代器生成器

var add = (function() {
  var counter = 0;
  return function() {
    counter++;
    return counter;
  };
})();
add(); // 1
add(); // 2
add(); // 3

这个例子我们使用了一个立即执行函数表达式(IIFE)创建了一个匿名函数也就是一个闭包,函数返回的函数会引用这个匿名函数的局部变量counter,所以我们每次调用add函数输出的值都不一样,add函数因为闭包有了状态。

这里使用立即执行函数主要是为了直接返回最终函数,也可以返回个普通函数像下面这样:

var genAddFunc = function() {
  var counter = 0;
  return function() {
    counter++;
    return counter;
  }
}
var add = genAddFunc();
add(); // 1
add(); // 2
add(); // 3
闭包的应用

比如我们需要对多个li绑定点击事件如下:

<html>
  <body>
    <ul id="itemList">
      <li>item 1</li>
      <li>item 2</li>
      <li>item 3</li>
    </ul>
  </body>
</html>
const itemList = document.getElementById('itemList');
const items = itemList.getElementsByTagName('li');
for (var i = 0; i < items.length; i++) {
  items[i].onclick = function() {
    console.log(`item ${i} clicked`);
  }
}
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked

我们发现虽然li的点击事件都已经创建了闭包记住了i的值,但是都是输出3而不是0,1,2这是为什么呢?
原因是for循环中大括号包含该的部分并不是一个封闭的作用域,通过下面代码可以验证:

for (var i = 0; i < 3; i++) {}
console.log(i); // 3
console.log(window.i); // 3

当for循环结束后变量i并没有被回收,实际上我们是创建了一个全局变量i,3个item的点击事件函数绑定的是全局变量i的值,所以最后都输出的是item 3 clicked,通过下面代码可以验证:

for (var i = 0; i < items.length; i++) {
  items[i].onclick = function() {
    console.log(`item ${i} clicked`);
  }
}
i = 99;
items[0].click(); // item 99 clicked
items[1].click(); // item 99 clicked
items[2].click(); // item 99 clicked

当我们修改了i的值发现3个点击事件的结果也都变成了item 99 clicked,说明它们输出的i绑定的都是最新i的值而不是当时循环时i的值

于是我们尝试一下使用一个局部变量看看能否保存i的值,这样我们就可以输出正确的i的值:

for (var i = 0; i < items.length; i++) {
  items[i].onclick = function() {
    var j = i;
    console.log(`item ${j} clicked`);
  }
}
i = 99;
items[0].click(); // item 99 clicked
items[1].click(); // item 99 clicked
items[2].click(); // item 99 clicked

使用局部变量j保存i还是没有成功,我们修改了i的值后j的值也发生了变化,说明js在执行过程中j还是引用i的值。

解决方法1:函数传参

for (var i = 0; i < items.length; i++) {
  items[i].onclick = function() {
    console.log(`item ${j} clicked`);
  } (i)
}
i = 99;
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked

通过传递参数我们得到了正确的结果,修改了i的值也没有影响点击事件的结果。

解决方法2: 使用闭包

for (var i = 0; i < items.length; i++) {
  items[i].onclick = (function() {
    var j = i;
    return function() {
      console.log(`item ${j} clicked`);
    }
  })();
}
i = 99;
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked

在闭包中我们用j来保存i的值,这样我们就能记住当时循环时i的值了,和之前的单独一个function区别是闭包可产生独立的作用域这样j就是i的值的拷贝而不是i的引用,我是这么理解的。

解决方法2: 使用let
使用let应该是最优雅的解决办法了,let是ES6的关键字它能够产生封闭的作用域:

for (let i = 0; i < items.length; i++) {
  items[i].onclick = function() {
    console.log(`item ${i} clicked`);
  }
}
items[0].click(); // item 3 clicked
items[1].click(); // item 3 clicked
items[2].click(); // item 3 clicked
上一篇下一篇

猜你喜欢

热点阅读