专栏名称: SegmentFault思否
SegmentFault (www.sf.gg)开发者社区,是中国年轻开发者喜爱的极客社区,我们为开发者提供最纯粹的技术交流和分享平台。
目录
相关文章推荐
程序员的那些事  ·  北京大学出的第二份 DeepSeek ... ·  23 小时前  
码农翻身  ·  漫画 | 为什么大家都愿意进入外企? ·  2 天前  
程序猿  ·  41岁DeepMind天才科学家去世:长期受 ... ·  2 天前  
程序员的那些事  ·  成人玩偶 + ... ·  6 天前  
51好读  ›  专栏  ›  SegmentFault思否

JS 中的闭包

SegmentFault思否  · 公众号  · 程序员  · 2017-10-21 08:00

正文

文章同步到 github:https://github.com/sunzhaoye/blog/issues/12

js的闭包概念几乎是任何面试官都会问的问题,最近把闭包这块的概念梳理了一下,记录成以下文章。

什么是闭包

我先列出一些官方及经典书籍等书中给出的概念,这些概念虽然表达的不一样,但是都在对闭包做了最正确的定义和翻译,也帮助大家更好的理解闭包,这阅读起来可能比较模糊,大家往后看,本文通过对多个经典书籍中的例子讲解,相信会让大家能很好的理解js中的闭包。文章开始,我会先铺垫一下闭包的概念和为什么要引入闭包的概念,然后结合例子来说明讲解,并讲解如何使用闭包。

百度百科中的定义

闭包包含自由(未绑定到特定对象)变量;这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域) -- 百度百科

《javaScript权威指南》中的概念

函数对象可以通过作用域链互相关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学中成为 闭包

《javaScript高级教程》中概念

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

MDN中的概念

个人总结的闭包概念

  1. 闭包就是子函数可以有权访问父函数的变量、父函数的父函数的变量、一直到全局变量。归根结底,就是利用js得词法(静态)作用域,即作用域链在函数创建的时候就确定了。

  2. 子函数如果不被销毁,整条作用域链上的变量仍然保存在内存中。

为什么引入闭包的概念

我引入 《深入理解JavaScript系列:闭包(Closures)》 文章中的例子来说明,也可以直接去看那篇文章,我结合其他书籍反复读了很多遍此文章才理解清楚。如下:

  1. function testFn() {

  2.  var localVar = 10;  // 自由变量

  3.  function innerFn(innerParam) {

  4.    alert(innerParam + localVar);

  5.  }

  6.  return innerFn;

  7. }

  8. var someFn = testFn();

  9. someFn(20); // 30

一般来说,在函数执行完毕之后,局部变量对象即被销毁,所以innerFn是不可能以返回值形式返回的,innerFn函数作为局部变量应该被销毁才对。

这是当函数以返回值时的问题,另外再看一个当函数以参数形式使用时的问题,还是直接引用《深入理解JavaScript系列》中的例子,也方便大家有兴趣可以直接去阅读那篇文章

  1. var z = 10;

  2. function foo() {

  3.  alert(z);

  4. }

  5. foo(); // 10 – 使用静态和动态作用域的时候

  6. (function () {

  7.  var z = 20;

  8.  foo(); // 10 – 使用静态作用域, 20 – 使用动态作用域

  9. })();

  10. // 将foo作为参数的时候是一样的

  11. (function (funArg) {

  12.   var z = 30;

  13.  funArg(); // 10 – 静态作用域, 30 – 动态作用域

  14. })(foo);

当函数foo在不同的函数中调用,z该取哪个上下文中的值呢,这就又是一个问题,所以就引入了闭包的概念,也可以理解为定义了一种规则。

理解闭包

函数以返回值返回

看一个《javsScript权威指南》中的一个例子,我稍微做一下修改如下:

  1. var scope = 'global scope';

  2. function checkScope() {

  3.     var scope = 'local scope';

  4.    return function() {

  5.        console.log(scope);

  6.    }

  7. }

  8. var result = checkScope();

  9. result();   // local scope checkScope变量对象中的scope,非全局变量scope

分析

即使匿名函数是在checkScope函数外调用,也没有使用全局变量scope,即是利用了js的静态作用域,当匿名函数初始化时,就创建了自己的作用域链(作用域链的概念这里不做解释,可以参考我的另一篇文章 js中的执行栈、执行环境(上下文)、作用域、作用域链、活动对象、变量对象的概念总结 ,其实当把作用域链理解好了之后,闭包也就理解了), 此匿名函数的作用域链包括checkScope的活动对象和全局变量对象, 当checkScope函数执行完毕后,checkScope的活动对象并不会被销毁,因为匿名函数的作用域链还在引用checkScope的活动对象,也就是checkScope的执行环境被销毁,但是其活动对象没有被销毁,留存在堆内存中,直到匿名函数销毁后,checkScope的活动对象才会销毁,解除对匿名函数的引用将其设置为null即可,垃圾回收将会将其清除,另外当外部对checkScope的自由变量存在引用的时候,其活动对象也不会被销毁

  1. result = null; //解除对匿名函数的引用

注释

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

补充

引用一下《javsScript权威指南》中的补充,帮助大家进一步理解

函数以参数形式使用

当函数以参数形式使用时一般用于利用闭包特性解决实际问题,比如浏览器中内置的方法等,下面我直接引用《深入理解JavaScript系列:闭包(Closures)》中关于闭包实战部分的例子如下:

sort

在sort的内置方法中,函数以参数形式传入回调函数,在sort的实现中调用:

  1. [1, 2, 3].sort(function (a, b) {

  2.  ... // 排序条件

  3. });

map

和sort的实现一样

  1. [1, 2, 3].map(function (element) {

  2.  return element * 2;

  3. }); // [2, 4, 6]

利用自执行匿名函数创建的闭包

  1. var foo = {};

  2. // 初始化

  3. ( function (object) {

  4.  var x = 10;

  5.  object.getX = function() {

  6.    return x;

  7.  };

  8. })(foo);

  9. alert(foo.getX()); // 获得闭包 "x" – 10

利用闭包实现私有属性的存取

先来看一个例子

  1. var fnBox = [];

  2. function foo() {

  3.    for(var i = 0; i < 3; i++) {

  4.        fnBox[i] = function() {

  5.             return i;

  6.        }

  7.    }

  8. }

  9. foo();

  10. var fn0 = fnBox[0];

  11. var fn1 = fnBox[1];

  12. var fn2 = fnBox[2];

  13. console.log(fn0()); //  3

  14. console.log(fn1()); //  3

  15. console.log(fn2()); //  3

用伪代码来说明如下:

  1. fn0.[[scope]]= {

  2.    // 其他变量对象,一直到全局变量对象

  3.    父级上下文中的活动对象AO: [data: [...], i: 3]

  4. }

  5. fn1.[[scope]]= {

  6.    // 其他变量对象,一直到全局变量对象

  7.    父级上下文中的活动对象AO: [data: [...], i: 3]

  8. }

  9. fn2.[[scope]]= {

  10.    // 其他变量对象,一直到全局变量对象

  11.    父级上下文中的活动对象AO: [data: [...], i: 3],

  12. }

分析

这是因为fn0、fn1、fn2的作用域链共享foo的活动对象, 而且js没有块级作用域,当函数foo执行完毕的时候foo的活动对象中i的值已经变为3,当fn0、fn1、fn2执行的时候,其最顶层的作用域没有i变量,就沿着作用域链查找foo的活动对象中的i,所以i都为3。

但是这种结果往往不是我们想要的,这时就可以利用认为创建一个闭包来解决这个问题,如下:

  1. var fnBox = [];

  2. function foo() {

  3.    for(var i = 0; i <







请到「今天看啥」查看全文