浅析神秘的闭包

444 阅读3分钟

闭包是一种现象。基于js引擎OC的一种运用作用域的一种手段。

这种现象是,有权访问另一个函数作用域中的变量。

也就是说闭包保存了函数执行完毕之后的执行上下文, 上下文按照我的理解就是此时的AO和GO, 即也就是说闭包保存了函数执行完毕之后的执行上下文, 上下文按照我的理解就是此时的AO和GO, 即[[scope]]

一旦产生了闭包,那么函数的上级作用域就可以访问到,所以说闭包产生的原因 存在上级作用域的引用

引入下面这张图,完整的描述和闭包相关的知识点。

那么怎么样才能够产生闭包呢?

一、何时会产生闭包

当内部函数被返回到外部并保存时,一定会产生闭包,闭包会产生原来的作用域链而不会销毁. 即使自己的被执行完毕,也只是会清除自己的AO,而它的上文作用域不会有影响,依旧在内存当中.

表现的形式,可以是return Function 有可以是return [Function1, Function2] ,甚至是对象函数也算在内.return {handle: Function}

二、栗子🌰

1. 闭包带来的作用

  • 保护私有变量不受外部的干扰
  • 形成不被销毁的栈内存
  • 将上级作用域引用保存下来,实现方法和属性的私有化
var n = 10;
function fn() {
  var n = 20;
  function f() {
    n++;
    console.log(n);
  }
  f();
  return f;
}

var x = fn();
x();
x();
console.log(n); 

结果如下:

// 21
// 22
// 23
// 10

n 由于被闭包所引用,闭包没有在销毁,它一直存在,且不被全局的n = 10 所影响;

每调用一次,n就自增1,说明n的变量没有被销毁, 将本来存在栈内存中的变量给变成了堆内存中,这个在预编译部分有很好的解释;

闭包引用的内存存储在堆内存当中

f()的上级作用域的n = 20 很好的被保存了下来,这个时候就随便你进行任何操作;

2.自执行函数也是闭包(IIFE)

var n = '蚂蚱'
(function p() {
  console.log(n) // 蚂蚱
})()

循环赋值产生的闭包问题:

我们可以使用闭包来解决for循环中全局变量的问题:

对照组:

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

i是全局作用域的变量,而且setTimeout是宏任务,所以需要等同步的任务运行完毕之后才放入到同步队列当中。结果如下:

// 10
// 10
.... 1010
for(var i = 0; i < 10; i++) {
  (function() {
    setTimout(function() {
      console.log(i)
    }, 1000)
  })()
}
  • 结果
// 1 ,2 ,3 ,4 ,5, 6, 7, 8,9 , 10

立即执行函数形成了闭包,闭包存在引用变量的保护,所以每一次的循环都产生了一个新的作用域,而这个作用域中的变量放入了堆内存当中,并没有被立即销毁. 所以产生了如上的现象.

以前我一直以为产生这个的原因是因为定时器是宏观任务, 循环中是微观任务,所以宏观任务是需要等到微观任务完结了才能够继续下去.现在才意思到不是这么一回事.应该是由于产生了闭包的副作用. 因为对照组出现的时机10个10.

3.使用回调函数就是使用闭包

window.name = 'www'
setTimeout(function timeHandle() {
  console.log(window.name);
}, 100)

3.1 函数数组

var data = []
for (var i = 0; i < 3; i++) {
  data[i] = function() {
    console.log(i)
  }
}
data[0]()
data[1]()
data[2]()
  • 结果

    3 、3、3

造成上面这样的结果,是应该i在这里面是全局变量.

为了解决这个问题,有如下两种方式

解决的办法1: let,能够形成块级作用域

var data = []
for (let i = 0; i < 3; i++) {
  data[i] = function() {
    console.log(i)
  }
}
data[0]()
data[1]()
data[2]()

解决的办法2: 使用自执行函数

var data = []

for (var i = 0; i < 3; i++) {
  (function (j) {
    data[j] = function () {
      console.log(j)
    }
  })(i)
}
data[0]()
data[1]()
data[2]()
上面的i传进行重新什么为j.外面的data[]一直在引用,所以没有对里面的j进行了销毁了,形成了闭包,形成了不销毁的私有作用域

4. 对于闭包的副作用,对引用的变量使其存储到了堆内存中的理解

var result = [];
var a = 3;
var total = 0;

function foo(a) {
    for (var i = 0; i < 3; i++) {
        result[i] = function () {
            total += i * a;
            console.log(total);
        }
    }
}

foo(1);
result[0]();
result[1]();
result[2]();
  • 结果

    3、6、9

这里有五个变量;

  • 值得注意的是,foo函数内部的形参自形成了作用域,所以它在里面是1;
  • 当在foo循环内部被外部的变量给存储了函数之后,就产生了外部变量能够引用内部函数的现象,故而形成了闭包.
  • 在foo()函数内部,i相对于data[index]是全局变量,所以data[index]任何参数,i都为3
  • 所以此时在堆内存的变量唯有total中,它被闭包所引用,所以不会销毁,所以造成了如此的结果.

三、使用闭包需要注意

容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。

四、其他容易混淆的点

很多时候你看到的例子都是return 一个函数来构成闭包. 是不是说闭包一定要return一个函数出去呢?

function test() {
  var a = 1

  function add() {
    a++
    console.log(a)
  }

  this.add = add
}

test()
add() // 2
add() // 3
add() // 4 

这里并没有return一个函数出去,但是依旧能够保存test()上面的a值.

在立即执行函数中可以更加优雅,这也是es5时代做插件的入口方法.

;(function() {
  var a = 1
  function add() {
    a++
    console.log(a)
  }
  windows.add = add
})()

add() // 2
add() // 3
add() // 4

上面也实现了闭包.

上面两个例子已经很直接的告诉你,return一个函数出去,只是实现闭包的一个手段.其目的是为了把内部函数作用域挂载在相对全局的作用域中.