资讯专栏INFORMATION COLUMN

作用域闭包,你真的懂了吗?

yangrd / 2826人阅读

摘要:曾几何时,闭包好像就是一个十分难以捉摸透的东西,看了很多文章,对闭包都各有说法,以致让我十分晕,什么内部变量外部变量的,而且大多数都只描述一个过程,没有给闭包的定义,最后,举几个例子,告诉你这就是闭包。

曾几何时,闭包好像就是一个十分难以捉摸透的东西,看了很多文章,对闭包都各有说法,以致让我十分晕,什么内部变量、外部变量的,而且大多数都只描述一个过程,没有给闭包的定义,最后,举几个例子,告诉你这就是闭包。于是乎,我从来都是带有疑问使用闭包的:闭包是指作用域,还是指函数,还是指访问外部变量的过程?还有外部变量有多外面?直到最近的学习,我才渐渐清晰......

前提

首先,我们需要知道 JavaScript 里面的函数会创建内部词法作用域,是的,JavaScript 是词法作用域,也就是说作用域与作用域的层级关系在你书写的时候就已经确定了,而不是调用的时候,调用的时候确定的称为动态作用域,由于不是本篇文章的重点,就不再详细解释了,举两个例子自己领悟:

var name = "fruit"
function apple () {
  console.log(name)
}
function orange () {
  var name = "orange"
  apple()
}

orange()  // fruit

由于 JavaScript 是词法作用域,所以 apple 函数的局部作用域的上层作用域是全局作用域,从书写的位置就看出来了。假设 JavaScript 是动态作用域,就要看函数的调用顺序了,由于 apple 是在 orange 中调用的,所以 apple 的上层作用域是 orange 的局部作用域,那样的话会输出 orange!

这样的话,就制定了一套作用域访问的规则,这也是会有闭包的原因之一!

什么是闭包?

函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。

当我看到这句话的时候,泪流满面,国外的作者就是一语道破真相。简单的说,闭包就是引用,对谁的引用呢,对作用域的引用,只不过这种引用是有条件的——首先要记住作用域,然后再访问作用域!

什么叫记住作用域?

首先,我们都知道,在 JavaScript 里面,如果函数被调用过了,并且以后不会被用到,那么垃圾回收机制就会销毁由函数创建的作用域,我们还知道,对象(函数也是对象)的传递属于传引用,也就是类似于C语言里面的指针,并不会把真正的值拷贝给变量,而是把对象所在的位置传递给变量,所以,当函数被传引用到一个还未销毁的作用域的某个变量,由于变量存在,所以函数得存在,又因为函数的存在依赖于函数所在的词法作用域,所以函数所在的词法作用域也得存在,这样一来,就记住了该词法作用域。也就解释了该节的标题!下面举个例子说明一下:

// 没有闭包现象的时候
function apple () {
  var count = 0

  function output () {
    console.log(count)
  }

  fruit(output)
}

function fruit (arg) {
  console.log("fruit")
}

apple() // fruit

当我们在调用 apple 的时候,本来 apple 在执行完毕之后 apple 的局部作用域就应该被销毁,但是由于 fruit(output)output 传引用给了 arg,所以在 fruit 执行的这段时间内,arg 肯定是存在的,被引用的函数 output 也得存在,而 output 依赖的 apple 函数产生的局部作用域也得存在,这就是所谓的“记住”,把作用域给记住了!

但是,上面的例子是闭包现象吗?不是,因为函数 output 内部并没有访问记住的词法作用域的变量!在执行 fruit(output) 的过程中,只发生了 arg = output 的传引用赋值,而这个过程,只是把二者关联起来了,并没有去取 arg 引用的对象的值,所以实际上也并没有访问 output 所在的词法作用域!

记住并访问

上面的代码,稍微修改一下就会产生闭包现象了:

function apple () {
  var count = 0

  function output () {
    console.log(count)
  }

  fruit(output)
}

function fruit (arg) {
  arg()
}

apple() // 0

现在,调用 fruit 时,apple 的局部作用域处于“记住”的状态,这时候, fruit 内部调用了 arg(),因为传引用,实际上访问并执行了 apple 局部作用域的 output,不仅仅是这样,output 内部还访问了 count 变量,这两次对 apple 局部作用域的引用都是闭包!

所以,之所以说所有回调函数的调用都会产生闭包现象,也是因为这个回调函数被传给了另外一个函数的参数,所以在另外一个函数的作用域消失之前,回调函数所在的词法作用域都被记住了,由于回调函数一定会被执行,所以回调函数所在的词法作用域至少被访问了一次,也就是至少访问回调函数本身,而这个对作用域的引用就是闭包。

闭包的作用

根据上面的讲解,估计你自己都能倒背如流了:

记住了函数所在的词法作用域,使其不被销毁;

能够访问函数所在词法作用域的变量;

创建模块(设计私有变量、公有函数等等)

还有很多,就不一一说了,下面就是利用闭包来解决一个常见的问题:

for (var i = 0; i < 5; i++) {
  // 为了方便说明,给函数起名叫 apple
  setTimeout(function apple () {
    console.log(i) // 5 个 5
  }, 0)
}

首先读者们先思考一个问题,这会产生闭包吗?

其实,上面也也会产生闭包,只不过 apple 记住并访问的是全局作用域,为什么呢?因为回调函数被当做 setTimeout 的参数传引用过去了,假设 setTimeout 实现如下

var setTimeout = function (callback, delay) {
  // 延迟
  callback()
}

看到没,因为 setTimeout 属于异步函数,所以会等到 JS 执行完毕之后再调用 callback,所以这段时间 callback 一直存在,所以函数 apple 也一直存在,所以全局作用域并不会等 JavaScript 执行完毕后就销毁(函数 apple 属于全局作用域的),这时候循环早结束了,所以 i 也变成了 5,于是乎,这个时候 apple 对全局作用域的引用称为闭包!

上面也说了回调函数调用都会产生闭包,这里就当举例说明一下!

那么怎么解决以上问题呢,很简单,让回调函数记住不同的作用域就行了!

for (var i = 0; i < 5; i++) {
  // 为了方便说明,给函数起名叫 apple
  (function baz (i) {
    setTimeout(function apple () {
      console.log(i)
    }, 0)
  })(i)  // 0 1 2 3 4
}

上面用立即执行函数解决了问题,因为函数有局部作用域,所以调用 5 次函数会产生 5 个局部作用域,每个作用域的 i 由各次循环的 i 传递赋值,而每个作用域内都存在 apple ,都记住了各自的作用域,也就取到了不同的 i

不过通常来说,闭包都是按以下方式产生:

function apple () {
  var name = "apple"

  var output = function () {
    console.log(name)
  }

  return output
}

var out = apple()
out()  // apple

上述将函数传引用给了全局作用域的变量,显然,闭包(对 apple 作用域的引用)在全局作用域都存在的情况下都可能发生,而且后面也执行了 out()

更常见的写法是下面这种:

function Apple () {
  var name = "apple"

  var output = function () {
    console.log(name)
  }

  var setName = function (arg) {
    name = arg
  }

  return {
    output: output,
    setName: setName
  }
}

var apple = Apple()
apple.output()  // apple
apple.setName("Apple")
apple.output()  // Apple

这就是模块的一个例子,name 通常被称为私有变量!

结语

闭包没什么了不起的,这是被人玩的过于玄乎,其实这是人们很自然的想法:我在别的地方调用函数,总得保持函数正常运行吧!“闭包”这种机制很轻松的帮你解决了这个问题,我们不必搞懂闭包是什么也经常在实现它(如果这句话写在前面,会不会很多人都不看了,哈哈),这是语言设计者的过人之处,但是,你不搞懂它,总被人质疑:你不懂闭包吧!实际上,我们都实现了很多次闭包,所以,你把内部机制详细搞清楚了,就不会再害怕别人的质疑了,哈哈!当然,如果你喜欢钻研,更有必要了解其中的机制了,体会到寻找语言设计者设计思路的快感!

最后,再总结一下:函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。
最后的最后,再强调一下:闭包就是引用!

更多文章请看我的个人博客

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80778.html

相关文章

  • JS 中的闭包是什么?

    摘要:大名鼎鼎的闭包面试必问。闭包的作用是什么。看到闭包在哪了吗闭包到底是什么五年前,我也被这个问题困扰,于是去搜了并总结下来。关于闭包的谣言闭包会造成内存泄露错。闭包里面的变量明明就是我们需要的变量,凭什么说是内存泄露这个谣言是如何来的因为。 本文为饥人谷讲师方方原创文章,首发于 前端学习指南。 大名鼎鼎的闭包!面试必问。请用自己的话简述 什么是「闭包」。 「闭包」的作用是什么。 首先...

    Enlightenment 评论0 收藏0
  • 闭包?反正看完我就懂了

    摘要:闭包反正看完我就懂了想要好好的理解闭包,你得首先理解作用域。其实这个闭包的产生过程可以理解为在里面的匿名函数定义时正处于怀孕阶段,到外面调用时,娃就出生了,娃就是闭包啦。闭包改变了变量的生命周期,变量将得到永生。 闭包?反正看完我就懂了 想要好好的理解闭包,你得首先理解作用域。别说了,赶紧去看作用域吧,?,这世界就是如此残酷。好,言归正传,我们是来学习闭包的。O(∩_∩)O 什么是闭包...

    sean 评论0 收藏0
  • 理解 JavaScript(二)

    摘要:所以形式参数是本地的,不是外部的或者全局的。这叫做函数声明,函数声明会连通命名和函数体一起被提升至作用域顶部。这叫做函数表达式,函数表达式只有命名会被提升,定义的函数体则不会。 Scoping & Hoisting var a = 1; function foo() { if (!a) { var a = 2; } alert(a); }; ...

    luxixing 评论0 收藏0
  • JS基础篇--函数声明与定义,作用,函数声明与表达式的区别

    摘要:在中,有四种方式可以让命名进入到作用域中按优先级语言定义的命名比如或者,它们在所有作用域内都有效且优先级最高,所以在任何地方你都不能把变量命名为之类的,这样是没有意义的形式参数函数定义时声明的形式参数会作为变量被至该函数的作用域内。 Scoping & Hoisting 例: var a = 1; function foo() { if (!a) { var ...

    TerryCai 评论0 收藏0
  • 8道经典JavaScript面试题解析,真的掌握JavaScript了吗

    摘要:浏览器的主要组成包括有调用堆栈,事件循环,任务队列和。好了,现在有了前面这些知识,我们可以看一下这道题的讲解过程实现步骤调用会将函数放入调用堆栈。由于调用堆栈是空的,事件循环将选择回调并将其推入调用堆栈进行处理。进程再次重复,堆栈不会溢出。 JavaScript是前端开发中非常重要的一门语言,浏览器是他主要运行的地方。JavaScript是一个非常有意思的语言,但是他有很多一些概念,大...

    taowen 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<