理解闭包

189 阅读4分钟

最近在看闭包的一些知识点,理解闭包之前先理解下调用栈的概念。现将书上的内容做下记录,方便查阅。

调用栈

function f1(){
  f2()
}
function f2(){
  f3()
}
function f3(){
  f4()
}
function f4(){
  console.log('f4');
}
f1()  //f4

调用栈:先进后出(后进先出)。f1 -> f2 -> f3-> f4 。个具体过程:f1先入栈,紧接着f1调用f2,f2再入栈。以此类推,直到f4执行完,然后f4先出栈,f3再出栈,接着f2出栈,最后f1出栈。这个过程是满足先进后出的规则,因此形成调用栈。

闭包定义:函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问。

function numGenerator(){
  let num = 1
  num++
  return ()=>{
    console.log(num);
  }
}
var getNum = numGenerator()
getNum()   //2

没有及时把变量清空导致的内存泄漏

var element = document.getElementById('element');
element.style.color = 'red'
function remove(){
  element.parentNode.removeChild(element)
}

这会导致内存泄漏,最好在remove()方法中增加element = null

不用的变量要置为null

function foo(){
  let value = 123
  function bar(){
    alert(value)
  }
  return bar
}
let bar = foo()()

上面代码中,变量value将会被保存在内存中,如果加上bar = null,则随着Bar不再被引用,value也会被清除。

下面实例来熟悉借助chrome devtool排查内存泄漏的具体应用。

var array = []
function createNodes(){
  let div
  let i = 100;
  let frag = document.createDocumentFragment()
  for(;i>0;i--){
    div = document.createElement('div');
    div.appendChild(document.createTextNode(i))
    frag.appendChild(div)
  }
  document.body.appendChild(frag)
}

function badCode(){
  array.push([...Array(100000).keys()])
  createNodes()
  setTimeout(badCode,1000)
}
badCode()

以上代码递归调用了 badCode, 这个函数每次向 array 数组中写入新 的由 100000 项 0~1 数字组 成的新数组, badCode 函数使用全局变址 array 后并没有手动释放内存 , 垃圾回收机制不会处理 array, 因此会导致内存泄涌 ;同时, badCode 函数调用了 createNodes 函数, 每秒会创建 100 个 div 节点 。

拍下Chrome devTools的performance快照

image.png

由上图可以看出,JS Heap和Nodes线随着时间线一直在上升,并没有被拉圾回收机制回收。因此,可以判定这段代码存在较大的内存泄漏风险。如果不知道问题代码的位置,要想找出风险点,那就需要在Chrome Memory标签中,对JS Heap中的每一项,尤其是Size较大的前几项展开调查。

下面进入闭包的实战环节

1. 下面代码会输出什么?

const foo = (function(){
  var v=0;
  return ()=>{
    return v++
  }
}())

for(let i=0; i<10; i++){
  foo()
}
console.log(foo());

答案是:10. 下面进行分析: foo是一个立即执行函数,当我们尝试打印foo时,要执行如下代码

const foo = (function(){
  var v=0;
  return ()=>{
    return v++
  }
}())
console.log(foo)

输出结果如下:

()=>{
    return v++
  }

在循环执行foo时,引用自由变量10次,v自增10次,最后执行foo时,得到10.这里的自由变量是指没有在相关函数作用域中声明,但却被使用了的变量。

2. 例题2 下面代码会输出什么?

const foo = ()=>{
  var arr = []
  var i
  for(i=0; i<10; i++){
    arr[i] = function(){
      console.log(i);
    }
  }
  return arr[0]
}
foo()()

答案依然是:10. 自由变量为i,烦人例题1,执行foo返回的是arr[0],arr[0]此时是函数,其中变量i的值为10.

3. 实战例题3 ,下面代码输出什么?

var fn = null
const foo = ()=>{
  var a = 2
  function innerFoo(){
    console.log(a)
  }
  fn = innerFoo
}
const bar = ()=>{
  fn()
}
foo()
bar() 

bar()运行后输出2. 正常来说,根据调用栈的知识,foo函数执行完毕后,其执行环境生命周期会结束,所占用的内存会被垃圾收集器释放,上下文消失。但是通过将innerFoo函数赋值给全局变量fn,foo的变量对象a也会被保留下来,所以,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象,输出结果为2.

4. 例题4 对3题的代码进行修改

var fn = null
const foo = ()=>{
  var a = 2
  function innerFoo(){
    console.log(c)
    console.log(a)
  }
  fn = innerFoo
}
const bar = ()=>{
  var c = 100
  fn()
}
foo()
bar()

会报错:ReferenceError: c is not defined 。因为bar中执行fn时,fn已经被复制为innerFoo,变量c并不在其作用域链上,c只是bar函数的内部变量。因此会报错。如下图:

image.png