JavaScript 之理解闭包

什么是闭包?

闭包的概念:
《JavaScript》权威指南: 函数对象可以通过作用域链相互关联起来,函数体内部的变量可以保存在函数作用域内,这种特性称为“闭包”。

通俗的说:所谓闭包,就是一个函数,这个函数能够访问其他函数作用域中的变量。
或者说:闭包,有权访问另一个函数作用域中的变量的函数;一般情况就是在一个函数中包含另一个函数(被包含的函数就是闭包)。

函数的作用域是独立的、封闭的,外部的执行环境是访问不了的,但是闭包具有这个能力和权限。

那闭包是怎样的一个表现形式呢?
第一,闭包是一个函数,而且存在于另一个函数当中
第二,闭包是可以访问到父级函数的变量,且该变量不会销毁

function person () {
  var name = '小樱'
  function cat() {   // 这个是一个内部的函数,是一个闭包
    console.log(name)
  }
  return cat
}
var per = person() // per 的值就是return后的结果,即cat函数
per();  // 结果: 小樱,per()就相当于cat()
per();  // 小樱
per();  // 小樱

闭包的原理

闭包的实现原理,其实是利用了作用域链的特性,我们都知道作用域链就是在当前执行环境下访问某个变量时,如果不存在就一直向外层寻找,最终寻找到最外层也就是全局作用域,这样就形成了一个链条。
例如:

var age = 18
function cat () {
  age++
  console.log(age)  // cat函数内输出了age,该作用域没有,则向外层寻找,找到了,输出19
}
cat() // 19

这个时候如果继续调用,结果就会一直增加,也就是变量age的值一直在递增

cat() // 20
cat() // 21
cat() // 22

如果程序还有其他函数,也需要用到age的值,则会受到影响,而且全局变量还容易被人修改,比较不安全,这就是全局变量容易污染的原因,所以我们必须解决变量污染问题,那就是把变量封装到函数内部,让它成为局部变量。
如下:

// f2 加括号表示返回的是函数值,不加括号返回的是函数体

// 示例一
function f1() {
  var age = 18
  function f2() { // 是一个内部函数,是一个闭包
    age++
    console.log('年龄-->', age)
  }
  return f2();
}
f1() // 19
f1() // 19


// 示例二
function f1() {
  var age = 18
  function f2() {  // 是一个内部函数,是一个闭包
    age++
    console.log('年龄-->', age)
  }
  return f2;
}
var a = f1()
a()  // 19
a()  // 20

示例一:内部函数 f2() 在执行前,从外部函数返回。
示例二:f1( ) 执行后,将其返回值(也就是内部的 f2() 函数)赋值给变量a, 并调用a(),实际上只是通过不同的标示符引用调用了内部的函数f2。

f1() 函数执行后,正常情况下f1() 的整个内部作用域被销毁,占用的内存被回收。但是现在的f1() 的内部作用域f2() 还在使用,所以不会对其进行回收。f2() 依然持有对改作用域的引用,这个引用叫做闭包。这个函数在定义的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。

常见的闭包

function foo(a) {
  setTimeout(function timer() {
    console.log(a)
  }, 1000)
}
foo(2)

foo执行1000ms后,它的内部作用域不会消失,timer 函数依然保有foo 作用域的引用。timer函数就是一个闭包。

定时器,事件监听器,Ajax请求,跨窗口通信,Web Workers 或者其他异步或同步任务中,只要使用回调函数,实际上就是闭包。

循环和闭包

for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, i * 1000)
}

上面的这段代码,预期是每隔一秒,分别输出0,1,2,3,4,但实际上依次输出的是5,5,5,5,5。首先解释一下5从哪里来的,这个循环的终止条件是 i 不在 < 5,条件首次成立时 i 的值是5,因此,输出显示的是循环结束时 i 的最终值。

延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的都是setTimeout(..., 0) ,所有的回调函数依然是在循环结束后才执行。因此每次都输出一个5来。

我们预期的是每个迭代在运行时都会给自己“捕获”一个 i 的的副本。但实际上,根据作用域的原理,尽管循环中的五个函数都是在各自迭代中分别定义的,但是他们都封闭在一个共享的全局作用域中,因此实际上只有一个 i。即所有函数共享一个 i 的引用。

改成下面这样,就可以按照我们期望的方式进行工作了。这样修改之后,在每次迭代内使用 IIFE (立即执行函数)会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代内部都会含有一个具有正确值的变量可以访问。

for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j)
    }, j * 1000)
  })(i)
}

当然,使用ES6 块级作用域的 let 替换 var 也可以达到我们的目的。

for (let i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i)
  }, i * 1000)
}

闭包的优缺点

优点:

  1. 隐藏变量,避免全局污染
  2. 可以读取函数内部的变量
    使用不当,优点就变成了缺点:
  3. 导致变量不会被垃圾回收机制回收,造成内存消耗
  4. 不恰当的使用闭包可能会造成内存泄漏的问题。

为什么使用闭包时变量不会被垃圾回收机制销毁呢?

JS垃圾回收机制:
JS规定在一个函数作用域内,程序执行完以后变量就会被销毁,这样可以节省内存;使用闭包时,按照作用域链的特点,闭包(函数)外面的变量不会被销毁,因为函数会一直被调用,所有一直存在,如果闭包使用过多会造成内存泄漏。

理解闭包

理解闭包首先要了解嵌套函数的词法作用域规则,如下代码

// 示例一
var str = 'Hello World'    // 全局变量
var a = function () {
  var str = 'Hello 你好'  // 局部变量
  function f() {
    return str
  }
  return f()
}
console.log('示例1 ---->', a())   // 打印结果: Hello 你好

a () 函数声明了一个局部变量,并定义了一个新的函数f(),函数f()返回了这个变量的值,最后将函数f()的执行结果返回。

变量提升

变量提升即将变量声明提升到它所在作用域的最开始的地方。
只有 var 可以将变量进行提升,const 跟 let 不会
下面举个例子:
变量提升(全局作用域)

// 示例1
console.log(a)   // undefined
var a = 8

// 示例1 等价于
var a;
console.log(a)   // undefined
a = 8

// 示例2 
var a = 8;
function fn() {
  console.log('1--->', a);   // undefined
  var a = 9;
  console.log('2--->', a);   // 9
}
fn()

// 示例2 等价于
var a = 8
function fn() {
    var a;
  console.log('1---->', a)  // undefined
  a = 9;
  console.log('2---->', a)  // 9
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • js之闭包 1、到底什么是闭包 闭包已经成为近乎神话的概念,它非常重要又难以掌握,而且还难以定义。 1.1 古老的...
    道无虚阅读 714评论 0 0
  • 一个简单的闭包 例1 理解闭包 闭包是指在 JavaScript 中,内部函数总是可以访问其所在的外部函数中声明的...
    howell5阅读 346评论 0 3
  • 什么是闭包? 闭包是指那些能够访问自由变量的函数。自由变量:指在函数中使用的,但既不是函数参数也不是函数的局部变量...
    BubbleM阅读 312评论 0 1
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zock阅读 1,086评论 2 6
  • 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。 一、变量...
    zouCode阅读 1,288评论 0 13