JavaScript总结分享之闭包

来自:互联网
时间:2022-11-07
阅读:
免费资源网 - https://freexyz.cn/

闭包是什么?

对于一个知识点来说,我一直认为不论是从什么方面入手,都需要彻底弄懂三个问题,才算真正了解这个知识点,然后具体再去实践中练习,才能称得上掌握。这三个问题就是:

  • 是什么?
  • 为什么要设计?
  • 能用在哪?

首先先回答闭包是什么这个问题。应该大多数人也看过很多与之相关的文章,很多人也给出了自己的解释,所以我也先给出自己理解的解释,那就是: 先有两个前置的概念:

  • 闭包是在词法分析时就已经被确定的, 所以它会与词法作用域有关。

  • 闭包存在的前置条件是需要支持函数作为一等公民的编程语言,所以它会与函数有关。

所以最终的结论就是:

  • 闭包首先是一个结构体,这个结构体的组成部分为 一个函数 + 该函数所处的词法作用域
  • 也就是闭包是由一个函数并且该函数能够记住声明自己的词法作用域所产生的结构体
  • 在内存中理解就是, 当一个函数被调用时,它所产生的函数执行上下文里的作用域链保存有其父词法作用域,所以父变量对象由于存在被引用而不会销毁,驻留在内存中供其使用。这样的情况就称为闭包。

上述的解释对于已经了解过闭包的人应该是一目了然的,但其实如果对于一个完全不知晓闭包的人来说,很可能是完全看不懂的。更甚至很多人其实仅仅只是记住了这种定义,而不是真的理解了这内涵。

所以我想用一个不一定精准的类比去帮助理解什么是闭包这东西,想象你写了一篇文章放在自己的服务器上,并且引用了自己的3篇文章作为参考。那么此时 一篇文章 + 服务器的环境 就类似于闭包。

在发表到网络上后被转载到其他的平台上,而其他平台上的读者点开你的文章阅读后想继续看你所引用的那些文章,就被准确无误的跳转到了你服务器里的文章中去。

在这个例子中,这篇文章保存了写这篇文章的服务器环境里的引用。 因而不论是在哪里读到文章,文章里所记得的参考文章引用指向永远是服务器里的地址。 这种情况叫做使用了闭包的特性。

可能例子还是不太好理解,毕竟它也没有很准确,闭包这概念就是有点抽象,没有想到现实中有什么具体的例子可以用来比喻。 如果有人想出更好的类比可以指出,我加以注释和描述。

为什么要设计出闭包?

对于为什么设计这点,仅以我自己粗浅的理解就是由于JavaScript是异步单线程的语言。对于异步编程来说,最大的问题就是当你编写了函数,而等到它真正调用的时机可能是之后任意的时间节点。

这对于内存管理来说是一个很大的问题,正常同步执行的代码,函数声明时和被调用时所需要的数据都还存留在内存中,可以无障碍的获取。而异步的代码,往往声明该函数的上下文可能已经销毁,等到在调用它时,如果内存中已经把它所需要的一些外部数据给清理了,这就是个很大的问题。

所以JavaScript解决的方案就是让函数能够记得自己之前所能获取数据的范围,统统都保存在内存里,只要该函数没有被内存回收,它自身以及所能记住的范围都不会被销毁

这里的所能记住的范围就是指词法作用域,就是由于它是静态的,所以才需要记住。

这又是JavaScript设计作用域为静态的导致的。 如果是动态作用域,函数被调用时只需要被调用时的那个环境,就不需要存在记住自身作用域的事了。

所以总结一下就是:

  • 闭包是为了解决词法作用域引发的问题内存不好管理异步编程里数据获取所产生的。

经典题

原本我的想法是从最底层来解释闭包的情况,后来在查阅各种文章时发现, 有一篇文章已经写的很好了。 那就是JavaScript闭包的底层运行机制, 我觉得可以先看看这篇的讲解然后在看我之后所写的内容。

由于有非常多的文章都从下面这个非常经典的面试题入手,但似乎都没有人真正从最底层讲解过,所以我就打算将整个过程梳理一遍,来明白这其中的差异性。

for (var i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}

基本所有有基础的人一眼就能看出输出的是三个3。

然后让修改成按顺序输出,通常只需要修改var成let:

for (let i = 0; i < 3; i++) {  setTimeout(function cb() {    console.log(i);
  }, 1000);
}

这样就成了输出为0,1,2.并且是同时间输出,而不是每间隔一秒输出一次。

那么问题来了,为什么?

这里可以先不看下面,先写写自己的解释,看看是否跟我写的一样。

1. 先来探讨变量i是var的情况。

当代码开始执行时,此时执行上下文栈和内存里的情况是这样: 其中全局对象里的变量i和全局执行上下文里变量环境里的变量i是同一个变量。

JavaScript总结分享之闭包

然后开始进行循环, 当 i = 0时,第一个定时器被丢入宏任务队列,关于宏任务相关的内容属于事件循环范畴,暂时只需要理解setTimeout会被丢入队列里,等之后执行。 此时在堆内存中会创建它的回调函数cb,并且函数创建时会创建[[scope]],在实际ECMA的规则中,[[scope]]会指向该函数的父作用域,也就是当前的全局对象(作用域是概念上的东西,实际体现在内存中就是保存数据的一种结构,可能是对象也可能是其他)。 但是在V8引擎的实现中,其实并不会指向全局对象,而是去分析该函数使用了父作用域中的哪些变量,将这些变量存储到Closure中,然后由scope指向。每个函数都有且只有一个Closure对象。

JavaScript总结分享之闭包


这里先插入一下关于Closure对象可以在Chrome中哪看到的情况: 可以看到,创建bar函数时,它只有引用了父作用域的name变量,所以在闭包对象中只会存储变量name, 而不会存在变量age。JavaScript总结分享之闭包


同理之后的 i = 1, 和 i = 2 都是一样的,最终结果会变成:

JavaScript总结分享之闭包

最终因为 i++导致 i = 3, 循环结束,全局代码执行完毕。此时的结果为:

然后开始进入定时器回调函数执行的过程, 开始执行第一个定时器里的回调函数,压入了执行上下文栈中,执行输出i, 但是在词法环境和变量环境中找不到这个变量i,所以去自身[[scope]]向上寻找,在Closure对象中找到了 i 等于3,输出结果3。

JavaScript总结分享之闭包

同理对于后面两个定时器也是一样的流程,并且实际上定时器开启的时间都是在循环中就立即执行的,导致实际上三个函数的定时1秒时间是一致的,最终输出的结果是几乎同时输出3个3。而不是每间隔1秒后输出3, 当然这是定时器相关的知识了。

2. 然后探讨通过var修改成let之后实际上变了什么

同样是刚创建时,所展示的情况为:

JavaScript总结分享之闭包

之后进入循环体,当i = 0时:

JavaScript总结分享之闭包

之后进入 i = 1时的情况:

JavaScript总结分享之闭包

最后进入到 i = 2的情况,与 i = 1基本类似:

JavaScript总结分享之闭包

最终 i++,变成i值为3,循环结束。开启定时器工作:

JavaScript总结分享之闭包

当执行第一个定时器的回调函数时,创建了函数执行上下文,此时执行输出语句i时,会先从自己的词法环境里寻找变量i的值,也就是在 record环境记录里搜索,但是不存在。因而通过自己外部环境引用outer找到原先创建的块级作用域里 i = 0的情况, 输出了i值为0的结果。

对于之后的定时器也都是一样的情况,原先的块级作用域由于被回调函数所引用到了,因而就产生了闭包的情况,不会在内存中被销毁,而是一直留着。

等到它们都执行完毕后,最终内存回收会将之全部都销毁。

其实以上画的图并不是很严谨,与实际在内存中的表现肯定是有差异的,但是对于理解闭包在内存里的情况还是不影响的。

闭包能用在哪?

首先需要先明确一点,那就是在JavaScript中,只要创建了函数,其实就产生了闭包。这是广义上的闭包,因为在全局作用域下声明的函数,也会记着全局作用域。而不是只有在函数内部声明的函数才叫做闭包。

通常意义上所讨论的闭包,是使用了闭包的特性

1. 函数作为返回值

let a = 1function outer() {  let a = 2

  function inside() {
    a += 1
    console.log(a)
  }  return inside
}const foo = outer()foo()

此处outer函数调用完时,返回了一个inside函数,在执行上下文栈中表示的既是outer函数执行上下文被销毁,但有一个返回值是一个函数。 该函数在内存中创建了一个空间,其[[scope]]指向着outer函数的作用域。因而outer函数的环境不会被销毁。

当foo函数开始调用时,调用的就是inside函数,所以它在执行时,先询问自身作用域是否存在变量a, 不存在则向上询问自己的父作用域outer,存在变量a且值为2,最终输出3。

2. 函数作为参数

var name = 'xavier'function foo() {  var name = 'parker'
  function bar() {    console.log(name)
  } console.log(name)  return bar
}function baz(fn) {  var name = 'coin'
  fn()
}baz(foo())baz(foo)

对于第一个baz函数调用,输出的结果为两个'parker'。 对于第二个baz函数的调用,输出为一个'parker'。

具体的理解其实跟上面一致,只要函数被其他函数调用,都会存在闭包。

3. 私有属性

闭包可以实现对于一些属性的隐藏,外部只能获取到属性,但是无法对属性进行操作。

function foo(name) {  let _name = name  return {    get: function() {      return _name
    }
  }
}let obj = foo('xavier')
obj.get()

4. 高阶函数,科里化,节流防抖等

对于一些需要存在状态的函数,都是使用到了闭包的特性。

// 节流function throttle(fn, timeout) {  let timer = null
  return function (...arg) {    if(timer) return
    timer = setTimeout(() => {
    fn.apply(this, arg)
    timer = null
    }, timeout)
  }
}// 防抖function debounce(fn, timeout){  let timer = null
  return function(...arg){    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, arg)
    }, timeout)
  }
}

5. 模块化

在没有模块之前,对于不同地方声明的变量,可能会产生冲突。而闭包能够创造出一个封闭的私有空间,为模块化提供了可能性。 可以使用IIFE+闭包实现模块。

var moduleA = (function (global, doc) {  var methodA = function() {};  var dataA = {};  return {    methodA: methodA,    dataA: dataA
  };
})(this, document);
免费资源网 - https://freexyz.cn/
返回顶部
顶部